## Ergebnis ### Hauptursache der konkurrierenden Explorer Im aktuellen Stand existieren drei getrennte Explorer-Implementierungen mit eigener Zuständigkeit: 1. **Alter Advanced Explorer auf Chart.js** - Markup in `scripts/health/dashboard_v5/render.py:128–145` - Steuerung und Rendering in `dashboard-v5.js`, insbesondere: - `explorerMode`, `explorerCadence` - `explorerSeries()` - `chartFor("explorer-chart", …)` - `updateExplorer()` - `renderExplorerPanels()` - `renderExplorerScatter()` - `renderExplorerPicker()` - Verwendet ausschließlich das eingebettete Bundle und dessen kleine Registry. 2. **Produktiver API-/ECharts-Explorer aus Sprint 6C** - Zusätzliches Markup in `render.py:136` - Client: `dashboard-v5-api-explorer.js` - Wird durch `explorer_6c` aktiviert; `--health-record-6e` aktiviert über `health_dashboard_v5.generate()` indirekt auch 6D und 6C. - Besitzt eigene Metriksuche, Auswahl, Von/Bis-Felder, 7/30/90-Presets, ECharts, Tabelle und Ereignisse. 3. **Synthetischer ECharts-Prototyp aus Sprint 6A** - `render.py:41–60`, `dashboard-v5-echarts-prototype.js` - Eigene Reihen-, Zoom-, Pan- und Brush-Steuerung. - Zwar default-off und CLI-seitig isoliert, aber `render()` kann technisch gleichzeitig `echarts_prototype=True` und `explorer_6c=True` erhalten. Dann würden ECharts-Asset und mehrere Explorerflächen doppelt ausgegeben. Damit zeigt das aktuelle 6E-Profil im Explorer gleichzeitig den alten Chart.js-Arbeitsplatz und den API-/ECharts-Explorer. Der 6C-Explorer ist zudem an Controls des alten Explorers gekoppelt: - `dashboard-v5-api-explorer.js:setSelectedModeFromMainUI()` - Listener auf `[data-explorer-mode="raw"]` und `[data-explorer-mode="baseline"]` Der alte Explorer kann daher nicht nur per CSS versteckt werden, ohne Rohwert-/Baseline-Verhalten des API-Explorers mitzunehmen. --- ## Ursache der konkurrierenden Zeitraumsteuerungen Es gibt derzeit mindestens drei verschiedene Zeitraumbegriffe: - **Globales Bundle-Fenster** - `render.py:28–31`: 7/30/90/180/Alle - `dashboard-v5.js:PERIODS`, `activePeriod`, `periodData()`, `selectPeriod()` - Filtert nur bereits eingebettete Bundle-Reihen. - Schreibt keinen Zeitraum in die URL. - **API-Explorer-Fenster** - `render.py:136`: Von/Bis und nur 7/30/90 - `dashboard-v5-api-explorer.js:defaultWindow()`, `setRangeByDays()`, `rangeSuffix()` - Steuert ausschließlich `/api/v1/series` und `/api/v1/events`. - Globaler 7/30/90/180/Alle-Klick aktualisiert diese Felder oder API-Requests nicht. - **ECharts-Viewport-Zoom** - `renderSeriesControls()` mit „Zoom 90%“ und „Zoom All“ - Verändert nur `dataZoom`, nicht den fachlichen Datenzeitraum. Zusätzlich konkurrieren die URL-Router: - `dashboard-v5-day-controller.js:deepLinkFromLocation()` akzeptiert nur `view,date`. - `openDay()` und `restorePrevious()` setzen `url.search = ''`. - `dashboard-v5-record.js:route()` akzeptiert nur `view,tab,document`. - `setRoute()` setzt ebenfalls `url.search = ''`. Ein zukünftiger zentraler `period/from/to`-Vertrag würde deshalb beim Tages- oder Aktenwechsel gelöscht beziehungsweise als ungültiger Deep Link behandelt. --- # Minimaler Zielzustand ## Genau ein sichtbarer Explorer **Behalten:** `dashboard-v5-api-explorer.js` als einziger produktiver Explorer. Begründung: - bereits ECharts 6.1.0 lokal, - freigegebener API-Metrikkatalog statt nur sechs Bundle-Metriken, - Laborreferenzen, Ereignisse, Missingness und Tabellenfallback vorhanden, - Browser-Session und Same-Origin-API bereits abgesichert. **Entfernen aus der sichtbaren Produktionsoberfläche:** - Chart.js-Explorer-Controls, - `#explorer-chart`, - Scatter-, Lag-, Phasen- und Korrelationspanels aus dem alten Bundle-Explorer. Chart.js selbst bleibt für Cockpit und Ernährung weiterhin geladen. Der synthetische Prototyp bleibt ein isoliertes 6A-Testprofil und darf nie zusammen mit dem produktiven API-Explorer gerendert werden. --- # Konkrete minimale Änderungen ## 1. `scripts/health/dashboard_v5/render.py` ### `render()` - Den Abschnitt `view-explorer` auf **eine** ECharts-Shell reduzieren. - Das heutige `data-api-explorer`-Element zum primären Explorer machen, beispielsweise mit eindeutigem Hook: - `data-echarts-explorer` - `data-api-explorer-enabled` - Folgende alte sichtbare Blöcke entfernen: - `.explorer-controls` mit Bundle-Metrikpicker und Kadenz - `#explorer-chart` - `#explorer-lag-heatmap` - `#explorer-scatter` - `#explorer-phase` - `#correlation-list` - Rohwert-/Baseline-Schalter in die API-ECharts-Shell verschieben, weil `dashboard-v5-api-explorer.js` sie aktuell aus dem alten Explorer liest. - Lokale Von/Bis- und 7/30/90-Controls aus der API-Shell entfernen; sie werden global zentral bereitgestellt. - Die Metriksuche und katalogbasierte Auswahl behalten. „Freie Metrikauswahl“ bedeutet dabei frei aus dem freigegebenen Katalog; das bestehende Zwei-Reihen-/Einheitenlimit kann aus Sicherheits- und Achsengründen bestehen bleiben. ### Globale Zeitraumsteuerung Die heutige `.global-controls` um Folgendes ergänzen: - 7 - 30 - 90 - 180 - Alle - Benutzerdefiniert - zentrale Von-/Bis-Felder - zentraler Anwenden-Button - Status-/Validierungsausgabe Sinnvolle Hooks: - `data-dashboard-period="custom"` - `data-dashboard-range-from` - `data-dashboard-range-to` - `data-dashboard-range-apply` - `data-dashboard-range-status` ### Feature-Flag-Härtung In `render()` explizit verhindern, dass Prototyp und produktiver Explorer gleichzeitig aktiv sind, z. B. durch Validierung oder klare gegenseitige Auswahl. Außerdem ECharts nur einmal in die Scriptliste aufnehmen. --- ## 2. `dashboard-v5.js` Diese Datei sollte alleiniger Besitzer des fachlichen Zeitraums werden. ### Ersetzen/erweitern - `PERIODS` → `["7", "30", "90", "180", "all", "custom"]` - `activePeriod` durch ein Range-Objekt ergänzen, z. B.: ```js { period: "30", from: "2026-06-17", to: "2026-07-16" } ``` ### Neue zentrale Funktionen Konkrete Symbolsätze: - `parseRangeFromUrl()` - `resolveRange(period, from, to, today)` - `writeRangeToUrl(range, { replace })` - `applyRange(range, { history, notify })` - `rangeContainsDay(day)` - `notifyRangeChange()` `periodData()` sollte nicht mehr direkt `activePeriod` interpretieren, sondern die zentral aufgelösten Grenzen verwenden. ### URL-Vertrag Empfohlene kanonische Form: ```text ?period=7 ?period=30 ?period=90 ?period=180 ?period=all ?period=custom&from=2026-01-01&to=2026-07-16 ``` Regeln: - `period` exakt einmal und nur aus der Allowlist. - `from` und `to` nur bei `period=custom`. - Custom benötigt beide ISO-Tage. - `from <= to <= bundle.today`. - Presets berechnen inklusive Fenster anhand des serverautoritativen `bundle.today`. - `all` enthält kein `from/to`. - Ungültige oder doppelte Parameter fallen fail-closed auf 30 Tage zurück und werden per `replaceState` kanonisiert. - Bestehende Routingparameter wie `view`, `date`, `tab`, `document` bleiben erhalten. - Keine Metrik-IDs, Suchbegriffe oder Messwerte in der URL. Nach jeder Änderung: ```js window.dispatchEvent(new CustomEvent("health:range-change", { detail: { period, from, to } })); ``` Zusätzlich sollte der aktuelle Stand für spät geladene Clients lesbar sein, etwa über: ```js window.healthDashboardRange ``` ### Explorer-Abkopplung In `renderDomainView()` nicht mehr aufrufen: ```js renderExplorerPicker(); updateExplorer(); ``` Die alten Chart.js-Explorerfunktionen können anschließend gelöscht werden. Für die kleinste risikobegrenzte Verhaltensänderung genügt zunächst, sie unerreichbar zu machen; eine direkte Löschung ist aber sauberer, weil sonst zwei fachliche Implementierungen im Bundle verbleiben. ### History Einen `popstate`-Listener ergänzen, der den Zeitraum aus der URL wiederherstellt, globale Controls synchronisiert und genau ein `health:range-change` auslöst. --- ## 3. `dashboard-v5-api-explorer.js` ### Lokale Zeitraumhoheit entfernen Entfernen beziehungsweise nicht mehr verwenden: - `defaultWindow()` - `setRangeByDays()` - lokale `[data-api-explorer-preset]`-Bindings - lokales Apply-Binding - `rangeSuffix()` auf Basis eigener Formularfelder Stattdessen: - Initialwert aus `window.healthDashboardRange` - Listener auf `health:range-change` - ein Helper wie: ```js function rangeQuery(range) ``` - `refresh()` ausschließlich mit diesem zentralen Range-Objekt ausführen. ### Modus entkoppeln `setSelectedModeFromMainUI()` durch API-shell-eigene Controls ersetzen. Die Listener dürfen nicht mehr von Markup des alten Chart.js-Explorers abhängen. ### „Alle“ Für `period=all`: - `/api/v1/series` zunächst ohne `from/to` aufrufen. - Ereignisgrenzen anschließend aus den zurückgegebenen `coverage.from`/`coverage.to` der selektierten Reihen bilden. - Erst danach `/api/v1/events` mit expliziten Grenzen laden. Wichtige Vertragsgrenze: `read_api.py` begrenzt mit `MAX_RANGE_DAYS = 3660`. „Alle“ ist daher aktuell nur bis maximal zehn Jahre garantiert. Falls Sprint 6E.4A wirklich den gesamten dokumentierten Bestand unabhängig von dessen Alter fordert, ist zusätzlich eine explizite Backendentscheidung nötig; das lässt sich im Frontend nicht korrekt umgehen. ### ECharts-Lifecycle Bei leerer Auswahl oder Range-Fehler: - vorhandene Instanz disposen, - `ResizeObserver` ebenfalls trennen, - Tabelle und Status synchron leeren. Aktuell wird bei jedem `renderChart()` ein neuer `ResizeObserver` erzeugt, ohne ihn beim Dispose zu entfernen. --- ## 4. `dashboard-v5-day-controller.js` Notwendige kleine Begleitänderung für den URL-Vertrag: - `deepLinkFromLocation()` muss `period`, `from`, `to` als fremde, zentral validierte Parameter tolerieren. - In `openDay()` und `restorePrevious()` nicht mehr `url.search = ''`. - Nur die eigenen Parameter `view` und `date` setzen/löschen. - Zeitraumparameter unverändert erhalten. Ohne diese Änderung geht der Zeitraum bei jedem Day-Drilldown verloren. --- ## 5. `dashboard-v5-record.js` Analog: - `route()` soll zentrale Range-Parameter tolerieren. - `setRoute()` darf nicht mehr den gesamten Querystring löschen. - Nur `view`, `tab`, `document` selbst verwalten. - `period/from/to` erhalten. --- ## 6. `dashboard-v5.css` - Neue zentrale Custom-Range-Gruppe gestalten: - Desktop inline, - Mobile umbrechend, - Inputs/Buttons mindestens 44 px, - Custom-Felder nur bei `period=custom` sichtbar. - API-ECharts-Shell als primären Explorer strukturieren: - Suche, - Auswahlchips, - Modus, - Chart, - Tabelle. - Alte `.explorer-panels`, `.explorer-chart-frame`, `.explorer-lag-*`-Regeln entfernen, sobald deren Markup entfernt ist. - Prototyp-CSS behalten, aber nur für das isolierte synthetische Profil. - Print: zentrale Konfiguration ausblenden, ECharts-Ergebnis, Tabelle und Sicherheitskontext erhalten. --- # Konkrete Tests ## Python-/Renderer-Tests In beziehungsweise neben `test_dashboard_v5_sprint6c_api_contracts.py`: 1. `test_health_record_profile_renders_exactly_one_echarts_explorer` - genau ein `data-echarts-explorer` - kein `#explorer-chart` - kein `data-echarts-prototype` - ECharts-Script exakt einmal - API-Explorer-Script exakt einmal 2. `test_prototype_and_api_explorer_cannot_be_enabled_together` 3. Renderer enthält exakt sechs zentrale Zeitraumoptionen: - 7/30/90/180/all/custom 4. Keine lokalen `[data-api-explorer-preset]`-Controls mehr. ## Playwright `dashboard_v5_explorer_6c.spec.js` erweitern beziehungsweise neu fokussieren: 1. **Ein Explorer** - genau eine sichtbare Explorer-Chartfläche - kein sichtbarer Chart.js-Explorer. 2. **Preset→URL→API** - Klick auf 7/30/90/180 schreibt kanonische URL. - `/series` und `/events` erhalten dieselben inklusiven Grenzen. - Enddatum entspricht serverautorisiertem `today`. 3. **Alle** - URL `period=all`, kein `from/to`. - Series-Request ohne Range. - Events-Request mit aus Coverage abgeleiteten Grenzen. 4. **Custom** - gültige ISO-Grenzen werden in URL und Requests übernommen. - umgekehrte, unvollständige oder zukünftige Range löst keinen API-Request aus. 5. **History** - Preset wechseln, Custom anwenden, Browser Back/Forward. - Controls, URL, Chart und Tabelle bleiben synchron. 6. **Metrikauswahl** - katalogbasierte HRV- und Laborwahl weiterhin möglich. - kompatible Zwei-Reihen-Auswahl funktioniert. - inkompatible Einheiten werden weiterhin abgewiesen. 7. **Router-Koexistenz** - Explorer-Range setzen → Tag öffnen → zurück. - `period/from/to` bleiben erhalten. - dasselbe für Akten-Tabs und Dokumentdetail. 8. **Responsive/Accessibility** - 390 px, Desktop und 200-%-Text. - kein horizontaler Seitenoverflow. - Tabelle als mobiler Fallback sichtbar. - alle sichtbaren Ziele mindestens 44×44 px. --- ## Read-only-Status - Keine Dateien erstellt oder geändert. - Repository bei Basis `80bf712` untersucht; Arbeitsbaum war sauber. - Keine Tests ausgeführt, da der Auftrag ausschließlich Analyse und Änderungsvorschlag war. - Wesentlicher offener Vertragsentscheid: „Alle“ kollidiert bei Datenbeständen über 3.660 Tage mit `read_api.py:MAX_RANGE_DAYS`.