Commit Graph

227 Commits

Author SHA1 Message Date
Jordan Ritter ecc506610d feat(showcase-dashboard): add UWCT legend to Packages tab L1-L4 header
Surface the U/W/C/T tier mapping inline next to the L1-L4 Status column

header so operators don't need to cross-reference docs to read the strip.

Maps to the underlying probe dimensions per level-strip.tsx:

  U = Up    (health:<slug>)

  W = Wired (agent:<slug>)

  C = Chats (chat:<slug>)

  T = Tools (tools:<slug>)
2026-04-26 17:04:26 -07:00
Jordan Ritter 747b446c8d fix(showcase/shell-dashboard): proxy contract hardening — build-arg, normalization, empty-env, tests
CR R1 follow-ups on top of the /api/ops proxy fix.

Bucket (a) — must-fix:

- Dockerfile: declare ARG/ENV OPS_BASE_URL in the builder stage. next.config.ts
  evaluates rewrites() at build time and throws if OPS_BASE_URL is unset, which
  aborted `next build` in CI. Mirrors the existing NEXT_PUBLIC_SHELL_URL /
  NEXT_PUBLIC_POCKETBASE_URL pattern.

- showcase_deploy.yml: pipe OPS_BASE_URL through to docker build for the
  shell-dashboard matrix entry, defaulting to the production
  showcase-ops-production.up.railway.app URL.

- next.config.ts: strip trailing slashes from OPS_BASE_URL before constructing
  the rewrite destination, matching the same normalization in
  src/lib/ops-api.ts:resolveBaseUrl so server-side rewrite and client-side
  fetch agree on the URL shape.

- src/lib/ops-api.ts: treat empty / whitespace NEXT_PUBLIC_OPS_BASE_URL as
  "no override". `??` only short-circuits on null/undefined, so an env var set
  to "" silently produced baseUrl="" and URLs of the form "/probes" with no
  /api/ops prefix.

- use-probes.integration.test.tsx: snapshot+restore process.env.NEXT_PUBLIC_OPS_BASE_URL
  in beforeEach/afterEach so tests never leak env state. Strengthen the proxy
  contract assertions: lock toHaveBeenCalledTimes(1), assert method=GET,
  cache=no-store, accept JSON header, and signal is an AbortSignal. Tighten
  the 404 regression to assert the canonical ensureOk message shape so a
  refactor that changes the format trips the test.

Bucket (b) — applied since the diff stayed focused:

- triggerProbe: add cache:"no-store" for parity with the GET fetches.
- fetchProbeDetail / triggerProbe: throw early when id is empty so callers
  get a clean error instead of a request to /probes//... .
- ensureOk: bump body-truncation cap from 200 to 500 chars and append a
  `[truncated, N bytes total]` marker so operators can see they're missing
  tail bytes when the server returns a long HTML/stack-trace body.
- ops-api.ts: drop dev-loop review-cycle tag prefixes (R2-C.3, R3-C, R3-D.1)
  from comments. Keep the actual rationale.
- ops-api.ts header docstring: clarify that NEXT_PUBLIC_OPS_BASE_URL is read
  live at runtime in this codebase (SSR + tests), not just statically inlined
  into the client bundle.

Verified:

- Tests: vitest run — 26 files, 293 passed, 1 skipped (no test count change).
- Typecheck: tsc --noEmit clean.
- Lint + format: oxlint + oxfmt clean on changed files.
- Local Docker build: `docker build --build-arg OPS_BASE_URL=https://...` succeeds.
  Without --build-arg the build fails with "OPS_BASE_URL must be set" as
  expected, confirming the fix is load-bearing.
2026-04-26 17:04:00 -07:00
Jordan Ritter c666deca65 fix(showcase/shell-dashboard): proxy /api/ops/* to showcase-ops via Next rewrite
Status tab was rendering "All probes idle. No upcoming runs scheduled."
even though showcase-ops `/api/probes` returns 9 probes.

Root cause:

* `lib/ops-api.ts` falls back to `/api/ops` when no explicit baseUrl and no
  `NEXT_PUBLIC_OPS_BASE_URL` is set. `NEXT_PUBLIC_OPS_BASE_URL` is not
  set on Railway shell-dashboard.
* `next.config.ts` was empty — there was no rewrite forwarding
  `/api/ops/*` to the showcase-ops service. So the browser hit
  `dashboard.showcase.copilotkit.ai/api/ops/probes` and got a 404 from
  the dashboard origin itself.
* Direct cross-origin fetch from the browser was not an option either:
  showcase-ops has no CORS allowlist.

Fix:

* Add a Next.js rewrite that forwards `/api/ops/:path*` to
  `${OPS_BASE_URL}/api/:path*` server-side. The browser only ever sees
  same-origin calls, and `OPS_BASE_URL` stays out of the client bundle.
* `OPS_BASE_URL` is required at build/start — fail loudly if unset
  rather than silently rendering an empty status table.
* Update `lib/ops-api.ts` doc comment to reflect that `/api/ops` is now
  the deterministic same-origin proxy path (contract), not a "guess".
* Add `src/hooks/use-probes.integration.test.tsx` — exercises the hook
  through the real `lib/ops-api` client to a mocked `fetch`. Asserts the
  default URL is exactly `/api/ops/probes`, parses real probe data, and
  surfaces a useful error when the proxy 404s. The pre-existing
  `lib/ops-api.test.ts` and `use-probes.test.ts` each mock at a different
  layer, so neither caught this contract gap.

Operational follow-up: `OPS_BASE_URL` set on Railway shell-dashboard service
points to `https://showcase-ops-production.up.railway.app`. Auto-update
will pick up the rewrite on the next image rebuild.
2026-04-26 17:03:20 -07:00
github-actions[bot] 6912a0720c style: auto-fix formatting 2026-04-26 21:36:10 +00:00
Jordan Ritter a2a82aaf45 Merge remote-tracking branch 'origin/main' into feat/d5-d6-probes
# Conflicts:
#	showcase/ops/src/probes/loader/probe-loader.test.ts
2026-04-26 14:34:18 -07:00
Jordan Ritter 544fb5794d feat(showcase-dashboard): add 'what do these chips mean?' explainer to Cells tab
Small, collapsible disclosure panel above StatsBar on the Cells tab with
plain-English definitions of the D0–D6 chip layers. Default collapsed;
open/closed state persists across navigations via
localStorage["chip-explainer-open"]. Footer link points to the Notion
plain-English page for deeper detail.

The Cells tab is consulted dozens of times a day by operators who already
know what the chips mean; the explainer is for first-time viewers and
rare refreshers, so the trigger is a small text button and the default
is collapsed to keep the layout clean.
2026-04-26 10:50:00 -07:00
Jordan Ritter b1c835ee5c fix(shell-dashboard): clear data on baseUrl change and honor supersession after fetch resolves
R4-B.1: useProbes effect now calls setData(null) on dep change to mirror
useProbeDetail's CR-B1.4 behavior. Without this, the dashboard renders
stale list data from the previous baseUrl until the new fetch resolves.

R4-B.6: useTriggerProbe checks controller.signal.aborted after the await
resolves. If a back-to-back trigger aborted the controller after the
fetch already returned, honor the R3-C.1 supersession contract and
resolve to null instead of returning the stale TriggerResponse.
2026-04-25 21:17:33 -07:00
github-actions[bot] 40610bbf35 style: auto-fix formatting 2026-04-26 03:10:52 +00:00
Jordan Ritter df88298429 merge: r3-fix-D 2026-04-25 20:09:42 -07:00
Jordan Ritter 8e061df87c merge: r3-fix-C 2026-04-25 20:09:42 -07:00
Jordan Ritter 38c8236aa2 fix(shell-dashboard): align status-table partial-pass tone with runs-list
R3-B.1: status-table lastRunTone() returned green for partial-pass runs
(failed=0, passed<total), reintroducing the misread that R2-D.4 fixed in
status-runs-list. Schedule table is the most-viewed surface, so this is
the highest-impact regression. Mirror runs-list semantics: amber tone +
"P/T (S skipped)" result text. Also explicit "P/T pass (F fail)" for
failed runs so the column reads consistently with the tone.

R3-B.2: StatusDetailPanel durations filter used \`typeof d === "number"\`
which admits NaN/Infinity (typeof NaN === "number"). The sparkline then
computed Math.min(...durations) = NaN and emitted "NaN,NaN" coords in
the polyline. Switch to Number.isFinite() so non-finite durations are
dropped.

Tests added (red-green TDD):
- status-table: amber tone + "skipped" text for partial pass; explicit
  "pass (N fail)" text for failed; "N/N pass" for fully-passed
- status-detail-panel: NaN/Infinity durations don't appear in sparkline
2026-04-25 20:02:43 -07:00
Jordan Ritter df5fa2d499 fix(showcase-dashboard): tighten error semantics and trigger return type in ops hooks
R3-C.1: useTriggerProbe.trigger now returns Promise<TriggerResponse | null>.
The supersession path resolves to `null` (typed) instead of the previous
`undefined as unknown as TriggerResponse` cast, which lied about the
return shape and would crash any caller that read fields off the result.

R3-C.2: useProbeDetail clears `error` on id change so the detail panel no
longer renders a stale error message under a freshly-selected probe header.

R3-C.3: useProbes clears `error` on dep change (e.g. baseUrl swap) so a
prior dep tuple's failure does not persist under the new one.

R3-C.4: useTriggerProbe clears `error` at the start of each trigger call
so a successful retry leaves no sticky error from the prior failure.

Bonus: parseJson now attaches the original parse error as `cause` on the
wrapper so debuggers can walk back to the SyntaxError name/stack without
re-parsing the wrapped message.

Tests: red-green tests added for each behavior; full file suite 49/49.
2026-04-25 20:02:02 -07:00
Jordan Ritter f2a7e73d4b fix(shell-dashboard): unify StatusTriggerButton close paths
R3-D.2: the toggle-close branch on the Trigger button forgot to reset
pickerOpen and selected, while outside-click and Escape both did. So
toggling the menu closed and reopening it showed ghost checks from a
prior session. Extract a closeMenu() helper used by all three close
paths so the invariant is structural, not duplicated.

Bonus: add aliveRef guard so async rejections from onTrigger that
resolve after unmount don't trigger setState-on-unmounted warnings.
2026-04-25 20:01:42 -07:00
Jordan Ritter 9eb20d7de1 fix(shell-dashboard): add cache: no-store to ops-api GET fetches
R3-D.1: fetchProbes and fetchProbeDetail are polled every ~10s for live
status data. Without an explicit cache directive the browser and Next.js
App Router fetch wrapper may serve stale responses, defeating the poll.
Add cache: 'no-store' to both GET call sites and cover with tests.
2026-04-25 20:01:35 -07:00
Jordan Ritter 4861ee043f fix(showcase-dashboard): correct misleading 'last pass' tooltip copy for degraded state
The degraded-state tooltip read "stale — last pass @ <observed_at>", but
observed_at is the most recent producer tick (when the row was last
*seen* in any state) — for a degraded row that is when degradation was
last observed, NOT when it last went green. Operators reading "last
pass @ ..." would mis-triage by treating that timestamp as the last
green tick.

Change copy to "stale — last seen @ <observed_at>" and add a covering
test asserting the new wording (and the absence of "last pass").
2026-04-25 19:54:07 -07:00
Jordan Ritter 17edaccee1 merge: origin auto-format 2026-04-25 19:50:54 -07:00
Jordan Ritter 3980bb1b61 merge: r2-fix-D R2 CR fixes 2026-04-25 19:48:01 -07:00
Jordan Ritter 5b915dd844 chore(shell-dashboard): humanizeCron HH:MM and rename completed→finished
Bucket-(b) bonus polish:
- humanizeCron now renders "M H * * *" with a non-zero minute as
  "Daily at HH:MM" instead of falling through to the raw expression.
- humanizeCron rejects */0 step values for both the every-Nh and every-Nm
  shapes — that's not a valid cron step.
