## Outcome Reviewed baseline `572ef44fc330911673ce190b7abfbbf210bff77c` read-only. The current contracts already support verified TTWROR/XIRR and a True Wealth confirmed/modelled series, but the default wealth history is confirmed-only and the UI conflates “complete,” “confirmed,” and “verified.” Correction markers and source/KPI sparklines are not represented. ## Current contract/UI map - `frontend/src/pages/PortfolioPage.vue:29-42` - Four separate sections: overview, performance, data/diagnostics, strategy. - Overview mounts only `WealthCockpitPanel`; verified performance is a separate route. - Refresh keys are independent; confirmed imports increment both. - `frontend/src/components/wealth/WealthCockpitPanel.vue` - Fetches `getWealthCockpit(period)` (`:163-196`). - Period vocabulary: `ytd | previous_year | 12m | all` (`:10-16`, API type `portfolio.ts:139`). - Default history is `cockpit.history.points` and a single SVG polyline (`:39-48`, `:193`). - Source badges/statuses come from readiness, freshness, reconciliation, performance, value basis and import coverage (`:92-121`). - Diagnostics are already collapsed (`:131-155`). - No source sparklines, correction markers, axes, point labels, or explicit per-point evidence basis. - Despite the copy saying gaps are not supplemented, sparse dates are connected continuously by one polyline. - `frontend/src/components/performance/PortfolioPerformancePanel.vue` - Eagerly loads coverage, performance, wealth, setup, scheduler status, and True Wealth projections (`:171-230`). - Period vocabulary differs: `ytd | 1y | 3y | 5y | all | custom` (`:23-30`, `:178`, `:213`). - KPI row exposes current value, contributions, result, XIRR, TTWROR and quality (`:144-151`). - True Wealth alone uses explicit confirmed/modelled points from `TrueWealthPerformanceView` (`:133-142`, `:228`). - General `PortfolioPerformance.time_series` is forcibly treated as confirmed in chart construction; model provenance is lost (`:228`). - `ttwror_series` exists in the API but is not rendered. - XIRR is only scalar. - The chart has cashflow and anchor markers but no correction marker (`:153-164`). - `canShowSeries` unnecessarily requires both TTWROR and XIRR complete for non-True-Wealth valuation charts (`:191-199`). - API client: `frontend/src/api/portfolio.ts` - Wealth contract: `WealthCockpit`, `WealthSourceRow`, `WealthDiagnostic` (`:85-157`). - Verified performance: `PortfolioPerformance`, `PerformanceCoverage` (`:11-36`). - True Wealth model contract: `TrueWealthPerformanceView`, `TrueWealthModelPreview` (`:80-81`). - Endpoints: - `getWealthCockpit` (`:181`) - coverage/performance (`:186-190`, `:202-207`) - True Wealth view/model (`:194-195`). - Backend: - Routes and response models: `src/jarvis_finance/api/routers/overview.py:130-146,217-229,303-328,474-524`. - Wealth schema: `api/schemas/wealth_cockpit.py:12-127`. - Performance schema: `api/schemas/portfolio_performance.py:8-142`. - Wealth history intentionally uses exact common stored dates, without interpolation: `services/wealth_cockpit.py:_household_history`, especially `:264-380`. - `build_wealth_cockpit` assembles history, six KPIs, readiness and diagnostics at `:1070-1264`. - `build_portfolio_performance` computes TTWROR/XIRR and emits `time_series`, `ttwror_series`, cashflows and quality at `services/portfolio_performance.py:852-1274`. - Immutable supersession exists and is cutoff-reproducible, but correction lineage is discarded before serialization; see `tests/unit/test_portfolio_performance_foundation.py:test_snapshot_versions_are_immutable_and_cutoff_reproducible`. ## Recommended additive contracts Add explicit facts rather than deriving “verified” from source-name strings: ```ts export type EvidenceBasis = 'confirmed' | 'modelled' export type SeriesQuality = 'complete' | 'partial' | 'unavailable' export type ModelledWealthPoint = { at: string value_chf: string basis: EvidenceBasis quality_status: Exclude anchor: boolean coverage: { included_sources: number expected_sources: number } } export type ValuationCorrectionMarker = { valuation_at: string recorded_at: string scope: 'portfolio' | 'postfinance' | 'truewealth' | 'crypto' kind: 'valuation_superseded' previous_value_chf: string replacement_value_chf: string } export type WealthDevelopment = { status: SeriesQuality points: ModelledWealthPoint[] correction_markers: ValuationCorrectionMarker[] reason_codes: string[] } ``` Add to `WealthCockpit`: ```ts development: WealthDevelopment ``` Optionally add source-level sparklines where aggregate development cannot be reused: ```ts // on WealthSourceRow trend_points: Array<{ at: string value_chf: string basis: EvidenceBasis quality_status: 'complete' | 'partial' }> ``` For verified performance, preserve legacy `time_series` and add an explicit versioned projection: ```ts export type VerifiedValuationPoint = { at: string value: string basis: EvidenceBasis anchor: boolean quality_status: 'complete' | 'partial' } export type ReturnSeriesPoint = { at: string value: string } export type PerformanceVerification = { ttwror: 'verified' | 'provisional' | 'unavailable' xirr: 'verified' | 'provisional' | 'unavailable' reason_codes: string[] } ``` Add to `PortfolioPerformance`: ```ts valuation_series_v2: VerifiedValuationPoint[] xirr_series: ReturnSeriesPoint[] // backend-computed rolling/endpoint XIRR correction_markers: ValuationCorrectionMarker[] verification: PerformanceVerification ``` Backend equivalents should be exact Pydantic models with `ConfigDict(extra="forbid")`, added to: - `api/schemas/wealth_cockpit.py` - `api/schemas/portfolio_performance.py` Do not call modelled values “verified.” A metric is verified only when its metric quality and period coverage are complete and its required valuation inputs are confirmed. Keep TTWROR cumulative and XIRR annualized labels explicit. ## Minimal Vue redesign 1. **Default overview** - Keep `WealthCockpitPanel` as the primary screen. - Render `cockpit.development` as “Modellierte Vermögensentwicklung,” with: - solid confirmed segments; - dashed modelled segments; - explicit `Modelliert`/`Bestätigt` badge; - cashflow and correction markers; - no return terminology. - Derive overview KPI sparklines from `development.points`; use `source.trend_points` for source cards. 2. **Collapsed verified area** - In `PortfolioPage.vue`, place an initially closed `
` below the standard cockpit: - summary: “Verifizierte Performance & Diagnose” - lazy-mount `PortfolioPerformancePanel` only after first expansion. - Keep `/portfolio/performance` as a direct expanded route for compatibility. - Add a small `embedded`/`compactHeader` prop to `PortfolioPerformancePanel` rather than duplicating it. 3. **Performance panel** - Use `verification.ttwror/xirr` for badges. - Render existing `ttwror_series`; render additive `xirr_series` as the metric sparklines. - Use `valuation_series_v2` for confirmed/modelled styling. - Render `correction_markers` separately from cashflows and anchors. - Decouple valuation-chart visibility from XIRR/TTWROR completeness; return metrics may remain unavailable while stored valuation points are still chartable. 4. **Shared period control** - Extract a small `PortfolioPeriodControl.vue`. - Canonical UI presets: `ytd | 1y | 3y | 5y | all | custom`. - Convert presets to explicit `from/to` client-side for performance. - Either extend the wealth endpoint to explicit `from/to`, or add `1y/3y/5y/custom` to its validated preset contract. Do not silently map `1y` to current `12m` while retaining two vocabularies. - Persist period in the URL so overview and expanded diagnostics use the same period. 5. **Laptop/iPad** - Change the performance filter from `lg:grid-cols-5` to a safer 12-column layout: scope/period 3 columns each, dates 3 each, apply button full-width or right-aligned below at tablet widths. - Keep the wealth history/distribution split at `xl`; it already stacks below 1280 px. - Fix `WealthCockpitPanel.vue:105`: `xl:grid-cols-4` is nested inside a one-third-width source card and produces four tiny columns. Use `sm:grid-cols-2` consistently. - Use horizontally scrollable segmented period controls only if each target remains at least 44 px. - Add explicit probes for 1366×768, 1440×900, iPad portrait 768×1024 and landscape 1024×768. ## Contract mismatch and semantic risks - Frontend marks backend-required performance fields optional: - `ttwror_version`, `xirr_version`, `attribution_version`, `ttwror_series`, `attribution` in `portfolio.ts:20-31`. - Backend requires them in `portfolio_performance.py:129-142`. - Frontend marks coverage classification/cashflow status optional (`portfolio.ts:35`), while backend requires both (`portfolio_performance.py:107-108`). - `WealthCockpitResponse` forbids top-level extras, so frontend-only `development` additions will be stripped/rejected unless schema and service change together. - Wealth nested `kpis`, `history`, `totals`, `policy` remain broad dictionaries (`wealth_cockpit.py:115-127`), allowing backend/frontend drift despite top-level validation. - “Bestätigt” currently maps directly from quality status `complete` (`PortfolioPerformancePanel.vue:212`); computational completeness is not evidence provenance. - Portfolio performance’s “Aktueller erfasster Wert” uses whole-household wealth, including bank cash, instead of investment closing value (`PortfolioPerformancePanel.vue:190`). This conflicts with the performance scope. - General performance chart labels all points confirmed (`:228`) even where modelled valuation sources may be present. - Sparse wealth observations are joined visually across calendar gaps (`WealthCockpitPanel.vue:193`). - Correction lineage is available in storage but not exposed after canonical valuation selection. - `all` chooses the earliest date across every coverage row, not the selected scope (`PortfolioPerformancePanel.vue:213`). - Client-clock preset boundaries and backend period boundaries can differ around timezone/day rollover. - There is no committed browser/layout suite; Vitest/jsdom cannot prove overflow, target sizes, or chart geometry. ## Tests to add/update - Backend: - `tests/unit/test_wealth_cockpit_v1.py` - ordered/unique/no-future development points; - partial coverage stays modelled, never confirmed; - correction markers respect `data_cutoff`; - exact Pydantic/OpenAPI fields. - `tests/unit/test_portfolio_performance_foundation.py` - `valuation_series_v2` provenance; - verified status fails closed for modelled/partial inputs; - deterministic `xirr_series`; - superseded valuation marker without exposing internal IDs. - Frontend: - `PortfolioPage.test.ts`: verified panel closed and unfetched initially; lazy load on expansion; direct performance route remains expanded. - `WealthCockpitPanel.test.ts`: confirmed/modelled badge and segment styling, source sparklines, correction marker, sparse-gap behavior. - `PortfolioPerformancePanel.test.ts`: separate verified/provisional badges, TTWROR/XIRR sparklines, corrections, valuation chart visible independently of return availability, shared period changes refresh all surfaces. - Add browser tests for the four target viewports, no horizontal overflow, 44 px controls, keyboard/ARIA details behavior and period persistence. ## Verification and workspace state - Frontend focused suite: **24 passed**. - Backend focused suite: **48 passed** using system Python. - Repository remained clean at the requested SHA. - No files created or modified. - The repository-local `.venv` was absent and the runtime venv lacked pytest; system `python3 -m pytest` succeeded.