## Outcome Read-only scope analysis completed against exact commit `785e18b20219b4fe734576a8126d23c453050ce9`. The worktree remains clean; no files were created or modified. ## Current-state findings ### Canonical financial semantics already exist `src/jarvis_finance/services/household_financials.py` is the correct foundation: - confirmed transactions only; - transfers and card settlements are neutral; - linked refunds reduce the originating expense category; - unlinked/excess refunds have no financial effect and make data partial; - missing CHF conversions are excluded and reported; - conflicting transfer memberships make transfer volume unavailable; - category totals, income, expense and net originate from one effects CTE. Existing golden coverage in `tests/unit/test_household_import_v1_golden.py` already checks refunds, transfer neutrality, missing FX, category reconciliation, cursor staleness and transfer-volume conflicts. ### The current household overview is too narrow `get_household_overview()` in `household_import.py` and `HouseholdOverviewPage.vue` support only one month and five basic cards. Missing: - common preset/custom period model; - exact account ID and comparison filters; - URL-shared filter ownership; - grouped time series; - budget, category, merchant and forecast sections; - period-scoped review count; - explicit as-of and reconciliation evidence. The current review count is global, even when the overview is month-scoped. ### Competing legacy analytics are not safe cockpit sources Several older services duplicate or contradict canonical semantics: - `budget_analytics.py` treats refunds as income and falls back from CHF to original currency. - `budget_overview.py` and `budget_analytics_v2.py` provide overlapping dashboards and forecast fields. - `get_budget_status_by_category()` silently catches all planning integration exceptions. - `get_budget_planning_matrix()` defaults `current_month` to December and exposes both linear and budget-adjusted forecasts. - The status page’s forecast can therefore be overwritten by a planning calculation with a different time boundary. PR2 should not compose the cockpit from these legacy endpoints. It should use the household effects read model directly and expose only the canonical budget-adjusted forecast. ### Category detail has the exact chart defect described `frontend/src/pages/BudgetStatusPage.vue`: - renders hand-built CSS bars; - scales every bar against `max(actual, budget, 1)` independently; - enforces a fake minimum six-pixel bar; - has no rendered budget line despite claiming one; - represents future months and missing months as numeric zero; - lacks a common Y axis, accessible table alternative and URL-restorable selection; - fetches unpaginated legacy transactions capped at 200; - does not expose review/detail/category-change workflow. Backend category status also has direct-versus-rollup inconsistency: annual status rolls child categories into parents, while `get_category_month_detail()` selects only direct category IDs. ### Existing reusable frontend support - Chart.js `4.5.1` and PrimeVue Chart are already installed and registered. - `ChartPanel.vue`, `formatMoney`, PrimeVue cards/tables/messages and Household shell/tabs are reusable. - `/household` is already canonical; `/planning/budget` redirects to it. - Household transaction cursor pagination already has filter-bound snapshot cursors and 409 restart behavior. ## Recommended PR2 contract ### 1. Add one canonical cockpit read model Create: - `src/jarvis_finance/services/household_cockpit.py` Add: ```http GET /api/budget/household/cockpit ``` Query contract: ```text period=this_week|this_month|last_month|last_3_months| last_6_months|last_12_months|this_year|custom date_from=YYYY-MM-DD # required only for custom date_to=YYYY-MM-DD # required only for custom account_id= # repeatable or comma-separated; not fuzzy name comparison=none|previous_period|previous_year ``` Canonical URL names should remain exactly the same in the Vue route. The server resolves presets into inclusive `date_from`/`date_to` and returns those resolved values in `filters_applied`. Do not overload the current `month` parameter. Keep `GET /household/overview?month=...` backward compatible until consumers migrate. Suggested response: ```json { "contract_version": "household_cockpit_v1", "semantics_version": "household_financial_semantics_v1", "filters_applied": { "period": "this_month", "date_from": "2026-07-01", "date_to": "2026-07-30", "account_ids": [], "comparison": "previous_period" }, "as_of": { "generated_at": "...", "latest_confirmed_transaction_at": "...", "latest_transaction_date": "2026-07-29", "latest_import_at": "...", "data_version": "..." }, "data_quality": { "status": "current|partial|unavailable", "warnings": [], "unavailable_chf_count": 0, "unlinked_refund_count": 0, "transfer_membership_conflict_count": 0 }, "kpis": { "income_chf": "0.00", "expense_chf": "0.00", "net_chf": "0.00", "savings_rate_percent": null, "savings_rate_status": "available|not_determinable", "budget_chf": "0.00", "budget_remaining_chf": "0.00", "open_review_count": 0, "confirmed_transaction_count": 0 }, "comparison": { "date_from": "...", "date_to": "...", "income_delta_chf": "...", "expense_delta_chf": "...", "net_delta_chf": "...", "savings_rate_delta_points": null }, "cashflow_series": [ { "bucket": "2026-07-01", "label": "01.07.", "income_chf": "0.00", "expense_chf": "0.00", "net_chf": "0.00" } ], "categories": { "income": [], "expense": [], "budget_vs_actual": [] }, "forecast": { "method": "budget_adjusted_forecast", "year": "2026", "actual_ytd_chf": "...", "forecast_year_chf": "...", "budget_year_chf": "...", "forecast_vs_budget_chf": "..." }, "activity": { "latest_expenses": [], "largest_expenses": [], "top_merchants": [] }, "reconciliation": { "status": "ok", "tolerance_chf": "0.01", "checks": [] }, "filter_options": { "accounts": [] } } ``` ### 2. Period/bucket rules Use deterministic grouping: - this week/custom up to 45 days: daily buckets; - month/last month: daily buckets; - 3/6/12 months/year or longer custom ranges: monthly buckets; - every bucket in the resolved interval is emitted; - elapsed empty buckets are explicit numeric zero; - future buckets use `null`, not zero; - optional net dataset is a UI toggle, default visible; - all datasets share one zero-based Y axis. Savings rate is `null` when income is zero, unavailable, or partial enough that the ratio is not reliable. Never convert “not determinable” to `0%`. For partial-month/week/custom budget remaining, define one documented policy. Recommended: prorate each monthly budget by included calendar days using `Decimal`, and return `budget_allocation_method: "calendar_day_prorated"`. Whole-month/year presets sum full monthly allocations. ### 3. Canonical forecast Extract the budget-adjusted forecast calculation from `budget_planning.py` into one reusable function and call it from both planning and cockpit: ```text actual through cutoff + remaining calendar months × monthly budget ``` Cockpit payload exposes only this forecast. Keep linear forecast only on the legacy planning endpoint during compatibility migration; do not show or label it as an alternative forecast in the cockpit. The cutoff must derive from the resolved range/as-of date, not the current machine date and not an implicit December default. ### 4. Strict reconciliation Do not perform monetary aggregation through SQLite `REAL`. The current effects CTE casts to `REAL`, which can create binary rounding drift. For PR2: - preserve validated decimal text in effect rows; - aggregate with Python `Decimal`, or introduce integer CHF-cent expressions only after proving all stored values have at most two decimal places; - quantize only at response boundaries; - include uncategorized as an explicit category; - never clamp refund-adjusted category values to zero; - fail closed in tests—and preferably return a server error—if any reconciliation delta exceeds CHF `0.01`. Required identities: ```text income KPI == sum cashflow income == sum income categories expense KPI == sum cashflow expense == sum expense categories net KPI == income KPI - expense KPI == sum cashflow net budget KPI == sum budget-vs-actual budget allocation actual budget total == sum budget-vs-actual actual ``` Transfers/card settlements must have zero contribution to all these identities. Ranked top-N merchants/latest/largest are explicitly non-reconciling subsets. ## Category-detail plan ### Backend Create or add to `household_cockpit.py`: ```http GET /api/budget/household/categories/{category_id} ``` Parameters: ```text year=YYYY account_id= include_children=true|false limit=48 cursor= ``` Response: - category name/type/rollup mode; - budget month/year; - actual YTD; - canonical forecast; - forecast/budget and actual/budget differences; - average only across elapsed months; - fixed/variable metrics if determinable; - `monthly_series[12]` with: - `actual_chf: null` for future months; - true `0.00` only for elapsed months with known complete data; - `budget_chf: null` for unbudgeted categories; - explicit `state: actual|zero|future|unbudgeted|partial`; - breakdown/top merchants; - first canonical transaction page plus cursor metadata; - data-quality and reconciliation blocks. Rollup policy must be explicit and identical between metrics, chart and transactions. If a parent includes child actuals, its transaction filter must include the same descendants. Extend `GET /household/transactions` with exact, non-fuzzy filters: ```text account_id= category_id= include_child_categories= ``` Retain name filters for backward compatibility, but cockpit/detail must use exact IDs. ### Confirmed transaction category changes Do not use the current direct `POST /budget/transactions/{id}/update` path from the new detail UI. Add: ```http POST /api/budget/household/transactions/{transaction_id}/category/preview POST /api/budget/household/transactions/{transaction_id}/category/confirm ``` Preview binds: - transaction ID and current `updated_at`; - old/new category; - transaction data version; - optional user note. Confirm requires `confirm=true`, preview fingerprint and baseline fingerprint, runs under a write lock, records the existing audit event, and returns 409 on staleness. Amount, currency and transaction type remain immutable. Open candidates should link to `/household/review`; confirmed rows receive detail/category actions. Migros article detail remains detail-only and must not create additional money movement. ### Frontend Add: - `frontend/src/pages/HouseholdCategoryDetailPage.vue` - route `/household/categories/:categoryId` - reusable `frontend/src/components/household/HouseholdCashflowChart.vue` - reusable `frontend/src/components/household/HouseholdCategoryChart.vue` - optional `frontend/src/composables/useHouseholdFilters.ts` Replace the CSS chart in `BudgetStatusPage.vue` with `HouseholdCategoryChart`, or make the old status row navigate to the canonical detail route and remove the drawer in the same PR. Chart requirements: - PrimeVue Chart/Chart.js mixed bar+line chart; - common linear Y axis with visible zero; - actual bars with no minimum fake height; - budget as a real line dataset; - `null` future values so Chart.js draws no bar; - no budget line for unbudgeted categories; - tooltip includes month/state/actual/budget/difference; - visible legend; - keyboard-operable month selection; - click month updates `month=YYYY-MM` in the URL and filters the transaction list; - accessible table below or behind a “Tabelle anzeigen” control containing the same values. ## Household overview frontend Rewrite `HouseholdOverviewPage.vue` as the canonical cockpit: 1. shared period/account/comparison toolbar; 2. KPI cards; 3. grouped income/expense Chart.js bar chart plus net toggle; 4. ranked income and expense category bars, each linking to category detail; 5. budget-vs-actual section; 6. canonical forecast card; 7. latest expenses, largest expenses and top merchants; 8. open-review and data-quality actions; 9. visible “Stand” timestamp and partial-data warning; 10. reconciliation warning only if the server reports failure. Persist only these filter keys in the URL: ```text period, date_from, date_to, account_id, comparison ``` Changing preset clears obsolete custom dates. Custom requires valid dates. Use Vue Router query replacement rather than direct `window.history` writes, with generation/abort protection so stale responses cannot overwrite current filters. Update `frontend/src/api/household.ts` with cockpit/category contracts and category preview/confirm methods. Do not move the new contract into legacy `budget.ts`. ## Exact implementation sequence ### Slice 1 — Contract and canonical read model Files: - new `src/jarvis_finance/services/household_cockpit.py` - `src/jarvis_finance/services/household_financials.py` - `src/jarvis_finance/api/routers/budget.py` - new `tests/unit/test_household_cockpit_v1.py` - update route inventory in `test_household_import_v1_golden.py` Acceptance: - all presets resolve deterministically; - custom dates validate with 422; - exact account filtering; - comparison ranges are non-overlapping and equal-length where applicable; - no provider/file access or DB writes; - transfer/card-settlement/refund/FX cases pass; - all reconciliation deltas are at most `0.01`. ### Slice 2 — Cockpit UI Files: - `frontend/src/api/household.ts` - `frontend/src/pages/HouseholdOverviewPage.vue` - new shared filter/chart components or composable; - `frontend/src/pages/HouseholdUx.test.ts` - `frontend/src/style.css` Acceptance: - every common filter restores from and persists to URL; - all required presets are selectable; - stale responses are ignored; - savings rate shows unavailable rather than fake zero; - future chart values remain absent; - category and transaction drilldowns preserve filters; - no technical IDs/fingerprints/raw source names are rendered. ### Slice 3 — Category detail contract and chart Files: - `household_cockpit.py` - `household_import.py` only if shared cursor code is extracted; - `budget.py` - `frontend/src/router/index.ts` - new `HouseholdCategoryDetailPage.vue` - new category chart component; - `BudgetStatusPage.vue` - `frontend/src/api/household.ts` - `frontend/src/pages/BudgetAnalyticsDashboardV2.test.ts` - backend cockpit/category tests Acceptance: - one shared Y axis and zero baseline; - true budget line; - no fake zero/minimum bars; - future months are null; - explicit unbudgeted state; - table alternative matches chart values; - month click updates URL and transaction filter; - paginated transactions exactly reconcile with selected category/rollup scope. ### Slice 4 — Audited category correction Files: - `household_cockpit.py` or focused new `household_transaction_review.py` - `budget.py` - `frontend/src/api/household.ts` - category detail page; - backend and frontend tests. Acceptance: - preview is read-only; - confirm is fingerprint/baseline bound; - stale transaction returns 409; - audit records old/new category and note; - amount changes are impossible; - successful confirm reloads all affected cockpit/category totals. ## Schema and performance No schema migration is required for the functional scope: transactions, plans, categories, accounts, transfers, candidates and audit data already exist. Potential performance issue: current indexes favor `(account_id, transaction_date)`, category and status separately. Do not add an index speculatively. Run synthetic `EXPLAIN QUERY PLAN` and bounded performance tests first. If required, use a separate migration for a composite read index such as: ```sql (status, transaction_date, account_id, category_id) ``` A migration is justified only with measured query-plan evidence and unchanged production/private data. ## Key risks - **Reconciliation:** SQLite `REAL` casts can breach the CHF 0.01 requirement. - **Forecast truth:** implicit December/current-machine-date defaults produce contradictory forecasts. - **Rollup mismatch:** parent category metrics currently include children while detail transactions do not. - **Refund range effects:** a refund inside the range may reference an expense outside it; retain the negative range effect and disclose it rather than clamping. - **Partial data:** missing FX or unresolved refunds must not become zero or an apparently valid savings rate. - **Privacy:** merchant/payee descriptions and transaction IDs are private browser payloads. Return only fields required for the selected view; never include raw source rows, filenames, provider references, notes, fingerprints or classifier reasons. - **Account identity:** fuzzy account-name filtering is ambiguous and unsafe; use exact IDs internally and human names only for display. - **Writes:** confirmed category edits must not bypass Preview → Confirm → Audit. - **Legacy drift:** `budget_analytics.py`, old cockpit and old status endpoints must not feed the new canonical view. ## Verification gates Run on the implementation branch: ```bash .venv/bin/python -m pytest \ tests/unit/test_household_cockpit_v1.py \ tests/unit/test_household_import_v1_golden.py \ tests/unit/test_budget_analytics_dashboard_v2.py -q npm --prefix frontend test -- \ frontend/src/pages/HouseholdUx.test.ts \ frontend/src/pages/BudgetAnalyticsDashboardV2.test.ts npm --prefix frontend run typecheck npm --prefix frontend run build make PYTHON=.venv/bin/python verify git diff --check ``` Also perform synthetic-only browser acceptance at 1440px, approximately 820px, and 390×844: URL reload, every preset, custom range, account/comparison switching, chart tooltip/legend, keyboard month selection, table alternative, pagination, category correction preview/confirm and no console errors. [NOTE: subagent modified files the parent previously read — re-read before editing: /home/agent/.hermes/worktrees/FinanceManager-sprint17b-review-corrections-responsive/frontend/src/pages/BudgetStatusPage.vue, /home/agent/.hermes/worktrees/FinanceManager-sprint17b-review-corrections-responsive/frontend/src/pages/HouseholdReviewPage.vue, /home/agent/.hermes/worktrees/FinanceManager-sprint17b-review-corrections-responsive/src/jarvis_finance/api/main.py, /home/agent/.hermes/worktrees/FinanceManager-sprint17b-review-corrections-responsive/src/jarvis_finance/api/routers/budget.py, /home/agent/.hermes/worktrees/FinanceManager-sprint17b-review-corrections-responsive/src/jarvis_finance/services/household_import.py]