- StatusRunningPanel: rename `completed` to `finished`. The variable
  counts services in either completed OR failed states, so "completed"
  read as misleadingly success-only. The data-completed attribute name
  stays for backwards compatibility with existing tests.
2026-04-25 19:31:39 -07:00
Jordan Ritter 5fe2058c79 fix(shell-dashboard): badge partial-pass runs amber instead of green
A run with failed=0 but passed<total represents some services that were
skipped/unknown — rendering this as green "completed" misleads operators
into thinking all targets passed (R2-D.4). Now badged amber with a
"partial (N skipped)" label so the partial-coverage case is visible.
2026-04-25 19:31:32 -07:00
Jordan Ritter 29a67b78e0 fix(shell-dashboard): surface trigger errors and reconcile selection
Three related fixes to StatusTriggerButton:
- R2-D.2: render onTrigger errors inline via data-testid="status-trigger-error"
  instead of discarding the value through a write-only setter.
- R2-D.3: when serviceSlugs prop changes, intersect selected with the new
  list so stale slugs cannot be POSTed; also clear selection on
  outside-click and Escape close so reopening doesn't show ghost checks.
- R2-D.5: drop unreachable serviceSlugs.length === 0 ternary inside the
  picker — the outer guard already requires length > 0.
2026-04-25 19:31:25 -07:00
Jordan Ritter 46aa14c35f fix(shell-dashboard): wire page.tsx to useProbes + useTriggerProbe
Replace the no-op handleTrigger stub and empty-list placeholder with real
hooks. useProbes feeds the schedule grid via 10s polling; useTriggerProbe
POSTs /trigger with the operator token. If NEXT_PUBLIC_OPS_TRIGGER_TOKEN
is unset, the trigger throws a clear "token required" error so operators
see fail-loud feedback instead of a silent no-op (R2-D.1).
2026-04-25 19:31:18 -07:00
Jordan Ritter 5cd22270a3 fix(shell-dashboard): keep useTriggerProbe POST alive on unmount, swallow supersession AbortError
POST to /trigger is non-idempotent — the server may have already received
the request and queued the run by the time the component unmounts.
Aborting the controller client-side hides the result, but does NOT
unfire the server-side action. Stop aborting on unmount; rely on
aliveRef to gate any setState after teardown.

