Commit Graph

365 Commits

Author SHA1 Message Date
Jordan Ritter 1b0aa557ba fix(showcase/shell-dashboard): keep client ops fetch same-origin via /api/ops
The staging D6 dashboard rendered no data because the client did a direct
cross-origin fetch to the harness URL — CORS-blocked and the wrong path
(`/probes` instead of `/api/probes`). Root cause: getRuntimeConfig() sourced
the client RuntimeConfig.opsBaseUrl from the server proxy target OPS_BASE_URL
(the harness URL), which the root layout serialized into
window.__SHOWCASE_CONFIG__, so resolveBaseUrl() used it as the fetch base
instead of falling through to the same-origin /api/ops proxy.

Decouple the two: the client direct override is now an explicit, opt-in,
client-intended env var (NEXT_PUBLIC_OPS_DIRECT_BASE_URL) that defaults to ""
in every environment, so the client lands on /api/ops. The Route Handler's
server-only OPS_BASE_URL read and its sentinel behavior are unchanged.

- showcase/shell-dashboard/src/lib/runtime-config.ts: opsBaseUrl now read from
  NEXT_PUBLIC_OPS_DIRECT_BASE_URL (default ""), not OPS_BASE_URL; no sentinel.
- showcase/shell-dashboard/src/lib/ops-api.ts: update resolveBaseUrl comments
  to describe the client override vs server proxy target distinction.
- showcase/shell-dashboard/src/app/layout.tsx: note opsBaseUrl is the client
  override, never the harness URL.
- tests: encode the contract (client falls through to /api/ops when no direct
  override; server proxy target does not leak into the client config), incl. an
  end-to-end regression in the env-switch spike test.
