mirror of
https://github.com/we-promise/sure.git
synced 2026-05-08 05:04:59 +00:00
Shifts the AI assistant from reactive (users ask questions) to proactive (the system surfaces personalized insights automatically). A nightly job analyzes every family's financial data across 7 insight types, writes natural-language explanations via Claude, and surfaces them in a feed on the dashboard and a standalone /insights page. Feature is behind a flag: off by default, enable with INSIGHTS_ENABLED=1 (or via Setting.insights_enabled in the admin UI). Insight types: - spending_anomaly: category spend >25% above/below 3-month rolling average - cash_flow_warning: projected cash balance drops below $500 in 30 days (uses RecurringTransaction + statistical daily baseline) - net_worth_milestone: crossed a round-number milestone or all-time high - subscription_audit: recurring transaction overdue by 45+ days - savings_rate_change: savings rate changed >5 percentage points vs last month - idle_cash: $5k+ sitting in depository account with no activity in 60 days - budget_at_risk / budget_on_track: spending pace vs monthly budget Architecture: - Insight model with dedup_key unique index (upsert, not re-create daily) - Insight::Generator base class + Insight::GeneratorRegistry orchestrator - LLM used as a writer only — financial math runs in pure Ruby - GenerateInsightsJob runs at 6 AM UTC daily via sidekiq-cron - InsightsController with read/dismiss Turbo Stream actions - Dashboard section gated by Current.user.insights_enabled? https://claude.ai/code/session_014vY9xohpm3abSAxVxRF27a
40 lines
1.0 KiB
Ruby
40 lines
1.0 KiB
Ruby
class InsightsController < ApplicationController
|
|
include FeatureGuardable
|
|
|
|
guard_feature unless: -> { Current.user.insights_enabled? }
|
|
|
|
before_action :set_insight, only: %i[read dismiss]
|
|
|
|
def index
|
|
@insights = Current.family.insights
|
|
.visible
|
|
.ordered
|
|
end
|
|
|
|
def read
|
|
@insight.mark_read!
|
|
respond_to do |format|
|
|
format.turbo_stream { render turbo_stream: turbo_stream.replace(@insight, partial: "insights/insight_card", locals: { insight: @insight, compact: false }) }
|
|
format.html { redirect_to insights_path }
|
|
end
|
|
end
|
|
|
|
def dismiss
|
|
@insight.dismiss!
|
|
respond_to do |format|
|
|
format.turbo_stream { render turbo_stream: turbo_stream.remove(@insight) }
|
|
format.html { redirect_to insights_path }
|
|
end
|
|
end
|
|
|
|
def refresh
|
|
GenerateInsightsJob.perform_later(family_id: Current.family.id)
|
|
redirect_to insights_path, notice: t("insights.refresh_queued")
|
|
end
|
|
|
|
private
|
|
def set_insight
|
|
@insight = Current.family.insights.find(params[:id])
|
|
end
|
|
end
|