Skip to content

Releasing latest from Stage to Prod#1544

Merged
dawsontoth merged 92 commits into
prodfrom
stage
Jul 22, 2026
Merged

Releasing latest from Stage to Prod#1544
dawsontoth merged 92 commits into
prodfrom
stage

Conversation

@dawsontoth

Copy link
Copy Markdown
Contributor

Hey neat! A bunch of this won't be seen until 5.2.0 gets in front of customers, but there's some cool stuff here, huh?

kylebernhardy and others added 30 commits July 13, 2026 22:13
… renames, dead theme plumbing

- 411 extensionful relative/alias imports rewritten extensionless
  repo-wide (the status feature was the only holdout convention-wise)
- 18 JSX-free .tsx modules renamed to .ts (factory re-export shims and
  derived recompute modules); request-rate stays .tsx (has JSX)
- the deprecated no-op theme prop chain removed end to end:
  SpecRegistryRendererProps, MetricRenderer pass-through, pipeline
  renderers, AnalyticsContext.theme + AnalyticsTheme, StatusTabs,
  ConnectionsPanel, StorageTab, TableSize charts; replication-latency's
  three theme-conditional banner backgrounds now use color-mix over CSS
  tokens (HeatmapMatrix keeps useResolvedTheme internally for its ramp)
- FallbackRenderer's vestigial window/nodes args dropped from
  MetricRenderer
- registry comments: response_200 wire-name exception, connections vs
  connection disambiguation
- test layout decision: colocated *.test.tsx is the going-forward
  convention; the existing __tests__ tree stays put (churn without value)

Fixes #1452

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rolls up nine verified findings from a post-merge review of the sweep:

- new --color-info token (maps to the existing --blue) restores the
  replication info banner, which rendered near-invisible gray after
  being pointed at --color-accent — a neutral surface token whose
  defined value beat the blue fallback (runtime-verified fixed)
- shared primitives/bannerStyle.ts replaces four diverging inline
  banner style copies, with fallbacks inside color-mix so banners
  survive contexts where tokens don't resolve
- 14 extensionful vi.mock/vi.importActual specifier strings made
  extensionless across 8 test files — empirically, a .tsx→.ts rename
  silently detaches such mocks (the test passes against the real
  module)
- allowImportingTsExtensions removed from both tsconfigs: with zero
  extensionful imports left, tsc (TS5097) now enforces the convention
  the sweep established
- 16 dead thin re-export shim files deleted; their ~15 test/panel
  importers retargeted to wrapperMetrics / mqtt-traffic directly
- type Theme un-exported (zero external importers remained)
- the Monaco user-code docstring example restored to '@/counter.ts'
  (end-user convention, deliberately unlike repo convention) and the
  FallbackRenderer dev hints now name both spec file forms

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
…s CSSProperties

Applied across all 13 retargeted files, not just the three the bot flagged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eme hooks

Review: the hook is not analytics-specific — it resolves the ThemeProvider's
.dark class for any JS code that must branch on effective theme. Chart-token
lookup (getChartColors) stays feature-local in analytics/lib/theme.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
…rker can't OOM

Automatic Type Acquisition streamed every `@types` declaration into the
language worker via `addExtraLib` with no size cap, no aggregate budget, and a
module-level dedupe set that never resets. With `setEagerModelSync(true)` those
extra libs are structured-cloned to the worker just like models, but the model
guards (`MAX_WORKER_MODEL_CHARS`, `MAX_APPLICATION_MODEL_CHARS_TOTAL`, the
150-model ceiling) don't see them — extra libs aren't `getModels()` entries and
are never swept on project switch — so they accumulate monotonically across
every project opened in a session until the clone buffer overflows with
"DataCloneError: Data cannot be cloned, out of memory." followed by "FAILED to
post message to worker", flooding the session and wedging the worker.

RUM caught this recurring in prod on 2026-07-13 (1,560 combined occurrences,
~400 per affected session), a regression of #1370 through the one worker-clone
path its fixes never covered.

Add `MAX_EXTRA_LIB_CHARS_TOTAL` (8 MB) and `canAdmitExtraLib`, and enforce them
in `receivedFile`: skip any single declaration over the per-file limit and stop
admitting once the session total is spent. A rejected lib degrades that package
to "cannot find module" — the same tradeoff `selectFilesWithinModelBudget`
already makes for skipped sibling models — instead of crashing the tab.

Closes #1499

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s spent

Short-circuit `acquireApplicationTypes` when `acquiredChars` has already reached
`MAX_EXTRA_LIB_CHARS_TOTAL`: the budget is monotonic and never reclaimed, so the
acquisition engine would otherwise walk the CDN and parse declarations only for
`receivedFile` to discard every one. Avoids the wasted network/CPU once the cap
is hit (per PR review).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion, log drops

Address PR review (kriszyp):

- The full-pass early-return was effectively dead: `acquiredChars` only advanced
  on an *admitted* file, so `acquiredChars >= MAX` was reachable only by an
  exact-fit landing and the network-churn skip practically never engaged. Move
  the accounting into an `ExtraLibBudget` that *seals* (`isSpent`) on the first
  aggregate overflow — a state that persists — so the early-return actually
  fires in the degraded long-session case it was written for.
- Silent drops now leave a breadcrumb: a one-time `console.warn` on the
  exhaustion transition and a `console.debug` per oversized declaration, so
  "why did IntelliSense stop resolving this package" is diagnosable without a
  repro.
- The stateful path is now tested: `ExtraLibBudget` is Monaco-free and unit
  tests cover accumulation, exact-fit-without-sealing, per-file oversize
  (which does not seal), the sealing overflow, and rejection-once-sealed.

No behavior change to the OOM invariant: `addExtraLib` remains hard-gated by
`admit()`, so the session total still can't exceed the cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…orktree

The "studio (worktree)" launch helper (.claude/preview-worktree.sh) failed to
start when run from a linked worktree:

- node_modules/.env.local were symlinked from $root (CLAUDE_PROJECT_DIR), which
  is the worktree itself — so the link pointed a worktree at its own node_modules
  and pnpm hit ELOOP. Derive the real main checkout from
  `git rev-parse --git-common-dir` instead.
- The launcher can inherit an older default Node than pnpm 11 requires (>=22.13);
  prepend the repo's pinned .nvmrc Node (or newest installed >=22) to PATH when
  the current node is too old.
- pnpm's pre-run deps check tried to reinstall/purge the symlinked node_modules
  (workspace-state mismatch), which would mutate the shared main checkout; skip it
  with --config.verify-deps-before-run=false.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses review feedback on #1503:
- Replace `sort -Vr` (a GNU / newer-BSD extension that stock `sort` on older
  macOS lacks) with a portable numeric sort, so the newest-installed-Node
  fallback can't crash under `set -e`.
