Skip to content

Commit bbcd3c4

Browse files
Nigel TatschnerNigel Tatschner
authored andcommitted
release: v0.0.2-beta — metrics-display redesign wave 1
Replaces hand-rolled bar divs / 30-day grid with a token-driven chart layer: YearHeatmap on /dashboard and /metrics, TypeBreakdown donut+barlist on /metrics Overview, plus the foundational MetricCard shell + lib helpers (feature-flags, telemetry, recharts theme bridge) so subsequent waves can layer in without rebuilding the chart contract. Six review passes (3 on strategic plan, 3 on impl plan, 3 on this PR) caught the load-bearing issues: timeline server cap mismatch (90 vs 365), telemetry firing for flag-disabled cards, SSR cache poisoning in the telemetry consent path, and an incorrect column- math in the year heatmap. All resolved before this commit. Notable bumps: - workspace 0.0.1-beta → 0.0.2-beta - tauri.conf.json 0.0.1 → 0.0.2 - TIMELINE_DAYS_MAX 90 → 366 in validation.rs (so the YearHeatmap can request a 365-day window) - recharts ^3.8.1 added to apps/web The cross-cutting checklist (a11y srTable / empty / error / flag / telemetry) is enforced at the type system level — `MetricCard` makes those five props required with no defaults, so cards that skip any fail `tsc` rather than silently degrading. Migration 0021_share_scopes.sql ships ahead of any consumer to future-proof the planned cross-user aggregate endpoints; column is additive with conservative defaults. Verification: cargo test (271 pass), pnpm typecheck clean, next build compiled successfully, cargo fmt clean. Plan docs at docs/DESIGN-METRICS-PLAN.md (strategic) and docs/DESIGN-METRICS-IMPLEMENTATION-PLAN.md (execution) capture both rounds of review findings.
1 parent f7a24ac commit bbcd3c4

22 files changed

Lines changed: 2257 additions & 10 deletions

CHANGELOG.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,63 @@ Tag-suffix → release-channel mapping (see `release-manifests/`):
3232

3333
- (nothing yet)
3434

35+
## [0.0.2-beta] — 2026-05-12
36+
37+
Metrics-display redesign, first wave. Replaces the hand-rolled 30-day
38+
heatmap on the dashboard with a GitHub-style 53-week heatmap, and
39+
rewires the metrics page's Overview tab with a donut+barlist Type
40+
breakdown alongside the heatmap. Foundational shell + lib helpers
41+
land so subsequent waves can layer in without rebuilding the chart
42+
contract.
43+
44+
### Added
45+
46+
- **Web:** `YearHeatmap` component — 53-week GitHub-style activity
47+
heatmap, inline SVG, renders against the `--grid-*` token ladder
48+
for theme reactivity. Shown on `/dashboard` and `/metrics` Overview.
49+
- **Web:** `TypeBreakdown` component — recharts donut + ranked-bar
50+
combo replacing the manual `<div>` bars previously on the metrics
51+
Overview tab.
52+
- **Web:** `SparklinePill` component — small stat tile with inline
53+
sparkline. Foundation for upcoming dashboard pill upgrades.
54+
- **Web:** `MetricCard` + `ChartCard` shells. Required-props pattern
55+
(`flagKey`, `telemetryKey`, `empty`, `error`, `srTable`) enforces
56+
the cross-cutting checklist (feature-flag gate, telemetry hook,
57+
empty/error states, screen-reader fallback) at the TypeScript
58+
level — cards that skip any of these fail `tsc`.
59+
- **Web:** Typed feature-flag registry (`lib/feature-flags.ts`) for
60+
the metrics surfaces. All flags default on for v0.0.2-beta; the
61+
`metrics.now_strip` flag stays off (cut per the impl plan).
62+
- **Web:** Frontend telemetry helper (`lib/metrics-telemetry.ts`).
63+
Opt-in (off by default via `localStorage["starstats.telemetry"]`).
64+
Server endpoint to receive POSTs is a follow-up.
65+
- **Web:** Recharts theme bridge (`lib/recharts-theme.ts`) — reads
66+
`ss-*` CSS-var hex values from `:root` and re-renders on
67+
`data-theme` mutations so chart colours swap with the active theme.
68+
- **Server:** Migration `0021_share_scopes.sql` — adds a per-user
69+
`share_scopes` JSONB column with conservative defaults (own data
70+
only for everything except summary, which defaults to friend).
71+
Future-proofs the planned cross-user aggregate endpoints; no code
72+
consumes the column yet.
73+
- **Dep:** `recharts ^3.8.1` in `apps/web`.
74+
75+
### Changed
76+
77+
- **Server:** Bumped `TIMELINE_DAYS_MAX` from 90 to 366 in
78+
`validation.rs` so `YearHeatmap` callers can request a 365-day
79+
window without tripping the validator. Existing
80+
`timeline_rejects_days_above_max` test updated accordingly.
81+
- **Web:** `/dashboard` and `/metrics` now request a 365-day timeline
82+
(was 30). DayHeatmap still renders cleanly with the wider window.
83+
- **Versions:** Workspace `0.0.1-beta → 0.0.2-beta`,
84+
`tauri.conf.json` `0.0.1 → 0.0.2`.
85+
86+
### Documentation
87+
88+
- Added `docs/DESIGN-METRICS-PLAN.md` (strategic plan) and
89+
`docs/DESIGN-METRICS-IMPLEMENTATION-PLAN.md` (execution plan,
90+
reflecting two rounds of independent review findings).
91+
3592
## [0.0.1-beta] — 2026-05-11
3693

