feat(ui,rest,observability): make the dashboard chart functional — real type picker, multi-series, tooltip, a11y - #574
Open
angela-helios wants to merge 17 commits into
Open
feat(ui,rest,observability): make the dashboard chart functional — real type picker, multi-series, tooltip, a11y#574angela-helios wants to merge 17 commits into
angela-helios wants to merge 17 commits into
Conversation
…lasses The Import and Tenants pages carried a second spelling for every primitive: .button vs .btn, .page-title vs .page-head__title, and a class="table" that matched no rule at all. .data-table and .notice were each defined twice, so what rendered was the cascade-merge of two authors' blocks. Canonical spellings win (.btn, .data-table, .page-head__title). The duplicate .data-table / .notice definitions are merged into single blocks that preserve today's computed result exactly, the retired classes are deleted, and the bulk-import tables gain the .table-wrap / .data-table structure the other tables already use.
@layer tokens, base, components, pages — declared once at the top, then each existing block wrapped in place so the diff stays reviewable. Rules keep their file positions; what changes is conflict resolution: a page rule now outranks a component rule regardless of specificity or source order, and the shared vocabulary (button, card, table, field, notice, pill, menu, page-head, toolbar) is separated from page-specific styling by layer rather than by convention.
Beyond the .data-table / .notice pairs the issue named, a duplicate-selector sweep found four more: .pane, .addbox__actions, .tag, and the .tabs / .tab / .tab:hover trio (history's underline tabs and the compartment viewer's pill tabs shared class names). Each pair rendered as its cascade-merge already, so every merge concatenates the two declaration blocks in source order — the computed result is unchanged, there is just one definition to find now.
… guards design-system.spec.ts makes the vocabulary executable: every class used on every page must match a rule in app.css (this is what would have caught class="table"), every <h1> must be the canonical .page-head__title, primary buttons must resolve to identical computed metrics on every page, and no selector may be defined twice (postcss walk, layer-aware: @layer does not license a duplicate, @media does). The route lists no-cdn.spec.ts and a11y.spec.ts each carried privately are now one export in pages/routes.ts — a new page added there is covered by every cross-page guard at once, instead of opted out of each by default. The bulk-import pages join the list; the detail page has no static URL, so the guards seed a submission and cover it too.
…d it A vocabulary table and the layering contract in crates/ui/README.md, a condensed version in the work-with-ui skill, a 'no second vocabulary' rule in the must-NOT list, and templates/pages/_scaffold.html as the copy-me starting point — so the default path for a new page is the canonical one, for humans and agents alike.
…imary button The design-system guard's first run over every page found the tail the issue predicted: 18 classes matching no rule (chart-legend__type, nav-panel__name, builder-section, query-results__open, modal__pane, the history/editor page wrappers, jt, col-created, backlink, mono, and friends) — all removed from the templates; .json-line--foldable stays because shipped script selects it, which the guard now recognizes (dot-classes inside JS string literals count as load-bearing, so a dead class="table" still fails). .toolbar__title / .toolbar__count were used but never defined — the Import section headers rendered as browser-default h2. They now match .card-head h3. And the one real primary-button divergence, .nl-input .btn--primary at 44px, drops its metric overrides: every primary button on every page now resolves identically, which the guard holds from here on.
…ype set The provider contract gains the two things the chart card needs to become functional (#555): the snapshot now carries every resource type the tenant actually stores (with totals, largest first) so the picker can offer real data instead of a hardcoded list of eight, and the caller passes the set of types to chart — an empty selection means the provider's default, the tenant's three largest types. Selections are capped at six, bounding the per-type delta queries and matching the palette. The charted set joins the snapshot cache key: two selections are two snapshots. DEFAULT_DASHBOARD_TYPES is gone; the console handler keeps its own copy and the dashboard now derives its defaults from what is stored.
… expand, a11y The chart card's controls now all do something (#555). The inert type pill is a real picker: every stored type as a checkbox row, each a plain link that toggles the type in or out of the charted set — same progressive-enhancement contract as the window selector. The dead expand button is a link that re-renders the taller plot server-side. Multiple series plot together on one shared scale, each with its own palette slot, and the legend entries double as remove links while more than one is plotted. Accessibility: the SVG carries an accessible name instead of aria-hidden, and the same numbers render as text — a details-folded table of the sampled buckets, one column per series. An explicit empty state replaces the silent bare axes, and the placeholder snapshot (no provider registered) is labeled as sample data instead of posing as real. All strings in en/es/de. The tooltip's data rides in an inert application/json carrier: bucket labels plus per-series values and SVG coordinates, so the hover script does no chart math of its own.
…r the picker Manual review: several series at once got noisy and the pill grew with every name. Three is the mockup's choice and stays legible — the cap now lives on both sides (the provider truncates, and a picker link that would exceed it swaps the oldest series out instead). The pill shows one name plus a +N overflow count. The picker's option list scrolls on its own with the heading and a typeahead filter pinned above it, so a tenant with a hundred stored types still finds the one it wants.
dashboard.spec.ts grows from two assertions to the whole card: accessible name on the SVG, window switching, the picker toggling types with the three-series cap (a fourth selection swaps the oldest out), the legend removing series, the typeahead filter, the hover tooltip appearing and leaving, expand/collapse re-rendering the taller plot, and the tabular alternative opening. A nojs spec walks the same controls with JavaScript off — picker, window, expand, and the table are all plain links and native disclosure. Specs seed through the ordinary FHIR API and outlast the 15s snapshot cache by polling. Also catches up the two observability cache tests the trait change missed.
Manual review round two: three was the right default but the wrong ceiling — an explicit selection can now go to six, the palette's full width, with the oldest series swapping out past that. The default stays at the tenant's three largest types. The dashboard demo tooling moves into the repo (crates/ui/dev): a batch-bundle seeder that stores ~1.2k resources across eight types through the ordinary FHIR API, and a SQLite backdater that spreads their history rows across the past 30 days with a distinct curve shape per type — so every window on the chart has something real to show. Local-only; nothing ships or runs in CI.
angela-helios
changed the base branch from
fix/543-stylesheet-unification
to
main
August 19, 2026 07:26
Conflicts were the dashboard data path (#565 job cards + #537 bulk export landing on main while this branch reworked the chart) and the UI's CSS/class vocabulary refactor. Resolution: - DashboardSnapshot carries both sides: `available`/`TypeCount` (#555) and `export_jobs`/`import_jobs_active` (#565). The provider keeps this branch's `distinct_types` (derived from `available`) instead of main's second `count_all_types` read, and gains main's job-store counts. - Rest dashboard tests updated for the three-argument `snapshot(window, tenant, types)` signature. - app.css: main's new rules folded into this branch's `@layer` structure. - Main's new templates moved onto the renamed vocabulary this branch introduced (`.button` -> `.btn`, `.page-title` -> `.page-head__title`). - Bulk-export status chips renamed `.chip*` -> `.status-chip*`: `.chip` is already the filter affordance on the search/registry pages, and the duplicate definition both collided visually and tripped the new design-system guard. The dead duplicate `.notice` block from #537 is dropped (the registry-viewer `.notice` won the cascade anyway). - Dashboard e2e expects five stat cards (main's job cards) and keeps this branch's `waitForSeries()`. Claude-Session: https://claude.ai/code/session_019E74WX1whW1NaT1SjUqXJG
Main brought Bulk Export (#538), the dashboard job cards (#565), and the menu close behaviors (#566). The vocabulary work carries over both export pages, which predate #543: canonical title/button classes, the job-status chips move onto the .tag status-pill family (main's new .chip block collided with the facet-chip component — exactly the duplicate this branch exists to kill), the typegrid joins the pages layer, the job-card additions join the components layer, and both export routes join the shared guard list. Also makes the design-system guard read app.css and the shipped scripts over HTTP from the server under test instead of the source tree: the CI runner drives a packaged binary with no checkout alongside it (the ENOENT on every merge-ref run), and in HFS_E2E_BASE_URL mode the running server is the only truth worth checking anyway.
… backlink The guard's script scan read only the landing page's script tags, but pages ship their scripts individually — editor.js never loads on /ui, so its .json-line--foldable hook read as undefined. The sweep now collects every script each route actually ships and legitimizes across the union. And the export pages' vestigial .backlink goes the way bulk-import's did.
…ashboard-chart Combines the multi-series chart with main's #565 job cards: the snapshot carries both the picker's available types and the job counts, the metrics render real export/import figures inside the rewritten build_dashboard, and the stat-card spec expects five cards with the chart's series wait.
Both integrations of main landed on this branch in parallel; this reconciles them. Steve's merge answered the .chip collision by minting a status-chip family — folded here into the existing .tag status-pill vocabulary instead (one pill family, per #543), and the vestigial .backlink stays gone. The export card keeps his structure otherwise.
Contributor
Author
|
Heads-up on the parallel merges: your main catch-up and mine landed on this branch at the same time — reconciled in |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #555. The "FHIR resources over time" card goes from decorative to functional, following the hi-fi mockup's resource picker design.
Stacked on #572 (base
fix/543-stylesheet-unification; GitHub retargets tomainwhen it merges).The provider learns what a tenant actually stores
DashboardSnapshotnow carries every type with at least one stored resource (via the existingcount_all_types, largest first), and the caller passes the set to chart. Empty selection = the tenant's three largest types, skipping definitional/infrastructure resources (a server that seeds its own SearchParameters would otherwise chart those instead of clinical data — they stay available in the picker). Selections are capped at three (the mockup's choice; also bounds the per-type delta queries) and join the snapshot cache key. The hardcoded eight-type list is gone.Every control does something
resPick): adetailsmenu of checkbox rows — every stored type with its count, a typeahead filter pinned above a scrolling list, each option a plain link that toggles the type. Selecting past the cap swaps the oldest series out. The pill label stays one name wide (Encounter +2).--series-Npalette slot; the legend entries carry the color dot and the total, and double as remove links while more than one is plotted.application/jsoncarrier with server-computed SVG coordinates — the script does no chart math.Honesty and access
aria-hidden, and the same numbers render as text: a details-folded table of the sampled buckets, one column per series.Tests
dashboard.spec.ts: 2 assertions → the whole card (accessible name, window switching, picker toggling with the cap, legend removal, filter, tooltip in/out, expand round-trip, tabular alternative), plus anojsspec walking the same controls with JavaScript off.Demo seeding
Verified against a seeded store: ~1.2k resources across 8 types created through ordinary batch bundles, with their history rows backdated across 30 days so every window shows real growth curves. The two scripts (batch seeder + SQLite backdater) are local dev tooling — happy to commit them under a dev/ path if we want them in-repo.