Commit Graph

10887 Commits

Author SHA1 Message Date
Maxim b91e1710c5 feat(saas-demo): v2 Hono runtime endpoint with BuiltInAgent 2026-06-02 22:40:02 +02:00
Jordan Ritter 7b0e663b22 fix(showcase): render reachable depth buckets (D0..D6) in stats bar, drop dead D1/D2
The depth-distribution row showed permanent-zero D2/D1 rows while the computed
D0 bucket (wired-but-unverified cells) was never rendered, so wired cells
vanished from the row and it never summed to the "Wired" count.
buildCellModel().achievedDepth is typed 0|3|4|5|6 and can never be 1 or 2.

- Remove unreachable d1/d2 from the DepthDistribution type, from
  computeDepthDistribution's init, and from the rendered levels array.
- Add a D0 row to the rendered levels so wired-unverified cells are visible
  and the distribution sums to the wired-cell count (D6,D5,D4,D3,D0).
- Reconcile section wrapper keys: use each section's stable key instead of the
  array index so overlay-toggle reconciliation is correct.
- Document that health/depth/d6 rollups need no dedup: catalogData.cells is
  one row per (integration, feature) grid cell (verified: 0 duplicate pairs),
  so these per-cell signals are counted exactly once.
2026-06-02 13:30:35 -07:00
Maxim 28167ef287 build(saas-demo): add @playwright/test devDependency for smoke tests 2026-06-02 22:29:00 +02:00
Jordan Ritter 13d7fa5ee5 fix(showcase): correct aggregate-stats divergence from per-cell model
Extract the AdaptiveStatsBar aggregate computations out of page.tsx into a
unit-testable pure module (src/lib/page-stats.ts), mirroring the
computeColumnTally pattern, and fix a cluster of correctness bugs:

1. D6 cells were dropped from the depth distribution: DepthDistribution
   lacked a `d6` key and the `\`d${depth}\` as keyof` cast produced
   `dist["d6"]++ === NaN`. Add `d6` to the type, render a D6 row in
   DepthDistributionSection, and replace the cast with an exhaustive
   Record<0|3|4|5|6, keyof DepthDistribution> map the compiler checks.
2. d6Stats folded amber (stale/degraded D6) into gray. Count degraded
   distinctly and surface it in D6Section.
3. healthStats counted gray (no-data) cells as green, contradicting the
   "stats bar matches the matrix" invariant. Track no-data separately and
   render it in HealthSection.
4. isSupported is correctly hardcoded true for wired-cell stats: a wired
   catalog cell can never be in not_supported_features (generate-registry
   resolves those to status "unsupported" before "wired"), so stats and
   renderCell cannot diverge. Comment updated to state the invariant.
5. parity_tier was indexed via an unchecked cast (unknown tier →
   `undefined++ === NaN`). Validate against the known tier set and skip +
   log loud on unknown.

The matrix render path (renderCell/buildCellModel) is untouched.
2026-06-02 13:19:43 -07:00
github-actions[bot] 8d071aa8a0 style: auto-fix formatting 2026-06-02 13:11:43 -07:00
Jordan Ritter 036ecf546a feat(showcase): single-service promote dropdown + unattended hardened promote
Convert the promote workflow's `service` input from a freeform string
(default "all", the accidental-fleet-promote footgun) to a generated
`type: choice` dropdown whose first/default option is a rejected sentinel
so a blind "Run workflow" aborts instead of promoting.