Back-to-back trigger() calls intentionally abort the prior controller.
The first call's promise was rejecting with AbortError, surfacing as a
user-facing error on the Status tab. Detect AbortError (and the
cancelled-controller case) in the catch block and resolve silently
instead of setError + rethrow. The owning-controller guard on the
finally block prevents a superseded call from flipping pending off
while a newer call is still in flight.

Bonus consistency for useProbes: setLoading(true) on dep change so
consumers can render a "refreshing" indicator across baseUrl swaps.

Tests:
- unmount during in-flight trigger does NOT abort the signal
- post-unmount resolution does not call setState (no act warnings)
- back-to-back triggers: first promise resolves silently, error stays null
2026-04-25 19:29:20 -07:00
Jordan Ritter 0243aa39bd fix(shell-dashboard): preserve AbortError name through ops-api error wrapping
ensureOk and parseJson previously caught body-read failures and rewrapped
them in a generic Error, losing the AbortError name. Hooks filter abort
noise via err.name === "AbortError", so the wrap defeated cancellation
filtering and surfaced "AbortError" as a user-facing failure on the
Status tab.

Re-throw AbortError as-is from both helpers; non-Abort body errors keep
the descriptive wrap path. Also tighten ProbeKind to use (string & {})
so known-kind autocomplete is preserved alongside forward-compat strings.

Add covering tests:
- response.text() rejects with AbortError → ensureOk re-throws as-is
- response.json() rejects with AbortError → parseJson re-throws as-is
- non-Abort body errors still wrap with descriptive message
- triggerProbe with already-aborted signal surfaces AbortError unwrapped
2026-04-25 19:29:08 -07:00
Jordan Ritter 15af4c4779 test(showcase-dashboard): tighten matrix tests, cover starter cells and regressions filter
Backstop the matrix fixes and address weak/no-op assertions:

  - Tighten depth-chip count assertions from '>= N' to exact 'N'
    where the fixture pins the count.
  - Replace the no-op filter=wired test with a fixture containing a
    feature whose every cell is unshipped, then assert that row IS
    visible under filter=all and IS HIDDEN under filter=wired.
  - Extend the collapse test with a re-expand click that asserts
    chip count returns to baseline (regression guard for one-way
    toggle state).
  - Cover filter=regressions empty-state.
  - Cover starter cells (feature === null) in both matrices: the
    cell index must skip them without polluting the grid or leaking
    a 'null' string into rendered output.
  - Pin parity-matrix integration-header ordering across all five
    parity tiers (reference, at_parity, partial, minimal, not_wired).
  - Cover D2 cap for starter cells in deriveDepth (chat/tools cannot
    lift past D2 because D3 is not evaluable without a feature id).
  - Replace the locally-declared CatalogCell shape in depth-utils.test
    with the source type so tests cannot drift from production.
  - Add afterEach(vi.unstubAllGlobals) to both test files so the
    localStorage stub does not leak across files in the same worker.