2026-06-01 08:14:03 -07:00
github-actions[bot] 688f2d31cf style: auto-fix formatting 2026-06-01 14:47:38 +00:00
Jordan Ritter b19c27f045 fix(showcase/shell-dashboard): resolve OPS_BASE_URL at runtime via /api/ops route handler
The /api/ops/* proxy was a next.config.ts rewrite, which Next.js evaluates at
`next build` and freezes into the prebuilt Docker image. The shared CI build
bakes a placeholder OPS_BASE_URL (http://ops.invalid) to satisfy a
throw-if-unset guard, so every deploy of the single :latest image proxied to a
dead host regardless of its runtime env — /api/ops/* returned 500
(ENOTFOUND ops.invalid), the Feature Matrix health overlay got no probe data,
and every cell downgraded to amber (zero green) despite green backend data.
The same freeze also baked the production harness URL into the shared image,
so even a correct rebuild would point staging at the prod harness.

Replace the build-time rewrite with a Route Handler at
src/app/api/ops/[...path]/route.ts that reads process.env.OPS_BASE_URL at
REQUEST time (force-dynamic, never statically cached) and proxies
/api/ops/<path> -> ${OPS_BASE_URL}/api/<path>, forwarding method, query
string, headers, and body. A missing OPS_BASE_URL now returns a clear 503
instead of a build throw. The build no longer depends on OPS_BASE_URL.

This fixes both traps with one image: each environment resolves its own
runtime OPS_BASE_URL from the same artifact, no rebuild. Requires the
dashboard image to rebuild + redeploy. Harness path is now /api/probes.
2026-06-01 07:46:27 -07:00
Jordan Ritter f7203f3368 fix(showcase): thread shared now through cell-matrix render path
Hoist a single now (memoized on liveStatus) and thread it into the
render-path buildCellModel so the chip, the regressions/gaps filter, and
the badges all judge staleness against one instant. Previously the render
path defaulted to a fresh Date.now() per cell, so a cell the filter
included could render a different staleness state across a window boundary.
2026-05-30 21:34:48 -07:00
Jordan Ritter 36f316b24d fix(showcase): resolveD5Row returns null for unmapped features
Drop the direct-key d5 fallback in resolveD5Row so the coverage badge
agrees with the chip (cell-model resolveD5) and depth (depth-utils
isD5Green), which both treat an unmapped feature as no-D5. Previously an
unmapped feature with a stray d5:<slug>/<featureId> row rendered a green
badge while the chip showed gray. Also guards the empty-map case.
2026-05-30 21:34:39 -07:00
Jordan Ritter 82f79b211f fix(showcase): unify resolveD5Row strict-missing + honest stale badge row
resolveD5Row now mirrors cell-model.ts resolveD5's STRICT missing-sub-row
handling: a mapped multi-key D5 family is credited green only when EVERY
sub-row is present. A missing sub-row collapses a present green/degraded
fold to no-data (gray), while a present red still dominates. Previously a
family with one present-green and missing siblings rendered a false-green
badge while buildCellModel's D5 rendered gray.

buildBadge now returns the EFFECTIVE (downgraded) row so badge.row.state
agrees with badge.tone — a stale-green badge had tone amber but
row.state green, a latent false-green for any consumer reading .row.state.
The spread preserves fail_count/first_failure_at/observed_at/signal so
drilldown metadata is unaffected; only .state becomes honest.
2026-05-30 21:01:44 -07:00
Jordan Ritter 79a22fb18d test(showcase): cover computeColumnTallyDetail health-dimension branch
Add cases for green-D3+D4+red-D5 and green-D3+red-D4, both landing in the
red bucket with dimension 'health'. No production change.
2026-05-30 20:52:07 -07:00
Jordan Ritter c506d8badf fix(showcase): deriveDepth D4 uses worst-state-wins instead of OR
A present red chat/tools row now pulls D4 down even if its sibling is green,
matching cell-model resolveD4. Single-side present-green cases still achieve
D4 (absent sibling is skipped).
2026-05-30 20:50:45 -07:00
Jordan Ritter afd3bdba36 fix(showcase): require emitted data above achievedDepth for isRegression
A cell is a regression only when the next rung above achievedDepth has
emitted data (exists && status !== null). Drops the no-data-D5 false
positive (mapped but unemitted D5) while keeping red-below-ceiling and
D3-red regressions.
2026-05-30 20:49:11 -07:00
Jordan Ritter 8c61713d66 fix(showcase): downgrade stale-green rows in resolveCell + resolveD5Row
Fold each contributing row's stale-green to degraded before tone/rollup
derivation so a frozen-green driver no longer reads as healthy. Per-badge
windows: e2e/d5/d6 6h, health/d2/smoke 45m. Thread co-render now from
cell-matrix gaps filter and cells-view stats.
2026-05-30 20:46:32 -07:00
Jordan Ritter d0da6e357d refactor(showcase): extract shared staleness helper into lib/staleness.ts
Move the three staleness windows and the byte-identical isStale/isRowStale
duplicate out of cell-model.ts and depth-utils.ts into one lib/staleness.ts.
Type-only import of StatusRow avoids a runtime cycle so live-status can also
consume it. No behavior change.
2026-05-30 20:41:25 -07:00
Jordan Ritter 5650004637 fix(showcase): compute CellModel.isRegression from achieved vs ceiling
isRegression is now ceilingDepth > 0 && achievedDepth < ceilingDepth instead
of a hardcoded false. The Coverage regressions filter reads the single source
on the model rather than inlining the same expression.
2026-05-30 18:35:08 -07:00
Jordan Ritter 34ae5667ce fix(showcase): make resolveD5 strict on missing mapped sub-rows
A multi-key D5 family with a missing mapped sub-row is now unverified
(status null), not credited green from the present rows. A present red
sub-row still yields red. Mirrors isD5Green's every(...) so both consumers
agree.
2026-05-30 18:35:08 -07:00
Jordan Ritter 268d12a390 fix(showcase): gate green chip on contiguous D5 and stale-gate D1/D2/D4
A green D6 no longer paints over a red/no-data D5: green requires an
intact verification ladder. Stale-green D1/D2 (45m) and D4 (1h) rows are
downgraded to amber via per-driver windows so frozen drivers can't credit
the depth ladder.
2026-05-30 18:35:08 -07:00
Jordan Ritter 3998f7fbdb fix(showcase): self-heal browser pool + flag stale-green e2e rows (#5112)
## Summary

Two confirmed bugs in the showcase harness + operator dashboard that
together produced a false-green D3 on the dashboard while the e2e probe
pipeline was actually dead.

### Bug 1 — Browser-pool death spiral
(`showcase/harness/src/probes/helpers/browser-pool.ts`)

When a chromium process crashed, `recycleSlot` relaunched it; if that
launch threw, the slot was permanently removed via `slots.splice`. A
single transient launch failure (OOM spike, fd exhaustion) thus
monotonically shrank the pool to empty with no self-heal, after which
every e2e probe failed with `launcher-error` / `BrowserPool acquire
timeout`.

Fix:
- The recycle retries the relaunch with bounded backoff (3 attempts,
doubling backoff).
- On exhaustion the slot is **kept** (capacity preserved) and parked as
`relaunchPending`; the next `acquire()` lazily re-attempts the launch.
- `acquire()` re-initializes the pool as a backstop when every slot has
been lost (`slots.length === 0`).
- Existing acquire/release/recycle semantics and logging style
preserved.

### Bug 2 — Stale-green e2e rows read as healthy
(`shell-dashboard/src/lib/cell-model.ts`,
`src/components/depth-utils.ts`)

When the e2e driver stops writing `e2e:<slug>/<feature>` rows, the last
row freezes. A green row then reads as a healthy D3 forever, so the
depth ladder shows a false-green D3 that masks a dead probe pipeline
instead of surfacing it.

Fix:
- `buildCellModel` and `deriveDepth` treat a green e2e row whose
`observed_at` is older than `E2E_STALE_AFTER_MS` (6h, matching the
original stale-window model) as degraded/amber, so it no longer credits
D3.
- Only green is downgraded — a stale red/degraded row already signals a
problem and is left as-is.
- An unparseable timestamp is treated as not-stale, so staleness is
never inferred from bad data.

## Test plan

- [x] New unit test: pool recovers after a transient relaunch failure
(does NOT shrink to 0) and re-inits when emptied — confirmed red→green.
- [x] New unit tests: a stale green `e2e:` row downgrades to amber (not
green) and does not credit D3; a fresh green row stays green; a stale
red row stays red — confirmed red→green.
- [x] Existing fixtures with hardcoded old `observed_at` updated to
default to a recent timestamp so legitimate green rows aren't treated as
stale.
- [x] `showcase/harness` full suite: 1646 tests pass.
- [x] `shell-dashboard` full suite: 630 pass (1 env-gated build-spike
skipped).
- [x] `packages/**` test graph: 17 projects pass.
- [x] typecheck clean (both packages); oxlint 0 errors; oxfmt clean.

Note: there is a separate operational lever (reduce `BROWSER_POOL_SIZE`
/ e2e concurrency, raise harness memory) handled outside this PR; this
is the code fix only.
2026-05-30 13:07:09 -07:00
Jordan Ritter 4c4e16a630 docs(showcase): correct resolveD5 comment on missing-row divergence from isD5Green
resolveD5 skips missing sub-rows and folds only present rows, so it does not
match isD5Green's every-key-present requirement; clarify staleness IS mirrored.
2026-05-30 11:05:28 -07:00
Jordan Ritter 5ab0c8e49b fix(showcase): apply D5 staleness per sub-row before the worst-state fold
A stale-green D5 sub-row was masked when a fresh-green sibling won the
all-green tie, since green is the lowest rank and staleness was checked
only on the post-fold winner. Downgrade each green-but-stale sub-row to
degraded before folding so any stale-green forces amber, matching
depth-utils isD5Green per-key semantics regardless of map order.
2026-05-30 10:52:40 -07:00
Jordan Ritter 993289cdfd fix(showcase): dedupe identical green-and-fresh depth helpers
Consolidate isE2eGreenAndFresh into isGreenAndFresh; both had identical
staleness semantics. Single helper now serves D3/D5/D6, removing a drift
hazard. No behavior change.
2026-05-30 10:30:55 -07:00
Jordan Ritter 9871d06671 refactor(showcase): rename getRuntimeConfigEdge to getRuntimeConfigForMiddleware
Clarify the wrapper's role (it forces noStore:false because unstable_noStore is
unavailable in middleware/Edge). Pure rename across shell, shell-docs, and
shell-dashboard: definitions, middleware call sites, and tests. No behavior
change.
2026-05-30 10:22:35 -07:00
Jordan Ritter e19159e3ce fix(showcase): downgrade stale-green D5/D6 dashboard rows like D3
Extend the e2e staleness downgrade to resolveD5/resolveD6 in cell-model and to
depth-utils so a frozen-green pipeline no longer credits D5/D6 as healthy. Also
correct misleading comments/test-doc nits flagged in review.
2026-05-30 10:21:33 -07:00
Jordan Ritter dff840b76a fix(showcase): downgrade stale-green e2e rows to degraded in the dashboard
When the e2e driver stops writing e2e:<slug>/<feature> status rows, the last
row freezes. A green row then reads as a healthy D3 forever, so the depth
ladder shows a false-green D3 that masks a dead probe pipeline (a wedged
browser pool, an outage) instead of surfacing it.

buildCellModel (cell-model.ts) and deriveDepth (depth-utils.ts) now treat a
green e2e row whose observed_at is older than E2E_STALE_AFTER_MS (6h, matching
the original stale-window model) as degraded/amber, so it no longer credits
D3. Only green is downgraded — a stale red/degraded row already signals a
problem and is left as-is. An unparseable timestamp is treated as not-stale so
staleness is never inferred from bad data.

Existing test fixtures that hardcoded old observed_at timestamps now default
to a recent value so green rows are not treated as stale; the new staleness
tests pass explicit timestamps.
2026-05-30 09:53:01 -07:00
Jordan Ritter 8714ab569b chore(showcase): apply oxfmt formatting across showcase scripts and shells
oxfmt --write normalized formatting on showcase scripts, the four shells, and the
new oxlint rule; required for the repo-root oxfmt --check CI gate.
2026-05-29 11:45:16 -07:00
Jordan Ritter 09b9f8910b chore(showcase): pre-push cleanup -- comment rot, log levels, env coalesce, test hardening
Non-functional cleanup pass on the showcase deploy-pipeline integration
branch. All changes are scoped to comment rot, log severity for already-
demoted runtime-config fields, length-aware env-name coalescing (a
deliberately-empty primary no longer masks a populated alternate), and
test-quality tightening. No production behavior change beyond the
specific items below.

Changes by area:

- shell/shell-dashboard/shell-docs runtime-config.ts: factor the
  `process.env[primary] ?? process.env[alt]` chain into a shared
  length-aware `readEnvPair` helper. The prior `??` form treated
  `PRIMARY=""` as set, masking a populated alternate; the helper now
  treats empty-string as unset and falls through to the alternate.
- shell-docs runtime-config.ts: demote the two recoverable URL fields
  (`intelligenceSignupUrl`, `posthogHost`) from console.info to
  console.warn. The `FATAL-CONFIG:` Sentry-alert prefix is preserved
  only on the true sentinels; the demoted fields now clear prod log-
  aggregation thresholds without raising ops alerts.
- All three shells' runtime-config.ts: prefix log lines with the shell
  name (e.g. `[shell-docs runtime-config]`) so the shared log stream
  identifies which shell emitted the line.
- shell-docs runtime-config-serialize.ts: rewrite the U+2028 / U+2029
  RegExp arguments using six-character ASCII backslash-u escape
  sequences (was: literal codepoints in the string arg). The literal
  codepoints are line terminators that a formatter or editor could
  silently strip, breaking the security-critical XSS escape. The
  ASCII form is robust to any such pass.
- shell-docs use-google-analytics.test.ts: de-tautologize the hook-
  order test. It now asserts `usePathname(` and `useEffect(` both
  exist in the source, so deleting all hooks would fail the test
  rather than trivially satisfying the early-return path.
- shell-dashboard baseline-types.test.ts: update the partner-count
  expectation from 25 to 26 -- the 26th entry (Cloudflare) is a
  legitimate integration that landed independently; the test was
  stale and had nothing to do with this branch.
- scripts/resolve-verify-matrix.ts: drop the `FIX 7 --` plan-
  internal prefix from a comment; keep the explanation.
- shell-docs/.env.example: correct the `NEXT_PUBLIC_SHELL_URL`
  fallback claim (sentinel, not canonical prod host) and document
  the remaining 7 consumed env vars with their FATAL/warn/silent
  semantics so the example matches runtime-config.ts.

Skipped:
- C-SENTINEL-DEDUP (`http://ops.invalid` shared constant across
  shell-dashboard's next.config.ts and runtime-config.ts): both
  files are at different module levels (root vs src/lib) and the
  string appears once in each; extracting to a shared module would
  widen the diff into a refactor for marginal benefit. Skipped per
  the spec's "if it widens diff awkwardly, skip" guidance.
- C-SSRTEST: already exhaustively covered. Each of the three shells
  has an SSR placeholder test that exercises every URL field via
  `new URL()` parseability and (for shell-docs) the analytics-key
  empty-string semantics. Treated as a no-op.

Validation: shell + shell-dashboard + shell-docs runtime-config /
serialize / GA tests green; bin/showcase Ruby suite green (87 runs);
showcase/scripts resolve-verify-matrix + aggregate-build-results +
lint-rule-no-public-env green (79 runs).
2026-05-29 11:45:15 -07:00
Jordan Ritter 28f33ecc8a fix(showcase): stop SSR 500 + hook-order regressions in shell runtime-config; tolerate env-name variants
Six fixes addressing CR findings on the Option-B runtime URL-injection migration:

1. SSR_PLACEHOLDER must be parseable URL sentinels — `new URL("")` throws on
   SSR causing 500s for any consumer that constructs URLs from runtime-config
   fields. Use `.invalid`-TLD sentinels (RFC 2606) for URL fields; analytics
   keys stay empty string. Add `suppressHydrationWarning` on consumers that
   render the placeholder server-side and the real value post-hydration
   (integration-grid, page-actions popover).

2. Hook-order: move `usePathname()`/`useEffect` ABOVE the early-return in
   use-google-analytics. Gate the effect bodies on `GA_ID` instead so React
   sees a stable hook order across renders.

3. `readUrl`/`readKey` accept either bare or `NEXT_PUBLIC_*`-prefixed env
   names via a fallback chain — covers both server-only and inlined-public
   variable conventions without forcing a rename across deploy targets.

4. Extract `serializeRuntimeConfig` to `lib/runtime-config-serialize.ts` so
   the OWASP-escape behavior (XSS via </script>, U+2028/U+2029 line-terminator
   injection) can be unit-tested without importing the layout into vitest.

5. Reclassify `intelligenceSignupUrl`/`posthogHost` from FATAL-CONFIG to
   info-level in shell-docs — these are optional integrations, not hard
   wiring failures, so absence should not poison the error stream.

6. Comment-rot cleanup: drop "Option B", B12, "the bug we are fixing", fix
   "four substrings"→"three substrings" miscounts, and refresh shell-docs
   .env.example to describe the runtime-injection contract instead of a
   stale next.config throw claim.

V1: shell + shell-docs `next build` succeeds (no Edge-runtime crash on
`unstable_noStore`).
V2: `OPS_BASE_URL=` shell-dashboard `next build` no longer throws —
`next.config.ts` is now a phase-aware function that emits a sentinel
destination at build time and throws only at start (PHASE_PRODUCTION_BUILD
from next/constants).

Tests: shell-docs 72/72, shell 12/12, shell-dashboard runtime-config 16/16
(pre-existing baseline-partner-count failure unchanged).
2026-05-29 11:45:15 -07:00
Jordan Ritter f2ba6b2564 test(showcase): add no-rebuild env-switch integration test (spike replay)
Replay the Option B runtime-config spike as a vitest integration test
that guards the no-rebuild env switching property going forward:

  - `next build` once with no per-env URL env vars (only a sentinel
    OPS_BASE_URL so next.config.ts's rewrites() can validate; Next
    evaluates rewrites at build, not only at start, so a placeholder
    here is unavoidable — the assertions don't depend on it).
  - `next start` twice, each on a fresh port and a DIFFERENT
    POCKETBASE_URL / SHELL_URL / OPS_BASE_URL set.
  - Fetch `/` on each boot and extract the inlined
    `window.__SHOWCASE_CONFIG__={...}` JSON from the served HTML.
  - Assert env-A URLs on the first boot and env-B URLs on the second
    boot of the SAME built artifact. If anyone re-introduces a
    build-time URL bake, the second boot's HTML still shows env-A
    values and this test fails.

Test lives at `showcase/shell-dashboard/tests/runtime-env-switch.spike.test.ts`
and is picked up via a new vitest include for `tests/**/*.spike.test.ts`
(the default include is `src/**/*.test.{ts,tsx}` and the integration-
weight spike doesn't fit there). The `.spike.test.ts` suffix keeps
the include narrow so the visual snapshot suite under
`tests/visual/` stays out.

Total wall time locally: ~12s (build ~2s warm with prebuild data
generation already run; two boots ~10s combined). Heavy enough to
gate behind a `tests:integration` script in CI rather than running
on every push.
2026-05-29 11:45:09 -07:00
Jordan Ritter cc52235bb8 fix(showcase): make shell-dashboard runtime-config client SSR-safe
The client-side getRuntimeConfig() reader previously threw when window
was undefined, on the assumption that "use client" components only run
post-hydration. That assumption is wrong for the Next.js App Router:
"use client" component bodies ARE executed on the server during the
initial SSR pass (that's how the HTML stream is built before the JS
arrives), so throwing breaks SSR entirely — every page that uses a
client component which reads runtime config 500s on the first request
with `[runtime-config.client] getRuntimeConfig() called on the server`.

This was surfaced by the B13 spike-replay integration test (`next build`
once, `next start` twice with different POCKETBASE_URL/SHELL_URL/
OPS_BASE_URL): the inline `<script id="__showcase_config__">` injected
by the root layout never reaches the rendered DOM because the page
crashes during SSR and falls back to the Next.js error boundary, with
the would-be script content captured (and JSON-escaped) inside the
RSC streaming payload instead of as a real `<script>` tag.

Switch the SSR branch to return a sentinel RuntimeConfig with empty
strings rather than throwing. Client components see the placeholder
during the initial server render, then re-read post-hydration when
window.__SHOWCASE_CONFIG__ is populated. The post-hydration "config
missing" branch still throws so genuine wiring bugs (layout bypass,
empty injection) stay loud. Updated the matching unit test to assert
the new sentinel behavior in place of the old throw assertion.
2026-05-29 11:45:08 -07:00
Jordan Ritter 21cffdf638 refactor(showcase): require OPS_BASE_URL at start, not build, in shell-dashboard
Clarify that next.config.ts's rewrites() is evaluated once at process
start (not build time), matching the Option B runtime-injection model
where every NEXT_PUBLIC_* URL is read at request time from the Railway
env. The validation message now communicates start-time semantics so
operators know to set OPS_BASE_URL on the Railway service rather than
at image build time.
2026-05-29 11:45:06 -07:00
Jordan Ritter 6d8f3a07ee refactor(showcase): migrate shell-dashboard consumers to runtime-config
B8.1/B8.2/B8.3. Replaces every process.env.NEXT_PUBLIC_* read in
shell-dashboard consumer code with the runtime-config readers, all
in a single commit so the tree never sits red across a partial
rename.

B8.1 — src/lib/pb.ts: replace the eager module-load read with a
lazy getPb() getter. The previous `const resolvedUrl =
resolvePbUrl()` at module top froze the URL at import time, which
defeated runtime injection. Now the PB client is constructed on
first getPb() call from runtimeConfig.pocketbaseUrl. The
PocketBase instance is returned directly (NOT wrapped in a Proxy)
so `instanceof PocketBase`, detached methods, and this-sensitive
chains all work without surprise. pbIsMisconfigured is converted
from a const boolean to a function — every runtime-config-derived
export in the module is now a function call.

B8.2 — src/lib/ops-api.ts: resolveBaseUrl() now reads
runtimeConfig.opsBaseUrl on the client (gated on typeof window so
SSR / server tests still fall through to the explicit-param or
/api/ops fallback). The runtime-config throw is caught and
treated as 'no override' so a missing __SHOWCASE_CONFIG__ wiring
bug degrades to the safe same-origin rewrite path.

B8.3 — src/components/feature-grid.tsx: resolveShellUrl() body
collapses to `return getRuntimeConfig().shellUrl`. The sentinel
about:blank#shell-url-missing now lives in runtime-config.ts (its
single source of truth) instead of being re-implemented here.

Consumers and test mocks updated in the same commit:
- hooks: useBaseline.ts, useLiveStatus.ts, useLastTransition.ts —
  swap `pb` import for `getPb`, capture `const pb = getPb()`
  at hook/effect entry, change pbIsMisconfigured reads to
  pbIsMisconfigured() calls.
- pb-auth-prompt.tsx — capture getPb() once at the top of the
  submit handler.
- test mocks: useLiveStatus.test.tsx, useLastTransition.test.tsx,
  __tests__/useBaseline.test.ts, cell-pieces.test.tsx — vi.mock
  stubs return { getPb: () => pb, pbIsMisconfigured: () => false }
  to match the new function-form API.
- use-probes.integration.test.tsx + ops-api.test.ts — swap the
  env-var snapshot/clear pattern for the window.__SHOWCASE_CONFIG__
  pattern that mirrors the production code path.
2026-05-29 11:45:06 -07:00
Jordan Ritter fb5c608c1f refactor(showcase): inject __SHOWCASE_CONFIG__ in shell-dashboard root layout
B5. Root server layout (shell-dashboard) now calls getRuntimeConfig()
once per request and injects window.__SHOWCASE_CONFIG__ via an
inline <script id="__showcase_config__"> as the FIRST child of
<head>, BEFORE the theme-init script and well before any client
component reads the global during hydration.

Serialization uses the OWASP-recommended escape for inline JSON in
HTML:
- < → \\u003c so a URL containing </script> in a hostile env value
  cannot break out of the inline script tag.
- U+2028 / U+2029 → \\u2028 / \\u2029 because those codepoints are
  legal in JSON strings but a syntax error inside JS string literals
  when the page is parsed as text/javascript.

The regex sources use new RegExp() with \\u escape strings rather
than regex literals — U+2028 and U+2029 are line terminators that
prematurely close a regex literal in TypeScript / many JS engines.
2026-05-29 11:45:06 -07:00
Jordan Ritter 7f480b152c feat(showcase): add shell-dashboard runtime-config server+client
Workstream B (Option B, runtime injection). Adds the shell-dashboard
runtime-config module pair plus their unit tests:

- src/lib/runtime-config.ts (server): reads URL env vars at REQUEST
  time via unstable_noStore() so a single built artifact can serve
  different values across staging vs prod by changing the Railway
  service env vars. Sentinel fallbacks in production (visible
  breakage) + console.error; localhost fallbacks + console.warn in
  dev. getRuntimeConfigEdge() variant skips unstable_noStore for
  Edge-runtime middleware.
- src/lib/runtime-config.client.ts (client): reads
  window.__SHOWCASE_CONFIG__ injected by the root layout. Throws on
  SSR (no window) and when the global is missing, so wiring bugs
  surface loudly.
- runtime-config.test.ts + runtime-config.client.test.ts: 9 tests
  covering env values, trailing-slash strip, dev defaults, sentinel
  fallbacks + console.error, live-env-on-each-call (no module-load
  freeze), and the Edge wrapper's noStore skip.
2026-05-29 11:45:06 -07:00
Jordan Ritter b7fae67f01 refactor(showcase): drop NEXT_PUBLIC_* build-args from CI and Dockerfiles
Implements plan-B B11. URL and analytics NEXT_PUBLIC_* values now reach
each shell at runtime via Option B (env-driven runtime-config), so the
GHA showcase_build.yml workflow no longer threads them through as Docker
build-args and the shell-dashboard/shell-docs Dockerfiles no longer
declare the matching ARG/ENV pairs.

- showcase_build.yml: shell-dashboard and shell-docs matrix entries lose
  build_args_pb_url / build_args_shell_url / build_args_ops_url /
  build_args_base_url / build_args_analytics; the 'Prepare build args'
  step drops the corresponding env: keys and if-branches plus the five
  analytics NEXT_PUBLIC_* secrets. COMMIT_SHA and BRANCH stay — they
  identify the artifact.
- showcase/shell-dashboard/Dockerfile: remove ARG/ENV for
  NEXT_PUBLIC_SHELL_URL, NEXT_PUBLIC_POCKETBASE_URL, OPS_BASE_URL plus
  the explanatory comments. Update the runner-stage comment to point at
  runtime-config.ts as the new source of truth.
- showcase/shell-docs/Dockerfile: remove ARG/ENV for
  NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_SHELL_URL, NEXT_PUBLIC_POSTHOG_KEY,
  NEXT_PUBLIC_REB2B_KEY, NEXT_PUBLIC_SCARF_PIXEL_ID, NEXT_PUBLIC_REO_KEY,
  NEXT_PUBLIC_GOOGLE_ANALYTICS_TRACKING_ID. COMMIT_SHA / BRANCH retained.

shell/Dockerfile and shell-dojo/Dockerfile already only declare commit-sha
and branch ARGs — no changes needed there (per plan-B B11.4).
2026-05-29 11:45:05 -07:00
Tyler Slaton 8e59b1e2e9 chore: run pnpm format
Signed-off-by: Tyler Slaton <tyler@copilotkit.ai>
2026-05-29 08:17:21 -07:00
Alem Tuzlak 2405a46fa6 feat(showcase): add ms agent harness dotnet chat 2026-05-26 13:36:38 -07:00
Jordan Ritter 76020857e2 feat(showcase): per-framework D4/D6 aimock fixtures + D6 probe driver + harness scoping (#5022)
## Summary

Lands the per-framework fixture reorg + D6 probe driver + harness
scoping work that was parked behind the ag-ui header-forwarding chain
(now shipped via #4984, #4951, #5015, #5016).

This is the foundational data layer the D6 dashboard needs to populate.
Slice 1 (#5018) shipped the rendering plumbing earlier today; this PR
makes d6:<slug>/<featureId> PB rows start flowing.

## What ships

- 477 per-integration aimock fixtures organized under `d4/`, `d6/`,
`shared/` directories (flat `d5-all.json` / `feature-parity.json`
deprecated)
- Every fixture keyed by `match.context` to enforce per-integration
isolation (server-side routing already merged in aimock #226)
- D6 all-pills probe driver + per-integration scoping in
showcase-harness
- X-AIMock-Context header propagation in 18 integration Playwright
configs
- D6-ceiling chip color algorithm (D6 is integration-scoped aggregate,
maxPossible raised from 5 to 6)
- Docker-compose updates for the new fixture dir layout
- 12 HITL fixtures migrated from main's d5-all.json additions into
shared/_migrated-from-d5-all-hitl.json

## Why this is safe to ship now

- ag-ui/LangGraph configurable+context HTTP 400 blocker is fixed (#5015
+ #5016)
- sdk-python 0.1.91 with `_extract_forwarded_headers_from_config` is on
PyPI and pinned across showcase
- aimock server-side context routing is already live (aimock #226)
- The SDK overlay hacks the branch carried locally are now redundant —
discarded before rebase

## Pre-existing CI note

`@copilotkit/web-inspector:test` has a pre-existing failure
(`window.localStorage.clear is not a function` in telemetry tests) —
verified on clean main checkout. Not introduced by this PR.

## Follow-ups

- Distribute migrated HITL fixtures from shared/_migrated-from-*.json
into per-integration d6/<slug>/ files
- Slice 2: D6 drilldown DIMENSIONS + AdaptiveStatsBar rollup section
- LGP gen-ui-interrupt second-pill framework bug (Hypothesis B in
useInterrupt hook)
2026-05-26 11:59:46 -07:00
Jordan Ritter 5badfb82d6 feat(showcase-dashboard): D6-ceiling chip color algorithm + aggregate key
Update depth-utils to treat D6 as an integration-scoped aggregate
(d6:<slug> not d6:<slug>/<featureId>), raise maxPossible from 5 to 6
when D5 mapping exists, and adjust chipColor derivation so D5-green
without D6 yields amber instead of green. Update composed-cell memo
comparator and cell-drilldown dimensions. Comprehensive test updates
across cell-model, depth-utils, compute-tally-detail, unified-cell,
and status-tab.
2026-05-26 11:26:19 -07:00
Jordan Ritter 0eec6318e8 feat(showcase-dashboard): D6 rollup counter in AdaptiveStatsBar 2026-05-26 11:24:59 -07:00
Jordan Ritter 9236b82338 feat(showcase-dashboard): expose D6 dimension in cell drilldown panel 2026-05-26 11:19:22 -07:00
Jordan Ritter 5f2bdd9e53 feat(showcase-dashboard): render D6 chips on cells
Extends the active rendering path with D6 support so dashboard cells
can display D6 ("parity vs reference") state. cell-model.ts gains
ceilingDepth | 6 + d6 TestLevel field + resolveD6() + D6-ceiling
chip-color algorithm. unified-cell.tsx + feature-grid.tsx render the
new D6 chip. live-status.ts gets the small Dimension/BadgeRender delta
the branch needed.

Until d6:<slug>/<featureId> PB rows start flowing (separate PR for the
D6 probe driver + per-framework fixtures), every cell renders D6 gray
("no data -- probe pending"). This is the accurate state -- visibility
without functional risk.

Cherry-picked from feat-d6-everything-works (the per-framework fixtures
branch). The remainder of that branch -- d6-all-pills driver, fixture
reorg, harness config -- lands in a follow-up PR now that the ag-ui
header-forwarding prerequisites (#4984, #4951, #5015, #5016) are in.
2026-05-26 11:12:41 -07:00
Sam Julien 9aa1db57ae fix(shell-dashboard): route dashboard docs cell to canonical docs host (#4804)
## Summary

- Shell pre-cutover fix. Flip the dashboard cell "Docs" link in
`showcase/shell-dashboard/src/components/cell-pieces.tsx:59` from
`${shellUrl}/${slug}${shellPath}` to
`https://docs.copilotkit.ai/${slug}${shellPath}`.
- Pre-cutover this CNAME serves the Vercel docs site; post-cutover it
serves shell-docs on Railway. Both resolve `/<framework>/<slug>`
correctly, so the change is safe across the DNS flip window.
- Demo + Code iframe links (lines 20, 21) intentionally untouched — they
iframe `/integrations/[slug]/[demo]/{preview,code}` routes that only
`showcase/shell` serves today; porting those is post-cutover work.

