## PR2 frontend/UX architecture plan **Baseline inspected:** `47d9ab1d2d66d939dc33ddb3df6e93ce5077669e` on `sprint17b/household-cockpit`; worktree clean. ### Key findings from the current code - `/household` is currently a KPI/status page only: `frontend/src/pages/HouseholdOverviewPage.vue`. - The overview API already returns a useful monthly category breakdown, but the frontend type omits it: - Producer: `src/jarvis_finance/services/household_import.py:2101-2155` - Category shape: `src/jarvis_finance/services/household_financials.py:216-269` - Missing in `HouseholdOverview`: `frontend/src/api/household.ts:9-21` - Cursor pagination already exists and is snapshot-bound: - Route: `src/jarvis_finance/api/routers/budget.py:263-291` - Service: `src/jarvis_finance/services/household_import.py:2220-2385` - It provides `total_all`, `filtered_total`, `page_count`, `has_more`, `next_cursor`, and rejects stale cursors with `409`. - The reusable audited transaction drawer already exists: - `frontend/src/components/household/TransactionDetailDrawer.vue` - It includes focus trapping, focus return, inert background, preview/confirm category correction, and special-workflow exclusions. - URL handling is fragmented: - Overview and transaction pages directly manipulate `window.history`. - Analytics uses the large `budgetFilterSchema` in `useQueryFilters.ts`. - `HouseholdTabs.vue` uses hard-coded paths and drops the active period/filter state. - Existing legacy redirects preserve `category_id`, but `HouseholdTransactionsPage.vue` reads only `category`; therefore some existing drilldowns silently arrive unfiltered. - Existing Chart.js surfaces lack a consistent accessible alternative. `ChartPanel.vue` is only a visual card wrapper. - `CategoryAnalysisPage.vue` uses clickable `
` rows, keeps selection only in component state, and drills into the old Data Explorer instead of the canonical household ledger. - Exact rendered viewport tests do not exist; there is no Playwright setup in the repository. --- ## Proposed architecture ### 1. Establish one canonical household URL contract Add a **page-specific schema**, rather than reusing the oversized and alias-duplicating `budgetFilterSchema`: ```text month=YYYY-MM date_from=YYYY-MM-DD date_to=YYYY-MM-DD account_id= type=income|expense|transfer|refund category_id= merchant= source_type= review=reviewed|open|ignored transaction= ``` Compatibility aliases: - `period` → `month` - `account` → `account_id` - `category` → `category_id` - `source` → `source_type` Implementation: - `frontend/src/composables/useQueryFilters.ts` - Export a narrow `householdFilterSchema`. - Preserve unrelated valid query state when changing one filter. - Do not write both canonical keys and aliases. - Add `frontend/src/navigation/householdRoutes.ts` - Central helpers for overview, transaction, and category drilldown route objects. - Translate overview `month` to the transaction endpoint’s current `period` only at the API boundary. - `frontend/src/components/household/HouseholdTabs.vue` - Generate `RouterLink` route objects that carry the selected month. - Drop page-private state such as `transaction` when changing tabs. - Replace direct `window.history` usage in: - `HouseholdOverviewPage.vue` - `HouseholdTransactionsPage.vue` with `useRoute`, `useRouter`, and `useQueryFilters`. This makes refresh, sharing, back/forward, redirects, and cross-page drilldowns deterministic. ### 2. Build the household overview from the existing truthful contract Refactor `frontend/src/pages/HouseholdOverviewPage.vue` into this hierarchy: 1. Header + month control 2. Data-quality warning/status 3. KPI row using shared `KpiCard.vue` 4. Main visual row: - **Monthly cashflow:** income, expense, balance bar chart - **Expense categories:** top categories plus “Andere” doughnut 5. Accessible category ranking/drilldown list 6. Review/import next-step cards Use only `getHouseholdOverview(month)` for PR2; it already returns reconciled financial semantics and categories. Avoid introducing parallel calculations through the older analytics APIs. Extend `HouseholdOverview` in `frontend/src/api/household.ts` with the fields already emitted by the backend: ```ts neutral_transfer_count: number unavailable_chf_count: number unlinked_refund_count: number semantics_version: string categories: Array<{ category_id: string category_name: string category_type: string income_chf: string expense_chf: string transaction_count: number }> ``` Derive Top-N + “Andere” in the frontend without changing totals. Never chart unavailable CHF values as zero. Every category row should be a real `RouterLink` to: ```text /household/transactions?month=2026-05&type=expense&category_id= ``` ### 3. Make Chart.js usage shared and accessible Enhance `frontend/src/components/ui/ChartPanel.vue` rather than creating page-local chart wrappers: - Render as a labelled `
`. - Generate stable title/description IDs. - Add an explicit `alternative` slot below the canvas. - The alternative must always contain the same values as semantic HTML—not tooltip-only content. - Mark decorative swatches `aria-hidden`. - Do not make canvas clicking the only drilldown mechanism. For the overview: - The cashflow chart alternative is a compact `
` containing income, expense, and balance. - The category chart alternative is an ordered list or table containing category, amount, share, transaction count, and drilldown link. For `CategoryAnalysisPage.vue`, reuse the same pattern and replace clickable `
` entries with buttons/links. ### 4. Canonical category detail and drilldown Update `frontend/src/pages/CategoryAnalysisPage.vue`: - Bind selected category to `category_id` in the URL. - Make the visible ranking keyboard-operable. - Show a selected-category summary with: - amount, - percentage, - transaction count, - period and flow type, - canonical “Transaktionen öffnen” link. - Replace `/planning/budget/analysis/data-explorer?...` drilldowns with `/household/transactions?...`. - Keep chart interaction optional; selecting through the semantic list must provide the same result. Bounded integration in `frontend/src/pages/BudgetStatusPage.vue`: - Replace legacy unpaginated `getBudgetTransactions(...)` for category detail with the household paginated contract where practical. - Pass exact `category_id` and selected month. - Retain `TransactionDetailDrawer`. - Add “Weitere Buchungen laden” inside the category drawer when `has_more`. - Ensure closing the nested transaction drawer restores focus to its transaction row while leaving the category drawer modal and background-inert state intact. Do not rewrite the entire budget-status page in PR2. ### 5. Finish the transaction ledger/drawer integration Update `frontend/src/pages/HouseholdTransactionsPage.vue`: - Use canonical URL filters and exact IDs. - Render transaction cards as native `