2026-04-25 19:25:09 -07:00
Jordan Ritter f6742c9871 fix(showcase-dashboard): drop depth typecasts and harden matrix cell index
Three related cleanups in cell-matrix.tsx and parity-matrix.tsx:

  1. Drop the 'as 0 | 1 | 2 | 3 | 4' typecasts on depth.achieved. The
     newly tightened DepthResult.achieved literal-union type makes
     them redundant.

  2. Skip starter cells (feature === null) when building the
     cell-by-key index. The previous code interpolated null into the
     key string, producing '<slug>/null' entries that orphaned
     starter rows in the index.

  3. Render an explicit empty-state in CellMatrix when filter is
     'regressions'. The placeholder logic returned false for every
     row, silently hiding the entire matrix. Until regression
     detection lands (DepthResult.isRegression always false), the
     matrix now surfaces a 'Regression detection not yet implemented'
     message via data-empty-reason='regressions-not-implemented'.
2026-04-25 19:25:09 -07:00
Jordan Ritter 56e8f41861 fix(showcase-dashboard): tighten DepthResult.achieved to 0|1|2|3|4 literal union
deriveDepth's return type carried the loose 'number' on achieved, forcing
every consumer to cast to '0 | 1 | 2 | 3 | 4' to satisfy DepthChip's
prop type. Expose AchievedDepth alias and narrow the source so the
casts at use sites (cell-matrix.tsx, parity-matrix.tsx) become
unnecessary.
2026-04-25 19:25:08 -07:00
Jordan Ritter efba71b224 fix(showcase-dashboard): CellStatus drops smoke + hides D5/D6 for testing features
Per-cell status row corrections + a focused unit-test file covering CP1,
CP2, CP5, CP7, CP8 behaviors:

- CP3: stop consuming `cell.smoke` in CellStatus. resolveCell still
  computes the field for backwards-compat with other call-sites, but
  per Phase 3 Decision #7 the smoke L1 signal is integration-scoped and
  lives in the per-integration strip — there's no per-feature smoke
  badge in the cell row. (The CellState type narrowing for the `smoke`
  field is owned by the live-status.ts fix in a sibling worktree; this
  patch only removes the consumer.)
- CP8: hide the D5/D6 LiveBadges when feature.kind === "testing".
  The e2e-deep / e2e-parity producers only emit rows for primary
  features per spec; testing features would otherwise render a
  perpetual gray "?" that adds noise without information. Mirrors the
  docs-row visibility rule (already gated on isTesting).
- CP9: document the missing per-cell drilldown URL convention for D5/D6
  with a TODO comment. No drilldown route exists today; once one does,
  wire it through keyFor here.

Tests cover red-green for CP1 (mouseleave handler wired), CP2 (first /
error / green_to_red transition formatting), CP5 (opt-out vs absent
tooltip copy), and CP8 (D5/D6 hidden for testing features). The
existing docs-link.test.tsx still passes.
2026-04-25 19:25:08 -07:00
Jordan Ritter 2900998889 fix(showcase-dashboard): LiveBadge tooltip-open lifecycle and transition copy
- CP1: wrap Badge in a span that resets `tooltipOpen` on mouseleave/blur
  so the lazy-fetch flag can flip back to false. Without this, the flag
  stayed true forever after the first hover, defeating the §5.6
  optimization and (once the hook upgrades to a subscription) leaking it.
- CP2: discriminate `first` and `error` transitions in the tooltip copy
  so operators can tell "first observation (initial: <state>)" apart
  from "producer error (error → <state>)" instead of seeing the same
  fallback string. Logic moved into a named `formatTransitionLine`
  helper for testability.