## Why this is flip-blocking

After the `docs.copilotkit.ai` DNS flip, `${shellUrl}` (=
`showcase.copilotkit.ai`) no longer serves `/docs/**` — those routes
were ripped out and the redirect middleware moved to shell-docs in PR
#4702 (merged 2026-05-08). Without this change, every dashboard "Docs"
cell goes 404 at T-0 of the flip.

## Test plan

- [ ] CI green
- [ ] Smoke locally: `pnpm nx run
@copilotkit/showcase-shell-dashboard:dev`, hover any cell's Docs link,
confirm href starts with `https://docs.copilotkit.ai/`
- [ ] After merge + Railway redeploy of shell-dashboard, hit production
and re-verify the href
2026-05-20 15:14:59 -07:00
Jordan Ritter 736d9aaba6 Add LinkPreview wrapping to LinksLayer in UnifiedCell 2026-05-15 12:22:55 -07:00
Jordan Ritter 415f9d674f feat(showcase/shell-dashboard): add iframe link preview on hover (#4864)
## Summary

- Adds hover-triggered iframe preview popups to Demo and Code links in
the showcase dashboard grid
- Hovering a link for 300ms shows a 400×300 popup with a scaled-down
(thumbnail) view of the target page
- Clicking the popup opens the URL in a new tab; moving away dismisses
it after 200ms
- Includes loading state (pulsing indicator), fade-in on load, and
"Preview unavailable" fallback after 8s timeout
- Only one popup visible at a time (singleton); touch devices skip
previews entirely

## Test plan

- [ ] 16 unit tests covering: hover delay, dismiss timing, bridge gap,
iframe src, click overlay, portal rendering, viewport flip, singleton,
touch device exclusion, loading/loaded/unavailable states
- [ ] `vitest run` — 533 tests pass, 0 failures
- [ ] `tsc --noEmit` — no new type errors
- [ ] `next build` — builds successfully
- [ ] Manual smoke test: hover delay, dismiss, bridge gap,
click-through, singleton, loading states all verified in browser

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-15 11:54:25 -07:00
github-actions[bot] 678d832f42 style: auto-fix formatting 2026-05-15 18:51:10 +00:00
Jordan Ritter 7247f60f3a Add loading and error states to link preview popup 2026-05-15 11:46:47 -07:00
Jordan Ritter 027b1ab2be fix(showcase/shell-dashboard): scale iframe content to fit preview popup 2026-05-15 11:34:58 -07:00
Jordan Ritter b439983444 fix(showcase/shell-dashboard): add react-dom type declaration 2026-05-15 11:08:04 -07:00
Jordan Ritter 6e0a15871f feat(showcase/shell-dashboard): integrate LinkPreview into LinksLayer 2026-05-15 11:04:45 -07:00
Jordan Ritter 4622e78068 Add LinkPreview component with hover iframe popup
Hover-activated iframe preview popups for demo links with:
- 300ms hover delay to prevent accidental triggers
- 200ms dismiss delay with bridge-gap support (link-to-popup mouse movement)
- Singleton behavior ensuring only one popup visible at a time
- Portal rendering into dedicated #link-preview-root div
- Viewport-aware positioning (flips above when near bottom)
- Transparent click overlay that opens URL in new tab
- Touch device exclusion via matchMedia check
- 11 comprehensive tests covering all behaviors
2026-05-15 11:02:57 -07:00
github-actions[bot] c0fb61e774 style: auto-fix formatting 2026-05-14 22:06:20 +00:00
Jordan Ritter a1b7cc7c4e feat(showcase/dashboard): add system health banners for auth and browser pool
DiscoveryAuthBanner renders above all tabs when discovery auth fails.
Two variants: serving-stale (probes running against cached data) and
no-cache (probes offline). Also surfaces browser pool degradation.
Runtime signal shape validation, auto-dismiss on recovery. 12 tests.
2026-05-14 15:05:15 -07:00
Jordan Ritter 25a76275e7 fix(showcase): red chipColor when tests exist but all fail
Previously achievedDepth=0 always produced gray regardless of whether
tests existed. Now: ceilingDepth=0 (no tests) = gray, ceilingDepth>0
with achievedDepth=0 (tests exist, all fail) = red. Tally dimension
derived from model instead of hardcoded "e2e".
2026-05-13 23:50:05 -07:00