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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
## 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.
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.
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.
Consolidate isE2eGreenAndFresh into isGreenAndFresh; both had identical
staleness semantics. Single helper now serves D3/D5/D6, removing a drift
hazard. No behavior change.
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.
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.
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.
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).
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).
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.
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.
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.
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.
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.
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.
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).
## 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)
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.
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.
## 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
## 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)
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
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.
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".