2026-04-25 19:25:08 -07:00
Jordan Ritter 72275e8b2f fix(showcase-dashboard): tighten DocsRow/DocsLink — exhaustiveness, honest tooltips, error linkability
Four fixes to the per-cell docs row:

- CP4: extract the `unselected` shell-routing fallback into
  SHELL_UNSELECTED_PATH so the coupling is documented and easy to find.
- CP5: split the `missing` tooltip into "framework opt-out" vs "no docs
  URL declared" so operators can distinguish a framework that declined
  docs from a registry entry that never declared one.
- CP6: add `default: never` exhaustiveness guard to DocsLink's switch so
  any new DocState variant is a compile error until handled.
- CP7: render `<a>` (not `<span>`) for `state === "error"` when an href
  is present so operators can manually verify a flaky probe — `notfound`
  stays a span because the URL is positively known broken.
2026-04-25 19:25:08 -07:00
Jordan Ritter 3a826168c8 fix(showcase-dashboard): preserve array identity on no-op upsert and warn on merge collisions
LS6: `mergeRowsToMap` now logs a `console.warn` when the same key is
seen twice across the merged groups. Each dimension is supposed to own
a disjoint slice of the keyspace, so a collision is an upstream bug.
Return semantics are unchanged (last-wins remains correct under
eventual consistency), but the warn surfaces the regression in dev mode.

LS7: `upsertByKey` returns the SAME array reference when the incoming
row is observably identical (key + state + observed_at +
transitioned_at). Producers re-emit unchanged rows on every poll tick;
allocating a fresh array for each one defeats React's reference-equality
short-circuit and re-renders every memoised consumer downstream.

Tests cover no-op identity, mutation paths (state change,
observed_at change), the disjoint-merge happy path, and the collision
warn.
2026-04-25 19:25:08 -07:00
Jordan Ritter ae0bf40724 fix(showcase-dashboard): close stale-green hole and align smoke key shape
LS1: `allGreen` now requires BOTH `healthRow` and `e2eRow` to be present
and green. Previously a missing e2e row was treated as green-eligible,
which let a brand-new cell read green before any e2e probe had ticked —
the same flavour of stale-green lie as the connection=error case.

LS11: smoke lookup now uses the integration-scoped key (`smoke:<slug>`)
to match the producer. Pre-fix, resolveCell looked up
`smoke:<slug>/<featureId>` which always missed, leaving every smoke badge
gray. Tests update fixtures accordingly and add a regression guard so a
stray feature-keyed smoke row cannot bleed into the resolver.
2026-04-25 19:25:08 -07:00
Jordan Ritter ec94f64886 fix(showcase-dashboard): make formatTooltip honest and signal-aware
- Drop the hardcoded '>6h' duration from the degraded tooltip; the
  threshold lives in the producer config and lying about it in copy
  misleads operators when the cadence changes.
- Replace the empty-string default branch with a loud unknown-state
  fallback gated by a `never` exhaustiveness check, mirroring rowTone /
  formatLabel.
- For red and degraded states, append a truncated (80-char) summary of
  `row.signal` so the tooltip carries triage context without ballooning.
- When the SSE connection is in error AND the cached row is red or
  degraded, append the row's last-known state so operators don't lose
  triage context to the offline banner.
2026-04-25 19:25:08 -07:00
Jordan Ritter 6f057665d6 fix(showcase-dashboard): add keyFor input guards and exhaustive state checks
- keyFor now throws when slug or featureId contain ':' or '/', preventing
  silent collisions in the live-status lookup map (e.g. `smoke:a/b` vs
  `smoke:a` + `/b`).
- rowTone() and formatLabel() switch over the State union with explicit
  `never` exhaustiveness checks so adding a new variant fails the build
  instead of silently mapping to a misleading green/gray default.
- Unknown row states now surface a distinct 'error' tone (rowTone) and a
  '?' glyph (formatLabel) rather than a silent green check.
- Tests cover the three throw paths and remain green for the existing
  per-feature key shapes.
