mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 03:54:08 +00:00
Merge pull request #618 from alessiocappa/FT-AddAutoSyncOptions
feat: Add automatic sync settings
This commit is contained in:
@@ -62,6 +62,29 @@ class Settings::HostingsController < ApplicationController
|
||||
Setting.syncs_include_pending = hosting_params[:syncs_include_pending] == "1"
|
||||
end
|
||||
|
||||
sync_settings_changed = false
|
||||
|
||||
if hosting_params.key?(:auto_sync_enabled)
|
||||
Setting.auto_sync_enabled = hosting_params[:auto_sync_enabled] == "1"
|
||||
sync_settings_changed = true
|
||||
end
|
||||
|
||||
if hosting_params.key?(:auto_sync_time)
|
||||
time_value = hosting_params[:auto_sync_time]
|
||||
unless Setting.valid_auto_sync_time?(time_value)
|
||||
flash[:alert] = t(".invalid_sync_time")
|
||||
return redirect_to settings_hosting_path
|
||||
end
|
||||
|
||||
Setting.auto_sync_time = time_value
|
||||
Setting.auto_sync_timezone = current_user_timezone
|
||||
sync_settings_changed = true
|
||||
end
|
||||
|
||||
if sync_settings_changed
|
||||
sync_auto_sync_scheduler!
|
||||
end
|
||||
|
||||
if hosting_params.key?(:openai_access_token)
|
||||
token_param = hosting_params[:openai_access_token].to_s.strip
|
||||
# Ignore blanks and redaction placeholders to prevent accidental overwrite
|
||||
@@ -103,10 +126,22 @@ class Settings::HostingsController < ApplicationController
|
||||
|
||||
private
|
||||
def hosting_params
|
||||
params.require(:setting).permit(:onboarding_state, :require_email_confirmation, :brand_fetch_client_id, :twelve_data_api_key, :openai_access_token, :openai_uri_base, :openai_model, :openai_json_mode, :exchange_rate_provider, :securities_provider, :syncs_include_pending)
|
||||
params.require(:setting).permit(:onboarding_state, :require_email_confirmation, :brand_fetch_client_id, :twelve_data_api_key, :openai_access_token, :openai_uri_base, :openai_model, :openai_json_mode, :exchange_rate_provider, :securities_provider, :syncs_include_pending, :auto_sync_enabled, :auto_sync_time)
|
||||
end
|
||||
|
||||
def ensure_admin
|
||||
redirect_to settings_hosting_path, alert: t(".not_authorized") unless Current.user.admin?
|
||||
end
|
||||
|
||||
def sync_auto_sync_scheduler!
|
||||
AutoSyncScheduler.sync!
|
||||
rescue StandardError => error
|
||||
Rails.logger.error("[AutoSyncScheduler] Failed to sync scheduler: #{error.message}")
|
||||
Rails.logger.error(error.backtrace.join("\n"))
|
||||
flash[:alert] = t(".scheduler_sync_failed")
|
||||
end
|
||||
|
||||
def current_user_timezone
|
||||
Current.family&.timezone.presence || "UTC"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,6 +24,21 @@ class Setting < RailsSettings::Base
|
||||
simplefin && plaid
|
||||
end
|
||||
field :syncs_include_pending, type: :boolean, default: SYNCS_INCLUDE_PENDING_DEFAULT
|
||||
field :auto_sync_enabled, type: :boolean, default: ENV.fetch("AUTO_SYNC_ENABLED", "1") == "1"
|
||||
field :auto_sync_time, type: :string, default: ENV.fetch("AUTO_SYNC_TIME", "02:22")
|
||||
field :auto_sync_timezone, type: :string, default: ENV.fetch("AUTO_SYNC_TIMEZONE", "UTC")
|
||||
|
||||
AUTO_SYNC_TIME_FORMAT = /\A([01]?\d|2[0-3]):([0-5]\d)\z/
|
||||
|
||||
def self.valid_auto_sync_time?(time_str)
|
||||
return false if time_str.blank?
|
||||
AUTO_SYNC_TIME_FORMAT.match?(time_str.to_s.strip)
|
||||
end
|
||||
|
||||
def self.valid_auto_sync_timezone?(timezone_str)
|
||||
return false if timezone_str.blank?
|
||||
ActiveSupport::TimeZone[timezone_str].present?
|
||||
end
|
||||
|
||||
# Dynamic fields are now stored as individual entries with "dynamic:" prefix
|
||||
# This prevents race conditions and ensures each field is independently managed
|
||||
|
||||
52
app/services/auto_sync_scheduler.rb
Normal file
52
app/services/auto_sync_scheduler.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
class AutoSyncScheduler
|
||||
JOB_NAME = "sync_all_accounts"
|
||||
|
||||
def self.sync!
|
||||
Rails.logger.info("[AutoSyncScheduler] auto_sync_enabled=#{Setting.auto_sync_enabled}, time=#{Setting.auto_sync_time}")
|
||||
if Setting.auto_sync_enabled?
|
||||
upsert_job
|
||||
else
|
||||
remove_job
|
||||
end
|
||||
end
|
||||
|
||||
def self.upsert_job
|
||||
time_str = Setting.auto_sync_time || "02:22"
|
||||
timezone_str = Setting.auto_sync_timezone || "UTC"
|
||||
|
||||
unless Setting.valid_auto_sync_time?(time_str)
|
||||
Rails.logger.error("[AutoSyncScheduler] Invalid time format: #{time_str}, using default 02:22")
|
||||
time_str = "02:22"
|
||||
end
|
||||
|
||||
hour, minute = time_str.split(":").map(&:to_i)
|
||||
timezone = ActiveSupport::TimeZone[timezone_str] || ActiveSupport::TimeZone["UTC"]
|
||||
local_time = timezone.now.change(hour: hour, min: minute, sec: 0)
|
||||
utc_time = local_time.utc
|
||||
|
||||
cron = "#{utc_time.min} #{utc_time.hour} * * *"
|
||||
|
||||
job = Sidekiq::Cron::Job.create(
|
||||
name: JOB_NAME,
|
||||
cron: cron,
|
||||
class: "SyncAllJob",
|
||||
queue: "scheduled",
|
||||
description: "Syncs all accounts for all families"
|
||||
)
|
||||
|
||||
if job.nil? || (job.respond_to?(:valid?) && !job.valid?)
|
||||
error_msg = job.respond_to?(:errors) ? job.errors.to_a.join(", ") : "unknown error"
|
||||
Rails.logger.error("[AutoSyncScheduler] Failed to create cron job: #{error_msg}")
|
||||
raise StandardError, "Failed to create sync schedule: #{error_msg}"
|
||||
end
|
||||
|
||||
Rails.logger.info("[AutoSyncScheduler] Created cron job with schedule: #{cron} (#{time_str} #{timezone_str})")
|
||||
job
|
||||
end
|
||||
|
||||
def self.remove_job
|
||||
if (job = Sidekiq::Cron::Job.find(JOB_NAME))
|
||||
job.destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -17,6 +17,40 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm"><%= t(".auto_sync_label") %></p>
|
||||
<p class="text-secondary text-sm"><%= t(".auto_sync_description") %></p>
|
||||
</div>
|
||||
|
||||
<%= styled_form_with model: Setting.new,
|
||||
url: settings_hosting_path,
|
||||
method: :patch,
|
||||
data: { controller: "auto-submit-form", auto_submit_form_trigger_event_value: "change" } do |form| %>
|
||||
<%= form.toggle :auto_sync_enabled,
|
||||
checked: Setting.auto_sync_enabled,
|
||||
data: { auto_submit_form_target: "auto" } %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm"><%= t(".auto_sync_time_label") %></p>
|
||||
<p class="text-secondary text-sm"><%= t(".auto_sync_time_description") %></p>
|
||||
</div>
|
||||
|
||||
<%= form_with model: Setting.new,
|
||||
url: settings_hosting_path,
|
||||
method: :patch,
|
||||
data: { controller: "auto-submit-form", auto_submit_form_trigger_event_value: "change" } do |form| %>
|
||||
<%= form.time_field :auto_sync_time,
|
||||
value: Setting.auto_sync_time,
|
||||
disabled: !Setting.auto_sync_enabled,
|
||||
class: "rounded-lg border border-primary px-3 py-2 text-sm bg-container text-primary w-full",
|
||||
data: { auto_submit_form_target: "auto" } %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if env_configured %>
|
||||
<div class="bg-warning-50 border border-warning-200 rounded-lg p-3">
|
||||
<div class="flex items-start gap-2">
|
||||
|
||||
@@ -75,10 +75,16 @@ en:
|
||||
failure: Invalid setting value
|
||||
success: Settings updated
|
||||
invalid_onboarding_state: Invalid onboarding state
|
||||
invalid_sync_time: Invalid sync time format. Please use HH:MM format (e.g., 02:30).
|
||||
scheduler_sync_failed: Settings saved, but failed to update the sync schedule. Please try again or check the server logs.
|
||||
clear_cache:
|
||||
cache_cleared: Data cache has been cleared. This may take a few moments to complete.
|
||||
not_authorized: You are not authorized to perform this action
|
||||
sync_settings:
|
||||
auto_sync_label: Enable automatic sync
|
||||
auto_sync_description: When enabled, all accounts will be automatically synced daily at the specified time.
|
||||
auto_sync_time_label: Sync time (HH:MM)
|
||||
auto_sync_time_description: Specify the time of day when automatic sync should occur.
|
||||
include_pending_label: Include pending transactions
|
||||
include_pending_description: When enabled, pending (uncleared) transactions will be imported and automatically reconciled when they post. Disable if your bank provides unreliable pending data.
|
||||
env_configured_message: This setting is disabled because a provider environment variable (SIMPLEFIN_INCLUDE_PENDING or PLAID_INCLUDE_PENDING) is set. Remove it to enable this setting.
|
||||
|
||||
@@ -19,12 +19,6 @@ run_security_health_checks:
|
||||
queue: "scheduled"
|
||||
description: "Runs security health checks to detect issues with security data"
|
||||
|
||||
sync_all_accounts:
|
||||
cron: "22 2 * * *" # every 24 hours at 2:22am
|
||||
class: "SyncAllJob"
|
||||
queue: "scheduled"
|
||||
description: "Syncs all accounts for all families"
|
||||
|
||||
sync_hourly:
|
||||
cron: "0 * * * *" # every hour at the top of the hour
|
||||
class: "SyncHourlyJob"
|
||||
|
||||
Reference in New Issue
Block a user