think
16px
820px

Task 7 report — Cross-view polish, light/dark verification + 3 folded-in Task-4 fixes

Supersedes an earlier stale file of the same name (the prior feat/dashboard-p1
deploy-drill report). This dir is gitignored; all task-1..6 siblings are Carbon-reskin
reports and this is the reskin's Task 7.

Branch: feat/carbon-reskin · Repo: ahu-observatory-dashboard
Suite: 209 passed / 31 files (was 199 — +10 new) · Build: ✓ · Lint (oxlint): exit 0
Skills invoked: dataviz (time-axis + reserved-status-color rules), ui-ux-pro-max (token/light-dark consistency).

All three folded fixes done TDD (RED → GREEN). Data/logic layer untouched — date-range.ts reused verbatim (its UTC-locked formatBucketLabel), no reinvention.


Folded fix 1 (IMPORTANT — Task-4 review finding): UTC time-axis

Problem: @carbon/charts' time x-axis renders VIEWER-LOCAL ticks with no zone label, silently regressing the prior build's deliberate UTC formatting — a browser in Asia/Jakarta reads every axis 7h off.

API verification: the ticks.formatter hook exists on the installed types — node_modules/@carbon/charts/dist/interfaces/axis-scales.d.ts:84: formatter?: ((tick: number | Date, i: number) => string) | ((tick: number | Date) => string). Installed @carbon/charts@1.27.16.

Fix (Task-3-owned wrapper): chart-options.tsbuildLineOptions now sets axes.bottom.ticks.formatter via a new utcTickFormatter(granularity) that reuses formatBucketLabel from date-range.ts (timeZone:'UTC' + a " UTC" suffix). It accepts the tick as number | Date (both forms @carbon/charts may pass). bucket defaults to '1m', so the UTC guarantee is structural — a caller that forgets still gets UTC ticks, never viewer-local. LineTimeSeries gained a bucket?: SeriesBucket prop; ExecutiveView passes bucket={displayBucket} to all three line charts.

  • RED: chart-options.test.tsformatter(new Date('2026-07-06T09:30:00Z')) expected '09:30 UTC' → failed (no formatter present).
  • GREEN: returns '09:30 UTC' (1m, time-only), 'Jul 5, 10 UTC' (1h, +date), and works with an epoch-ms tick and with no bucket supplied. Wrapper-threading asserted in chart-wrappers.test.tsx.

Scope note: the axis-tick formatter is the review's finding and what the brief scoped. The chart tooltip uses a separate data-formatter hook (out of this brief's scope); the axis + the built-in table now read UTC.

Folded fix 2 (MINOR): transient rate-label/data mismatch

Problem: on a bucket-changing preset switch (24h→7d, 1m→1h), keepPreviousData keeps the prior 1m-bucket points on screen while the 1h series refetches — briefly showing them under a "per hour" title (a 60× throughput misread).

Fix (ExecutiveView.tsx): track the bucket the currently-settled engine series belongs to via a render-updated ref (settledBucketRef, committed only when !isPlaceholderData). While a switch is inflight AND that settled bucket ≠ the selected range's bucket (bucketSwitching), the rate label follows the settled bucket (displayBucket) and the three time-series panels show loading (timeSeriesLoading = isPending || bucketSwitching) — so a stale-bucket series is never shown under the new bucket's rate. The bucket-independent breakdown panels (range totals) are unaffected.

  • RED: new populated-fixture test holds the 7d (1h) engine fetch open and asserts the panels are NOT labeled "per hour" during the window → failed (label flipped immediately).
  • GREEN: during the held window the panels show panel-loading and read "per minute"; on release they commit to "per hour". (The existing EMPTY_FIXTURES test can't see this window — no points to mislabel — which is why a populated fixture was added, per the brief.)

Folded fix 3 (MINOR): status bars semantic color

Problem: "Calls by status" bars used the entity categorical scale (OK→purple, Error→blue), throwing away the one dimension where color should carry meaning. dataviz: status colors are reserved and never a categorical slot.

Fix (chart wrapper's color scale): new status-colors.tsstatusColorScale(groups, theme) maps ok→support-success / error→support-error using Carbon's own chart-surface tokens per theme — g10: ok #24a148, error #da1e28; g90: ok #42be65, error #ff8389 (lifted from @carbon/charts styles.css --cds-support-*, keyed by theme — the palette-level half of the set-both rule, mirroring entity-colors.ts). buildBarOptions/BarBreakdown gained a colorRole?: 'entity' | 'status' switch; ExecutiveView passes colorRole="status" to the status chart only. Identity is still never color-alone — the bars are x-axis-labelled OK/Error. Unrecognized labels fall back to the entity scale (defensive).

  • RED: expected { OK:'#24a148', Error:'#da1e28' } (g10) / { OK:'#42be65', Error:'#ff8389' } (g90) → failed (entity slots returned).
  • GREEN: both themes return the reserved scale; a default (entity) breakdown is asserted to NOT contain the status green/red. Wrapper-threading asserted in chart-wrappers.test.tsx.

Task 7 own scope

Residual-Tailwind / shadcn / recharts / cn( grep — ZERO real hits.
- Every className value is either a semantic BEM class (.stat-tile, .panel__body, .dash-grid, …) or a Carbon type token (cds--type-*). No Tailwind utilities (flex, gap-N, text-sm, bg-*, p-N, rounded, shadow, …). The only regex hits were grid inside the BEM names calls-grid/dash-grid — not the Tailwind grid utility.
- No components/ui/* (shadcn), no recharts import (one stale comment in src/test/setup.ts, harmless), no clsx/class-variance-authority/tailwind-merge/cn(.
- No tailwind.config.* / postcss.config.* present.

Dead deps: none. package.json has no tailwind/shadcn/recharts/clsx/cva/autoprefixer/postcss/lucide. All 9 runtime deps are imported in src (verified count per dep). Nothing to remove.

Light/dark verification (the set-both rule, end-to-end):
- ExecutiveView.test.tsx new test mounts the view under g90 (localStorage) with populated fixtures so all six charts render, then asserts every captured chart's options.theme === 'g90' and that both the line and bar families actually rendered.
- Wrapper-level theme:'g90' under the dark provider already covered for Line + Bar in chart-wrappers.test.tsx.
- Operator/Auditor views are chart-free (Carbon Datagrid follows the <Theme> provider — no chart-theme to set). Spacing/type tokens are uniform across views (Carbon Grid/Stack/gap, --cds-spacing-*, cds--type-*). Full visual light+dark screenshots are Task 8's exit gate.


Files

File Change
src/components/charts/chart-options.ts UTC utcTickFormatter on line bottom-axis (fix 1); colorRole → status scale switch (fix 3)
src/components/charts/status-colors.ts NEW — reserved ok/error semantic scale, per-theme Carbon tokens (fix 3)
src/components/charts/LineTimeSeries.tsx bucket?: SeriesBucket prop → formatter granularity (fix 1)
src/components/charts/BarBreakdown.tsx colorRole?: 'entity'\|'status' prop (fix 3)
src/views/ExecutiveView.tsx pass bucket to line charts (fix 1); settled-bucket race guard (fix 2); colorRole="status" (fix 3)
src/components/charts/chart-options.test.ts NEW — fix 1 UTC formatter + fix 3 status-scale unit tests
src/components/charts/chart-wrappers.test.tsx wrapper-threading tests for fixes 1 & 3
src/views/ExecutiveView.test.tsx fix 2 held-fetch race test + light/dark end-to-end test

Concerns / notes

  • None blocking. The 500 kB chunk-size warning at build is pre-existing (Carbon + Plex is heavy) and expected — a warning, not an error; Task 8 owns the image-size call.
  • No data/logic-layer files touched; all carry-forward invariants intact.