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
+52
View File
@@ -0,0 +1,52 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
export interface CountByLabel {
label: string;
count: number;
}
/** Each stage is a subset of the one before, so these read in order. */
export interface CompletionFunnel {
owned: number;
played: number;
finished: number;
}
export interface ValueSummary {
total: number;
pricedCount: number;
unpricedCount: number;
totalPaid: number;
paidCount: number;
oldestValuedAt: string | null;
newestValuedAt: string | null;
sources: string[];
totalIfCib: number | null;
}
export interface Stats {
totalGames: number;
funnel: CompletionFunnel;
backlog: number;
inProgress: number;
dumped: number;
ratedCount: number;
averageRating: number | null;
bySystem: CountByLabel[];
byGenre: CountByLabel[];
byDecade: CountByLabel[];
byCondition: CountByLabel[];
byRating: CountByLabel[];
value: ValueSummary;
}
@Injectable({ providedIn: 'root' })
export class StatsService {
private readonly http = inject(HttpClient);
get(): Observable<Stats> {
return this.http.get<Stats>('/api/stats');
}
}