## Outcome - Audited the exact base commit `039bbeaca97770af9f650754efd80d1977fd49d4`; worktree was clean. - Mapped rendering, CSS, route/query ownership, URL writers, browser-history behavior, and existing tests across Today, day, cockpit, explorer, nutrition, record, and calendar. - No files changed or created. ## Current ownership ### Renderer / markup `scripts/health/dashboard_v5/render.py` - Global header: lines 130–132. - Global day picker/open/Today controls are composed by `calendar_header` at lines 73–77. - Global period label and actions are in `.header-actions`. - Global range toolbar is always rendered when the V5 shell is rendered; `dashboard-v5-range.js` is feature-gated by `explorer_6c`. - Local/context headers: - Calendar: lines 80–87, with its own month toolbar and compare-range panel. - Day: lines 88–104, with its own previous/next/calendar/compare/capture toolbar. - Record: lines 111–116, with hero, overview button, tabs, breadcrumb. - Today: lines 135–141. - Cockpit: line 142. - Explorer: lines 143–160. - Nutrition: line 162. - Script order is range → main shell → explorer → day controller → record → global search at line 167. This creates multiple independent `popstate` owners. ### Main shell / embedded-bundle range consumers `scripts/health/assets/health-assets/dashboard-v5.js` - Owns primary panel visibility, navigation tab state, Today/Cockpit/Nutrition rendering, and embedded-bundle filtering. - `periodData()` lines 61–79 consumes `window.healthRange` for all embedded arrays. - Consequently the range currently affects: - **Today** via `latestPoint()` and `metricSeries()`. - **Cockpit** charts/cards/events. - **Nutrition** summary/chart. - **Explorer** indirectly through its API client. - `pushDashboardView()` lines 378–390 and generic navigation are inconsistent: - Programmatic task/card navigation writes history. - Ordinary nav clicks only call `selectView()` and usually do **not** update the URL. - Main `popstate` lines 1322–1327 restores only named primary views; it does not restore blank/default Today, day, calendar, or `view=record`. - Initial `view=record` is specially mapped to the `doctor` panel at lines 1341–1345. ### Range / Explorer URL state `scripts/health/assets/health-assets/dashboard-v5-range.js` - Sole range state owner. - Current parser, lines 28–44: - Accepts only `view=explorer`. - Allowed keys: `view`, `from`, `to`, `metric`. - Rejects duplicates, invalid dates, future end dates, spans over 3,660 days, and invalid metric IDs. - Infers `7/30/90/180/all/custom` from explicit bounds; there is no `period=` query parameter. - URL writer lines 60–69: - Runs only while Explorer is selected. - Executes `url.search = ''`, then writes Explorer-owned fields. - Invalid Explorer input clears the **entire** query at lines 100–105. - Its `popstate` only restores a valid Explorer route. ### API Explorer `scripts/health/assets/health-assets/dashboard-v5-api-explorer.js` - Consumes `window.healthRange.get()` for series/events requests and ECharts axis. - Metric URL synchronization is delegated back to `healthRange`. - Has another `popstate` listener at lines 371–374 to restore the first metric. - Search, mode, resolution, and multi-metric selection are intentionally not fully URL-backed. ### Day and calendar `scripts/health/assets/health-assets/dashboard-v5-day-controller.js` - Day parser lines 38–41: - Allows only `view` and `date`. - Requires exactly one `view=day` and one valid date. - Any range parameter makes the route invalid. - Day URL writes clear the complete query at lines 138–151. - Calendar nav writes `?view=calendar` and clears the query at line 243. - Calendar’s displayed month is DOM/FullCalendar state only; it is not URL-backed, so a reload loses a previously selected month. - `data-day-calendar` only switches the panel and does not update the URL, leaving a stale `view=day` URL. - Calendar/day comparison transfers a custom range into `healthRange`, selects Explorer, and relies on Explorer selection to rewrite the URL. - Own independent `popstate` listener at line 245. ### Record `scripts/health/assets/health-assets/dashboard-v5-record.js` - Route parser lines 98–108: - Allows only `view`, `tab`, `document`. - Requires `view=record`. - Validates tab and opaque document ID. - Any central `from/to` parameter invalidates the record route. - `setRoute()` lines 110–120 clears the full query before writing record keys. - Record filters are deliberately held in `history.state.record.filtersByTab`, not URL parameters. - This prevents tab-to-tab API query bleed. - Back/Forward restores filters. - A full reload restores tab/document but not active filters. - Own independent `popstate` listener at lines 552–562. ### CSS `scripts/health/assets/health-assets/dashboard-v5.css` - Global header/control layout: lines 37–42, 90–121. - Range toolbar/custom popover: lines 341–347; mobile behavior lines 379–387 and 481–487. - Calendar/day local toolbars and responsive layout: lines 301–333 and 440–447. - Record local tabs/toolbars: lines 335–339 and 403–439. - Print currently hides `.app-header`, `.global-controls`, record tabs and filter forms at lines 274–280. ## Route-state problems 1. **Normal tab navigation is not durable.** - Clicking Cockpit, Nutrition, or Today usually leaves the old URL unchanged. - Reload can reopen the previous URL-owned route rather than the visible panel. - There is no history entry for ordinary primary-tab changes. 2. **Range state is durable only in Explorer.** - Cockpit, Today, and Nutrition consume the range but cannot encode it in their URLs. - Reload from those views resets to 30 days or reopens whatever stale route remains. 3. **All writers use `url.search = ''`.** - This prevents accidental query carryover but also discards legitimate state. - Range, day, calendar, record, and programmatic primary navigation each do this independently. 4. **Strict parsers cannot coexist.** - Explorer rejects day/record keys. - Day rejects range keys. - Record rejects range/day keys. - Blindly “preserving global range parameters” in record/day/calendar would break those routes and risks `from/to` meaning different things in Record filters versus central dashboard range. 5. **Multiple `popstate` listeners race.** - Range, main shell, API Explorer, day/calendar, and record all react independently. - Blank/default Today is not restored by the main shell. - Day controller can transiently select a fallback before the record controller restores Record. 6. **Calendar month and day→calendar navigation are unstable.** - Selected calendar month is lost on reload. - “Kalender öffnen” from Day changes the panel without changing the day URL. 7. **Global toolbar is semantically over-broad.** - Range controls are shown on exact-day, calendar, and record views even though those surfaces use their own date/filter controls. - The global day picker duplicates Day and Calendar controls. ## Recommended A.3 implementation ### Contextual toolbar contract Keep global header actions limited to Search, data status, Privacy, and More. Add one persistent contextual toolbar region with explicit view applicability: | View | Context toolbar | |---|---| | Today | Central range controls, because Today currently uses `periodData`; optionally move the day-jump picker here | | Cockpit | Central range controls | | Explorer | Central range controls | | Nutrition | Central range controls | | Day | No global range; retain the local day toolbar | | Calendar | No global range; retain month toolbar and local compare-range panel | | Record | No global range; retain record tabs and per-tab filters | Do not hide range controls from Today without also removing its hidden dependency on `healthRange`; otherwise changing Cockpit’s range would silently alter Today. Renderer changes: - `render.py`: wrap the range controls in `data-context-toolbar` / `data-context-range`. - Move `calendar_header` out of the always-global header; expose a Today-only jump control if retained. - Give the contextual row an accessible label and status target. - `dashboard-v5.css`: replace `.global-controls.range-toolbar` assumptions with contextual visibility classes/data attributes; keep the existing mobile popover and print hiding behavior. ### One route codec and one history owner Centralize route parsing/writing in `dashboard-v5.js`, rather than extending each client’s allowlist independently. Canonical route shapes: - Today: empty URL initially; after a range/user navigation, `?view=today&from=…&to=…` - Cockpit: `?view=cockpit&from=…&to=…` - Explorer: `?view=explorer&from=…&to=…&metric=…` - Nutrition: `?view=nutrition&from=…&to=…` - Day: `?view=day&date=YYYY-MM-DD` - Calendar: `?view=calendar&month=YYYY-MM` - Record: `?view=record&tab=…&document=…` Rules: - Parse duplicates fail-closed. - Validate route-specific key sets; rebuild URLs from pathname/hash using only the destination route’s keys. - Never carry central `from/to` into day, calendar, or record. - Never carry record `tab/document`, calendar `month`, day `date`, or Explorer `metric` into another route. - Keep record search/filter terms in `history.state`, not URL. - Use `pushState` for user actions, `replaceState` only for initial canonicalization/transient cleanup, and no writes during `popstate`. - Ordinary nav clicks must call the route owner, not `selectView()` directly. - Restore the complete route with one `popstate` listener; clients expose restoration methods instead of registering their own listeners. - Preserve the current server-authoritative Today/date validation from `healthRange`. - Retain explicit bounds for this sprint to minimize contract churn; a later migration to `period=…` can be separate. ### Client refactors - `dashboard-v5-range.js` - Keep date validation, range state, control updates, and range-change event. - Export parse/restore helpers. - Remove view parsing, `url.search=''`, and its `popstate` listener. - `dashboard-v5-api-explorer.js` - Remove its `popstate` listener. - Expose `restoreMetric(metric)`; do not write route state independently. - `dashboard-v5-day-controller.js` - Replace `dayRoute`, direct history writes, and its `popstate` listener with calls into the central router. - Add calendar month restoration. - Make Day’s calendar button push `view=calendar&month=`. - `dashboard-v5-record.js` - Keep filter sanitization/history-state snapshots. - Replace route parsing/writes and its `popstate` listener with central route callbacks. - `dashboard-v5-global-search.js` - Navigate through the central route API instead of synthetic `.click()` calls. - `dashboard-v5.js` - Make `selectView()` rendering-only. - Route all user navigation through `navigate(route)`. - Update contextual toolbar visibility from resolved route/view. ## Test plan Add a focused Playwright route/toolbar spec to the isolated **record-files** profile, which contains every target view. ### Route matrix For each of Today, Cockpit, Explorer, Nutrition, Day, Calendar, and Record: 1. Navigate through the UI. 2. Assert exact canonical query keys. 3. Reload and assert the same visible view and controls. 4. Navigate elsewhere, Back, and Forward; assert URL, selected nav item, toolbar, panel, and API requests. 5. Assert exactly one history step per user action. ### Range cases - 7/30/90/180/all/custom on Today, Cockpit, Explorer, and Nutrition. - Assert URL bounds, `window.healthRange`, pressed controls, embedded data, Explorer requests, and chart axis agree. - Cross-navigate Cockpit → Explorer → Nutrition → Today and verify the range remains stable. - Navigate to Day/Calendar/Record and assert `from/to/metric` disappear. - Back to a range view and verify exact restoration. - Duplicate `from`, duplicate `view`, missing bound, invalid date, future end, unknown key: fail closed and canonicalize via `replaceState`. ### Route isolation / no-query-bleed - Explorer metric must not appear in Nutrition/Cockpit/Today. - Day date must not appear in Calendar/Record. - Calendar month must not appear in Day/Record. - Record tab/document must not appear outside Record. - Record filter API requests must contain only that tab’s sanitized filters. - Calendar and day API requests must never inherit dashboard range or record filters. - Global search result navigation must produce the same canonical routes. ### Existing suites to extend - `dashboard_v5_explorer_6c.spec.js`: retain range/request/chart equality; add cross-view range restoration. - `dashboard_v5_calendar_day_6d.spec.js`: add month reload/back and Day→Calendar URL checks. - `dashboard_v5_record_6e.spec.js`: add leave/re-enter Record and foreign-query rejection/no-bleed. - `dashboard_v5_sprint6g_a1.spec.js`: retain Cockpit→Explorer and Today→Nutrition back behavior under canonical routes. - `dashboard_v5_sprint6g_echarts.spec.js`: verify Nutrition range URL/reload and day drill-down restoration. - `dashboard_v5.spec.js`: add blank/default Today restoration and contextual-toolbar visibility. - Python renderer tests: assert toolbar markup, script ownership/order, and absence of duplicate global/local controls. Run the focused spec first, then: ```bash HEALTH_DASHBOARD_BROWSER_GROUP=explorer tests/browser/run_v5_isolated_matrix.sh HEALTH_DASHBOARD_BROWSER_GROUP=calendar tests/browser/run_v5_isolated_matrix.sh HEALTH_DASHBOARD_BROWSER_GROUP=record tests/browser/run_v5_isolated_matrix.sh HEALTH_DASHBOARD_BROWSER_GROUP=nutrition tests/browser/run_v5_isolated_matrix.sh HEALTH_DASHBOARD_BROWSER_GROUP=a1 tests/browser/run_v5_isolated_matrix.sh tests/browser/run_v5_isolated_matrix.sh ``` ## Existing coverage versus gaps Already covered: Explorer range reload/back-forward, day deep links/reload/back-forward, record tab/document reload/back-forward, Cockpit-card→Explorer back, Today-task→Nutrition back, Nutrition chart→Day back, mobile/touch/overflow, and strict duplicate handling for individual parsers. Not covered: ordinary tab URL ownership, cross-view range persistence, blank→Today popstate, calendar month reload, Day→Calendar URL correction, leaving Record via primary navigation, contextual toolbar visibility, and a complete destination-key/no-query-bleed matrix.