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.
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.
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.
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).
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
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
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.
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'.
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.
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.
- 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.
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.
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.
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.
- 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.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
- 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).
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.
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.
Adds three new components for the Status tab drilldown:
- StatusRunsList: recent-runs table with relative-time, duration, state badge
(running/completed/failed tones), manual chip when triggered=true, and
pass/total summary with em-dash fallback.
- StatusDurationSparkline: pure-SVG inline sparkline (no charting deps),
oldest-to-newest convention, dash placeholder for <2 points, configurable
width/height.
- StatusDetailPanel: renders nothing when probeId is null, shows loading,
error, and data states from useProbeDetail; composes the runs list and the
sparkline; close button calls onClose.
All three components ship with red-green vitest + RTL coverage (21 new tests).
Extends `resolveCell` to read the `d5:<slug>/<featureType>` and
`d6:<slug>/<featureType>` PB row keys produced by the e2e-deep (D5)
and e2e-parity (D6) drivers. The new badges are informational and do
NOT contribute to the rollup — same model as the post-Phase-3 smoke
slot — because the alert engine routes those dimensions independently.
`useLiveStatus` already pulls every dimension via the page-level
unfiltered subscription, so no subscription-shape change is required.
The `d5` / `d6` literals already live in the closed `DIMENSIONS` enum
in showcase/ops/src/types/index.ts.
Adds D5 / D6 chips to `CellStatus` next to the existing E2E badge —
two ~4-line additions that reuse the existing LiveBadge wrapper, no
structural changes to feature-grid / cells-view / parity-view.
Provides the data layer for the dashboard's Status tab against the
showcase-ops HTTP contract being built in parallel (B3).
- lib/ops-api.ts: types + fetchProbes / fetchProbeDetail / triggerProbe.
baseUrl resolves explicit param → NEXT_PUBLIC_OPS_BASE_URL → /api/ops.
Trigger sends Bearer token from caller; URL-encodes ids; uniform
status-line errors on non-2xx.
- hooks/use-probes.ts: useProbes / useProbeDetail / useTriggerProbe.
10s default polling, AbortController cancels in-flight on unmount or
interval rollover, AbortError suppressed from user-visible state.
33 new tests (red→green), full shell-dashboard suite green (163/164,
1 pre-existing skip).
Adds a 5th "Status" tab to shell-dashboard that visualizes the probe
schedule (humanized cron, relative next/last run, duration, pass/fail
result), a "currently running" panel with progress bar and per-service
state grid, and a per-row Trigger control with "Run all" / "Run
specific..." menu.
Components live in src/components: status-tab, status-table,
status-running-panel, status-trigger-button. Local ProbeScheduleEntry
interface is a placeholder; B4b's lib/ops-api types replace it at
merge time. page.tsx wires the tab with an empty placeholder list and
no-op trigger; B4b's useProbes() hook lands in a follow-up.
19 new vitest cases cover humanized cron, relative-time tick, last-run
color tone (green/red/gray), trigger menu actions, and idle vs inflight
panel modes. Full dashboard suite green (150 tests, 1 skipped).
The e2e-demos probe writes per-demo rows keyed as e2e:<slug>/<featureId>
but resolveCell looked up e2e_smoke:<slug>/<featureId>. The mismatch
caused every per-demo depth chip to show gray (no data), leaving cells
stuck at D2. Fix: read the e2e dimension the probe actually writes.
Seven concurrent useLiveStatus hooks each opened their own PocketBase
SSE connection during hydration. The PB SDK multiplexes subscriptions
over a single EventSource, but the concurrent subscribe() calls raced
the initial SSE handshake — some POST /api/realtime calls fired before
the clientId was available, returning 400. After MAX_RECONNECT_ATTEMPTS
(3), affected hooks set connection="error", triggering the "dashboard
unavailable" banner on all tabs.
Fix: replace 7 per-dimension hooks with a single useLiveStatus() call
(no dimension filter). All rows are fetched in one subscription and
the existing client-side keyFor() lookups filter by dimension
implicitly. Eliminates the 7-way race, reduces PB connection count
from 7 to 1, and removes the aggregateConnection helper entirely.
Verified live at dashboard.showcase.copilotkit.ai — zero banners, all
four tabs show real PB data with D2/D4 depth chips.
Three fixes batched to minimize PR churn:
1. Dockerfile: add ARG NEXT_PUBLIC_POCKETBASE_URL so the PB URL gets
baked into the Next.js bundle at build time. Without this, pb.ts
resolves to the sentinel URL and the dashboard shows "unavailable"
on every tab. Pre-existing bug exposed by fresh deploys.
2. showcase_deploy.yml: pass NEXT_PUBLIC_POCKETBASE_URL and
NEXT_PUBLIC_SHELL_URL as build args for the shell-dashboard service
in the CI matrix. Neither was ever passed before.
3. cell-matrix.tsx + parity-matrix.tsx: flatten nested table pattern
that caused column misalignment. Category rows used colSpan with
an inner <table> whose columns floated independently of the header.
Replaced with useCollapsible hook + flat sibling <tr> rows.
Also regenerates package-lock.json for the plugin-react downgrade
from PR #4241 (npm ci was failing in Docker).
Local Docker build verified with --build-arg for both NEXT_PUBLIC vars.
6.0.1 requires vite ^8.0.0 but pnpm resolves vite 7.3.1, causing
ERR_PACKAGE_PATH_NOT_EXPORTED when vitest loads. 5.2.0 supports
vite ^4-8 and resolves the issue. Pre-existing on main.
Shell apps should be independent — no shell cross-imports another
shell's data directory. Replace ../../../shell/src/data/ imports in
shell-dashboard with @/data/ (resolves to src/data/ via tsconfig path
alias). Update Dockerfile to copy shell-docs content (for probe-docs
MDX checks) instead of the entire shell package. Remove unused
bundle-demo-content from dashboard's build pipeline.