Add the collection dashboard

One GET /api/stats call, aggregated in a single pass over the library.
Completion funnel, breakdowns by system, genre, decade, condition and
rating, a backlog that links into the filtered library, and a value card.

Form was picked before colour, and most of the page is not a chart: single
numbers are stat tiles, the breakdowns are bar lists with the value printed
per row, which is also the table view.

The colour work, in order:

  * one hue for the breakdown bars — identity is on the axis labels, so
    colour has nothing to encode, and a darker-where-bigger ramp would just
    double-encode bar length
  * an ordinal ramp for the funnel, since owned/played/finished are ordered
    stages rather than peers
  * validated with the dataviz validator against this app's real card
    surfaces rather than a reference one, which caught that the documented
    ordinal light-end measures 1.91:1 here and fails the 2:1 floor; the ramp
    starts a step darker
  * status colour used once, on the stale-valuation notice, with an icon and
    text so it never carries meaning alone

Two bugs found by rendering it and looking, which the validator cannot see:

  * the ratings card was showing condition data under a ratings heading — a
    chart whose title did not describe its contents. Fixed by adding a real
    rating distribution rather than relabelling the card.
  * dark mode rendered light cards on a dark page. Copying the reference
    pattern's `color-scheme` onto the container overrode how every
    descendant resolved light-dark(), and the `:root`-prefixed media
    override never matched at all, because Angular's emulated encapsulation
    scopes selectors in component styles. Both replaced by light-dark()
    values that inherit the app's own scheme.

The value card reports coverage, age and source beside the total, and flags
valuations older than 90 days, because a bare figure mixes fresh with stale
and silently omits everything unpriced.

148 backend tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-04 17:08:39 -04:00
co-authored by Claude Opus 5
parent d34e6b4ced
commit ddc618fc9c
12 changed files with 1228 additions and 1 deletions
+34
View File
@@ -291,6 +291,40 @@ parser follows the widely-used convention — integer pennies under hyphenated
keys — and is tolerant enough that a naming mismatch reads as "no price" rather
than throwing. `PriceChartingProvider.Parse` is the one place to adjust.
### Dashboard
`/dashboard` — one `GET /api/stats` call, aggregated server-side in a single pass
over the library rather than a dozen grouped queries that could disagree.
Form was chosen before colour, and most of the page is deliberately not a chart:
headline numbers are stat tiles, the backlog is a link into a filtered library
view, and the breakdowns are horizontal bar lists with the value printed on each
row — which doubles as the table view.
Colour decisions worth keeping:
- **One hue for the breakdown bars.** Identity is carried by the axis labels, so
colour has nothing to encode; twelve systems in twelve hues would be twelve
ways to be wrong, and a darker-where-bigger ramp would double-encode length.
- **An ordinal ramp for the funnel**, because owned → played → finished are
ordered stages, not peer categories.
- **The palette was validated with the dataviz validator against this app's own
card surfaces** (`#f8f2f6` light, `#1d1b1e` dark), not against a reference
surface. That mattered: the documented ordinal light-end measured 1.91:1 here
and failed the 2:1 floor, so the ramp starts a step darker.
- **Status colour appears once**, on the stale-valuation notice, always with an
icon and text so it never carries meaning alone.
The value card is deliberately wordy. A bare total silently mixes fresh and old
valuations and excludes everything unpriced, so coverage, age and source travel
with the figure, and a valuation older than 90 days is called out.
Theming note: the chart variables use `light-dark()` rather than a
`prefers-color-scheme` block. Angular's emulated encapsulation scopes selectors
in component styles, so a `:root`-prefixed media override never matches from
there — and setting `color-scheme` on the container overrides how every
descendant resolves `light-dark()`, which rendered light cards on a dark page.
### Database changes
```bash