## 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 `