3794
Fresh start on the `beta` channel after the alpha history scrub.

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ members = [
77
]
88

99
[workspace.package]
10-
version = "0.0.1-beta"
10+
version = "0.0.2-beta"
1111
edition = "2021"
1212
license = "MPL-2.0"
1313
authors = ["StarStats contributors"]

apps/web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,20 @@
1313
"test:e2e:install": "playwright install --with-deps chromium"
1414
},
1515
"dependencies": {
16-
"api-client-ts": "workspace:*",
1716
"@opentelemetry/api": "^1.9.1",
1817
"@opentelemetry/auto-instrumentations-node": "^0.75.0",
1918
"@opentelemetry/exporter-trace-otlp-grpc": "^0.217.0",
2019
"@opentelemetry/resources": "^2.7.1",
2120
"@opentelemetry/sdk-node": "^0.217.0",
2221
"@opentelemetry/semantic-conventions": "^1.40.0",
2322
"@sentry/node": "^10.52.0",
23+
"api-client-ts": "workspace:*",
2424
"next": "15.5.18",
2525
"pino": "^9.6.0",
2626
"prom-client": "^15.1.3",
2727
"react": "19.0.0",
2828
"react-dom": "19.0.0",
29+
"recharts": "^3.8.1",
2930
"server-only": "^0.0.1"
3031
},
3132
"devDependencies": {

apps/web/src/app/dashboard/page.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { DayHeatmap } from '@/components/DayHeatmap';
2828
import { HangarCard } from '@/components/HangarCard';
2929
import { OrgsCard } from '@/components/OrgsCard';
3030
import { ProfileCard } from '@/components/ProfileCard';
31+
import { YearHeatmap } from '@/components/metrics/YearHeatmap';
3132

3233
const PAGE_LIMIT = 50;
3334

@@ -75,7 +76,10 @@ export default async function DashboardPage(props: {
7576
since,
7677
until,
7778
}),
78-
getTimeline(session.token, { days: 30 }),
79+
// Bumped from 30 to 365 days to feed the YearHeatmap card.
80+
// DayHeatmap still renders cleanly with the wider window
81+
// (it walks `buckets` directly).
82+
getTimeline(session.token, { days: 365 }),
7983
// Location resolver — 204 means "no recent activity", which
8084
// we treat as null and the pill renders nothing.
8185
getCurrentLocation(session.token).catch((e) => {
@@ -342,6 +346,10 @@ export default async function DashboardPage(props: {
342346
</div>
343347
</section>
344348

349+
{/* Year view — GitHub-style 53-week grid. Same timeline
350+
data, wider window. Card-styled via MetricCard shell. */}
351+
<YearHeatmap timeline={timeline} />
352+
345353
<div
346354
data-rspgrid="2"
347355
style={{

apps/web/src/app/globals.css

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,119 @@ main h1 {
214214
opacity: 1;
215215
}
216216
}
217+
218+
/* ---- Metrics redesign (v0.0.2-beta) -------------------------------
219+
* Cards, pills, and chart shells consumed by `components/metrics/*`.
220+
* Token-driven so theme swaps just repaint.
221+
* ----------------------------------------------------------------- */
222+
223+
/* Screen-reader-only utility. Used by `MetricCard` for the table
224+
* fallback that mirrors each chart's data for keyboard / SR users. */
225+
.sr-only {
226+
position: absolute;
227+
width: 1px;
228+
height: 1px;
229+
padding: 0;
230+
margin: -1px;
231+
overflow: hidden;
232+
clip: rect(0, 0, 0, 0);
233+
white-space: nowrap;
234+
border: 0;
235+
}
236+
237+
.metric-card {
238+
display: flex;
239+
flex-direction: column;
240+
}
241+
.metric-card__header {
242+
padding: 20px 24px 0;
243+
display: flex;
244+
justify-content: space-between;
245+
align-items: flex-start;
246+
gap: 12px;
247+
}
248+
.metric-card__title {
249+
margin: 0;
250+
font-size: 17px;
251+
font-weight: 600;
252+
letter-spacing: -0.01em;
253+
}
254+
.metric-card__caption {
255+
margin: 6px 0 0;
256+
color: var(--fg-muted);
257+
font-size: 13px;
258+
}
259+
.metric-card__body {
260+
padding: 16px 24px 22px;
261+
}
262+
.metric-card__empty,
263+
.metric-card__error {
264+
padding: 24px;
265+
color: var(--fg-muted);
266+
font-size: 14px;
267+
}
268+
.metric-card__error {
269+
color: var(--danger);
270+
}
271+
.metric-card__badge {
272+
font-size: 11px;
273+
letter-spacing: 0.08em;
274+
text-transform: uppercase;
275+
padding: 4px 8px;
276+
border-radius: var(--r-pill);
277+
background: var(--surface-2);
278+
border: 1px solid var(--border);
279+
color: var(--fg-muted);
280+
cursor: help;
281+
}
282+
.metric-card__badge--best-effort {
283+
color: var(--warn);
284+
border-color: var(--warn);
285+
}
286+
.chart-card__chart {
287+
padding: 0 24px;
288+
}
289+
290+
.sparkline-pill {
291+
flex: 1 1 0;
292+
min-width: 0;
293+
padding: 12px 14px;
294+
border-radius: var(--r-md);
295+
background: var(--surface);
296+
border: 1px solid var(--border);
297+
}
298+
.sparkline-pill__head {
299+
display: flex;
300+
align-items: baseline;
301+
gap: 8px;
302+
}
303+
.sparkline-pill__value {
304+
font-size: 22px;
305+
font-weight: 600;
306+
font-family: var(--font-mono);
307+
letter-spacing: -0.01em;
308+
}
309+
.sparkline-pill__label {
310+
font-size: 12px;
311+
color: var(--fg-muted);
312+
text-transform: uppercase;
313+
letter-spacing: 0.06em;
314+
}
315+
.sparkline-pill__svg {
316+
display: block;
317+
margin-top: 6px;
318+
}
319+
.sparkline-pill__svg--empty {
320+
height: 28px;
321+
background: var(--surface-2);
322+
border-radius: var(--r-xs);
323+
}
324+
.sparkline-pill__caption {
325+
margin-top: 4px;
326+
font-size: 11px;
327+
color: var(--fg-dim);
328+
}
329+
330+
.year-heatmap {
331+
scrollbar-width: thin;
332+
}

apps/web/src/app/metrics/page.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {
3434
} from '@/lib/api';
3535
import { formatEventSummary } from '@/lib/event-summary';
3636
import { getSession } from '@/lib/session';
37+
import { YearHeatmap } from '@/components/metrics/YearHeatmap';
38+
import { TypeBreakdown } from '@/components/metrics/TypeBreakdown';
3739

3840
const TAB_IDS = ['overview', 'types', 'sessions', 'raw'] as const;
3941
type TabId = (typeof TAB_IDS)[number];
@@ -85,7 +87,7 @@ export default async function MetricsPage(props: {
8587
if (view === 'overview') {
8688
const [s, t, ts, recent, hdr] = await Promise.all([
8789
getSummary(session.token),
88-
getTimeline(session.token, { days: 30 }),
90+
getTimeline(session.token, { days: 365 }),
8991
getMetricsEventTypes(session.token, '30d'),
9092
getMetricsSessions(session.token, { limit: 5 }),
9193
getMetricsSessions(session.token, { limit: HEADER_SESSION_PROBE_LIMIT }),
@@ -436,6 +438,23 @@ function OverviewTab({
436438
/>
437439
</div>
438440

441+
{/* Year-view activity heatmap — successor to the 30-day grid.
442+
Reads the same `timeline` data (now 365-day window). */}
443+
{timeline ? <YearHeatmap timeline={timeline} /> : null}
444+
445+
{/* Donut + ranked-bar combo replacing the manual bar divs that
446+
previously occupied the Types tab. Lives on Overview too so
447+
the breakdown is visible without a tab change. */}
448+
{typesBreakdown ? (
449+
<TypeBreakdown
450+
types={typesBreakdown.types.map((t) => ({
451+
event_type: t.event_type,
452+
count: t.count,
453+
}))}
454+
caption={`${typesBreakdown.types.length} distinct types`}
455+
/>
456+
) : null}
457+
439458
<div
440459
data-rspgrid="2"
441460
style={{
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* ChartCard — MetricCard variant tuned for chart layouts.
3+
*
4+
* Same required props as MetricCard. Adds `height` for the chart area
5+
* (recharts needs an explicit pixel height inside a flex parent).
6+
*/
7+
8+
'use client';
9+
10+
import type { ReactNode } from 'react';
11+
import { MetricCard, type MetricCardProps } from './MetricCard';
12+
13+
export interface ChartCardProps extends Omit<MetricCardProps, 'children'> {
14+
height?: number;
15+
children: ReactNode;
16+
}
17+
18+
export function ChartCard(props: ChartCardProps) {
19+
const { height = 220, children, ...rest } = props;
20+
return (
21+
<MetricCard {...rest}>
22+
<div className="chart-card__chart" style={{ height }}>
23+
{children}
24+
</div>
25+
</MetricCard>
26+
);
27+
}

0 commit comments

Comments
 (0)