- Strip a leading `v` from the parsed `.nvmrc` value (`${want#v}`), so a
  `vX.Y.Z`-style pin resolves to the correct nvm directory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ppened

Addresses review nit on #1503: the "Adjusted PATH to Node …" message printed
unconditionally inside the <22 block, even when neither the .nvmrc dir nor the
nvm fallback matched — falsely claiming success in the one case an operator
most needs accurate output. Track whether PATH actually changed (`adjusted`)
and gate the message on it, warning clearly otherwise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hovering any time-series panel on a Status tab now shows a synchronized
crosshair/tooltip on every other time-series panel on that tab, via
Recharts' native syncId with syncMethod="value" (index-based sync
mis-aligns sparse series that don't share point counts).

- StatusTabs puts syncId = `${entityId}:${tab}` into AnalyticsContext,
  so sync never crosses tabs or two instances' Status pages.
- LineChart / StackedAreaChart primitives read it through a new
  non-throwing useAnalyticsSyncId() hook; charts rendered outside the
  provider get no syncId.
- The expand-to-fullscreen dialog (the only fillParent caller) gets its
  own `${syncId}:expanded` scope: it never drives the panels behind the
  overlay, while a SmallMultiples panel expanded into the dialog keeps
  intra-dialog sync (a single-chart dialog is a sync group of one).
- TableSizeTrend (Storage tab, raw Recharts) gets the same treatment via
  an inExpandDialog prop threaded from StorageTab's renderTrend opts.
  Known limitation: its bucket grid is anchored at range.startTime while
  metric pipelines snap to epoch-aligned period boundaries, so exact
  value matches with sibling panels are rare until that grid is aligned
  (follow-up); mismatches degrade gracefully (tooltip hides).
- Heatmap and table-size snapshot panels are unaffected (not cartesian
  time-series wrappers).

Fixes #1454

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
…are chart sync props

Two review findings on the crosshair-sync PR:

1. Tab-switch refetch regression: adding `tab` to the ctxValue memo deps
   made every tab switch recompute Date.now() and mint a new [start, end]
   window — the window is baked into every panel's query key, so each
   flip re-keyed every query and defeated useAnalyticsRecords'
   staleTime-Infinity tab-flip cache. The window (and preset/bucketMs
   lookup) now lives in its own memo keyed only on [presetId, tick];
   ctxValue composes the stable timeRange with the tab-scoped syncId, so
   tab switches change only the syncId. Regression-tested: the new
   window-stability test fails on the previous ctxValue memo.

2. The `syncId`/`syncMethod` props expression was copy-pasted in
   LineChart, StackedAreaChart, and TableSizeTrend, with TableSizeTrend
   inventing an `inExpandDialog` alias for the primitives' `fillParent`.
   Extracted a useChartSyncProps(inDialog) hook next to
   useAnalyticsSyncId and renamed the alias to `fillParent` (still
   sync-scoping only; fillParent sizing remains unimplemented there).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
…s (review)

Inert today (the client is memoized per route mount) but a client rebuild
with an unchanged entityId now propagates instead of serving a stale one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
Adds a row of five stat tiles above the Health tab's panel grid — CPU
(p95, harper+user scopes summed), heap used, main-thread utilization,
error rate, and request p95 — so "is anything wrong right now?" reads at
a glance. Each tile shows the latest-bucket cluster value, a delta vs
the previous window of equal length (arrow + %, up-is-bad coloring), and
an axis-less inline-SVG sparkline of the current window; em-dash + no
delta when data is absent, skeletons while loading.

Tiles reuse useAnalyticsRecords + runPipeline: the current-window fetch
passes exactly the panels' arguments so it lands on their existing query
key (react-query dedupes to one POST); the shifted previous-window query
is the only new key — at most one extra get_analytics POST per metric
per window slide, and it shares the key prefix StatusTabs' in-flight
refresh guard scans. The error-rate tile projects 1 − total/count per
record under count-weighted-mean, which collapses algebraically to the
Σ-correct (Σcount − Σtotal)/Σcount — same arithmetic as the derived
error-rate panel.

Fixes #1457
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
… and prev-window caching

- CPU tile: constrain the scope sum to the harper/user dims via a new
  includeDims on KpiTileDef. The profiler also emits per-hot-function-
  location cpu-usage records (>100 hits at one location) whose samples
  are already counted inside the harper/user scope totals, so summing
  every 'path' dim double-counted CPU on busy nodes. A bucket missing
  an expected scope now gaps instead of presenting a partial sum as
  the total.

- Delta: useAnalyticsRecords now exposes isPlaceholderData, and the
  tile only computes a fresh delta when BOTH windows hold settled
  (non-placeholder) data, holding the last settled delta otherwise.
  Previously each refresh tick had a settle-gap where keepPreviousData
  kept isLoading false while one side still held the older window's
  rows, so computeDelta paired mismatched windows and flipped the
  arrow through a bogus near-zero reading.

- Previous window: quantize its boundaries to the bucket grid so
  consecutive refresh ticks within one bucket share a query key and
  hit the staleTime-Infinity cache (the current-window args stay
  byte-identical to MetricPanel's). Corrected the dedupe comments to
  the honest accounting: the success/duration panels live on the
  unmounted Requests tab, so those two current-window fetches are real
  POSTs per tick that seed the cache for a Requests visit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
… empty sparkline path (review)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
Clicking (or pressing Enter/Space on) a data-bearing replication-heatmap
cell now opens a near-fullscreen dialog titled `source → target` with
that pair's latency line chart over the selected window and quantile.

- HeatmapMatrix gains a generic optional onCellSelect(row, col) prop.
  Click and Enter/Space share one isActivatable guard: only 'ok'/'grey'
  cells fire; suppressed/absent cells render aria-disabled and Space is
  consumed so it can't scroll the page. The hidden grid description
  announces the interaction when a handler is present.
- New HeatmapDrilldownDialog reuses the ChartExpandButton dialog shell
  (95vw × 90vh flex column) as a controlled component.
- The replication renderer keeps the drilldown series live across
  quantile/window changes, re-applies the greyBelow confidence gate on
  refresh (sub-threshold pairs show a 'series hidden' notice instead of
  leaking data), and renders the dialog in every branch so a background
  refresh that collapses the matrix to the line fallback doesn't unmount
  an open drilldown. The heatmap's measureLabel now tracks the selected
  quantile.
- Onboarding hint advertises the new interaction; storage key bumped to
  v2 so previously-dismissed users see the updated tip once.

Fixes #1455

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
…view)

