## Ergebnis: Sprint-20E-Gap-Map ### Aktueller Statuspfad - **Scheduler/Job:** `finance-manager-market-valuation.timer` → CLI `run-daily-market-valuation` - `deploy/systemd/finance-manager-market-valuation.{timer,service}` - `src/jarvis_finance/cli/main.py:185-282` - orchestriert isoliert **PostFinance, Krypto und True Wealth**. - **Read-only API:** `GET /api/portfolio/performance/daily-job` - Route: `src/jarvis_finance/api/routers/overview.py:232-235` - Projektion: `src/jarvis_finance/services/performance_activation.py:34-78` - Schema: `src/jarvis_finance/api/schemas/performance_activation.py:101-117` - **Frontend-Client:** `frontend/src/api/portfolio.ts:37,160` - **Aktuelle Anzeige:** ausschließlich unter `/portfolio/data` - `frontend/src/components/portfolio-data/DataIngestionReconciliationPanel.vue:25-34` - **Performance-Seite:** `/portfolio/performance` - `PortfolioPerformancePanel.vue` lädt Performance, Coverage, Wealth, Setup und True-Wealth-Modell, aber **nicht den Daily-Job-Status**. - **Expliziter Providerpfad:** Crypto-Seite besitzt bereits eine nutzerinitiierte POST-Aktion: - `POST /api/market/crypto/update-live-stats` - `frontend/src/pages/CryptoPage.vue:18,137` - durch den zentralen Write-Mode standardmäßig gesperrt; sie aktualisiert jedoch nur Marktdaten, nicht zwingend den Performance-Tagesabschluss. ## Präzise Gap-Map | Sprint-20E-Anforderung | Ist | Gap / Risiko | Priorität | |---|---|---|---| | Status `aktiv/pausiert/teilweise/fehlgeschlagen` | Nur `enabled` und roher letzter Runstatus (`complete`, `partial`, `never_run`, ggf. `failed`) | Kein kanonischer Gesamtstatus, keine definierte Aggregationslogik oder UI-Lokalisierung | **P0** | | Status auf Performance-Seite | Jobkarte liegt unter „Daten & Diagnose“ | Nutzer sieht Betriebszustand nicht dort, wo Performance konsumiert wird | **P0** | | Letzter Schweizer Lauf | API hat `last_run_at`; UI blendet es aus und zeigt nur `last_confirmed_date` | Kein sichtbarer Laufzeitpunkt in `Europe/Zurich`; bestätigter Bewertungstag und technischer Lauf werden vermischt | **P0** | | Nächster Lauf | Vorhanden, wenn Env-Flag aktiv | Rein aus aktueller Uhrzeit und Env-Flag berechnet; tatsächlicher systemd-Timerzustand wird nicht geprüft | **P1** | | Preisalter | Nicht im Daily-Job-Contract | Weder Preisstichtag noch Alter/Kategorie sichtbar | **P0** | | Bewertete/fehlende Assets | DB-Run enthält `price_total` und `missing_instruments_json`, API fragt sie nicht ab | UI kann Krypto-Readiness nicht beziffern; `price_stored` wäre zudem nicht „bewertet“, sondern nur „neu geschrieben“ | **P0** | | Nächste Aktion | Setup-API hat `next_action`, Daily-Job-API nicht | Betriebsstatus und konkrete Behebung sind nicht zusammengeführt | **P0** | | Krypto-Performance-Readiness | Coverage/Setup zeigt grob `ready/review_inputs/not_ready`; Daily-Run enthält Gründe | Keine geschlossene Projektion aus Aktivierung, Holdings, Preisen, Tagesbewertung und nächster Aktion | **P0** | | True-Wealth-Vorbereitung | Performance-Setup, Cashflow-Confirm, Modellpreview und Performance-View existieren | Daily-CLI verarbeitet True Wealth, aber Daily-Job-Status listet **nur PostFinance und Krypto** | **P0** | | Kein Provider beim Render | Daily-Job-GET und vorhandene Performance-GETs lesen lokal | Invariant ist im Backend gegeben; es fehlt ein fokussierter Sprint-20E-Regressionstest über den vollständigen Renderpfad | **P1** | | Abgesicherte Aktualisieren-Aktion | Crypto-Live-Stats-POST existiert und ist write-mode-geschützt | Aktion ist von Performance getrennt, erzeugt keinen garantierten Tageswert und besitzt keinen Preview-/Confirm-Vertrag | **P1 / optional** | | Browser-/Responsive-Abdeckung | Historische manuelle UATs 1440/820/390; aktuelle Tests sind Vitest/jsdom | Kein eingechecktes Playwright-/Browser-Gate für Statusvarianten, Overflow, Touchziele oder Netzwerk-Allowlist | **P1** | | Refresh-Verhalten | Performance-Header remountet Panel über einen Key | `onMounted(load())` nutzt danach weiterhin Session-Cache (`refresh=false`); sichtbares „Aktualisieren“ kann ohne neuen GET enden | **P0** | | Fehlertoleranz | Performance-Daten werden weitgehend zusammen geladen | Ein zusätzlicher Statusrequest darf die bestehende Performance nicht blockieren; separate Fehler-/Stale-Darstellung erforderlich | **P1** | ### Weitere Modellschwächen - `DailyValuationSourceStatus.status` und `reason_codes` sind unbeschränktes `str`; unbekannte Werte können ungeprüft bis ins UI gelangen. - `configured=True` ist hart codiert und bedeutet nicht, dass der Timer tatsächlich installiert/aktiv ist. - `last_confirmed_date` wird aus irgendeinem letzten `complete` Run gelesen, unabhängig davon, ob die aktuellste Ausführung teilweise oder fehlgeschlagen war. Das ist korrekt als letzter guter Stand, benötigt aber zusätzlich einen sichtbaren **letzten Versuch**. - True-Wealth-Modellpreview ist bereits sicher geschlossen: keine Positionen, Providerzeilen oder Rohprovenienz (`public_truewealth_model_preview`, `truewealth_valuation.py:280-293`). Das eignet sich als Grundlage für die Vorbereitungskarte. ## Minimaler Änderungsvorschlag ### 1. Bestehenden Daily-Job-Contract additiv erweitern In `performance_activation.py` und dem Pydantic-/TypeScript-Modell: - Gesamtfelder: - `status: active | paused | partial | failed` - `last_run_at`, optional zusätzlich `last_run_at_ch` - `next_run` - `next_action` - Je Quelle: - `source: postfinance | crypto | truewealth` - `last_attempt_at` - `last_confirmed_date` - `price_as_of` - `price_age_seconds` oder stabilere Kategorie `fresh | stale | missing` - `assets_total` - `assets_valued` - `assets_missing` - geschlossenes `status` - nutzerverständliche `next_action` - technische `reason_codes` nur eingeklappt. Empfohlene Gesamtstatuslogik: 1. `paused`, wenn Ausführung nicht aktiviert ist. 2. `failed`, wenn aktiviert und der jüngste Gesamt-/Quellenlauf hart fehlgeschlagen ist, ohne brauchbares Teilergebnis. 3. `partial`, wenn aktiviert, aber mindestens eine aktivierte Quelle teilweise, blockiert oder veraltet ist. 4. `active`, wenn aktiviert und alle aktivierten Quellen den erwarteten Status erfüllen. Keine neue Tabelle nötig: `market_data_runs`, Scope/Coverage, lokale Preiszeilen und bestehende True-Wealth-Projektionen reichen aus. ### 2. True Wealth in die Statusprojektion aufnehmen - Modellierter Lauf: `truewealth_modelled_daily`. - Vorbereitung aus: - Source-Aktivierung, - bestätigtem Anker, - Cashflow-Coverage, - lokalem Modellpreview, - letztem modellierten/Bestätigungswert. - Fehlende Markt-/FX-Daten nur als Zähler/Kategorie und nutzerverständliche nächste Aktion ausgeben; keine Provider- oder Instrumentdetails. ### 3. Statuskarte auf `/portfolio/performance` - Kompakte Karte oberhalb von Zeitraum/KPIs: - Ampelstatus, - letzter Schweizer Lauf, - nächster Lauf, - Preisalter, - `x/y Assets bewertet`, - nächste Aktion. - Quellen darunter responsiv als 1/2/3 Spalten; Reason-Codes nur unter „Technische Details“. - Bestehende ausführliche Karte unter `/portfolio/data` entweder auf dieselbe Komponente umstellen oder dort entfernen, um zwei divergierende Projektionen zu vermeiden. - Daily-Status separat laden: Fehler darf vorhandene Performancewerte nicht verdecken. ### 4. Refresh korrigieren - Parent-Refresh als Prop/Generation an das bestehende Panel geben und tatsächlich `load(true)` auslösen. - „Ansicht aktualisieren“ bleibt **reiner lokaler GET-Refresh**, ohne Provider. - Optionaler „Bewertung jetzt aktualisieren“-Button nur separat: - POST, - standardmäßig durch Write-Mode gesperrt, - Loopback/local-only, - explizite Bestätigung, - isoliert nur Krypto, - auditiert, - anschließend Status-GET. - Nicht den existierenden Live-Stats-Button als Performance-Refresh umetikettieren. ## Minimaler Testplan ### Backend 1. Contracttests für alle vier Gesamtzustände. 2. Jüngster Lauf `partial/failed`, aber älterer bestätigter Stand bleibt separat sichtbar. 3. Krypto: - alle Assets bewertet, - einige Preise fehlen, - Preise veraltet, - Scope/Aktivierung fehlt, - `assets_valued + assets_missing == assets_total`. 4. True Wealth: - Anker fehlt, - Cashflow-Coverage fehlt, - Preis/FX fehlt, - Modell bereit/provisorisch. 5. GET bleibt strikt read-only: `conn.total_changes` unverändert und Provider-Mocks werden nicht aufgerufen. 6. Schweizer DST-Fälle für letzten/nächsten Lauf. 7. Optionaler POST: disabled/remote `403`, local-only erlaubt, Audit/Idempotenz und kein Überschreiben des letzten bestätigten Werts bei Providerfehler. ### Frontend/Vitest - Exakte Labels für aktiv, pausiert, teilweise, fehlgeschlagen. - Letzter Lauf und nächster Lauf in Schweizer Zeitzone. - Preisalter sowie `bewertet/fehlend`. - Nächste Aktion sichtbar; Reason-Codes standardmäßig verborgen. - Statusfehler lässt bestehende Performance-KPIs sichtbar. - Header-Refresh erzeugt neue GETs mit Cache-Bypass. - Kein Provideraufruf bei Mount oder normalem Refresh. - Optionaler Aktualisieren-Button zeigt 403 verständlich und startet nie automatisch. ### Responsive Browser-UAT Mit synthetischer API bei: - `1440×900` - `1024×768` beziehungsweise bestehende `820px`-Zwischenbreite - `390×844` - zusätzlich 200 % Textzoom Prüfen: - kein horizontaler Overflow; - Status, Laufzeiten, Assetzahlen und nächste Aktion ohne Abschneiden; - mindestens 44 px hohe Buttons/Details-Summaries; - sinnvolle Reihenfolge und Fokusführung; - Status nicht nur farblich codiert; - keine Console-Fehler; - Netzwerk-Allowlist beim Render ausschließlich lokale GET-Pfade, kein `/market/*update*` und keine externen Requests. ## Verifikation und Arbeitsstand - Basis/HEAD bestätigt: `9667b08a14a04feccc2d8089b374af7eb4046d2b`. - Relevante Python-Dateien erfolgreich mit `compileall` geprüft. - `git diff --check` erfolgreich; Worktree blieb sauber. - Fokustests konnten nicht ausgeführt werden: - Repository-`.venv` fehlt; - aktives Python enthält kein `pytest`; - Frontend-`node_modules` fehlt, daher sind `vite` und `@vitejs/plugin-vue` nicht auflösbar. - **Keine Dateien erstellt oder geändert; keine Produktionsdaten geöffnet.**