Nathan: the members axis should be whole numbers, since it counts people. It
was drawing fractional ticks (19.17 people) because the 1.08 headroom factor
fed straight into the label math.
Ticks now come from a step, with the axis top set to step x 4, so labels land
on round numbers:
- counts (members, requests): whole numbers, rounded up to a half power of ten
(requests read 3,000, not 2,888). Never below 1, so a near-empty series still
gets an integer axis rather than fractions.
- money: the 1/2/5 x 10^k rule, so ticks read $10.00 rather than $4.92.
Rendered now: members 0 | 20 | 40 | 60 | 80; requests 0 | 3,000 | 6,000 |
9,000 | 12,000; spend $0.00 | $10.00 | $20.00 | $30.00 | $40.00.
The node harness gained assertions for this, checked geometrically (tick values
ascend while their screen positions descend, the zero tick sits lowest, ticks
are evenly spaced) rather than assuming a text order, so a fractional
people-axis or a misplaced label fails before a deploy.
Nathan asked to see a chart. The portal now owns a daily metrics series
(metrics_daily, written 03:15 UTC by the portal's own scheduler); this is the
first surface to read it.
- New route /api/metrics proxies the portal's /admin/metrics/series (admin
token stays server-side, same pattern as /api/model-usage).
- New Trends tab, added to switchView in the same edit (every view toggled
there must be shown/hidden there). Lazy: the series loads on first visit, so
the panel's initial load is unchanged.
- Metric toggle (Members / Spend / Requests) and period toggle (30 / 90 / all
time). Fetched once at the widest range and re-rendered client-side, so the
toggles never refetch.
- Inline SVG, hand-rolled: no chart library, no CDN, no external dependency.
Colours come from the panel's existing CSS vars, so it matches the rest.
- Gaps are drawn as gaps. A NULL metric means "not measured" and the line
deliberately breaks rather than drawing through a day we never recorded
(backfilled members_active is NULL every day, by design). The sub-line says
how many days are unmeasured.
- Members shows the cumulative total; spend and requests are per-day bars.
Verified against the live 55-day series with a node harness running the real
page functions: 22 checks, including that a NULL breaks the line into two runs,
that no NaN reaches the markup, that the toggles do not refetch, and that the
SVG carries no hardcoded hex.
No manifest change yet — proxyAuth stays active, so get_identity() still works
via header. OIDC routes only activate when CLOUDRON_OIDC_* env vars are present.
Validates id_token signature (JWKS), iss/aud/nonce, exp/nbf.