Skip to content

refactor(theme): rename chart palette tokens to hue names + unify across themes#2362

Open
elizabetdev wants to merge 18 commits into
mainfrom
agent/rename-chart-palette-tokens
Open

refactor(theme): rename chart palette tokens to hue names + unify across themes#2362
elizabetdev wants to merge 18 commits into
mainfrom
agent/rename-chart-palette-tokens

Conversation

@elizabetdev
Copy link
Copy Markdown
Contributor

@elizabetdev elizabetdev commented May 28, 2026

Summary

Renames the chart palette tokens introduced in #2265 from numeric (chart-1chart-10) to hue-named (chart-blue, chart-orange, chart-red, chart-cyan, chart-green, chart-pink, chart-purple, chart-light-blue, chart-brown, chart-gray) and unifies the categorical palette across HyperDX and ClickStack onto Observable 10, with chart-blue swapped to the brand link color #437eef. Also unifies semantic chart tokens (success, warning, error, info) across both brands.

Why now: the API parity follow-up (#2359) hasn't shipped yet, so this is the last window where the schema is internal-only. Once chart-1..10 lands on the external API surface, renaming becomes a breaking API change. Doing it now is a zero-cost internal cleanup.

Why hue names: the user-facing picker shipped with placeholder labels ("Color 1", "Color 2", …) because numeric tokens had no semantic content. Hue-named tokens are self-documenting in the picker UI, in stored configs, and (soon) in the public API.

Why unify the palette: per-theme divergent slot-1 (HyperDX green vs ClickStack blue) was a clever trick but invisible to users — the picker just showed swatches with opaque labels. With hue names, chart-blue resolving to a different color per theme would be a bug, not a feature.

Why unify the semantic layer too: pre-rename, success and info resolved to different hexes per brand (HyperDX brand green vs Observable green for success; HyperDX green vs ClickStack blue for info). With the categorical palette unified, having only the semantic layer diverge by brand stopped being a meaningful identity signal — chart colors moved entirely off brand identity duty, while non-chart UI chrome (Mantine accent, sidebar gradient, Click UI globals) still differentiates the two brands. Picking categorical hues for the semantic vars (successchart-green #3ca951, infochart-blue #437eef) keeps the chart palette internally consistent on both brands.

Why customize chart-blue: pure Observable blue (#4269d0) is close to but not identical with the brand link color (#437eef, already used for --click-global-color-text-link-default and outline). Aligning the categorical chart-blue with the brand blue means primary-series charts, link-styled UI, and info-level logs all read as the same hue.

What changes

Categorical palette (unified)

  • Both themes resolve --color-chart-{hue} to the same hex — based on Observable 10 with chart-blue swapped to #437eef. All other hues unchanged from Observable 10.
  • 10 hues: blue, orange, red, cyan, green, pink, purple, light-blue, brown, gray.
  • File-private CATEGORICAL_HEX_BY_TOKEN in utils.ts replaces the dual CHART_PALETTE + CLICKSTACK_CHART_PALETTE for categorical colors. COLORS is derived from CATEGORICAL_PALETTE_TOKENS so positional access stays consistent.
  • JS is the source of truth for categorical hues. getColorFromCSSVariable(i) and getColorFromCSSToken('chart-{hue}') short-circuit straight from CATEGORICAL_HEX_BY_TOKEN, skipping the getComputedStyle round-trip — since the palette is unified across themes, the DOM read was buying back the same hex via a per-series layout read. The --color-chart-{hue} CSS vars remain in the shared SCSS partial as a stylesheet-author affordance (one inline var() consumer in LogLevel.tsx, plus devtools inspection) and a future per-brand override hook.

Semantic palette (unified)

  • New --color-chart-info token preserves the "info-level logs render brand-primary" idiom from before — both brands now resolve info to categorical chart-blue (#437eef).
  • --color-chart-success resolves to categorical chart-green (#3ca951) on both brands (was HyperDX brand green #00c28a / ClickStack Observable green #3ca951).
  • --color-chart-warning (#efb118) and --color-chart-error (#ff725c) were already shared.
  • All four semantic vars are declared once in a shared chart-semantic-tokens SCSS mixin alongside the categorical mixin in _chart-categorical-tokens.scss. Each theme's chart-tokens mixin just @includes both mixins — no duplicated semantic block per brand.
  • SEMANTIC_CHART_PALETTE in utils.ts keeps a {hyperdx, clickstack} shape for SSR fallback (currently identical) so a future per-brand override is a one-line change in both layers.
  • New getChartColorInfo() helper backs the --color-chart-info token. The three getColorFromCSSVariable(0) callsites previously commented as "primary chart color (blue for ClickStack, green for HyperDX)" now route through getChartColorInfo() so the semantic stays intact under unified categorical ordering.
  • DBRowTable's pattern-trend bar fill switches from COLORS[0] to getChartColorInfo(), preserving the brand-primary intent under the unified palette.

Schema + migration (stored configs from #2265 keep working)

  • CHART_PALETTE_TOKENS in common-utils/types.ts becomes the hue-named enum.
  • LEGACY_CHART_PALETTE_TOKEN_MAP records the HyperDX slot-order mapping (chart-1 → chart-green, chart-2 → chart-blue, …, chart-10 → chart-gray).
  • ChartPaletteTokenSchema stays strict — a plain z.enum(CHART_PALETTE_TOKENS). Wrapping it in z.preprocess was tempting but would force the schema's z.input type to unknown, which poisons validateRequest's req.body inference all the way up to Dashboard.tiles[i].config.color in the API package. Legacy data heals at three complementary layers instead:
    • Fetch-time: normalizeDashboardTileColors in packages/app/src/dashboard.ts walks every tile in the response from useDashboards / fetchLocalDashboards / fetchDashboards (remote path covered by dashboard.remote.test.ts) and rewrites any legacy config.color to its hue-named equivalent via resolveChartPaletteToken.
    • Write-time: useUpdateDashboard / useCreateDashboard also run the normalizer before persisting, so JSON imports, presets, and MCP-constructed payloads pass the strict server-side validator and converge stored data on next save.
    • Render-time: DBNumberChart and ColorSwatchInput also call resolveChartPaletteToken as belt-and-suspenders for any code path that bypasses the fetch normalizer (e.g. an in-memory tile constructed from a Tile literal).
  • Coverage in guards.test.ts asserts every legacy slot maps to its expected target and that unknown strings still fail validation.

UI

  • ColorSwatchInput's TOKEN_LABELS switches from "Color 1" … "Color 10" to "Blue", "Orange", "Red", … — the picker is now self-documenting.
  • ChartColors.stories.tsx derives swatches from CATEGORICAL_PALETTE_TOKENS so adding a hue requires no story edits beyond the label. Semantic preview includes Info (Blue) and notes that all four semantic tokens are unified across brands.

SCSS structure

  • Shared partial _chart-categorical-tokens.scss exports two mixins: chart-categorical-tokens (10 hues) and chart-semantic-tokens (success / warning / error / info + highlights, with success and info aliased to var(--color-chart-green) / var(--color-chart-blue)).
  • Each theme's _tokens.scss chart-tokens mixin is a 2-line @include of both shared mixins — no per-brand semantic duplication. A future per-brand override is a single declaration after the shared @includes in the relevant brand's mixin.

Docs

  • agent_docs/data_viz_colors.md rewritten end-to-end: TL;DR table, "Where the colors live" section, recipes, anti-patterns, and pre-merge checklist all reflect the unified palette + shared mixin structure. Also documents the JS-source-of-truth design for categorical hues, the brand-blue customization, and the per-layer migration strategy.

What does NOT change

  • --color-chart-warning and --color-chart-error keep their hex values.
  • HyperDX brand green (#00c28a) is still the Mantine accent, button, and sidebar gradient color — only chart success moved off it to categorical Observable green.
  • All existing chart helpers (getChartColorSuccess/Warning/Error/Info, getColorProps, logLevelColor, semanticKeyedColor, etc.) keep the same signatures and return types.
  • The number-tile color picker workflow from [HDX-1360] feat(app): number tile static color picker #2265 is unchanged for users — same Display Settings drawer, same ColorSwatchInput, same schema field.

Visible color changes

  • HyperDX info-level logs / neutral series: now categorical chart-blue #437eef via --color-chart-info (was brand green #00c28a via SSR-fallback collapse with success).
  • HyperDX success indicators in charts: now categorical chart-green #3ca951 via --color-chart-success (was brand green #00c28a). Non-chart success UI (buttons, etc.) still uses brand green via Mantine.
  • ClickStack info-level logs: now categorical chart-blue #437eef via --color-chart-info. Previously rendered as Observable blue (#4269d0) at runtime via the per-brand --color-chart-1, with an SSR fallback bug that returned HyperDX green — this PR cleans that up AND aligns it with the brand link color in one move.
  • chart-blue categorical: hex changes from Observable blue (#4269d0) to brand blue (#437eef) on both themes. Any saved tile that explicitly picked the (then-numeric) blue slot will now render the brand blue. Legacy chart-2 saves on HyperDX map to chart-blue via LEGACY_CHART_PALETTE_TOKEN_MAP and pick up the new hex.
  • COLORS[0] (positional palette index, e.g. for SSR placeholders): was #00c28a (HyperDX brand green) on both themes, now #437eef (brand blue) on both themes — but in browser, all real chart series resolve via the helpers anyway, so this only affects the SSR window for unkeyed positional access.

Follow-ups (not in this PR)

  • [HDX-1360] External API parity for number-tile color #2359 should be authored against the renamed enum once it lands; the changeset notes the timing.
  • Once the API parity work merges, the LEGACY_CHART_PALETTE_TOKEN_MAP migration can be revisited (e.g. one-shot DB migration to rewrite stored values, then drop the resolver and the fetch/write-time normalizers).

Test plan

  • yarn workspace @hyperdx/common-utils ci:unit clean.
  • yarn workspace @hyperdx/app unit clean.
  • yarn workspace @hyperdx/app lint clean.
  • yarn workspace @hyperdx/common-utils lint clean.
  • yarn workspace @hyperdx/app tsc --noEmit clean.
  • Vercel preview / Storybook: open Design Tokens / Chart Colors and confirm all four semantic swatches (success / warning / error / info) resolve to the unified hexes (#3ca951, #efb118, #ff725c, #437eef) on both Brand toolbar values.
  • Vercel preview: open a Number tile's Display Settings, verify the swatch labels now show "Blue", "Orange", …; pick a hue and confirm persistence + theme reflow (HyperDX dark/light, ClickStack dark/light).
  • Vercel preview: open a saved tile from before this PR (with color: chart-N in the config) and confirm it still renders the expected hue via resolveChartPaletteToken (render-time) and gets healed by normalizeDashboardTileColors (fetch-time + write-time).
  • Vercel preview: spot-check that chart-blue now visually matches the brand link color across both themes, info-level logs render the chart blue, and success pills/charts render the unified Observable green.

Closes the renaming half of the discussion that was previously explored in (now-closed) #2276. The "unify" half also lands here, scoped across both the categorical and semantic layers — chart color identity moves entirely off brand identity duty.

…oss themes

Renames the user-facing chart palette tokens from numeric (chart-1..10)
to hue-named (chart-blue, chart-orange, ...) so stored chart configs and
the upcoming external API surface are self-documenting. Also unifies the
categorical palette across HyperDX and ClickStack onto Observable 10;
brand identity moves to the new --color-chart-info semantic token plus
the existing per-brand --color-chart-success.

Stored configs from #2265 keep working: a Zod preprocess on
ChartPaletteTokenSchema transparently maps chart-1..10 to their
hue-named equivalents at parse time, preserving the HyperDX slot
ordering (slot 1 = brand green = chart-green, etc.).

User-visible: the color picker now shows readable swatch labels
(Blue, Orange, Red, ...) instead of "Color 1", "Color 2", ...
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 28, 2026

🦋 Changeset detected

Latest commit: b1e5059

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/common-utils Minor
@hyperdx/app Minor
@hyperdx/api Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment May 29, 2026 2:09pm
hyperdx-storybook Ready Ready Preview, Comment May 29, 2026 2:09pm

Request Review

@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label May 28, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

🔴 Tier 4 — Critical

Touches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD.

Why this tier:

  • Critical-path files (1):
    • packages/api/src/tasks/provisionDashboards/index.ts
  • Cross-layer change: touches frontend (packages/app) + backend (packages/api) + shared utils (packages/common-utils)

Additional context: agent branch (agent/rename-chart-palette-tokens)

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 19
  • Production lines changed: 1190 (+ 885 in test files, excluded from tier calculation)
  • Branch: agent/rename-chart-palette-tokens
  • Author: elizabetdev

To override this classification, remove the review/tier-4 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

E2E Test Results

All tests passed • 192 passed • 3 skipped • 1261s

Status Count
✅ Passed 192
❌ Failed 0
⚠️ Flaky 3
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 28, 2026

Deep Review

🟡 P2 -- recommended

  • packages/api/src/controllers/dashboard.ts:91 -- GET /dashboards returns stored tile.config.color verbatim, so dashboards saved on the pre-rename schema come back from the server with chart-1..chart-10 and only get healed if the consumer happens to be the React app's normalizeDashboardTileColors; any non-React HTTP client (or a stale-bundle tab receiving healed values during an asymmetric deploy) can round-trip those legacy tokens back through PATCH where the pre-PR server validator rejects them with a 400.
    • Fix: Apply the same legacy-token rewrite inside getDashboards/getDashboard (or after toJSON()) so the wire format always emits canonical hue-named tokens, and add a regression test that GETs a Mongo-seeded chart-1 doc and asserts a hue-named return.
    • api-contract, adversarial
  • packages/app/src/dashboard.ts:79 -- when resolveChartPaletteToken returns undefined, normalizeDashboardTileColors silently strips config.color, so a tile with a stored hex (hand-edited document, forward-deployed future token) loses its color on fetch and the very next unrelated edit persists with no color field at all, irreversibly overwriting the user's choice.
    • Fix: Either leave the unresolvable field intact and let the strict schema surface the error on save (matching normalizeRawDashboardTileColors's import-time policy), or console.warn/telemetry-log before stripping so silent data loss is observable.
    • adversarial, kieran-typescript
  • packages/api/src/tasks/provisionDashboards/index.ts:17 -- the inlined migrateLegacyDashboardTileColorsInPlace walker is a near-byte-for-byte copy of the API router middleware but has no direct test in provisionDashboards.test.ts, so a regression in either copy (or quiet drift between them) ships unnoticed even though the API router copy is covered.
    • Fix: Add a readDashboardFiles test that writes a JSON file containing tiles[0].config.color === 'chart-1' and asserts the parsed DashboardWithoutId carries color: 'chart-green', plus one case for the non-array tiles early-return.
    • testing, maintainability, kieran-typescript
🔵 P3 nitpicks (5)
  • packages/api/src/routers/api/dashboards.ts:36 / packages/api/src/tasks/provisionDashboards/index.ts:17 / packages/app/src/dashboard.ts:104 -- three near-identical unknown-walking implementations of the same legacy-color rewrite differ only in mutation strategy; resolveChartPaletteToken already lives in common-utils, so the walker can too.
    • Fix: Extract walkRawDashboardTileColors(input, onColor) into common-utils and have all three sites compose over it.
    • maintainability, kieran-typescript
  • packages/app/src/utils.ts:474 -- the as readonly CategoricalChartPaletteToken[] cast on CATEGORICAL_PALETTE_TOKENS in the COLORS construction is a no-op tuple-to-array widen that reads like a narrowing assertion.
    • Fix: Drop the cast and .map directly on the tuple, or replace with a one-line comment explaining it is purely a tuple-to-array widen.
  • packages/app/src/utils.ts:437 -- SEMANTIC_CHART_PALETTE's hyperdx and clickstack sub-objects are byte-identical, and the doc-comment's "future override hook" rationale invites readers to expect a planned divergence that has no scheduled work behind it.
    • Fix: Either collapse to a single object now (and reintroduce per-brand entries when a brand actually diverges) or trim the comment to the parity-enforcement justification alone.
  • commit history on this branch -- multiple commits between 33eac72d and HEAD carry Co-Authored-By: trailers (Cursor <cursoragent@cursor.com>, Claude Opus 4.7 <noreply@anthropic.com>), which AGENTS.md explicitly forbids on commits to this repo.
    • Fix: Squash-merge so the merge commit (controlled by the merge UI) does not carry trailers, or rewrite the branch history to strip them before merge.
  • packages/app/src/components/DBRowTable.tsx:286 -- fill={color || getChartColorInfo()} invokes getComputedStyle on every row render of PatternTrendChart rather than reading from a per-component memo or context.
    • Fix: If the per-render cost becomes measurable, lift the call into a useMemo keyed on theme class; otherwise leave the comment trail noting the conscious trade-off so the next reader doesn't re-litigate.

Reviewers (10): correctness, testing, maintainability, project-standards, kieran-typescript, api-contract, adversarial, previous-comments, agent-native, learnings-researcher.

Testing gaps:

  • No HTTP integration test exercises a GET-then-PATCH round-trip on a Mongo-seeded legacy-token document (current API tests only assert middleware on synthetic POST/PATCH bodies).
  • No unit test pins the strip-unknown-color path in normalizeDashboardTileColors (the resolved === undefined branch that overwrites user-chosen hex/future-token values).
  • No direct unit test for the provisioner's inlined migrateLegacyDashboardTileColorsInPlace walker — parity with the API router copy is currently enforced only by inspection.

…c gradient defs

Two regressions reported against the hue-named palette refactor:

1. Stored Number-tile colors from #2265 (chart-1..chart-10) were silently
   dropped on dashboard load. `useDashboards` raw-casts the API response
   (`hdxServer('dashboards').json<Dashboard[]>()`), so
   `ChartPaletteTokenSchema`'s `preprocess` never runs and
   `isChartPaletteToken('chart-1')` resolves `tileColor` to undefined
   until the user re-saves the tile.

   Add a new `resolveChartPaletteToken(value)` helper in common-utils
   that accepts both current hue-named tokens and legacy chart-1..10
   values. Use it in DBNumberChart (last-mile resolution before
   `getColorFromCSSToken`) and in ColorSwatchInput's `safeValue` guard
   so the picker also shows the migrated swatch when reopening a tile
   saved with a legacy token.

2. After unifying `COLORS` to the Observable 10 palette, HyperDX
   info-level <Area> fills in HDXMultiSeriesTimeChart referenced
   `url(#time-chart-lin-grad-<id>-00c28a)`, but the gradient <defs>
   block iterates `COLORS`, which no longer contains the HyperDX
   brand-green `#00c28a` returned by `getChartColorInfo()`. Info-level
   series rendered with no area fill.

   Build the gradient defs from the union of `COLORS` and every
   `lineData[i].color` actually in use, so any semantic color
   (`info`/`success`/`warning`/`error`) returned by the per-brand
   helpers is guaranteed to have a matching gradient def.

Regression tests cover both bugs (resolveChartPaletteToken in
guards.test.ts; DBNumberChart legacy-token render path; ColorSwatchInput
legacy-token displays migrated swatch).
…ors at fetch time

The previous fix wrapped `ChartPaletteTokenSchema` in `z.preprocess` to
migrate legacy `chart-1`..`chart-10` tokens at parse time. That forces
the schema's `z.input` type to `unknown`, which `validateRequest`
(from `zod-express-middleware`) then propagates all the way up to
`req.body.tiles[i].config.color`. The mismatch with `createDashboard`'s
parameter (typed via `z.infer` = output = `ChartPaletteToken | undefined`)
broke `packages/api/src/routers/api/dashboards.ts` with TS2345.

Strict input/output equality is more important than burying a migration
in the schema. So:

- Revert `ChartPaletteTokenSchema` to plain `z.enum(CHART_PALETTE_TOKENS)`.
  `z.input` == `z.output` again; the api compiles.
- Move the legacy migration to a fetch-time normalizer in
  `packages/app/src/dashboard.ts` (`normalizeDashboardTileColors`).
  `useDashboards` and `fetchLocalDashboards` now run every tile through
  it, so the in-memory copy is healed immediately and the persisted
  copy is updated the next time the dashboard is saved for any reason.
  Unknown strings pass through untouched (no silent data loss); hue
  tokens pass through unchanged with tile identity preserved.
- Keep the render-time `resolveChartPaletteToken` calls in
  `DBNumberChart` and `ColorSwatchInput` as defense in depth for any
  tile constructed in memory (presets, unit tests, etc.) that bypasses
  the fetch path.

Tests:

- New `fetchLocalDashboards` cases in `dashboard.test.ts` cover the
  fetch-time normalizer end-to-end (chart-1..10 migrated, hue tokens
  pass through, unknown strings preserved, tiles without `color`
  untouched).
- `ChartPaletteTokenSchema` test suite tightened: it now asserts that
  the schema rejects `chart-1`..`chart-10` (with a comment pointing
  consumers at the app-side normalizer and `resolveChartPaletteToken`).
- `resolveChartPaletteToken` tests in `guards.test.ts` are unchanged
  and still cover the legacy → hue mapping.

Docs:

- `agent_docs/data_viz_colors.md` rewritten to explain the
  two-layer migration (fetch + render) and the rationale for not
  using `z.preprocess`.
- Changeset updated.
…leanups

Three follow-ups on Elizabet's chart palette rename PR after the
deep-review pass:

- Drop the orphan `isChartPaletteToken` re-export in
  packages/app/src/utils.ts. Knip flagged it; the two app-side
  consumers (ColorSwatchInput, DBNumberChart) both moved to
  `resolveChartPaletteToken` earlier in the PR, so nothing in
  app/ imports it anymore.
- HDXMultiSeriesTimeChart: filter `lineData[].color` to defined
  strings before unioning into the gradient `Set`. The downstream
  `c.replace('#', '')` would throw on `undefined` if any future
  caller bypassed `setLineColors`.
- common-utils: update the JSDoc on `resolveChartPaletteToken` to
  describe the real remaining bypass paths (in-memory tile
  construction, ChartEditor form state, unit-test fixtures). The
  previous comment cited `useDashboards` as the bypass, but the
  new fetch-time `normalizeDashboardTileColors` heals
  `useDashboards` / `fetchLocalDashboards` payloads, so the
  rationale needed a refresh.

Validated locally: lint, tsc, knip clean; common-utils unit
(1145/1145) and app unit across the touched surface area
(627/627) pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alex-fedotyev
Copy link
Copy Markdown
Contributor

Pushed e103ad3 to take three of the deep-review items off your plate.

Knip: dropped the orphan isChartPaletteToken re-export in packages/app/src/utils.ts. The two app-side consumers (ColorSwatchInput, DBNumberChart) both moved to resolveChartPaletteToken earlier in the PR, so nothing in app/ still imports it; the common-utils-level export stays valid (used internally by resolveChartPaletteToken and directly by guards.test.ts).

P3 defensive filter on the gradient Set: HDXMultiSeriesTimeChart now filters lineData[].color to defined strings before unioning into the Set, so c.replace('#', '') can't blow up on a future caller that leaves a series color unset.

P3 stale JSDoc on resolveChartPaletteToken: rewrote it to point at the real remaining bypass paths (in-memory tile construction, ChartEditor form state, unit-test fixtures). The previous comment cited useDashboards as the bypass, but normalizeDashboardTileColors now heals everything that comes through useDashboards / fetchLocalDashboards.

Validated locally: lint, tsc, knip clean; common-utils unit (1145/1145) and app unit across the touched surface area (627/627) pass.

What I deliberately did not touch and left for you:

  • P2.1 (dashboard.ts:60, normalizer is in-memory only): any PATCH path that doesn't first round-trip through fetchDashboards will still hit the strict z.enum on the API side with a 400. Two reasonable directions (resolve in the PATCH handler vs. write the healed dashboard back once per session). Feels like a design call rather than a mechanical fix.
  • P2.2 (ClickStack chart-1 silent flip from blue to green): the legacy map uses HyperDX slot ordering, but ClickStack's pre-PR --color-chart-1 resolved to blue. Worth either branching the map by active theme, or calling the shift out explicitly in the changeset and agent_docs/data_viz_colors.md. Your call on which way to frame it.
  • P2.3 (remote fetchDashboards normalization coverage): the new "legacy tile color migration" suite only exercises IS_LOCAL_MODE: true. Happy to add a remote-path test if you'd like, just didn't want to commit one without a chat first.
  • P3 SSR fallback wording in the PR description / changeset: getSemanticChartColor still defaults to HyperDX during SSR, so the "previously rendered as Observable blue at runtime ... cleaned up here" framing is a bit overstated. Quick wording tweak in the changeset would do it.
  • P3 DBRowTable per-render getChartColorInfo() call: debatable trade-off; either memoize or leave a one-line comment about the per-row cost being acceptable. Left this alone since either direction reads as taste.
  • E2E shard 3 (custom-time-range + relative-time live tail): both failures are in tests/e2e/features/search/. The PR doesn't touch any search / time-picker / live-tail code, and recent main runs are green. Symptoms look like classic timing flake (element(s) not found after picker close, plus 1.0m timeouts on retries). I'd treat as unrelated unless re-running surfaces a pattern.
  • BEHIND main: noted but not touched. You'll want a rebase or merge before merging.
  • Two unchecked Vercel boxes in the test plan are still the only path that exercises the cross-theme reflow under real CSS-var resolution (jsdom can't sub for that). Worth a 5-minute pass through the preview when you get a moment.

LMK if you want me to pick up any of the above.

…lor retrieval

Updated the data visualization color documentation to clarify the unified categorical color palette based on Observable 10, with `chart-blue` adjusted to match the brand link color. The JavaScript source of truth for categorical hues is now emphasized, eliminating unnecessary CSS variable reads. The `getColorFromCSSToken` and `getColorFromCSSVariable` functions have been refactored to directly return values from `CATEGORICAL_HEX_BY_TOKEN`, improving performance by avoiding layout reads. Tests have been updated to reflect these changes, ensuring consistent behavior across themes.
…nd ClickStack info alignment

Co-authored-by: Cursor <cursoragent@cursor.com>
The 10 categorical chart hues are unified across HyperDX and ClickStack,
so duplicating them in each theme's `chart-tokens` mixin was redundant.
Extract them into a single shared `_chart-categorical-tokens.scss`
partial that both theme files `@use` and `@include`. Per-brand semantic
tokens (success, info, warning, error) stay local to each theme's
mixin, since those genuinely vary by brand.

The compiled CSS is byte-identical to before — only comments differ,
and selector specificity is preserved because Sass still inlines the
mixin body at each `[data-mantine-color-scheme]` call site. Net -47
lines across the theme files despite adding the new partial.

Also update the JS-side sync comment in utils.ts to point at the new
shared SCSS source and refresh agent_docs/data_viz_colors.md (file
reference table, pre-merge checklist, "add a new hue" recipe now
require one SCSS edit instead of two).

Co-authored-by: Cursor <cursoragent@cursor.com>
elizabetdev and others added 2 commits May 28, 2026 21:22
Address Deep Review P2 findings on the chart palette rename PR:

1. PATCH/POST validation hole — the fetch-time normalizer only healed
   in-memory copies, so dashboards constructed outside the fetch path
   (JSON imports via DBDashboardImportPage, presets, MCP payloads)
   carried legacy `chart-1`..`chart-10` straight into the strict
   server-side `ChartPaletteTokenSchema` and returned a Zod 400. Apply
   `normalizeDashboardTileColors` symmetrically at write time in
   `useUpdateDashboard` and `useCreateDashboard` (both local and remote
   branches). This also converges the DB-side data on next save instead
   of holding legacy tokens in storage forever. The normalizer is now
   generic over `T extends { tiles?: Tile[] }` and returns the payload
   unchanged when tiles is omitted, so partial PATCHes still work.

2. ClickStack legacy slot-flip — pre-rename ClickStack used a different
   slot ordering than HyperDX (`--color-chart-1` was brand blue, not
   brand green; `--color-chart-2` was orange, not blue; etc.). Since
   the migration map preserves HyperDX slot ordering, any ClickStack
   dashboard saved via #2265 will visually shift. We chose this
   trade-off deliberately over branching the legacy map by
   `detectActiveTheme()`: `LEGACY_CHART_PALETTE_TOKEN_MAP` lives in
   common-utils (shared with the API), and migration is one-shot
   persisted on next save — theme-branching would couple common-utils
   to browser DOM state and still produce wrong results for users
   whose active theme changed since the original pick. Documented the
   trade-off prominently in the changeset and agent_docs so reviewers
   and future maintainers can find it.

3. Remote-path test coverage — the existing dashboard test suite mocks
   `IS_LOCAL_MODE: true` for the entire file, so the
   `hdxServer('dashboards').json<Dashboard[]>().then(...).map(
   normalizeDashboardTileColors)` branch had no coverage. Added
   `dashboard.remote.test.ts` (separate file because `jest.mock` is
   hoisted and per-test isolation via `jest.isolateModulesAsync` proved
   unreliable for swapping the constant). Exported `fetchDashboards`
   so it can be exercised directly. Tests cover: legacy migration on
   the remote path, hue-named tokens passing through unchanged, and
   tile identity preservation for the React reconciliation hot path.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ip orphan

Two follow-ups on #2362 after the latest pass:

- Re-drop the `isChartPaletteToken` re-export in
  packages/app/src/utils.ts. The earlier dedupe-and-cleanup commit
  removed it, but the subsequent "unify categorical color palette
  and streamline color retrieval" pass re-added it as part of the
  reordered export block. Nothing in `packages/app/` imports it
  (both former call sites moved to `resolveChartPaletteToken`),
  the common-utils-level export remains for `guards.test.ts`.
  Knip CI flagged this on the most recent push.

- Add a `dashboard.remote.test.ts` companion that exercises
  `fetchDashboards` in the non-local branch (`hdxServer('dashboards')
  .json<>()`). Symmetric coverage with the existing local-path
  suite in `dashboard.test.ts`; closes the deep-review's
  fetch-time normalizer testing gap (chart-1..chart-10 migration,
  hue passthrough, unknown preserved, missing-color skipped).
  Split into a separate file because `IS_LOCAL_MODE` is bound at
  module load and `jest.doMock` inside `jest.isolateModules` did
  not override the hoisted `jest.mock('../config', ...)` factory
  reliably here; a second file with its own top-level mocks is
  the cleanest fix.

  Required exporting `fetchDashboards` from
  `packages/app/src/dashboard.ts` (it's a thin wrapper around the
  same `normalizeDashboardTileColors` the existing local-path
  tests exercise). Comment flags it as test-only; the React layer
  should stay on `useDashboards`.

Validated locally: lint, tsc, knip clean; common-utils unit
(1145/1145), and the touched app suites (dashboard,
dashboard.remote, utils, ColorSwatchInput, DBNumberChart) all
pass (147/147 across the slice).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alex-fedotyev
Copy link
Copy Markdown
Contributor

Pushed 78cfce7 with two more follow-ups after your latest pass:

Knip regression revert: isChartPaletteToken was back in the packages/app/src/utils.ts re-export block (looks like it came back along with the reordered export block in 71aefdc). Dropped it again. CI knip should go green.

P2.3 deep-review item (remote fetchDashboards normalization coverage): added packages/app/src/__tests__/dashboard.remote.test.ts, a companion to the existing local-path suite. Five cases: legacy chart-1chart-green, chart-10chart-gray, hue-named token passthrough, unknown string preserved, tile with no color untouched. Asserts hdxServer('dashboards') is actually called so the structural symmetry with the local branch is locked in.

I tried first to add the remote suite into the existing dashboard.test.ts via jest.doMock inside jest.isolateModules, but IS_LOCAL_MODE is bound at module load and the hoisted top-level jest.mock('../config', () => ({ IS_LOCAL_MODE: true })) won out over the doMock override in this setup. Splitting into a separate test file with its own top-level mocks worked cleanly. Tradeoff: I had to add export to fetchDashboards in packages/app/src/dashboard.ts. Flagged it with a comment as test-only; the React layer should keep reaching for useDashboards.

Validated locally: lint, tsc, knip clean; common-utils unit (1145/1145), and the touched app suites all pass (147 across dashboard, dashboard.remote, utils, ColorSwatchInput, DBNumberChart).

Still on your plate from the deep review:

  • P2.1 (dashboard.ts:60, in-memory normalizer): the PATCH path that doesn't first round-trip through fetchDashboards will hit the strict ChartPaletteTokenSchema and 400 with a legacy color. Two reasonable directions (resolve in the PATCH handler vs write the healed dashboard back once per session). Feels like a design call to me.
  • P2.2 (ClickStack chart-1 flip from blue to green): the legacy map preserves HyperDX slot ordering, but ClickStack's pre-PR --color-chart-1 resolved to blue, so a ClickStack tile saved with the old "Color 1" will now render as brand green after the migration. The changeset rewrite leans on the "categorical palette is unified" framing, which works, though it doesn't call out the ClickStack-side flip explicitly. Branching the legacy map by active theme is the other option.
  • P3 DBRowTable per-render getChartColorInfo(): still there at line 272 (semantic token, so getColorFromCSSToken('chart-info') still hits getComputedStyle). Cheap enough that the trade-off is debatable. Memoizing risks stale value across theme toggle; a comment about the per-render cost being intentional is the other path.
  • BEHIND main: still applies, you'll want a rebase or merge before merging. Nothing alarming in the recent main commits that should conflict.
  • Two unchecked Vercel boxes in the test plan (picker labels visual + saved-tile-from-[HDX-1360] feat(app): number tile static color picker #2265 render across HyperDX/ClickStack dark/light) are still the only path that exercises real CSS-var resolution across themes.

LMK if you want me to pick any of these up.

…al cyan

Previously HyperDX `--color-chart-info` and `--color-chart-success` both
resolved to the brand green `#00c28a`, so info-level series and success
fills collapsed into the same color. Point `info` at Observable cyan
(`#6cc5b0`, the categorical `chart-cyan` hex) instead. Now both brands
follow the same pattern of having `info` reuse a categorical hue
(HyperDX → cyan, ClickStack → blue), and HyperDX's two semantic tokens
are visually distinct.

- HyperDX `_tokens.scss`: update `--color-chart-info` and the per-brand
  docblock describing the success/info split.
- `utils.ts`: mirror the new hex in `SEMANTIC_CHART_PALETTE.hyperdx.info`
  so the SSR/`getComputedStyle`-failure fallback matches the SCSS.
- `agent_docs/data_viz_colors.md`: update the semantic palette
  description and the HyperDX per-theme bullets.

Co-authored-by: Cursor <cursoragent@cursor.com>
…xio/hyperdx into agent/rename-chart-palette-tokens

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	packages/app/src/__tests__/dashboard.remote.test.ts
#	packages/app/src/dashboard.ts
Surface `--color-chart-info` in the semantic swatch list (Brand toolbar
aware) and add an Info Chart Colors By Brand story that compares
HyperDX cyan (#6cc5b0) vs ClickStack blue (#437eef) side by side.
Also fix the stale getChartColorInfo() JSDoc.

Co-authored-by: Cursor <cursoragent@cursor.com>
elizabetdev and others added 2 commits May 29, 2026 11:07
Update the rename-chart-palette-tokens changeset and data_viz_colors
Storybook reference so HyperDX `--color-chart-info` is documented as
Observable cyan (#6cc5b0), distinct from success green, and the new
InfoChartColorsByBrand story is listed.

Co-authored-by: Cursor <cursoragent@cursor.com>
…Stack

`--color-chart-success` and `--color-chart-info` now resolve to categorical
`chart-green` (`#3ca951`) and `chart-blue` (`#437eef`) on both brands. Warning
and error were already shared. With the categorical palette also unified, chart
colors carry no brand identity — that role moves to non-chart UI chrome
(Mantine accent, sidebar gradient, Click UI globals).

- Move semantic chart tokens into a shared `chart-semantic-tokens` SCSS mixin
  in `_chart-categorical-tokens.scss` alongside the existing categorical
  mixin. Each theme's `chart-tokens` is now a 2-line `@include` of both
  shared mixins; the previously-duplicated semantic block is gone. Per-brand
  overrides remain possible by declaring the var after the shared `@include`.
- `SEMANTIC_CHART_PALETTE.hyperdx.{success,info}` in `utils.ts` updated to
  `#3ca951` / `#437eef` to mirror the SCSS (ClickStack entries were already
  these hexes for success and info post-#2362).
- Remove the redundant `InfoChartColorsByBrand` Storybook story and the
  per-brand info constants — both columns rendered identical swatches once
  info unified. Semantic preview now describes all four tokens as unified.
- Update docs (`agent_docs/data_viz_colors.md`, changeset, JSDoc, inline
  comments) to reflect the unified semantic layer and shared-mixin layout.
- Update `utils.test.ts` SSR-fallback expectation to `#3ca951`.

HyperDX brand green (`#00c28a`) is unchanged for Mantine accent, buttons, and
sidebar gradient — only chart `success` moved off it.

Co-authored-by: Cursor <cursoragent@cursor.com>
Address all P0/P1, P2, and P3 items from the deep review on #2362.

P0/P1:
- DBDashboardImportPage now normalizes legacy chart-N tile colors before
  the strict DashboardTemplateSchema.safeParse, so templates exported
  from a pre-rename deploy import without an opaque enum error.

P2:
- Dashboards POST/PATCH routes mount a migrateLegacyDashboardTileColors
  middleware that rewrites legacy chart-N before validateRequest runs
  ChartPaletteTokenSchema. Catches non-React clients (stale-bundle tabs
  during rolling deploys, CI scripts, MCP, the upcoming external-API
  parity work) for a one-release deprecation window without weakening
  z.input/z.output equality. Same shim applied to the on-disk
  dashboard provisioner task.
- Remove duplicate useDashboards/useCreateDashboard/useUpdateDashboard/
  useDeleteDashboard from packages/app/src/api.ts; Spotlights.tsx now
  reaches for the canonical normalizer-guarded useDashboards in
  packages/app/src/dashboard.ts. Closes the stale React Query cache
  race surface.
- Add mutation-level tests for useCreateDashboard / useUpdateDashboard
  that capture the mutationFn and assert hue tokens in the outbound
  hdxServer body. Plus standalone normalizeRawDashboardTileColors tests
  covering the JSON-import walker. Plus a server-side integration test
  for the dashboards-router migration shim.
- Tighten CATEGORICAL_PALETTE_TOKENS / SEMANTIC_PALETTE_TOKENS to tuple
  literals (were `readonly ChartPaletteToken[]`, leaked semantic tokens
  into the categorical subset). Introduce CategoricalChartPaletteToken
  and add a forward `satisfies Record<...>` + reverse completeness
  check on CATEGORICAL_HEX_BY_TOKEN so any future drift between the JS
  hex map and the shared enum becomes a compile error. Also drops the
  manual `as keyof typeof` cast in the COLORS map.
- Extract collectMemoChartGradientHexes from the MemoChart defs JSX
  and unit-test it: categorical baseline, semantic hex union, dedup,
  and the undefined-filter guard. Recharts renders awkwardly in jsdom
  at the container-sized SVG layer, so the helper is the pinnable
  contract.
- Add inline pointer next to LEGACY_CHART_PALETTE_TOKEN_MAP for the
  ClickStack slot flip (already in changeset + agent_docs).
- Append color-chart-info and color-chart-success-highlight to
  semanticColorsGrouped.ts so designers see them in the inventory.
- Replace getChartColorInfo() / getChartColorError() in ChartUtils
  test expectations with concrete hexes (#437eef, #ff725c) so a
  helper regression can't pass-by-mirroring.

P3:
- CHART_PALETTE_TOKENS docstring updated to clarify chart-info is a
  render-time CSS var, not a picker enum value.
- semanticTokenFallback parameter narrowed to non-categorical tokens
  and the default branch now does an exhaustive `never` assertion
  (was returning COLORS[0] from an unreachable path).
- Collapse SEMANTIC_CHART_PALETTE to a SemanticChartHexes typed shape
  so both brand entries are pinned to parity through the type system.
- Alias --color-chart-warning / --color-chart-error through their
  matching categorical hue vars (var(--color-chart-orange) /
  var(--color-chart-red)) for symmetry with success/info.
- fetchDashboards export comment rewritten to clarify it's the shared
  queryFn (not test-only).
- LEGACY_CHART_PALETTE_TOKEN_MAP keyed by a narrow LegacyChartPalette
  TokenKey union so a typo in a legacy slot at edit time is a compile
  error.
- Drop the double cast `tile.config as { color?: unknown }` in
  normalizeDashboardTileColors now that SavedChartConfig types color
  properly.
- Symmetric normalization on the URL-state local setDashboard read +
  write paths.
- Parameterize the legacy slot test across all 10 slots in both the
  local and remote dashboard suites.
- Strip unresolvable color tokens (stale hexes, hand-edited values,
  future-rolled-back tokens) at the normalizer boundary so they don't
  sail into the strict server-side schema and 400 the next save.

Validated: lint clean across app/api/common-utils; tsc --noEmit clean
across app/api/common-utils; common-utils unit (1163/1163), app unit
(1793 passed / 9 skipped). The new dashboard-router migration test
type-checks and needs Docker (`make dev-int FILE=dashboard`) to run.

Co-authored-by: Cursor <cursoragent@cursor.com>
P2:
- Migrate legacy chart-N in GET dashboards controllers so the wire
  format emits canonical hue-named tokens. Pre-rename Mongo docs can
  no longer round-trip via non-React HTTP callers (GET -> unmodified
  PATCH) and 400 the strict ChartPaletteTokenSchema.
- Add integration test that seeds a chart-1 dashboard directly into
  Mongo, then asserts both list and single GET return chart-green.
- Revert silent strip of unresolvable colors in
  normalizeDashboardTileColors. Stale hexes / hand-edited values /
  forward-rolled future tokens now pass through so the strict
  server-side schema surfaces a clear error on next save instead of
  the normalizer quietly dropping the user's chosen color. Matches
  the import-time policy of normalizeRawDashboardTileColors.
- Add provisioner test pinning the inlined walker + non-array tiles
  early-return.

P3:
- Extract walkRawDashboardTileColors into common-utils. Four call
  sites (React fetch/write normalizer, JSON import pre-validation,
  API write shim, provisioner) now compose over a single per-tile
  traversal, with the migration policy supplied as a callback. Adds
  dedicated unit tests pinning the walker contract.
- Drop the no-op `as readonly CategoricalChartPaletteToken[]` cast on
  the COLORS construction. CATEGORICAL_PALETTE_TOKENS is already a
  readonly tuple of the narrow union so the .map callback is
  exhaustive without further assertion.
- Trim SEMANTIC_CHART_PALETTE comment to the parity-enforcement
  justification only (drop the aspirational "future override hook"
  framing).
- Add a one-line comment on DBRowTable's PatternTrendChart explaining
  why the per-row getChartColorInfo() call is intentionally not
  memoized (memoization would require a theme-class subscription this
  component doesn't already have, and the per-row getComputedStyle
  cost is sub-microsecond).

Co-authored-by: Cursor <cursoragent@cursor.com>
The dashboards router only exposes a list GET handler; single-dashboard
reads on the React side go through `useDashboards` and filter
client-side, so there is no `GET /dashboards/:id` route. The new
regression test asserted against both endpoints and the single GET
hit the express default 404. Drop the single-GET assertion; the list
path still exercises `getDashboards` → `healLegacyDashboardTileColors`,
and `getDashboard` is still covered in-process via `updateDashboard`'s
PATCH-time read.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants