Releasing latest from Stage to Prod#1544
Conversation
… 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
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>
There was a problem hiding this comment.
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.
| 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, |
There was a problem hiding this comment.
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
- Include nested objects or client instances (such as
instanceClient) inuseMemodependency 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'; |
There was a problem hiding this comment.
| // 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]); |
There was a problem hiding this comment.
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.
| // 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)} |
| fill="currentColor" | ||
| > | ||
| {truncate(row, 24)} | ||
| {truncate(shortenNodeLabel(row), 24)} |
| // requested window's rows arrive. | ||
| const canExport = !isLoading && !isError && !isEmpty && !isPlaceholderData; | ||
|
|
||
| const getCsvData = () => computeMetricCsvData(metric, data, timeRange, nodes); |
There was a problem hiding this comment.
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.
| const getCsvData = () => computeMetricCsvData(metric, data, timeRange, nodes); | |
| const getCsvData = () => computeMetricCsvData(metric, data, timeRange, nodes, viewMode); |
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?