mirror of
https://github.com/we-promise/sure.git
synced 2026-05-12 15:15:01 +00:00
Add proactive financial intelligence feed
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
This commit is contained in:
39
app/controllers/insights_controller.rb
Normal file
39
app/controllers/insights_controller.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
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
|
||||
@@ -84,7 +84,17 @@ class PagesController < ApplicationController
|
||||
end
|
||||
|
||||
def build_dashboard_sections
|
||||
insights = Current.user.insights_enabled? ? Current.family.insights.for_dashboard.to_a : []
|
||||
|
||||
all_sections = [
|
||||
{
|
||||
key: "insights_feed",
|
||||
title: "pages.dashboard.insights_feed.title",
|
||||
partial: "pages/dashboard/insights_feed",
|
||||
locals: { insights: insights },
|
||||
visible: Current.user.insights_enabled? && insights.any?,
|
||||
collapsible: true
|
||||
},
|
||||
{
|
||||
key: "cashflow_sankey",
|
||||
title: "pages.dashboard.cashflow_sankey.title",
|
||||
|
||||
Reference in New Issue
Block a user