drillPair is kept on close for the Radix exit animation, so every background
refresh re-filtered all parsed records for an invisible dialog. Gate the memo
on drillOpen and serve the last computed content from a ref for the exit
animation; reopening recomputes against the live window/quantile as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
Every analytics panel with PNG copy/export actions now also offers a CSV
download of the rendered data — panel header and expand dialog. Time-series
CSV is an ISO-8601 timestamp column plus one RFC-4180-escaped column per
series; the replication-latency heatmap serializes as row,col,value,count.
Filenames are <metric>-<start>-<end>.csv. Data recomputes synchronously from
the records the panel already fetched — no new network calls — via
computeMetricCsvData, which mirrors MetricRenderer's dispatch.

Fixes #1461

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
… CSV; share renderer dispatch

Three review findings on the CSV-export feature:

1. Stale-window export: useAnalyticsRecords now exposes React Query's
   isPlaceholderData, and MetricPanel/ConnectionsPanel fold it into
   canExport — during an in-flight window change keepPreviousData serves
   the PREVIOUS window's rows with isLoading=false, so a click exported
   old rows under a filename (and PNG title) claiming the new window.
   The whole export action row now hides until the requested window's
   rows settle.

2. Heatmap CSV vs chart drift: the exporter always exports the spec
   default quantile while the renderer has a user-selected one, and it
   emitted confidence-suppressed cells as bare values the chart hides.
   The CSV is now self-describing: the value column is named for the
   quantile actually exported (p95) and a trailing confidence column
   mirrors the chart's classifyCell tiers (ok/grey/suppress/absent),
   keeping the numeric value so consumers can filter.

3. Duplicated dispatch logic: csvExport re-implemented MetricRenderer's
   wantsStackedAreaNodeRemap / wantsClusterLineFold /
   wantsDimensionLineSplit predicates and ConnectionRenderer's record
   preprocessing token-for-token. Those are now exported from their
   owning modules and imported, deleting the copies and the
   keep-in-sync comments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
…ld (review)