resolve-targets: reject the sentinel, fail loud on ambiguous matches
(no silent head -n1), independently re-filter probe.prod, and reject
--digest combined with `all`. promote: run unattended in CI
(--yes --non-interactive) — the manual dispatch + service selection is
the human authorization. notify: empty-webhook guard + fallback warning,
neutral state for sentinel-abort and any cancellation, and surface
resolve-targets.result in the failure alert for triage.
2026-06-02 13:11:43 -07:00
Jordan Ritter a7499569d7 feat(showcase): self-maintaining promote service dropdown generator
Add showcase/scripts/sync-promote-service-options.ts: generates the
promote workflow's service `choice` options from the SSOT
(railway-envs.ts), spliced between BEGIN/END markers in
showcase_promote.yml. Fail-loud throughout — every emitted token must
resolve to exactly one service under the resolve-step predicate
(name|dispatchName match AND probe.prod), tokens are YAML-safe, args are
strict (a typo'd flag cannot trigger a destructive write), and markers
are validated before any rewrite.

Wire it into a lefthook pre-commit hook (regenerate + restage; set -e so
a failed regen blocks the commit) and an advisory (never-failing) drift
check in showcase_validate.yml. Vitest coverage for ordering, exclusion,
collision/ambiguity guards, marker errors, exit codes, idempotency, and
the import-side-effect guard.
2026-06-02 13:11:43 -07:00
Maxim a39c559152 build(saas-demo): move banking demo onto CopilotKit v2 workspace packages 2026-06-02 22:09:05 +02:00
Jordan Ritter 377b56cb19 test(showcase): harden D6 rollup tests — order-independent worst-state, comment precision
Add a D5 any-fail fan-out case where the red sub-key is NOT first (greens
then a trailing red) and assert the rollup still resolves red, proving the
worst-state fold is order-independent. Also tighten three imprecise/misleading
comments: clarify the absent-tools D4 worst-state skip, annotate the d6:lgp
aggregate as a distractor not consulted by per-cell resolveD6, and drop the
inapplicable amber-path clause from the aggregate-only gray case.
2026-06-02 13:06:01 -07:00
Jordan Ritter 07abb37ea4 fix(showcase): thread shared now + periodic staleness re-render in dashboard page 2026-06-02 13:05:59 -07:00
Jordan Ritter 952209977e test(showcase): harden useOverlays mocks and cover baseline + selectProbe
Stub window.location explicitly via Object.defineProperty (instead of the
global) so the hook's window.location.hash read-path is genuinely exercised
and robust even if window !== globalThis. Add coverage for setTab persistence,
the #baseline parse resolving to the baseline tab, and selectProbe leaving the
tab and hash consistent (ops probe drilldown).
2026-06-02 13:05:58 -07:00
Jordan Ritter b6f865adce fix(showcase): persist overlays in setTab for storage symmetry
setTab wrote the URL hash but, unlike toggle/updateOverlays, never called
saveToStorage, so a tab switch dropped overlay persistence asymmetrically.
Persist the current overlay set in setTab so the user's overlay selection
survives tab switches and reloads. Covered by a red-green test in the
useOverlays suite.
2026-06-02 13:05:58 -07:00
Jordan Ritter e6da6ba3ed refactor(showcase): re-export overlay/preset definitions from overlay-types
OverlayToggleBar redefined ALL_OVERLAYS, PRESETS, and OverlayPreset as local
literals duplicating src/lib/overlay-types.ts (drift hazard if an overlay or
preset is added to one but not the other). Replace the local definitions with
re-exports from overlay-types so there is a single source of truth. Behavior
identical; existing component-level imports keep working via the re-export.
2026-06-02 13:05:58 -07:00
Jordan Ritter e408489313 fix(showcase): correct cell renderer memo keys and d6 overlay blank matrix
ComposedCell.arePropsEqual watched the wrong liveStatus keys: it omitted
agent/chat/tools (which its only consumer, deriveDepth, reads for D2/D4) and
watched an unused smoke key. Watch exactly deriveDepth's reads and fix the
stale "keep in sync with resolveCell" comment.

UnifiedCell rendered a blank cell for a {d6}-only overlay set because d6 is
consumed only by AdaptiveStatsBar and produced no per-cell content. Treat d6
as content-bearing: surface the depth chip + health row (which renders the
per-cell D6 badge). Default overlay set (links/health/depth) is unchanged.
2026-06-02 13:05:56 -07:00
Jordan Ritter 7f5c3ac16e fix(showcase): correct live-status/depth-utils CR findings
- Rename module-private D5_STATE_RANK to WORST_STATE_RANK (used by both
  resolveD5Row and resolveD6Row) and move the misplaced resolveD5Row doc
  block onto resolveD5Row.
- Add a test asserting every CATALOG_TO_D5_KEY mapping value is free of the
  ':' / '/' key delimiters (keyFor's guard did not cover mapping values).
- mergeRowsToMap: fire the collision warning only on genuine state
  divergence (rowsAreNoop content compare) instead of reference inequality,
  eliminating noisy false warnings.
- isD6Green JSDoc: note the caller only invokes it after D5 is green
  (contiguous ladder), so D6 can never be credited over a broken D5.
- computeMaxPossible: treat stub like unshipped (maxPossible=0) so a
  not-yet-wired stub cell no longer false-positives isRegression.
2026-06-02 13:05:55 -07:00
Jordan Ritter 461621af0e fix(showcase): return effective (downgraded) row from cell-model resolvers
resolveD4/D5/D6 returned the RAW status row in `.row` while `.status` was
derived from the stale-downgraded effective state, so a stale-green fold
reported `.row.state === "green"` but `.status === "amber"`. Store the
effective (downgraded) row instead so `.row.state` agrees with `.status`,
mirroring the invariant in live-status.ts `buildBadge`. Also replace
resolveD4's `worstState!`/`winner!` non-null assertions with a guard like
resolveD5/D6, and pin the resolveD3 producer-invariant (D1/D2 gate is
enforced upstream; buildCellModel never reads health:/agent: rows) with a
characterization test and comment.
2026-06-02 13:05:54 -07:00
Jordan Ritter 19a1f750de fix(showcase): point dashboard OPS_BASE_URL at harness, not itself 2026-06-02 12:37:58 -07:00
Jordan Ritter c32a877a91 test(showcase): add dashboard cell color/rollup matrix 2026-06-02 12:37:55 -07:00
Jordan Ritter a510aa08d6 feat(showcase): enable Depth (and D6) overlays by default 2026-06-02 12:37:55 -07:00
Maxim 6e1f955b2e docs(saas-demo): remove stale SQL/MSA references from README 2026-06-02 21:29:21 +02:00
Jordan Ritter acf2e135a2 feat(showcase/harness): env-gate LOCAL_SERVICES_JSON injection seam in railway-services discovery
Make LOCAL_SERVICES_JSON a permanent, env-gated feature of the railway-services
discovery source so the harness (especially the d6-all-pills-e2e probe driver)
can run against LOCAL backends instead of querying Railway.

When LOCAL_SERVICES_JSON is set, enumerate() builds the IDENTICAL
RailwayServiceInfo[] shape from the injected static list and returns it without
consulting Railway creds — only the service URLs differ (local container
hostnames vs Railway public domains). The same namePrefix/nameExcludes filter is
applied, shape is recomputed from the name via classifyShape (single source of
truth), and malformed JSON throws DiscoverySourceSchemaError (same taxonomy as a
Railway shape failure). When the var is unset OR empty, the Railway discovery
path is byte-identical to before.

Critically, the injected demos array is plumbed end-to-end
(demos: svc.demos ?? []) so the d6-all-pills driver's
demosToFeatureTypes(input.demos) produces a real feature matrix; an empty demos
would short-circuit the driver to a zero-cell false-green.
2026-06-02 12:18:10 -07:00
Maxim d0faa1ea28 chore(saas-demo): drop SQL/MSA feature and dedupe lockfile 2026-06-02 21:17:41 +02:00
Austin Merrick 19370f9bf9 Merge branch 'main' into austin/oss-85-reference-documentation-for-copilotkitcore 2026-06-02 12:08:32 -07:00
github-actions[bot] 601e7f17dc style: auto-fix formatting 2026-06-02 18:49:04 +00:00
Austin Merrick 83769e17ee fix(shell-docs): render reference code blocks with syntax highlighting
The reference route rendered fenced code blocks as bare, unstyled
<pre> (no highlighting, no copy button) because its MDXRemote call
omitted the rehypeCode plugin and the pre: MdxCodeBlock override that
the main docs pipeline uses. Wire both in (verbatim from the framework
route) so reference code blocks match the rest of the docs. Fixes
rendering for all reference SDKs (React v2/v1 + Core).
2026-06-02 11:46:44 -07:00
Tyler Slaton 4c6ecbb550 docs: simplify cookbook navigation (#5166)
## Problem

The cookbook sidebar reused the docs framework picker and showed extra
grouping chrome even though Daytona is the only cookbook page.

## Why

The cookbook routes render through the shared docs page view, which
always injected the framework selector into the sidebar and included the
cookbook overview/section header from `meta.json`.

## Fix

- Let cookbook pages explicitly suppress the shared sidebar selector.
- Redirect `/cookbook` to `/cookbook/daytona`.
- Keep the cookbook sidebar to the single Daytona page link.

Validation:
- `npm run lint` in `showcase/shell-docs` passes with existing warnings.
- `npm run build` in `showcase/shell-docs` passes with existing
Next/Turbopack warnings.
- Repo pre-commit package checks passed on both commits.
- `npm run typecheck` currently fails on existing `SignupLink` test prop
errors unrelated to this change.
2026-06-02 11:36:24 -07:00
Jordan Ritter 377b7e8163 test(showcase): complete D6 per-cell test migration
Rewrite the two stale live-status keyFor tests that still asserted the old
integration-scoped D6 model (and the wrong e2e-full driver) to assert the
per-cell d6:<slug>/<featureId> shape; expand the two beautiful-chat D5 fan-out
tests to emit all 5 sub-keys so the worst-state fold (not the missing-row path)
is what's exercised; drop the dead unmapped-feature else branch in unified-cell
arePropsEqual (resolvers never read a direct key for unmapped features).
2026-06-02 11:35:51 -07:00
Tyler Slaton 9ef184ec76 Merge branch 'main' into docs/cookbook-daytona-default 2026-06-02 11:35:26 -07:00
Tyler Slaton 2e540efdd4 docs(shell-docs): remove cookbook section header 2026-06-02 11:31:18 -07:00
Tyler Slaton 3486944356 fix(showcase): controlled gen-UI — reliable 2nd-suggestion render + sidebar tag (OSS-137) (#5029)
## What & why


[OSS-137](https://linear.app/copilotkit/issue/OSS-137/controlled-gen-ui-demo-optimize-2nd-suggestion-prompt-rename-sidebar)
— the **Controlled Generative UI** demo (`gen-ui-tool-based`) had two
issues, scoped here to **LangGraph-Python** and **Google ADK** (per the
ticket; other 16 integrations roll out later).

### 1. 2nd suggestion didn't reliably render UI
The "Traffic pie chart" chip (`"Show me a pie chart of website traffic
by source."`) names a subject but supplies no numbers, so the agent
**asked the user for data** instead of rendering a chart.

**Fix:** a system-prompt directive (both LGP + ADK agents) instructing
the agent to invent plausible illustrative sample values, call
`render_*` immediately, and **never** reply with a clarifying question.
The suggestion copy stays clean — behavior is carried by the system
prompt, not by leaking "(use sample data)" hints into the UI.

### 2. Sidebar tag → product language
Retagged the demo from `generative-ui` → `controlled-generative-ui` (LGP
+ ADK), so the dojo sidebar pill reads **"Controlled Generative UI"** —
the established taxonomy already used in `shared/feature-registry.json`
and the dashboard catalog.

## Tests
Added D5 aimock fixture entries mirroring all three suggestion chips
(bar / traffic-pie / market-share) so the suggestion-click path has
deterministic coverage. The existing `"revenue by category"` probe
message is **preserved**, so the
[dashboard](https://dashboard.showcase.copilotkit.ai/#matrix:links,health)
D5 row for the edited row stays green.

## Acceptance check
- [x] 2nd suggestion renders UI without asking for data (system-prompt
directive; verified locally against the live agent)
- [x] Sidebar entry tagged "Controlled Generative UI"
- [x] Sample/hallucinated data supplied via system prompt
- [x] Scoped to LGP + ADK
- [x] Tests augmented (D5 fixtures for every chip)
- [x] D5 still shown for the edited row (probe message unchanged)

## Out of scope (left out deliberately)
`package-lock.json` churn from a local reinstall (un-pins `latest`) was
**not** committed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-02 11:31:17 -07:00
Tyler Slaton 5d9f66c2ce docs(shell-docs): simplify cookbook navigation 2026-06-02 11:27:42 -07:00
Austin Merrick e35f776c64 ci(format): fix checkout for fork PRs (#5164)
## Problem

The `format` job sets `ref: github.head_ref` but leaves `repository:`
unset, so it checks the head branch out of the **base** repo. For fork
PRs the head branch only exists on the fork, so checkout fails before
formatting even runs.

Seen on #5099 (fork PR, branch `fix/5072`):

```
##[error]A branch or tag with the name 'fix/5072' could not be found
```

## Fix

Resolve `repository` to the head repo on PRs, matching `ref`:

```yaml
ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
```

Same-repo PRs are unchanged (head repo == base repo), so the auto-format
push-back still works.
2026-06-02 11:11:01 -07:00
Tyler Slaton a4fc41aae2 docs(shell-docs): audit v2 package guidance 2026-06-02 10:53:41 -07:00
Tyler Slaton 9ad9e736ff docs(shell-docs): clarify v2 CopilotKit provider import 2026-06-02 10:47:56 -07:00
Austin Merrick 27ff2689b2 ci(format): check out head branch from head repo for fork PRs
The format job checked out github.head_ref without setting repository,
so it defaulted to the base repo. For fork PRs the head branch only
exists on the fork, making checkout fail with 'a branch or tag with the
name <branch> could not be found' (e.g. PR #5099). Resolve repository to
the head repo on pull_request events; same-repo PRs are unchanged, so
the same-repo-guarded auto-format push-back still works.
2026-06-02 10:28:53 -07:00
Tyler Slaton f73771e865 docs(shell-docs): import CopilotKit from v2 2026-06-02 10:08:50 -07:00
Tyler Slaton 5b0ff5a844 test(shell-docs): fix signup link ssr typing 2026-06-02 10:00:28 -07:00
Tyler Slaton 959ad33738 docs(shell-docs): recommend root CopilotKit provider 2026-06-02 09:56:01 -07:00
Jordan Ritter c64aebc422 fix(showcase): resolve D6 per-cell instead of integration aggregate
The Feature Matrix rendered every D6 cell in an integration's column from
the aggregate PocketBase row `d6:<slug>` (red whenever ANY cell fails),
so genuinely-green cells (e.g. langgraph-python/voice) rendered red. The
harness already emits per-cell `d6:<slug>/<featureType>` rows over the same
featureType keyspace as D5.

Resolve D6 per-cell through the same CATALOG_TO_D5_KEY bridge D5 uses,
mirroring resolveD5/resolveD5Row exactly: per-sub-row stale-green→degraded
fold before the worst-state fold, and strict handling of a missing mapped
sub-row (no-data unless a present sub-row is red). The aggregate `d6:<slug>`
row no longer drives per-cell rendering. Updated the two memo comparators
(composed-cell, unified-cell) to diff per-cell D6 sub-keys, and the stale
doc-comments that described D6 as an integration-level aggregate.
2026-06-02 09:47:34 -07:00
Austin Merrick a98c4b67bc docs(shell-docs): add @copilotkit/core API reference
Hand-written reference for the framework-agnostic TypeScript client,
matching the v2 React reference convention and verified against source:
- index + 2 classes (CopilotKitCore, ProxiedCopilotRuntimeAgent)
- 6 types (configs, FrontendTool, CopilotKitCoreSubscriber, Suggestion,
  SuggestionsConfig)
- 3 enums (ToolCallStatus, CopilotKitCoreErrorCode,
  CopilotKitCoreRuntimeConnectionStatus)
2026-06-02 09:12:16 -07:00
Austin Merrick 987d4892d6 feat(shell-docs): add @copilotkit/core to the reference SDK picker
Introduces a third SDK in the reference docs alongside React v2 and v1:
- reference-items.ts: add the 'core' version, generalize root-vs-nested
  routing, add 'types'/'enums' subdirs + categories, recognize a core/
  slug prefix (literal strip), and emit its static params
- reference-version-selector.tsx: relabel the picker as an SDK switch
  (React v2 / React v1 / Core (TypeScript)), import ReferenceVersion from
  reference-items, give listbox options role=option/aria-selected
- app/reference/page.tsx: rename 'API Reference' to 'Overview' and add a
  'Choose your SDK' card chooser
2026-06-02 09:12:16 -07:00
Alem Tuzlak c6d71e9721 feat(showcase): add a2ui pdf analyst demo (#5076)
## Summary

A new showcase at `examples/showcases/a2ui-pdf-analyst` - an agent that
answers questions about a PDF by composing UI from your own design
system.

PDF text is extracted client-side via `pdfjs-dist` and inlined into the
message using CopilotKit's multimodal attachment support.
`multimodal_middleware.py` patches `ag-ui-langgraph` so it survives
serialization and arrives intact at the agent.

Two rendering strategies on the same 21-component catalog:

- `/fixed` : you author the layout once as a JSON tree. The agent
extracts KPIs, trend, and table rows and fills the slots via
`update_data_model` ops. One LLM call per turn, predictable,
brand-locked.
- `/dynamic` : no pre-written layout. The agent calls `query_pdf` to
extract a structured answer, then `generate_a2ui` — which spawns a
secondary LLM bound to a `render_a2ui` shim via `tool_choice`, forcing
structured component output that becomes A2UI `create_surface` +
`update_components` ops. A stat question lands as a StatCard; a
breakdown becomes a DonutChart; a research paper becomes Heading +
Callout + BulletList.
- `/catalog` : all 21 components live, filterable by group.

Frontend: Next.js 16 · React 19 · Tailwind v4 ·
`@copilotkit/a2ui-renderer` · `pdfjs-dist`.
Backend: Python 3.12 · FastAPI · LangChain · LangGraph.

Demo video (with all use cases), sample PDFs with prompts and full
architecture in `README.md`.

## Test plan

- [ ] `pnpm install` completes (`uv sync` runs via postinstall)
- [ ] `pnpm dev` boots web on :3000 and agent on :8123
- [ ] `/fixed` — attach Apple Q4 PDF, ask `Render the dashboard.` →
KPIs, trend chart, donut, and table all render. Click a scope chip →
re-renders without re-uploading.
- [ ] `/dynamic` with Apple Q4 PDF — `What was net income last quarter?`
→ StatCard, `Break iPhone vs Mac vs iPad as a donut.` → DonutChart
- [ ] `/dynamic` with Tesla Q3 PDF — `Plot quarterly production against
deliveries as a scatter chart` → ScatterChart
- [ ] `/catalog` — all 21 components visible, group filters work
2026-06-02 13:05:07 +02:00
Anmol Baranwal 3e79fe0941 fix: add @ag-ui/core dep and fix pnpm scripts
- add @ag-ui/core as explicit dependency to fix production build
- replace npm run with pnpm run in dev and postinstall scripts
2026-06-02 16:04:33 +05:30
Anmol Baranwal 38d7c5720e Merge branch 'main' into showcase-a2ui-pdf-analyst 2026-06-02 15:07:32 +05:30
Mike Ryan 1f3c5fb007 fix(runtime): filter generated thread titles 2026-06-01 17:51:50 -07:00
Markus Ecker 7a7d4a7d8f fix(angular): disable unlicensed watermark (#5150)
## What

Disables the "CopilotKit Unlicensed" watermark in the Angular SDK. When
no
CopilotCloud license key is provided, the SDK no longer:

- injects the fixed-position `copilotkit-license-watermark` element into
the DOM, and
- logs the "License Required" console warning.

## How

The watermark is **not deleted** — it's gated behind a single
`LICENSE_WATERMARK_ENABLED` flag (now `false`) in
`packages/angular/src/lib/license-watermark.ts`.
`ensureLicenseWatermark()`
early-returns when disabled, and `provideCopilotKit()` skips the console
warning on the same flag. Re-enabling is a one-line flip back to `true`.

The `licenseKey` option and its `X-CopilotCloud-Public-Api-Key` header
injection are unchanged — supplying a valid key still sets the Cloud
header.

## Tests

Updated the two specs that asserted the active-watermark behavior to
assert
the now-dormant behavior (no watermark element, no console warning).
Full
Angular suite passes (48 tests).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-01 18:21:48 +02:00
Markus Ecker 46307df595 Merge branch 'main' into feat/angular-remove-license-watermark 2026-06-01 18:03:46 +02:00
Markus Ecker 319ad44661 fix(angular): disable unlicensed watermark
Gate the Angular SDK's "CopilotKit Unlicensed" watermark and "License
Required" console warning behind a LICENSE_WATERMARK_ENABLED flag set to
false. The watermark implementation is retained for easy re-enablement;
the licenseKey option and its X-CopilotCloud-Public-Api-Key header
injection are unchanged.
2026-06-01 17:59:40 +02:00
Jordan Ritter 4049469622 fix(showcase/shell-dashboard): keep client ops fetch same-origin via /api/ops (#5149)
## Summary

Follow-on to #5148. The staging D6 dashboard rendered **no data** (empty
Ops tab, no green cells in the matrix overlay) even though the backend
was healthy. Browser-console ground truth showed the **client** doing a
direct cross-origin fetch to
`https://harness-staging-2ee4.up.railway.app/probes` — CORS-blocked
(harness has no `Access-Control-Allow-Origin`) and the wrong path
(`/probes` vs `/api/probes`).

### Root cause
`getRuntimeConfig()` sourced the client `RuntimeConfig.opsBaseUrl` from
the **server proxy target** `OPS_BASE_URL` (the harness URL on staging)
and `layout.tsx` serialized it into `window.__SHOWCASE_CONFIG__`. So
`ops-api.ts:resolveBaseUrl()` used the harness URL as the fetch base and
bypassed the same-origin `/api/ops` Route Handler that #5148 added. The
Route Handler reads `process.env.OPS_BASE_URL` itself, so the server
proxy worked — but the client never called it.

### Fix
Decouple the two env surfaces:
- **Server proxy target** `OPS_BASE_URL` — server-only, read by the
`/api/ops/[...path]` Route Handler; **never** injected into the client.
- **Client direct override** `NEXT_PUBLIC_OPS_DIRECT_BASE_URL` — opt-in
escape hatch for direct cross-origin dev calls; **defaults to `""`** so
the client falls through to the same-origin `/api/ops` proxy.

With the override empty (the normal production/staging case), the
browser fetches `/api/ops/probes` (same-origin) → Route Handler →
`${OPS_BASE_URL}/api/probes`. No CORS, correct path.

## Test plan
- [x] Red→green regression tests: `opsBaseUrl` defaults to `""` even
when `OPS_BASE_URL` is set; sourced only from
`NEXT_PUBLIC_OPS_DIRECT_BASE_URL`; empty override → client uses
`/api/ops/probes`, never the harness directly; server proxy target does
NOT leak into the client config.
- [x] `tsc --noEmit` clean; `vitest run` 48 files / 693 passing; `next
build` green (incl. `/api/ops/[...path]` dynamic route).
- [ ] Post-merge: dashboard image rebuilds → staging redeploy → verify
cells render green and Ops tab populates.
2026-06-01 08:37:17 -07:00
Jordan Ritter f565f73add chore(showcase/shell-dashboard): apply oxfmt formatting to runtime-config 2026-06-01 08:18:10 -07:00