2026-04-25 19:25:07 -07:00
github-actions[bot] 938d734c34 style: auto-fix formatting 2026-04-26 02:15:30 +00:00
Jordan Ritter c743bebb6a merge: pull origin auto-format commit
# Conflicts:
#	showcase/shell-dashboard/src/components/status-duration-sparkline.tsx
2026-04-25 19:14:14 -07:00
Jordan Ritter 6e5427922a fix(shell-dashboard): null-guard lastRun.summary in status-table
After B1 tightened ProbeLastRun.summary to ProbeRunSummary | null, three
sites in status-table.tsx accessed .summary fields without null-guards
(TS2533 errors at lines 121 and 165).

- lastRunTone: when summary is null but state === "completed", return
  "gray" (matches runs-list "unknown" semantics — finished but no count
  info means we can't claim coverage). state === "failed" still returns
  "red" regardless of summary.
- Result cell: when summary is null, render "—" (matches existing
  fallbacks for unknown values); otherwise render passed/total as before.

Adds two covering tests for the null-summary completed (gray + "—") and
null-summary failed (red + "—") cases.
2026-04-25 19:12:41 -07:00
Jordan Ritter 8b3f53aefb fix(shell-dashboard): replace double cast with single CatalogData cast
CR-C-page.1: `catalog as unknown as CatalogData` violated the project
policy against double casts. catalog.json is generated by pretest/
predev/prebuild from generate-registry.ts and conforms to CatalogData
by construction, so a single `as CatalogData` cast is safe and clearer.

Typecheck: no new errors (pre-existing __tests__ + status-table.tsx
errors are unrelated and unchanged). All 220 vitest tests pass.
2026-04-25 19:07:22 -07:00
Jordan Ritter d24a9d9a5f merge: cr-fix-B2 CR fixes 2026-04-25 19:05:05 -07:00
Jordan Ritter d3ab9d0387 fix(showcase-dashboard): collapse sub-second formatRelative to 'now' sentinel
Sub-second deltas previously rendered as "in 0s" / "0s ago", which
flickered between sides of the boundary on every tick. Collapse to a
single "now" sentinel for |delta| < 1s and add a Number.isFinite guard
that falls back to "—" so non-finite inputs don't cascade into
unreadable output.
2026-04-25 18:56:15 -07:00
Jordan Ritter f727665124 fix(showcase-dashboard): guard sparkline against tiny widths and drop dead branch
Sparkline previously divided unconditionally by (durations.length - 1)
inside a length===1 branch that the early return at length<2 made
unreachable. Removed the dead conditional. Added a small-viewport guard
that falls back to the flat-dash placeholder when innerW or innerH is
non-positive, preventing negative or NaN coords when callers pass
width <= PADDING * 2.
2026-04-25 18:56:10 -07:00
Jordan Ritter 8a8e4d81d5 fix(showcase-dashboard): stabilize handleTrigger with useCallback for tabs deps
The tabs useMemo closed over handleTrigger but did not list it in its
dependency array. Today the handler is a no-op so the latent bug is
invisible, but once B4b wires this to the real ops API trigger
endpoint the memo would retain a stale closure. Wrap handleTrigger in
useCallback and add it to the tabs deps array so the memo refreshes
correctly.
2026-04-25 18:56:04 -07:00
Jordan Ritter fa612ac4a7 fix(showcase-dashboard): hide Run specific... when no service slugs are known
The trigger menu's "Run specific..." entry was unusable for idle probes
because slugs are sourced from inflight.services, which is null in the
typical manual-trigger case. Hide the entry entirely when serviceSlugs
is empty (Option C); operators can still trigger all targets via "Run
all". A richer ProbeScheduleEntry contract that exposes discovered
targets for idle probes is tracked separately.

Also wraps onTrigger awaits in try/catch so async rejections surface
through a local error state instead of bubbling up as unhandled promise
rejections, and resets the selected slug set on completion so the next
trigger session starts clean.
2026-04-25 18:55:57 -07:00
Jordan Ritter d719e8380e fix(showcase-dashboard): align run state label with tone for missing summary
runTone returned "gray" (unknown) when finishedAt was set but summary
was null, while runStateLabel returned "completed" for the same case —
producing a gray badge labeled "completed", exactly the misleading
combination the tone comment said it was avoiding. runStateLabel now
returns "unknown" so label and tone agree.
2026-04-25 18:55:49 -07:00
Jordan Ritter 7cec1033c2 fix(showcase-dashboard): filter past nextRunAt from idle next-run banner
When all probes are idle, findNextScheduled previously returned the
entry with the smallest nextRunAt regardless of whether that timestamp
was already in the past, producing nonsensical "Next: smoke 5m ago"
banners. Filter out entries whose nextRunAt is < now and fall back to
the existing "No upcoming runs scheduled." sentinel.

Also typed STATE_ICON as Record<ServiceState, string> so missing service
states fail to compile rather than silently rendering undefined icons.
2026-04-25 18:55:43 -07:00
Jordan Ritter 7d8228075d fix(shell-dashboard): drop duplicate ProbeScheduleEntry, re-export canonical
The local interface in status-tab.tsx was marked TODO and had drifted
from the canonical lib/ops-api definition: it lacked error? and
finishedAt? on service progress entries, and used a bare 'string' for
'kind' instead of the ProbeKind union.

Delete the local interface and re-export the canonical type from
lib/ops-api so existing 'import type { ProbeScheduleEntry } from
"./status-tab"' call sites continue to compile against the single
source of truth (CR-B1.1).
2026-04-25 18:55:30 -07:00
Jordan Ritter 02a54712b9 fix(shell-dashboard): replace aliveRef with per-effect cancelled flag
The previous useProbes/useProbeDetail hooks reset aliveRef.current to
true at the top of every effect run. A fetch from the prior cycle that
resolved between cleanup and re-init could pass the new effect's alive
check and clobber state with stale data.

- useProbes: per-effect 'cancelledRef' (mirrored so the imperative
  refetch callback can honor it). Cleanup flips cancelledRef true.
- useProbeDetail: per-effect 'let cancelled = false' closure; cleanup
  flips it. Each effect run gets its own closure, so a stale fetch from
  the prior cycle cannot land setData (CR-B1.3).
- useProbeDetail also clears data immediately on id change so the
  panel header (driven by prop) is never out of sync with the body
  while the new fetch is still in flight (CR-B1.4).
- useTriggerProbe gains a per-call AbortController whose signal is
  forwarded to triggerProbe; unmount aborts any in-flight POST so we
  don't fire duplicate runs after the user navigates away (CR-B1.5).

Tests cover: rapid-deps stale-data path, rapid-id stale-data path,
data-clear on id change, and unmount-during-trigger abort.
2026-04-25 18:55:18 -07:00
Jordan Ritter c3382e5ea1 fix(shell-dashboard): harden ops-api types and error reporting
- ProbeLastRun.summary is now nullable to match ProbeRun.summary; the
  server can complete a run with no summary (showcase-ops CR-A1.5).
- ensureOk surfaces body-read failures with a 'body read failed: ...'
  marker instead of swallowing them silently (CR-B1.6 / CR-C15).
- New parseJson helper attributes JSON parse failures to the requesting
  URL so call sites can tell which endpoint produced bad bytes.
- triggerProbe gains a TriggerProbeOptions interface with an optional
  AbortSignal; passed straight through to fetch() so callers can cancel
  in-flight POSTs (CR-B1.5).
2026-04-25 18:55:07 -07:00
github-actions[bot] d258bcf7d8 style: auto-fix formatting 2026-04-26 01:39:40 +00:00
Jordan Ritter dfdd8d4e88 chore(showcase-dashboard): add missing CatalogCell fields to test fixtures
The CatalogCell type gained three required fields (integration_name,
feature_name, category_name) before this branch landed; the existing
test fixtures predate that change and surface as 32 typecheck errors
in CI. Add plausible Title-Case names to each fixture so the type
checker is happy. Test logic and assertions are unchanged.
2026-04-25 18:34:27 -07:00
Jordan Ritter 77c510c0b5 feat(showcase-dashboard): wire row click selection to open the probe-detail panel
StatusTable gains an optional onSelect(probeId) prop. When provided, rows
become clickable (cursor-pointer) and the action cell stops propagation so
that clicking the trigger button does not also open the drilldown.

StatusTab owns the selection state (useState) and renders StatusDetailPanel
beneath the running panel. The panel returns null when no probe is
selected, so this is a zero-cost addition to the default render.
2026-04-25 18:30:24 -07:00