Gemini review: revoke the download URL on a delayed timer (a synchronous
revoke can abort Firefox's async download start), prepend a UTF-8 BOM so
Excel renders the ' · ' separator and node names correctly, and let
csvField coerce non-string identifiers instead of assuming strings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
…ng sibling renders

useAnalyticsFreshness called setState synchronously from its QueryCache
subscription. useQuery registers new queries during render and RQ fires the
cache event synchronously, so any component whose render introduced a new
analytics query (a KPI tile's previous-window fetch, a MetricPanel on a fresh
refresh window) made React log 'Cannot update a component (TimeRangePicker)
while rendering a different component'.

Reimplement on useSyncExternalStore with the notification deferred through
notifyManager.batchCalls — the same bridge RQ's useIsFetching uses. A bare
uSES onStoreChange still schedules the cross-fiber update mid-render; the
regression test fails on both the old setState version and the bare-uSES
version, passes only with the deferral.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
Gemini review: splitting isFetching / lastFetchedAt into separate
useSyncExternalStore calls returning primitives removes the ref-cached
object snapshot and the FreshnessSnapshot interface entirely — Object.is
handles primitive stability for free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
The rotated destination (column) axis labels were partly hidden behind the
top row of cells. They anchored at y = HEADER_HEIGHT - 8 (only 8px above the
grid) and rendered with textAnchor="end" + rotate(-45, cx, cy), which sent
each label's far end DOWN-and-left — 30-45px below the anchor for an 8-20 char
label, inside the first cell row. Because the header <g role="row"> paints
before the data rows, the cells painted over the overlap.

Fix the geometry so a label can never enter the cell region:

- Flip the rotation to +45 (still textAnchor="end") so labels lean UP-and-
  left, away from the grid. The anchored END glyph is now the label's LOWEST
  point, so its lowest extent is fixed at the anchor baseline regardless of
  label length — extra length only pushes the far (top) end higher, never
  down into the cells.
- Anchor that near end just above the first cell row (headerHeight -
  HEADER_BOTTOM_PAD) so it clears the grid.
- Replace the fixed HEADER_HEIGHT constant with computeHeaderHeight(cols,
  cellSize): it sizes the header to the longest rendered (post-truncation)
  label's diagonal vertical extent, floored at 72px, so the up-left far end is
  never clipped at the top of the SVG. svgHeight/viewBox derive from it, so the
  taller header is always contained.

Extends the HeatmapMatrix tests with a column-label geometry block (labels
anchored above the first cell row, +45 not -45, viewBox contains the header)
and unit tests for computeHeaderHeight (floor, growth for long labels,
monotonicity, per-cellSize truncation cap).

Fixes #1518

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
…ng (#1515)

The replication heatmap's source/destination axis labels char-truncated the
full FQDN mid-string (hpr-us-e…); show the short node name (first DNS segment)
like the charts and tooltip. Header height sizes to the shortened labels; the
full FQDN stays on each label's title and aria-label. Collision-aware
shortNodeLabelMap follows once #1522 lands it on stage (noted in a TODO).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
…xed 45deg (review)

Gemini: COL_LABEL_ANGLE_DEG is 45deg, so sin = cos = SQRT1_2; use it directly
and drop the per-render deg-to-rad + sin/cos. Comment documents the coupling
so a future angle change restores the general form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
Replace the three divergent tooltip implementations (LineChart's default
<Tooltip> formatter, StackedAreaChart's bespoke StackedAreaTooltip,
TableSizeTrend's default variant) with one shared ChartTooltip content
component: consistent formatTooltipTime header, formatValue values (with
per-entry axis resolution for dual-axis charts), and the stacked charts'
Total row kept behind a showTotal prop.

Displayed series names shorten node FQDNs via a new collision-aware
shortNodeLabelMap (colliding first segments extend until unique).
Display-layer only: pipeline series label fields are untouched, so CSV
exports and legends keep full names.

Fixes #1515

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
Recharts' syncId renders every synced chart's full tooltip and animates it
on each mouse-move, so a Status tab with stacked panels showed a screenful
of jerking tooltips. Two-part fix:

- isAnimationActive={false} on the chart Tooltips kills the move easing.
- A per-chart hover gate (useTooltipGate: onMouseEnter/onMouseLeave on the
  chart container) renders the shared ChartTooltip as null on charts not
  under the pointer. Recharts still draws the Tooltip cursor when content
  returns null, so non-hovered synced charts keep the crosshair line —
  verified end-to-end with two synced real-Recharts charts in happy-dom
  (tooltip-gating.test.tsx) plus prop-capture tests for the charts whose
  unconditional Legend defeats plot-area layout under the test shim.

Fixes #1513

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
syncMethod="value" crosshair sync only fires when the hovered x exists
exactly in the other chart's data. The metric panels snap timestamps to
epoch-aligned period multiples (pipeline snapToPeriod, round-to-nearest),
but the table-size trend anchored its bucket grid at range.startTime — a
lattice no other chart shared — so the Storage-tab trend almost never lit
up (documented in #1507).

- computeTrendFactory now snaps records to the nearest epoch-aligned
  bucket multiple, exactly matching snapToBucketTime, and truncates
  trailing buckets by last populated bucket instead of raw sample time.
- computeBucketMs takes an optional alignMs (the tab's fetch bucketMs)
  and rounds the trend bucket up to a multiple of it, so wide windows
  (e.g. 24h: 16min → 20min buckets) still land on the panels' 5-min grid.
- StorageTab passes the tab bucketMs through buildDerived.
- Audited charts/ and lib/ for other startTime-anchored grids: the trend
  was the only one.

Tests updated where they encoded the old anchoring, plus a cross-metric
test asserting the trend and a snapToPeriod pipeline panel bucket the
same instants to identical timestamps.

Fixes #1514

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
dawsontoth and others added 26 commits July 20, 2026 15:21
The all-non-major bump moves @tanstack/react-router 1.170.17 -> 1.170.18,
which depends on @tanstack/router-core@1.171.15 (was 1.171.14). Our pnpm
patch was pinned to 1.171.14, so the fresh resolve orphaned it
(ERR_PNPM_UNUSED_PATCH) and left the lockfile out of date.

The patch (guards evicted in-flight preload matches against a TypeError via
isMatchLoadCancelledError) is still absent from upstream 1.171.15 -
load-matches.* is byte-identical between 1.171.14 and 1.171.15 and router.*
changed only in an unrelated scroll/history region - so the fix is still
needed. Regenerated the patch against 1.171.15 (exact line numbers) and
refreshed the lockfile. Verified frozen-lockfile install, build, lint, and
the full test suite pass and the patch applies to the installed package.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n auth

Self-hosted clusters have no managed container lifecycle (Harper doesn't
control their runtime), so the Container action group — Start, Start in
safe mode, Restart, Restart in safe mode, Stop — is now hidden for them
in both the cluster-card context menu (ClusterCard) and the per-instance
menu (useInstanceMenuItems). This matches ClusterStateMenu, which already
hides the "Cluster actions" dropdown entirely for self-hosted clusters.

Also gate the "Deployments" cluster-card menu item on cluster
authentication (!!auth.user, the same signal ClusterHome uses for its
connected state), so it only appears once you've authenticated with the
cluster.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Under caret semantics ^0.x doesn't satisfy the next 0.y, so Renovate splits every pre-1.0 dprint/oxfmt bump into its own PR. These formatters are stable in practice and breakage surfaces immediately in tooling, so group them with the other non-major updates until they reach 1.0.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renovate classifies a 0.x minor bump (e.g. 0.58.0 -> 0.59.0) as a major update under semver, so restricting matchUpdateTypes to [minor, patch] missed exactly the bumps we want to group. Drop the restriction; matchCurrentVersion < 1.0.0 already scopes the rule to pre-1.0. Addresses Gemini review feedback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a table's @PrimaryKey is changed on a populated table, Harper does not
re-key the existing rows, so `describe` reports a primary key attribute the old
rows have no value for. Clicking such a row sent `ids: [null]` to search_by_id
(JSON.stringify turns `[undefined]` into `[null]`), which Harper rejects with a
500 'hash_values' must be strings or numbers — leaving the edit modal spinning
on a loading spinner forever.

- onRowClick detects the missing primary key and shows the row read-only using
  the data the list query already returned, instead of firing a doomed lookup.
- EditTableRowModal shows a warning banner explaining the row can't be looked
  up, edited, or deleted individually, and hides the Save/Delete actions.
- getSearchByIdOptions filters null/undefined ids so a null hash value is never
  sent (belt-and-suspenders for any caller).
- Delete guard uses `!= null` so a record with a falsy-but-valid key (0, "")
  can still be deleted.

The root-cause data-loss bug (a `delete` with a null hash value wiped the whole
table in Harper) is fixed in HarperFast/harper#1837.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Covers the new behavior surfaced in the coverage report: with a missing primary
key the modal warns, goes read-only, and hides Save/Delete; a normal row still
edits and deletes. Monaco is stubbed since it can't load in jsdom.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rimary key (#1199)

A table whose primary key was changed after rows existed reports one primary key
via describe but is still keyed by the original attribute. Two consequences in the
edit modal, both now handled by falling back to the row the list already gave us:

- The edit fetch hardcoded `onlyIfCached: true`; on a key miss Harper answers
  `{message: "Entry is not cached"}`, which the modal rendered as the record. It now
  sends `onlyIfCached: false`, so a genuine miss returns an empty result and a real
  record loads for editing regardless of cache state.
- A row whose declared primary-key value exists but resolves to no stored record
  (e.g. `email` present but the table is really keyed by `id`) now shows a read-only
  "couldn't be loaded" banner instead of a raw error/empty editor.

The underlying Harper bug (changing @PrimaryKey on a populated table leaves the real
hash attribute unchanged, so describe reports the wrong key) is fixed in
HarperFast/harper#1837.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes #1504. Follow-up to #1500, which added the safety bound and a
console-only breadcrumb; this surfaces the same degradation to the user.

Two silent degradations now show a dismissible notice above the editor:

- Type-acquisition budget exhausted: once the session-wide @types budget
  seals (ExtraLibBudget.isSpent), further packages report a spurious
  "cannot find module". A new isTypeAcquisitionBudgetSpent() surfaces the
  signal, and useApplicationTypeIntelligence now returns it as status.
- Oversized file: a file over MAX_WORKER_MODEL_CHARS renders as plaintext
  (the existing `oversized` flag).

The notice is a thin bar (role="status", aria-live="polite") sitting flush
below the toolbar so it never covers the first line of code; dismissal is
keyed per file+mode so it re-shows for a different file/degradation.

Wrapping the editor in a flex column needs a definite-height column
(h-full) and a flex-1/min-h-0 box, or Monaco's height:100% fails to
resolve and the editor collapses to a few px.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Reopening the tab resets this" is misleading: the app references more
packages than the type budget can hold, so it would just re-throttle.
Explain the cause instead of promising a fix that won't hold.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address PR #1528 review: extract the inline prop type into an exported
DegradedIntelliSenseBannerProps interface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The users table already offers a "Resend invite" action on PENDING rows.
Surface the same action inside EditUserModal so it's reachable after opening
a pending user, not only from the row. Reuses the existing ResendInviteButton
(same permission gating, invite mutation, and toasts); rendered only when the
user's status is PENDING.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An email/password login for an unverified account is rejected by
central-manager with 403 "User has not verified email address" (checked
after the password, so reaching it proves the credentials are valid). The
cloud login mutation had no onError, so this dead-ended on a generic red
"Error" toast with no way to get a new verification link.

Detect that specific rejection (403 + message match, so a 403 deactivated
account isn't mis-routed), auto-resend a fresh verification link, show a
friendly toast, and redirect to the existing /verifying page. All other
login failures keep the standard toast.

Adds a meta.skipGlobalErrorToast opt-out to the MutationCache so a mutation
can render its own error UX instead of the shared toast.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- isEmailNotVerifiedError: narrow with the `isAxiosError` type guard instead
  of an unsafe `as AxiosError` assertion.
- useCloudSignIn: show the "verification link sent" toast only after the
  resend mutation actually succeeds (a failed resend no longer produces a
  misleading success message). Still navigate to /verifying unconditionally
  so an unverified user never dead-ends on the sign-in page. Adds a
  resend-failure test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CM api client had no response interceptor, and the dashboard route
guard only redirects when the cached authStore user is already null. So
when a cloud session is lost mid-use (expiry, revocation, or a fabric
admin hitting the new session max-age cap), the SPA keeps rendering off
the stale cached user while every data call fails with errors until a
manual refresh.

Add a response interceptor on apiClient: a 401 clears the cached cloud
auth (OverallAppSignIn -> null), which re-runs the route guards and
redirects to /sign-in. Only 401 (unauthenticated) triggers it; 403 is
left alone since it can be a legitimate permission denial for a still
-authenticated user.

The 401-detection core is factored into a small app-import-free module so
it unit-tests in the default node env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review feedback (Gemini): an upstream interceptor could reject with a
non-AxiosError (even undefined); optional-chain the error so the handler
re-rejects with the original reason instead of its own TypeError. Keeps
the AxiosError typing rather than widening to any.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tall

Review feedback (Kris): CM's verifyToken rejects a bad/stale reset or
verify-email token with 401, so a stale email link opened in a second
tab would have signed the user out of a live session. The interceptor
now takes an exemption predicate and skips the unauthenticated auth
flows (/Login, /ForgotPassword, /ResetPassword, /VerifyEmail,
/ResendVerificationEmail). Also guards install against re-invocation
(HMR) to match the sibling install* helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review feedback (Kris):

1. The 401 handler did only a partial clear (cloud-user slot), leaving A's
   in-memory entity connections / Fabric tokens / query cache alive — so a
   same-tab re-login as B could inherit them. Added authStore.signOutAllLocally()
   (local-only: clears every connection, Fabric token, and flag for all
   entities + the cloud slot, no network logout) and a clearAuthStateLocally()
   handler that also clears both React Query caches and storage. The 401 path
   now uses it. Test asserts A's connections/flags are gone after sign-out.

2. Auth-generation race: a slow 401 from a request sent under the OLD session
   could clear a freshly re-established session (A expires → 401 → re-login as
   B → B's earlier in-flight request 401s). A request interceptor now stamps the
   cloud identity at send time; the handler's new isStillCurrent guard only
   clears when the identity is unchanged at response time. Tests cover both the
   normal-expiry (clears) and stale-401-after-relogin (does not clear) cases.

Full suite 1599 passing; tsc/lint/format clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New top-level "Fabric Admin" area, gated to fabric admins / super users,
built as an extensible section (layout + sub-nav rail) so future admin
endpoints slot in as sub-pages. First page generates a short-lived API
token for programmatic access.

- Navbar: fabric-admin-gated "Fabric Admin" item (isAdminMode).
- routes: fabricAdminLayoutRoute (under dashboardLayout) + api-token index,
  wired into rootRouteTree keeping getParentRoute/addChildren in lockstep.
- FabricAdminShell: SubNavRail shell, render-gated via useCloudAuth/isAdminMode
  (dashboard guard still handles the unauthenticated redirect).
- ApiToken page: generate button -> POST /Admin/ApiToken, shows the token
  (read-only field + copy) with expiry and a "copy it now" note.
- ApiTokenResult type in api.patch.d.ts; endpoint isn't in generated types
  so the mutation casts the URL + response (like getCurrentUser).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review feedback (Gemini): the global MutationCache.onError in
react-query/queryClient already toasts mutation failures, so the local
onError produced a duplicate toast.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ription

Review feedback (Kris): isAdminMode includes super_user, but the token
endpoint requires an SSO session only fabric_admin accounts have — a
password-authenticated super user would see the page and only ever get
403. New isFabricAdmin helper (accepts the cloud/local user union)
gates the shell and navbar item. Navbar now derives it from its
existing useOverallAuth() subscription instead of useAdminMode(),
which added a second authStore listener per Navbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review feedback (Dawson): we're already on Fabric and there's no admin
area for anyone but us, so "Admin" is unambiguous. Renames the route
(/fabric-admin -> /admin), nav label, section heading, feature dir
(features/fabricAdmin -> features/admin), and FabricAdminShell ->
AdminShell. The Harper role identifier (fabric_admin / fabricRole /
isFabricAdmin) is unchanged — that's the actual role name, not the
section label — and it now aligns with CM's existing /Admin namespace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oundary

Review feedback (Kris):
- The minted token is a bearer credential but was stored in React Query's
  global MutationCache — readable via getMutationCache(), shown in the
  mutation inspector, and NOT cleared on logout (logoutOnSuccess clears
  only the QueryCache). Set gcTime:0 and reset() after copying it into
  local component state, so it lives only in ApiTokenIndex state. Test
  asserts the cache holds no token after success and is empty after unmount.
- Add an isFabricAdmin table test pinning the role boundary (fabric_admin
  true; super_user / least_privileged / local user / null false) so the
  section can't be re-exposed to a password-auth super_user.

Full suite 1725 passing; tsc/lint/format clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pass the current organizationId into the /HarperVersions query so the deploy
version selector can include versions currently deployed on the org's clusters
(the backend merges them in, tagged `deployed`, for enterprise orgs).

- getHarperVersions / getHarperVersionsOptions take an optional organizationId,
  appended as a query param and folded into the query key so the cache is
  scoped per org.
- UpsertCluster passes the route's organizationId through.

Requires the companion central-manager change adding organizationId to
GET /HarperVersions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The edit-flow version picker injects a synthetic "current" entry for the cluster
being edited. Now that the backend also returns org-deployed versions, the current
cluster's own version comes back as a "deployed on <cluster>" entry — so exclude
every version the edited cluster runs (not just the max) to avoid a duplicate row.

Also copy the array before sort() (sort mutates) so the full instance-version set
is intact for the exclusion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…es axios params

Per review (dawsontoth):
- Make organizationId a required parameter of getHarperVersions/getHarperVersionsOptions
  — the picker never requests versions without an org, so require it (matches the
  sibling getPlanTypesOptions pattern) and let it splash outward to the route.
- Pass organizationId via axios `params` instead of hand-building the query string
  (axios handles encoding).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review (DavidCockerill, dawsontoth): use an org-first query key
`[organizationId, 'HarperVersions']` to match the sibling queries
(getPlanTypesQuery, getRegionLocationsQuery, …). Beyond consistency, this makes
the list participate in the repo's org-scoped cache busting
(`invalidateQueries({ queryKey: [organizationId] })`, used after cluster ops) —
with the old namespace-first key, prefix matching skipped it, so deployed
versions could go stale after a cluster change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dawsontoth
dawsontoth requested a review from a team as a code owner July 22, 2026 20:10
@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 51.86% 5557 / 10714
🔵 Statements 52.41% 5946 / 11344
🔵 Functions 44.35% 1375 / 3100
🔵 Branches 44.85% 3733 / 8322
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/components/Navbar.tsx 30% 23.07% 15.78% 32.43% 32-42, 98-121, 164-303
src/components/ui/table.tsx 41.3% 35.89% 46.66% 42.22% 42-47, 84-87, 105-134, 210-211
src/components/ui/utils/badgeStatus.tsx 0% 0% 0% 0% 22-108
src/features/admin/routes.ts 71.42% 100% 50% 71.42% 14-15
src/features/admin/apiToken/mutations/useGenerateApiToken.ts 100% 100% 100% 100%
src/features/admin/components/AdminShell.tsx 14.28% 0% 0% 20% 17-32
src/features/auth/isEmailNotVerifiedError.ts 100% 100% 100% 100%
src/features/auth/hooks/useCloudSignIn.ts 60% 25% 87.5% 60% 32-46, 93-97
src/features/auth/store/authStore.ts 67.92% 54.7% 80.48% 67.61% 85-91, 107-116, 126, 147, 166-177, 184-193, 248-251, 395-406, 448-454, 488, 492-499, 511, 524-552
src/features/cluster/ClusterHome.tsx 0% 0% 0% 0% 37-479
src/features/cluster/InstanceActionsMenu.tsx 0% 0% 0% 0% 15-35
src/features/cluster/InstanceLogInCell.tsx 0% 0% 0% 0% 18-85
src/features/cluster/InstanceRowContextMenu.tsx 0% 0% 0% 0% 20-34
src/features/cluster/InstanceStatusCell.tsx 0% 0% 0% 0% 18-116
src/features/cluster/Instances.tsx 0% 0% 0% 0% 28-174
src/features/cluster/useInstanceMenuItems.tsx 2.17% 0% 0% 2.27% 49-221
src/features/clusters/components/ClusterCard.tsx 0% 0% 0% 0% 53-439
src/features/clusters/components/ClusterContainerOpModals.tsx 0% 100% 0% 0% 37-92
src/features/clusters/components/ClusterStateMenu.tsx 0% 0% 0% 0% 32-152
src/features/clusters/components/SafeModeConfirmDialog.tsx 0% 0% 0% 0% 35-66
src/features/clusters/queries/getHarperVersionsQuery.ts 100% 100% 100% 100%
src/features/clusters/upsert/index.tsx 0% 0% 0% 0% 41-301
src/features/instance/applications/components/TextEditorView/DegradedIntelliSenseBanner.tsx 100% 100% 100% 100%
src/features/instance/config/secrets/SecretGrantsEditor.tsx 0% 0% 0% 0% 29-73
src/features/instance/config/secrets/grantableComponents.ts 100% 100% 100% 100%
src/features/instance/config/secrets/index.tsx 0% 0% 0% 0% 27-183
src/features/instance/databases/index.tsx 0% 0% 0% 0% 21-103
src/features/instance/databases/components/DatabaseTableView.tsx 0% 0% 0% 0% 71-571
src/features/instance/databases/components/TableView.tsx 60.6% 31.25% 80% 59.37% 111-121, 218-226, 234, 261-267
src/features/instance/databases/functions/formatBrowseDataTableHeader.ts 71.42% 61.53% 66.66% 70% 23-26, 37-42, 62, 100-107, 118-125
src/features/instance/databases/hooks/useResizableDatabasesSidebar.ts 98.18% 75% 100% 98.03% 78
src/features/instance/databases/modals/EditTableRowModal.tsx 52% 65.78% 37.5% 52% 59, 66-67, 79-169
src/features/instance/secrets/ComponentGrantCombobox.tsx 86.74% 89.65% 88.23% 86.07% 144-146, 149-155, 168, 214, 235
src/features/instance/secrets/PendingGrantsInput.tsx 66.66% 75% 50% 75% 32, 49
src/features/instance/secrets/SecretDeliveryPicker.tsx 100% 100% 100% 100%
src/features/instance/secrets/SecretModals.tsx 89.83% 94.11% 92.3% 89.83% 117, 124-126, 280, 299
src/features/instance/secrets/SecretsManager.tsx 96.55% 91.42% 94.11% 100% 111
src/features/instance/secrets/accessExample.ts 100% 100% 100% 100%
src/features/instance/status/routes.ts 60% 100% 50% 60% 15-16
src/features/instance/status/statusSearch.ts 100% 100% 100% 100%
src/features/instance/status/analytics/StatusTabs.tsx 95.58% 96.87% 86.36% 95.16% 158, 166, 262
src/features/instance/status/analytics/charts/NodeLegend.tsx 70% 66.66% 66.66% 77.77% 49-50
src/features/instance/status/analytics/charts/TableSizeChipRow.tsx 100% 100% 100% 100%
src/features/instance/status/analytics/charts/TableSizeSnapshot.tsx 72.22% 50% 84.61% 71.87% 79-82, 107, 118-126, 146
src/features/instance/status/analytics/charts/TableSizeTrend.tsx 90.9% 59.09% 83.33% 89.65% 98-101, 138-150
src/features/instance/status/analytics/components/AnalyticsOnboardingHint.tsx 92.3% 100% 100% 91.66% 23
src/features/instance/status/analytics/components/ChartCopyButton.tsx 76.47% 50% 100% 86.66% 27, 29, 41-44
src/features/instance/status/analytics/components/ChartCsvButton.tsx 100% 75% 100% 100%
src/features/instance/status/analytics/components/ChartExpandButton.tsx 100% 100% 100% 100%
src/features/instance/status/analytics/components/ChartExportButton.tsx 88.23% 50% 100% 100% 27, 29
src/features/instance/status/analytics/components/HeatmapDrilldownDialog.tsx 100% 100% 100% 100%
src/features/instance/status/analytics/components/TimeRangePicker.tsx 87.5% 33.33% 80% 87.5% 50
src/features/instance/status/analytics/context/AnalyticsContext.tsx 90.9% 83.33% 100% 100% 41
src/features/instance/status/analytics/hooks/useAnalyticsCapability.ts 96% 88.23% 100% 95.65% 92
src/features/instance/status/analytics/hooks/useAnalyticsFreshness.ts 98.14% 80.55% 100% 100% 86
src/features/instance/status/analytics/hooks/useAnalyticsRecords.ts 95.23% 93.33% 100% 94.73% 125-129
src/features/instance/status/analytics/hooks/useNodeFilteredSeries.ts 100% 75% 100% 100%
src/features/instance/status/analytics/hooks/useNodeSelection.ts 100% 100% 100% 100%
src/features/instance/status/analytics/lib/chartExport.ts 91.17% 77.77% 88.88% 100% 11, 17, 52
src/features/instance/status/analytics/lib/csvExport.ts 76.13% 64.93% 100% 76.92% 44, 213, 216-256
src/features/instance/status/analytics/lib/nodeLabels.ts 100% 100% 100% 100%
src/features/instance/status/analytics/lib/specRequiredFields.ts 96% 93.33% 100% 95.45% 112, 132
src/features/instance/status/analytics/lib/tableSize.ts 97.84% 89.52% 100% 99.35% 266, 285, 306, 414
src/features/instance/status/analytics/lib/theme.ts 100% 100% 100% 100%
src/features/instance/status/analytics/pipeline/aggregators.ts 94.73% 85.71% 100% 96.55% 40, 51
src/features/instance/status/analytics/pipeline/approxLabel.ts 100% 100% 100% 100%
src/features/instance/status/analytics/pipeline/confidence.ts 100% 100% 100% 100%
src/features/instance/status/analytics/pipeline/connection.tsx 93.75% 75.6% 100% 97.5% 58, 125, 131
src/features/instance/status/analytics/pipeline/fieldExpr.ts 76.47% 89.47% 100% 75% 28-35
src/features/instance/status/analytics/pipeline/index.ts 100% 100% 100% 100%
src/features/instance/status/analytics/pipeline/main-thread-utilization.tsx 80% 62.5% 75% 86.66% 114-115
src/features/instance/status/analytics/pipeline/memory.tsx 100% 83.33% 100% 100%
src/features/instance/status/analytics/pipeline/pipeline.ts 94.83% 86.98% 94.73% 96.39% 123-126, 231-232, 235, 307, 326, 340
src/features/instance/status/analytics/pipeline/replication-latency.tsx 95.88% 75.63% 100% 96.15% 293, 296-297, 300, 304-310, 553-555, 558-562
src/features/instance/status/analytics/pipeline/resource-usage.ts 100% 100% 100% 100%
src/features/instance/status/analytics/pipeline/runTransform.ts 85.71% 88.23% 100% 88.23% 29, 41-42
src/features/instance/status/analytics/pipeline/storage-volume.ts 100% 100% 100% 100%
src/features/instance/status/analytics/pipeline/tls-reused.ts 100% 100% 100% 100%
src/features/instance/status/analytics/pipeline/utilization.ts 100% 100% 100% 100%
src/features/instance/status/analytics/pipeline/wrapperMetrics.tsx 100% 100% 100% 100%
src/features/instance/status/analytics/pipeline/derived/index.ts 100% 100% 100% 100%
src/features/instance/status/analytics/pipeline/derived/mqtt-traffic.tsx 100% 83.33% 100% 100%
src/features/instance/status/analytics/pipeline/derived/request-rate.tsx 93.54% 76% 80% 98.21% 21, 39, 63, 95
src/features/instance/status/analytics/primitives/ChartTooltip.tsx 100% 82.75% 100% 100%
src/features/instance/status/analytics/primitives/DimensionChipRow.tsx 100% 90.9% 100% 100%
src/features/instance/status/analytics/primitives/DimensionSelectorRenderer.tsx 95.12% 84.78% 100% 100% 75, 96
src/features/instance/status/analytics/primitives/FallbackRenderer.tsx 84.61% 50% 87.5% 84% 81-128
src/features/instance/status/analytics/primitives/HeatmapMatrix.tsx 90.9% 80.37% 94.44% 93.15% 321, 330, 359, 361-367, 373, 430-431, 433-434, 438-443, 445, 455
src/features/instance/status/analytics/primitives/LineChart.tsx 100% 89.65% 100% 100%
src/features/instance/status/analytics/primitives/LineChartWithNodeLegend.tsx 100% 87.5% 100% 100%
src/features/instance/status/analytics/primitives/MetricRenderer.tsx 66.66% 58.33% 88.88% 66.66% 22-35, 85-88, 159, 178-183, 185-191, 193-197, 206
src/features/instance/status/analytics/primitives/PerPathRateRenderer.tsx 96.15% 84.37% 100% 100% 83
src/features/instance/status/analytics/primitives/SmallMultiples.tsx 100% 100% 100% 100%
src/features/instance/status/analytics/primitives/StackedAreaChart.tsx 86.53% 64.44% 92.85% 92.3% 36, 86, 95, 109-110
src/features/instance/status/analytics/primitives/TrafficByTypeRenderer.tsx 96.22% 88.88% 100% 100% 83, 84
src/features/instance/status/analytics/primitives/bannerStyle.ts 100% 100% 100% 100%
src/features/instance/status/analytics/tabs/ConnectionsPanel.tsx 96.42% 100% 85.71% 100% 69
src/features/instance/status/analytics/tabs/DatabaseTab.tsx 100% 100% 100% 100%
src/features/instance/status/analytics/tabs/HealthTab.tsx 100% 100% 100% 100%
src/features/instance/status/analytics/tabs/MetricPanel.tsx 94.44% 100% 80% 100% 53
src/features/instance/status/analytics/tabs/OverviewTab.tsx 97.29% 100% 92.3% 97.22% 113-116
src/features/instance/status/analytics/tabs/PanelShell.tsx 100% 90.47% 100% 100%
src/features/instance/status/analytics/tabs/ReplicationTab.tsx 100% 100% 100% 100%
src/features/instance/status/analytics/tabs/RequestsTab.tsx 100% 100% 100% 100%
src/features/instance/status/analytics/tabs/StorageTab.tsx 92.3% 84.61% 100% 91.89% 59, 63-66, 103-114
src/features/instance/status/analytics/tabs/TrafficTab.tsx 100% 100% 100% 100%
src/features/instance/status/analytics/tabs/__tests__/testHelpers.tsx 83.33% 100% 80% 83.33% 10
src/features/instance/status/analytics/tabs/kpi/KpiSparkline.tsx 100% 87.5% 100% 100%
src/features/instance/status/analytics/tabs/kpi/KpiStrip.tsx 100% 100% 100% 100%
src/features/instance/status/analytics/tabs/kpi/KpiTile.tsx 100% 85% 100% 100%
src/features/instance/status/analytics/tabs/kpi/kpiMath.ts 100% 100% 100% 100%
src/features/instance/status/analytics/tabs/kpi/kpiTiles.ts 100% 100% 100% 100%
src/features/instance/status/analytics/tabs/kpi/useKpiTileData.ts 95.23% 90% 100% 100% 70
src/features/organization/users/index.tsx 0% 0% 0% 0% 22-163
src/features/organization/users/orgUserRemovalPolicy.ts 100% 100% 100% 100%
src/features/organization/users/components/RemoveUserFromOrgButton.tsx 100% 100% 100% 100%
src/features/organization/users/components/ResendInviteButton.tsx 0% 0% 0% 0% 19-57
src/features/organization/users/modals/EditUserModal.tsx 0% 0% 0% 0% 35-171
src/hooks/useAuth.ts 43.47% 50% 46.15% 47.36% 17-26, 58-64
src/hooks/useClusterContainerOps.ts 6.25% 0% 0% 6.25% 27-56
src/hooks/useInstanceContainerOps.ts 5.26% 0% 0% 5.26% 28-62
src/hooks/useResolvedTheme.ts 100% 100% 100% 100%
src/integrations/api/cluster/containerOperation.ts 0% 0% 0% 0% 39-47
src/integrations/api/instance/containerOperation.ts 0% 0% 0% 0% 38-47
src/integrations/api/instance/database/getSearchById.ts 100% 100% 100% 100%
src/integrations/api/instance/status/getAnalytics.ts 87.5% 92.85% 100% 87.5% 95-101
src/lib/humanFileSize.ts 100% 100% 100% 100%
src/lib/unauthorizedResponseHandler.ts 100% 100% 100% 100%
src/lib/monaco/workerLimits.ts 100% 100% 100% 100%
src/lib/storage/localStorageKeys.ts 100% 100% 100% 100%
src/react-query/queryClient.ts 91.3% 86.36% 66.66% 91.3% 16, 43
src/router/rootRouteTree.ts 100% 50% 100% 100%
Generated in workflow #1571 for commit 2ccba97 by the Vitest Coverage Report Action

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several key enhancements, including a new Admin section for generating short-lived API tokens, container lifecycle operations (stop, start, restart, and safe-mode recovery) at both cluster and instance levels, and a CSV export feature for status analytics. It also improves database table usability by handling unaddressable rows gracefully, adds persistent column resizing, and refactors the organization user removal flow to prevent accidental lockouts. The reviewer's feedback focuses on optimizing these changes, specifically by ensuring entityType is included in the AnalyticsContext memoization dependencies, resolving a TODO in HeatmapMatrix to immediately adopt collision-aware short labels for rows and columns, and passing the active viewMode to the CSV exporter so downloaded data matches the on-screen view.

Comment on lines 29 to +34
value.instanceParams.entityId,
// The client is memoized per route mount (useInstanceClientIdParams),
// so this dep is inert today — it exists so a future client rebuild
// with the same entityId propagates instead of serving a stale one.
value.instanceParams.instanceClient,
value.syncId,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The instanceParams object is passed directly into the memoized context value (instanceParams: value.instanceParams), but its entityType property is omitted from the useMemo dependency array. If entityType changes, or if value.instanceParams is replaced with a new object reference where only entityType differs, the memoized context will retain a stale reference to the old instanceParams object. We should include value.instanceParams.entityType in the dependency array to ensure the context remains in sync. Additionally, ensure nested objects or client instances (such as instanceClient) are included in the useMemo dependency array to guarantee future updates propagate correctly.

		value.instanceParams.entityId,
		value.instanceParams.entityType,
		// The client is memoized per route mount (useInstanceClientIdParams),
		// so this dep is inert today — it exists so a future client rebuild
		// with the same entityId propagates instead of serving a stale one.
		value.instanceParams.instanceClient,
		value.syncId,
References
  1. Include nested objects or client instances (such as instanceClient) in useMemo dependency arrays to ensure that any future recreation or updates (e.g., during authentication refreshes) correctly propagate to context consumers, even if the reference is currently stable.

import type { AxisSpec, HeatmapCell, HeatmapData } from '../types/analytics.ts';
import { computeCellSize } from './computeCellSize.ts';
export { computeCellSize } from './computeCellSize.ts';
import { shortenNodeLabel } from '../lib/nodeLabels';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import shortNodeLabelMap alongside shortenNodeLabel so we can use it for collision-aware label rendering in the heatmap.

Suggested change
import { shortenNodeLabel } from '../lib/nodeLabels';
import { shortenNodeLabel, shortNodeLabelMap } from '../lib/nodeLabels';

Comment on lines +341 to +343
// TODO: once shortNodeLabelMap (collision-aware, added in #1522) lands on
// stage, swap to it so two sources sharing a first segment stay distinct.
const shortColLabels = useMemo(() => cols.map(shortenNodeLabel), [cols]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since shortNodeLabelMap is already implemented and available in this PR, we can resolve this TODO immediately by using it to compute collision-aware short labels for both columns and rows.

Suggested change
// TODO: once shortNodeLabelMap (collision-aware, added in #1522) lands on
// stage, swap to it so two sources sharing a first segment stay distinct.
const shortColLabels = useMemo(() => cols.map(shortenNodeLabel), [cols]);
// Use the collision-aware shortNodeLabelMap helper to compute short labels for columns and rows.
const shortColLabelsMap = useMemo(() => shortNodeLabelMap(cols), [cols]);
const shortColLabels = useMemo(() => cols.map((col) => shortColLabelsMap.get(col) ?? col), [cols, shortColLabelsMap]);
const shortRowLabelsMap = useMemo(() => shortNodeLabelMap(rows), [rows]);
const shortRowLabels = useMemo(() => rows.map((row) => shortRowLabelsMap.get(row) ?? row), [rows, shortRowLabelsMap]);

fill="currentColor"
>
{truncate(col, truncateLength)}
{truncate(shortenNodeLabel(col), truncateLength)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use the computed shortColLabels array to render the collision-aware short labels instead of falling back to the simple shortenNodeLabel helper.

Suggested change
{truncate(shortenNodeLabel(col), truncateLength)}
{truncate(shortColLabels[ci], truncateLength)}

fill="currentColor"
>
{truncate(row, 24)}
{truncate(shortenNodeLabel(row), 24)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use the computed shortRowLabels array to render the collision-aware short labels instead of falling back to the simple shortenNodeLabel helper.

Suggested change
{truncate(shortenNodeLabel(row), 24)}
{truncate(shortRowLabels[ri], 24)}

// requested window's rows arrive.
const canExport = !isLoading && !isError && !isEmpty && !isPlaceholderData;

const getCsvData = () => computeMetricCsvData(metric, data, timeRange, nodes);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Pass viewMode to computeMetricCsvData so that the exported CSV matches the current view mode (aggregate vs per-node) selected by the user on the screen.

Suggested change
const getCsvData = () => computeMetricCsvData(metric, data, timeRange, nodes);
const getCsvData = () => computeMetricCsvData(metric, data, timeRange, nodes, viewMode);

@dawsontoth
dawsontoth merged commit 4c357f3 into prod Jul 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants