## Sprint 20D implementation map ### Current vertical slice - Route: `frontend/src/router/index.ts` - `/portfolio/performance` → `PortfolioPage` with `section: 'performance'`. - Stale `/analysis/performance` roadmap route still says performance is not built. - Page host: `frontend/src/pages/PortfolioPage.vue` - Mounts `PortfolioPerformancePanel`. - Refresh works by remounting the panel. - Main UI: `frontend/src/components/performance/PortfolioPerformancePanel.vue` - Currently combines filters, five KPIs, setup workflow, True Wealth write flow, diagnostics, and a hand-built SVG chart in one 163-line component. - Loads four endpoints concurrently: coverage, performance, wealth cockpit, and setup. - Frontend API/types: `frontend/src/api/portfolio.ts` - Backend routes: `src/jarvis_finance/api/routers/overview.py` - `GET /api/portfolio/performance` - `GET /api/portfolio/performance/coverage` - `GET /api/portfolio/performance/setup` - Existing guarded True Wealth preview/confirm endpoints. - Calculation/contracts: - `src/jarvis_finance/services/portfolio_performance.py` - `src/jarvis_finance/services/performance_hardening.py` - `src/jarvis_finance/api/schemas/portfolio_performance.py` - `src/jarvis_finance/api/schemas/performance_activation.py` ## Recommended vertical implementation ### 1. Backend: make the performance response display-ready **Files** - `src/jarvis_finance/services/portfolio_performance.py` - `src/jarvis_finance/api/schemas/portfolio_performance.py` - `src/jarvis_finance/api/routers/overview.py` **Contract additions** Preserve the requested `period`, but add: ```json { "period": {"from": "2026-01-01", "to": "2026-08-03"}, "actual_period": { "from": "2026-06-30", "to": "2026-07-27", "fallback_applied": true }, "readiness": { "status": "complete|partial|unavailable", "scope": "portfolio|postfinance|truewealth|crypto", "summary": "string", "next_action": "string|null" } } ``` Add an explicit query such as `period_mode=available` rather than silently changing existing exact-boundary behavior. The backend should: 1. Resolve the selected scope’s stored valuation bounds. 2. Intersect them with the requested period. 3. Verify cashflow coverage for that actual interval. 4. Re-run the canonical engine using the resolved boundaries. 5. Return requested and actual periods in the same response. This avoids a frontend race where coverage is fetched first and then used to formulate a second performance request. **Source-independent readiness** - Derive readiness solely from the requested `scope` and its canonical accounts. - Do not require all three sources to be ready before showing an independently valid PostFinance, True Wealth, or crypto result. - For portfolio scope, aggregate readiness remains fail-closed if mandatory scope coverage is incomplete. - Remove frontend dependence on `setupProgress === 3` for chart visibility. ### 2. Backend: explicit chart semantics The current `time_series` loses provenance after aggregation, while the UI renders every point identically. Add a typed chart projection, for example: ```json { "chart": { "valuation_points": [ { "at": "2026-06-30", "value_chf": "150000.00", "valuation_kind": "official|modelled|mixed", "anchor_kind": "opening|closing|cashflow|null" } ], "cashflow_events": [ { "at": "2026-07-10", "amount_chf": "1000.00", "direction": "deposit|withdrawal" } ] } } ``` Implementation belongs in `portfolio_performance.py`, close to `_aggregate_account_valuations`, while source classification must be a backend allowlist—not inferred from source-name prefixes in Vue. Preserve existing `time_series`, `ttwror_series`, and `external_cashflows` for compatibility. Required visual semantics: - Official valuation: solid line/point. - Modelled daily close: dashed line/point. - Mixed aggregate: distinct dash/marker and legend entry. - External flow: directional event marker, not a second cumulative line that can be mistaken for portfolio value. - Opening/closing/cashflow valuation anchors: explicit markers. - Do not expose raw snapshot IDs, fingerprints, or provider source strings in the normal chart contract. ### 3. Frontend: split the monolithic panel **Primary files** - Refactor `frontend/src/components/performance/PortfolioPerformancePanel.vue` - Add focused components under `frontend/src/components/performance/`, suggested: - `PerformancePeriodToolbar.vue` - `PerformanceKpiGrid.vue` - `PerformanceChart.vue` - `PerformanceReadiness.vue` - `PerformanceSetupCard.vue` - `TrueWealthCashflowSetup.vue` Keep `PortfolioPerformancePanel` as orchestration only. **Professional KPI set** Replace the current mixture of current wealth and period performance with six period-consistent KPIs: 1. Anfangswert 2. Endwert 3. Netto externe Mittel 4. Anlageergebnis 5. TTWROR 6. XIRR Use existing `KpiCard.vue` or a compact performance variant. Every unavailable value stays “Noch nicht beurteilbar”; never render zero as fallback. Show the actual period once above the grid rather than repeating dates in every card. Remove `getWealthCockpit()` from this page. It currently supplies “Aktueller erfasster Wert” and “Anlagevermögen,” but those values are not necessarily aligned with the selected performance scope or period. ### 4. Compact setup and structured diagnostics **Backend** - Give `/portfolio/performance/setup` an explicit Pydantic response schema; it currently has no `response_model`. - Replace arbitrary `Record` as the UI-facing diagnostics boundary with structured fields such as: - status - summary - next action - available metrics - opening/closing anchors - cashflow coverage - Raw reclassification rows, reason codes, fingerprints, and component provenance may remain technical API data, but should not be the normal UI projection. **Frontend** - Show one compact setup summary, scoped to the selected source. - For portfolio scope, show a three-row readiness summary, not three large nested accordions. - Move the True Wealth cashflow form behind an explicit “Cashflows einrichten” disclosure/action. - Remove `formatDiagnostics()` and the raw JSON `
`.
- Render allowlisted diagnostic rows with human labels; link technical investigation to `/portfolio/data`.
- Never display raw reason codes in the normal view. Unknown codes should map to a neutral generic message, not leak the identifier.

Preserve the existing Preview → explicit Confirm → Audit workflow unchanged.

### 5. Request lifecycle and local-only rendering

In `PortfolioPerformancePanel.vue`:

- Add request generation or `AbortController` handling so rapid scope/period changes cannot let stale responses replace newer state.
- Fetch only local stored-data endpoints needed for the selected display.
- Keep refresh explicit.
- Do not call daily-job, provider, quote, FX, market update, backfill, or activation-confirm routes during render.

The backend path is currently DB-only, but add a regression test proving render endpoints do not invoke provider/market services.

## Test map

### Frontend unit/component tests

Extend:

- `frontend/src/components/performance/PortfolioPerformancePanel.test.ts`
  - Six standard KPIs.
  - Requested versus actual period fallback notice.
  - Independently complete source displays despite other sources being unavailable.
  - Portfolio remains unavailable when aggregate scope is incomplete.
  - No `reason_codes`, fingerprints, engine versions, or JSON dump in normal DOM.
  - No `getWealthCockpit()` call.
  - Stale response suppression.
  - Compact setup: one summary surface; source detail appears only on user action.
- Add `PerformanceChart.test.ts`
  - Official/modelled/mixed styles and legend.
  - Flow and anchor markers.
  - Empty/partial series behavior.
  - Accessible textual/table fallback.
- Extend `frontend/src/pages/PortfolioPage.test.ts`
  - Performance route host and refresh behavior.
- Extend navigation tests:
  - `frontend/src/navigation/UserNavigationSmoke.test.ts`
  - Reconcile/remove stale `/analysis/performance` roadmap route.

### Backend tests

Add a focused `tests/unit/test_sprint20d_professional_performance_page.py`, covering:

- Requested period outside available bounds resolves deterministic `actual_period`.
- No fallback when exact boundaries exist.
- No fabricated period when fewer than two valid anchors exist.
- Cashflow coverage is checked against the actual interval.
- PostFinance can be ready while True Wealth/crypto are not.
- Portfolio scope remains fail-closed.
- Chart points classify official/modelled/mixed provenance correctly.
- Opening, closing, and cashflow anchors are deterministic.
- No provider/market/FX client calls from performance, coverage, or setup GETs.
- Setup and performance responses validate against strict Pydantic schemas.

Retain regression coverage in:

- `tests/unit/test_sprint14_performance_contract.py`
- `tests/unit/test_sprint20c_performance_activation_hardening.py`
- `tests/unit/test_sprint20b_performance_activation_daily_valuations.py`

### Responsive/browser acceptance

Exercise exact widths `1440`, `820`, and `390`:

- No horizontal overflow.
- Filter controls remain labelled and at least 44px high.
- KPI grid: 6 columns/compact desktop, 2 columns tablet, 1–2 columns mobile.
- Setup workflow does not dominate initial viewport.
- Chart legend wraps without clipping.
- Actual-period fallback remains visible at 390px.
- No console errors or external/provider requests.
- Keyboard-only access to period controls, setup disclosure, chart fallback, preview, and confirm.

## UX and accessibility risks

- The existing SVG has only a generic `aria-label`; it provides no values, dates, flow events, or official/modelled distinction. Add a concise summary and accessible data table.
- Color alone cannot communicate provenance; pair color with dash patterns, marker shapes, and text legend.
- Native `
` is acceptable, but focus restoration and expanded state should be tested after async setup updates. - The current date preset uses module-load `new Date()` and UTC conversion; test local-date boundaries and midnight rollover. - Current concurrent loads lack stale-response protection. - The nested True Wealth form is extremely tall on 390px and obscures the primary performance result. - “Bereit zur Aktivierung” is misleading when a source already has independently calculable metrics; use separate labels for “Performance verfügbar” and “Einrichtung vollständig.” - `AvailabilityState` action buttons use only `py-1.5`, potentially below the 44px target. - The stale `/analysis/performance` roadmap route risks contradictory navigation and direct-link behavior. ## Repository state - Inspected exact base `107de783afac36bf48c95a996fc920aec2a46270`. - No files created or modified. - Existing untracked `.venv` was present before inspection.