Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions backend/ai/analytics_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,9 @@ def get_anomalies(self, user_id: int, risk_level: str = None) -> List[Dict]:

return anomalies

def get_forecast(self, user_id: int, days_ahead: int = 30) -> Dict:
"""Get spending forecast"""
return self.forecast_agent.forecast_spending(user_id, days_ahead)
def get_forecast(self, user_id: int, days_ahead: int = 30, use_llm: bool = True) -> Dict:
"""Get spending forecast (use_llm=False skips the LLM-written insight text, e.g. for Home's chart)"""
return self.forecast_agent.forecast_spending(user_id, days_ahead, use_llm=use_llm)

def _risk(self, user_id) -> Dict:
"""The risk score with the user's own budget and income from onboarding, when given (SPEC-ONBOARD)."""
Expand Down
4 changes: 3 additions & 1 deletion backend/routes/ai_analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,16 @@ def get_forecast():

Query Params:
- days_ahead: Forecast period (default: 30)
- insights: 0 to skip the LLM-written insight text (Home only needs the numbers)
"""
try:
init_analytics() # Ensure orchestrator is initialized

user_id = g.user_id
days_ahead = request.args.get('days_ahead', 30, type=int)

forecast = orchestrator.get_forecast(user_id, days_ahead)
use_llm = request.args.get('insights', '1') != '0'
forecast = orchestrator.get_forecast(user_id, days_ahead, use_llm=use_llm)

return jsonify({
'success': forecast.get('success', True),
Expand Down
20 changes: 20 additions & 0 deletions backend/tests/test_ai_analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -1087,3 +1087,23 @@ def test_require_auth_routes_are_all_registered_as_views():

missing = decorated_names - registered_names
assert not missing, f"@require_auth handlers not registered as routes: {missing}"


@pytest.mark.parametrize("query, expected", [("", True), ("&insights=0", False)])
def test_forecast_route_can_skip_llm_insights(authed_client, monkeypatch, query, expected):
"""Home asks for the forecast numbers only (?insights=0): no LLM call per page load. Default keeps insights."""
import routes.ai_analytics as route

seen = {}

class FakeOrchestrator:
def get_forecast(self, user_id, days_ahead, use_llm=True):
seen["use_llm"] = use_llm
return {"success": True, "forecast": {"dates": [], "values": []}}

monkeypatch.setattr(route, "init_analytics", lambda: None)
monkeypatch.setattr(route, "orchestrator", FakeOrchestrator())
resp = authed_client.get(f"/api/analytics/forecast?days_ahead=31{query}", headers={"Authorization": "Bearer t"})

assert resp.status_code == 200
assert seen["use_llm"] is expected
9 changes: 9 additions & 0 deletions docs/CODEBASE-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,15 @@ embeds the demo documents once per server and uploads the files in the backgroun
`npm run test:e2e` (Playwright, local only), which drives the dev-only harness `app/dev/layout-shift` (404 in
production). Firefox is opt-in with `E2E_FIREFOX=1`. `html, body` use `overflow-x: clip`, never `hidden`:
`hidden` makes `<body>` a scroll container whose scrollbar disappears when a menu opens.
- **Home** (`app/(app)/home/`, UI revamp Phase 2): greeting and a serif brief over a sky band, then the cards.
The main column has Spent this period (a hand-drawn SVG: cumulative now vs the previous period, a forecast to month-end, bill pins,
1W/1M/3M/YTD), Lumen found, and Spending calendar + Recent transactions. The side column has Budget, Upcoming bills, the draft-budget promo and
Activity. `use-home-data.ts` fetches today's endpoints in parallel (transactions, reminders, forecast, review
queue). A failed call empties only its card. Skeletons show only on the first load, so changing the chart range
never blanks other cards. All the maths is pure, in `lib/home-math.ts` (unit tests: `e2e/home-math.spec.ts`).
**Interim until Phase 3:** the brief is one rule-based sentence, Lumen found only shows the review queue, the
Budget card is an empty state, and Activity is derived from transactions' `created_at`. `(app)/layout.tsx` is a
tiny server file exporting the light `theme-color`; the client shell is `components/app-shell.tsx`.
- **Signed-in e2e:** `e2e/fixtures.ts` `signIn(page)` fakes a Supabase session (localStorage) plus the
`lumen_session` cookie, and stubs every backend call, so `e2e/shell.spec.ts` drives the real shell without servers.
- **Motion:** use `motion/react` only (framer-motion was removed). Shared timings live in `lib/motion.ts`
Expand Down
14 changes: 14 additions & 0 deletions docs/direction/STORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,17 @@ with the numbers and the files that prove them. Newest last. Raw numbers live in
while typing, a live review badge and the mobile sheet closing. Avatar-menu shift checks passed 24/24 on repeat
(failed 1 in 4 before). Story point: a flaky test is a bug report. Here it caught the half of the original bug
that the first fix missed.

## 28. A Home that says something (UI revamp, Phase 2)
- **S:** The signed-in home was a stock "Financial Overview": four KPI tiles, an invoice list and a "What to watch"
card of static tips. Nothing on it was about *your* month.
- **T:** Build the approved Origin-inspired Home: a brief, a spend-vs-last-month chart, "Lumen found", a calendar,
bills, budget and activity. Use only real data, keep it light on the free Vercel tier, and keep the owner's
"nothing moves unless you asked" rule.
- **A:** Put all the maths in pure functions with 13 unit tests (periods, cumulative spend, the same-day comparison
clamped to month length, the brief sentence). Drew the chart as one SVG instead of pulling in the chart library.
Cards with no backend yet show honest empty states, not invented numbers. A test caught that changing the chart's
range blanked the brief; Home now keeps its old numbers until new ones arrive.
- **R:** `/home` is static at 15.7 kB of route JS. 65 checks pass, including zero layout shift with a 1.5 s slow
backend, a failed forecast, a brand-new user with no data, and 15-lakh amounts with long vendor names. Story point:
an honest empty state beats a convincing placeholder.
Loading
Loading