Frontend Revamp — Phase-by-Phase Execution Prompts
Companion to: docs/reports/FRONTEND_UI_IMPROVEMENT_PLAN.md
Target app: miz-oki-command-center-ui/ (Next.js 15 App Router, React 19, TypeScript, Tailwind, Zustand, React Query)
Date: August 5, 2026
How to use this document: Each phase below is written as a self-contained, execution-ready prompt. Copy the entire "PROMPT" block for a phase into a fresh agent session (or hand it to an engineer) and it carries everything needed: mission, context, technical spec, visual/style spec, explicit non-goals, acceptance gates, and verification commands. Phases are ordered by dependency — do not start a phase until the previous phase's exit gate is green.
Table of Contents
- Phase Map & Dependencies
- Shared Context Block (paste into every prompt)
- Phase 0 Prompt — Safety Net & Ground Truth
- Phase 1 Prompt — Wiring Correctness (One Gateway)
- Phase 2 Prompt — Consolidation & Dead-Code Removal
- Phase 3 Prompt — Rendering & Performance
- Phase 4 Prompt — UX Overhaul & Design System
- Phase 5 Prompt — Expansion Surfaces
- Cross-Phase Style Guide (referenced by every prompt)
- Prompt-Writing Conventions Used Here
Phase Map & Dependencies
flowchart LR
P0["Phase 0<br/>Safety Net<br/>(tests, strictness,<br/>convention files)"]
P1["Phase 1<br/>Wiring Correctness<br/>(one gateway client,<br/>kill hardcoded URLs)"]
P2["Phase 2<br/>Consolidation<br/>(dead code, route merge,<br/>state cleanup)"]
P3["Phase 3<br/>Rendering & Perf<br/>(RSC, streaming,<br/>bundle diet)"]
P4["Phase 4<br/>UX Overhaul<br/>(nav, palette, design<br/>tokens, feedback kit)"]
P5["Phase 5<br/>Expansion<br/>(ORACLE, governance,<br/>mobile, realtime hub)"]
P0 --> P1 --> P2 --> P3 --> P4 --> P5
style P0 fill:#7c2d12,color:#fff
style P1 fill:#9a3412,color:#fff
style P2 fill:#b45309,color:#fff
style P3 fill:#4d7c0f,color:#fff
style P4 fill:#0f766e,color:#fff
style P5 fill:#1d4ed8,color:#fff
| Phase | Theme | Risk if skipped | Hard dependency |
|---|---|---|---|
| 0 | Safety net | Every later phase ships regressions invisibly | none |
| 1 | Wiring correctness | Auth-locked services keep failing from the browser; SSR breaks on hostname changes | 0 (tests catch the migration) |
| 2 | Consolidation | Bundle stays bloated; every future change is made in 2–3 duplicate places | 1 (canonical client must exist before deleting duplicates that reference old paths) |
| 3 | Rendering & performance | TTI stays poor; force-dynamic blocks all caching |
2 (can't optimize routes that are about to be deleted) |
| 4 | UX overhaul | Users keep drowning in 100+ undifferentiated nav entries | 3 (nav rebuild should target the final route set) |
| 5 | Expansion | New surfaces built on old foundations inherit all the debt | 4 (new pages consume the design system) |
Shared Context Block
Paste this block verbatim at the top of every phase prompt. It gives the executing agent the ground truth it needs without re-deriving it.
CONTEXT — MIZ OKI Command Center UI (read before doing anything)
Repo: mediaintelligence/MIZOKICloudRun (monorepo). The frontend is
miz-oki-command-center-ui/ — Next.js 15.5 App Router, React 19, TypeScript,
Tailwind CSS + Radix UI, Zustand (store/ with slices), React Query, Socket.io,
SSE via hooks/useSSE.ts. It deploys to Cloud Run as
miz-oki-command-center-ui via cloudbuild.yaml (root build context — the ROOT
.gcloudignore governs the upload; the lockfile must stay allowlisted).
Measured inventory (2026-08): 155 page.tsx files, 210 API route handlers,
243 component files, 65 hooks, 135 lib modules, ~391 hardcoded *.run.app URL
literals, 16 app/boss* directory variants (7 archived), zero test files,
zero loading.tsx/not-found.tsx/global-error.tsx.
Known constraints you MUST respect:
1. next.config.mjs currently sets eslint.ignoreDuringBuilds and
typescript.ignoreBuildErrors — the CI has NO type gate. Any TS cleanup
must be measured with a local delta-tsc against the current error
baseline (record the count first; ship only zero-genuinely-new).
2. Navigation safety: all dynamic router.push()/redirects MUST go through
toSafeUrl() from lib/safe-navigation.ts. ESLint no-restricted-syntax
rules exist for this — do not weaken them.
3. Backend services are IAM-locked Cloud Run services. Browser code cannot
mint OIDC tokens — every backend call from the client must go through a
same-origin Next.js API route (BFF) that attaches identity tokens
server-side (lib/service-auth.ts pattern, authedFetch).
4. Boss Agent chat is served by the boss-agent-adk service through
app/api/boss/chat/route.ts. Do not point chat at any other service.
5. The repo has an auto-merge bot: pushing a cursor/* or claude/* branch is
a de-facto merge to main within minutes. Keep every push shippable.
6. Do NOT touch app/api/stream + app/api/streaming SSE routes' contract
without updating hooks/useSSE.ts and providers in the same commit.
7. The design canon governance applies to "# MIZ OKI 3.5/" (marketing site)
— that folder is OUT OF SCOPE for all of these phases. Never edit it.
Phase 0 Prompt — Safety Net & Ground Truth
Why this phase exists (explanation)
Every later phase moves or deletes code. Today the app has zero automated tests, builds that ignore both TypeScript and ESLint errors, and none of the App Router convention files that catch failures at runtime (loading.tsx, not-found.tsx, global-error.tsx). That combination means a migration mistake in Phase 1 or 2 would ship to production silently. Phase 0 builds the tripwires first: a minimal but real test harness, a recorded type-error baseline with a delta gate, error/loading conventions, and a CI job that actually fails. Nothing in this phase changes user-visible behavior — it only makes future change safe and measurable.
Visual outcome
Users see three small but real improvements: skeleton loading states instead of white flashes on slow routes, a branded 404 page instead of the Next.js default, and a graceful full-app error screen with a "reload" action instead of a blank crash.
flowchart TD
subgraph BEFORE["Before Phase 0"]
A1["Route transition"] --> A2["White flash / layout jump"]
A3["Bad URL"] --> A4["Default Next.js 404"]
A5["Root render crash"] --> A6["Blank screen"]
end
subgraph AFTER["After Phase 0"]
B1["Route transition"] --> B2["Branded skeleton via loading.tsx"]
B3["Bad URL"] --> B4["Branded not-found.tsx with nav back"]
B5["Root render crash"] --> B6["global-error.tsx: message + reset()"]
end
style BEFORE fill:#450a0a,color:#fff
style AFTER fill:#052e16,color:#fff
PROMPT — copy from here
[PASTE SHARED CONTEXT BLOCK]
MISSION: Build the safety net for the miz-oki-command-center-ui frontend so
all later refactors are measurable and reversible. No user-facing behavior
changes except the three convention screens described below. Work on a new
branch; keep commits small and topical.
TECHNICAL SPEC — do these in order:
1. TYPE BASELINE (do this FIRST, before any edit):
- Run `npx tsc --noEmit` in miz-oki-command-center-ui and save the full
error list to docs/reports/frontend-tsc-baseline-<date>.txt. Record
the count in the PR description.
- Add an npm script "typecheck": "tsc --noEmit" and a
"typecheck:delta" script (node script) that diffs current errors
against the committed baseline file and exits non-zero only on
GENUINELY NEW errors (set-diff on file:line-normalized messages).
- Do NOT flip strict:true or remove ignoreBuildErrors yet — that is a
later step gated on the baseline shrinking.
2. TEST HARNESS:
- Add Vitest + React Testing Library + @testing-library/jest-dom.
Config: vitest.config.ts with jsdom environment, path aliases mirrored
from tsconfig paths, and setupFiles for jest-dom.
- Write the first REAL tests (not placeholders):
a. lib/safe-navigation.ts — toSafeUrl() rejects objects, absolute
externals, javascript: URIs; passes through safe relative paths.
b. One API route handler test: app/api/boss/chat/route.ts — mock the
upstream fetch, assert the route validates the body shape and
returns 4xx on malformed input (not 500).
c. One component smoke test: render the navigation sidebar with a
minimal config and assert external links get target="_blank".
- Add "test" and "test:watch" npm scripts.
3. CONVENTION FILES (App Router):
- app/loading.tsx: full-width skeleton (see VISUAL SPEC).
- app/not-found.tsx: branded 404 with links to / and the command palette
hint. All links via toSafeUrl or static hrefs.
- app/global-error.tsx: 'use client', renders its own <html><body>,
shows error.digest when present, and a reset() button. Keep the
component dependency-free (no store, no providers — it must render
when everything else is broken).
- Route-group loading.tsx for the 3 heaviest sections (boss, knowledge
graph, dashboards) with skeletons that match those layouts' shapes.
4. CI GATE:
- Add .github/workflows/frontend-ci.yml (or extend the existing
frontend-guard workflow — check first; it currently targets apps/web,
retarget or add a job for miz-oki-command-center-ui): jobs for
`npm ci`, `npm run lint` (warnings allowed, errors fail),
`npm run typecheck:delta`, `npm run test`. Path-filter to
miz-oki-command-center-ui/**.
- The workflow must NOT run the full Next build (too slow/flaky for
PR gate); the Cloud Build deploy already builds.
5. GUARDRAIL AUDIT (report only, no fixes yet):
- Grep for router.push(, window.location.href =, redirect( and produce
docs/reports/frontend-nav-safety-audit-<date>.md listing every call
site that does NOT flow through toSafeUrl. This becomes Phase 1/2
input. Do not fix them in this phase.
VISUAL / STYLE SPEC:
- Skeletons: use existing Tailwind theme tokens only. Base block:
rounded-lg bg-muted/60 animate-pulse. Compose per-layout: a header bar
(h-8 w-48), a filter row (h-9 w-full max-w-md), then a grid of cards
(h-32) matching the real page's grid columns. NEVER use spinners for
route-level loading — spinners are reserved for in-component actions.
- not-found.tsx: centered column, max-w-md, an icon from lucide-react
(CircleSlash or Compass), text-2xl font-semibold heading "Page not
found", one sentence of body copy in text-muted-foreground, and two
actions: primary Button → "/", ghost Button → "Open command palette
(⌘K)" (the palette itself ships in Phase 4 — link to / until then and
leave a TODO with the Phase 4 ticket reference).
- global-error.tsx: dark-neutral full screen (bg-zinc-950 text-zinc-50
hardcoded — do NOT depend on the Tailwind config being alive), an
AlertTriangle icon, the message "Something went wrong at the root of
the app", the digest in a <code> block, and a single solid button
calling reset(). Inline styles are acceptable here; resilience beats
purity in this one file.
- Tone of all copy: calm, specific, no exclamation marks, no blame
("We couldn't find that page" not "Oops!!").
NON-GOALS (do not do these in Phase 0):
- No dependency upgrades beyond the test tooling.
- No fixing of the ~391 hardcoded URLs (Phase 1).
- No deletion of any route or component (Phase 2).
- No flipping strict TypeScript or removing ignoreBuildErrors.
ACCEPTANCE GATES (all must pass before merging):
[ ] `npm run test` green with ≥3 real test files.
[ ] `npm run typecheck:delta` green (zero new errors vs baseline).
[ ] Visiting a garbage URL shows the branded 404.
[ ] Throwing in app/layout children (temporary test) shows global-error.
[ ] CI workflow runs and fails when a test is deliberately broken
(prove it once in the PR, then revert the break).
[ ] Baseline + audit reports committed under docs/reports/.
VERIFICATION COMMANDS:
cd miz-oki-command-center-ui
npm run test
npm run typecheck:delta
npm run lint
npm run dev # manual check: /, /nonexistent-url, slow route skeleton
Phase 1 Prompt — Wiring Correctness (One Gateway)
Why this phase exists (explanation)
The review found ~391 hardcoded *.run.app URL literals spread across components, hooks, and lib modules, plus components calling backends directly with fetch/axios. This breaks in three ways: (1) browser calls to IAM-locked Cloud Run services fail because browsers can't mint OIDC tokens — the documented D19/Wave-3 lesson; (2) any Cloud Run hostname change requires a 391-site grep-and-pray; (3) there is no single place to attach auth, tracing, retries, or timeouts. Phase 1 builds one typed gateway client and routes every backend call through the same-origin BFF (app/api/**), where lib/service-auth.ts attaches identity tokens server-side. It also replaces the in-memory conversation store in the chat route with a durable store, because in-memory state on Cloud Run evaporates per-instance.
Target architecture
flowchart LR
subgraph Browser
C["Components / Hooks"] --> GW["lib/api/gateway.ts<br/>ONE typed client<br/>(zod-validated, traced)"]
end
subgraph NextServer["Next.js server (BFF)"]
GW -->|"same-origin /api/*"| R["app/api/** route handlers"]
R --> SA["lib/service-auth.ts<br/>OIDC mint + audience=origin"]
R --> CFG["lib/config/services.ts<br/>SINGLE service URL registry<br/>(env-driven)"]
end
subgraph CloudRun["IAM-locked Cloud Run"]
SA --> B["boss-agent-adk"]
SA --> M["moa / moe / cells"]
end
X["❌ component → fetch('https://…run.app')"] -.forbidden by lint rule.-> B
style X fill:#7f1d1d,color:#fff
style GW fill:#1d4ed8,color:#fff
style CFG fill:#0f766e,color:#fff
PROMPT — copy from here
[PASTE SHARED CONTEXT BLOCK]
MISSION: Make every backend call in miz-oki-command-center-ui flow through
one typed gateway client and the same-origin BFF. Eliminate direct
browser→run.app calls. Replace in-memory API-route state with a durable
store. Phase 0's tests and delta-tsc gate are your regression net — run
them after every batch of changes.
TECHNICAL SPEC:
1. SERVICE REGISTRY (single source of URLs):
- Create/finish lib/config/services.ts: a typed map
{ bossAgent, moaController, moeRouter, cell(n), … } resolved from env
vars with the documented production URLs as defaults. Export
getServiceUrl(name) and a serviceNames const union type.
- This module is SERVER-ONLY (import 'server-only' at top). The browser
must never see raw service URLs.
2. GATEWAY CLIENT (browser side):
- Create lib/api/gateway.ts: a thin typed client exposing
get/post/stream methods that ONLY hit same-origin /api/* paths.
Features, in this order of importance:
a. zod schema parameter per call → parse response, return typed data
or a discriminated { ok:false, error } — never throw raw fetch
errors into components.
b. AbortController + default 30s timeout (override per call).
c. Correlation header (x-request-id: crypto.randomUUID()) echoed by
the BFF into upstream calls for tracing.
d. streamSSE(path, handlers) helper that wraps EventSource with
reconnect + backoff, replacing ad-hoc EventSource usage.
- Add a React Query integration layer lib/api/queries.ts exporting
typed hooks (useBossHealth, useMcpTools, useCellStatus, …) built on
the gateway. New code uses hooks; old code migrates incrementally.
3. BFF HARDENING (server side):
- Every app/api/** route that proxies a backend must: use
getServiceUrl() (no literals), attach OIDC via the canonical
service-auth helper with audience = service ORIGIN (scheme://host —
never a path; this is the documented D17b failure mode), validate the
inbound body with zod, and propagate x-request-id.
- Migrate the generic proxy pattern to
app/api/orchestration/[svc]/[...path]/route.ts (exists — extend it)
with an explicit allowlist of svc values from the registry. Reject
unlisted services with 404. NEVER proxy arbitrary hostnames.
4. KILL HARDCODED URLS (the 391):
- Work from the Phase 0 audit + `grep -rn "run\.app" --include='*.ts*'`.
Triage each hit into: (a) browser code → replace with gateway call to
an /api/* path (create the BFF route if missing); (b) server
code → replace with getServiceUrl(); (c) dead code → list for Phase 2
deletion, do NOT fix (leave a `// PHASE2-DELETE` marker comment).
- Add ESLint no-restricted-syntax rules: forbid string literals matching
/run\.app/ in components/, hooks/, app/(?!api) — allow only in
lib/config/services.ts and tests.
5. DURABLE CHAT STATE:
- app/api/boss/chat/route.ts keeps conversation history in a module-
level Map — per-instance and lost on scale-to-zero. Replace with
Firestore (collection ui_conversations, doc per conversation_id, TTL
field) using the existing server-side Firebase Admin setup
(FIREBASE_PROJECT_ID env is already wired in cloudbuild). Fall back to
the in-memory Map ONLY when Firestore init fails, and surface
storage_mode in the route's response metadata so degradation is
visible, never silent.
6. TESTS (extend the Phase 0 harness):
- gateway.ts: timeout fires abort; zod failure returns { ok:false };
x-request-id set.
- services.ts: unknown service name is a type error (compile-time) and
runtime throw.
- orchestration proxy: unlisted svc → 404; listed svc → upstream called
with Authorization header (mock the minter).
VISUAL / STYLE SPEC (this phase is mostly invisible; make failures visible):
- Error surfaces: when the gateway returns { ok:false }, components must
render the shared <InlineError> pattern — a compact rose-tinted row
(border-l-2 border-rose-500 bg-rose-500/10 px-3 py-2 text-sm) with the
human message and a retry button when a refetch fn exists. Build this
one component now in components/ui/inline-error.tsx; Phase 4 will
absorb it into the feedback kit.
- Degraded-mode indicator: when chat storage_mode !== 'firestore', show
an amber dot + "memory-only session" tooltip near the chat header.
Subtle (h-2 w-2 rounded-full bg-amber-400), not a banner.
- No layout changes, no restyling of existing pages in this phase.
NON-GOALS:
- Do not delete duplicate routes/components (Phase 2).
- Do not convert pages to Server Components (Phase 3).
- Do not redesign navigation (Phase 4).
ACCEPTANCE GATES:
[ ] grep -rn "run\.app" in components/ hooks/ app/(non-api) returns ONLY
`// PHASE2-DELETE`-marked dead files (count recorded in PR).
[ ] New ESLint rules active and passing.
[ ] Chat survives a simulated instance restart (conversation persists —
prove with a test against the Firestore emulator or a mocked store).
[ ] All Phase 0 + new tests green; typecheck:delta green.
[ ] Manual smoke: Boss chat round-trip, one cell status widget, one SSE
stream reconnects after a killed connection.
VERIFICATION COMMANDS:
cd miz-oki-command-center-ui
grep -rn "run\.app" components hooks app --include='*.ts*' | grep -v PHASE2-DELETE | wc -l # must be 0
npm run test && npm run typecheck:delta && npm run lint
Phase 2 Prompt — Consolidation & Dead-Code Removal
Why this phase exists (explanation)
The app has 16 app/boss* directory variants (7 explicitly archived), 155 pages, 210 API routes, and 243 components — far more surface than the product actually exposes. Duplicates mean every fix is applied to one copy and silently missed in others, and they bloat both the build and every grep. Phase 2 is a disciplined shrink: verify each candidate is truly dead (zero importers, zero nav references, zero inbound links), delete it in its own commit (git-recoverable), and merge near-duplicates into one canonical implementation. This phase is where the // PHASE2-DELETE markers from Phase 1 get resolved. The measured-before-migrated rule from the platform's SRDAL migration applies: measure the inventory before deleting; never trust a name alone.
Consolidation flow
flowchart TD
S["Candidate surface<br/>(page / route / component)"] --> Q1{"Referenced by nav config,<br/>any import, any redirect,<br/>or external deep link?"}
Q1 -- no --> Q2{"Archived variant or<br/>PHASE2-DELETE marked?"}
Q2 -- yes --> DEL["DELETE in its own commit<br/>(subject lists the path)"]
Q2 -- no --> HOLD["Log in keep-list with reason"]
Q1 -- yes --> Q3{"Duplicate of a<br/>canonical surface?"}
Q3 -- yes --> MERGE["Merge: port unique features into<br/>canonical, add redirect() shim,<br/>then delete duplicate"]
Q3 -- no --> KEEP["Keep — record as canonical"]
style DEL fill:#7f1d1d,color:#fff
style MERGE fill:#b45309,color:#fff
style KEEP fill:#14532d,color:#fff
PROMPT — copy from here
[PASTE SHARED CONTEXT BLOCK]
MISSION: Shrink miz-oki-command-center-ui to its canonical surface. Delete
verified-dead pages, API routes, components, hooks, and lib modules; merge
near-duplicates; leave redirect shims for any URL that ever shipped in a
nav config. Every deletion must be provably safe and individually
recoverable from git history.
TECHNICAL SPEC:
1. INVENTORY FIRST (commit the evidence before deleting anything):
- Build docs/reports/frontend-consolidation-ledger-<date>.md with one
table per category (pages, api routes, components, hooks, lib):
path | importer count | nav reference? | verdict (KEEP/MERGE/DELETE) |
evidence. Generate importer counts with a script (ts-prune or a
madge/grep pass), not by eyeball. Commit the ledger BEFORE the first
deletion commit so review can challenge verdicts.
2. BOSS SURFACE CONSOLIDATION (the flagship merge):
- Enumerate all app/boss* directories. Identify THE canonical boss
surface (the one referenced by config/navigation.ts — verify, don't
assume). For each variant: diff against canonical; port any unique,
still-wired feature into canonical; then delete the variant. Variants
whose URL appeared in a shipped nav config get a permanent
redirect() shim page (a 5-line page.tsx calling redirect('/boss/…'))
— mirror of the platform's 307-shim convention.
3. API ROUTE MERGE:
- Collapse per-service one-off proxy routes into the Phase 1
orchestration/[svc]/[...path] allowlisted proxy wherever the route
adds no logic beyond forwarding. Routes WITH logic (validation,
shaping, memory) stay. Target: measurably fewer route handlers;
record before/after counts in the ledger.
- Resolve every // PHASE2-DELETE marker from Phase 1: delete the file
or justify keeping it in the ledger.
4. STATE & HOOK CLEANUP:
- Zustand: enumerate store slices; delete unconsumed selectors/slices;
merge globalStore.ts / global.ts / app-store.ts overlap into ONE
store entry point with slices (document the winner in the ledger).
- Hooks: 65 hooks — same treatment. Merge useSSE/useMultiSSE overlap
into the Phase 1 gateway streamSSE-backed hook. Delete hooks with
zero importers.
- Dependencies: run depcheck; remove packages with zero imports (verify
each against next.config/scripts before removal — e.g. WASM tooling
is used by build scripts, not imports). Record removals + resulting
node_modules/bundle size delta.
5. MECHANICAL RULES:
- One logical target per commit ("remove app/boss-v2 (0 importers,
archived 2026-03)"), so any single deletion is trivially revertable.
- Never delete and refactor in the same commit.
- After each batch: npm run test && npm run typecheck:delta — the
Phase 0 net must stay green at every commit, not just at the end.
- If ANY verdict is uncertain, downgrade to KEEP + note. Deleting late
is cheap; resurrecting a half-remembered feature is not.
VISUAL / STYLE SPEC:
- Redirect shim pages must be invisible (immediate server redirect(),
no flash content).
- Where a merge changes a page's internal layout, preserve the
canonical page's visual hierarchy — ported features arrive as new
tabs/sections styled with existing patterns, never as pasted-in
foreign-styled blocks. If the variant's styling was better, take that
decision to Phase 4 as a note in the ledger instead of restyling now.
NON-GOALS:
- No rendering-strategy changes (Phase 3).
- No new features or redesign (Phases 4–5).
- No touching app/api/stream|streaming contracts.
ACCEPTANCE GATES:
[ ] Consolidation ledger committed with evidence for every verdict.
[ ] app/boss* count reduced to canonical + shims only.
[ ] Page / route / component / hook counts measurably reduced —
report before/after in the PR (target: pages ≤ ~100, routes ≤ ~150,
but the ledger's evidence rules over any fixed number).
[ ] Zero PHASE2-DELETE markers remaining in the tree.
[ ] All tests + delta-tsc green on every commit in the PR.
[ ] Manual smoke of the 10 most-used surfaces (boss chat, KG, cells,
dashboards, settings) after the final deletion commit.
VERIFICATION COMMANDS:
cd miz-oki-command-center-ui
grep -rn "PHASE2-DELETE" --include='*.ts*' | wc -l # must be 0
find app -name page.tsx | wc -l # report
find app/api -name route.ts | wc -l # report
npm run test && npm run typecheck:delta && npm run lint
Phase 3 Prompt — Rendering & Performance
Why this phase exists (explanation)
The root layout exports dynamic = 'force-dynamic', which opts the entire app out of static and cached rendering, and nearly every page is a 'use client' component — so the App Router is being used as a client-side SPA with extra steps. Data arrives via a mix of polling and SSE for the same resources, duplicating load. Heavy visualization libraries (Cytoscape, D3, react-force-graph, ReactFlow) are imported statically, landing in the shared bundle even for users who never open a graph. Phase 3 makes rendering intentional: server-render what is server-renderable, stream what is slow, lazy-load what is heavy, and pick exactly one freshness mechanism per resource.
Rendering decision model
flowchart TD
P["Page or section"] --> Q1{"Needs per-request data<br/>or user session?"}
Q1 -- no --> STATIC["Static / revalidated RSC<br/>export const revalidate = N"]
Q1 -- yes --> Q2{"Data slow (>300ms)<br/>or independent sections?"}
Q2 -- yes --> STREAM["RSC + Suspense boundaries<br/>stream sections as ready"]
Q2 -- no --> Q3{"Interactive after load?<br/>(charts, editors, chat)"}
Q3 -- yes --> ISLAND["Server shell + client island<br/>next/dynamic({ ssr:false }) for<br/>heavy viz libs"]
Q3 -- no --> RSC["Plain Server Component"]
style STATIC fill:#14532d,color:#fff
style STREAM fill:#0f766e,color:#fff
style ISLAND fill:#1d4ed8,color:#fff
style RSC fill:#334155,color:#fff
PROMPT — copy from here
[PASTE SHARED CONTEXT BLOCK]
MISSION: Make rendering and data-freshness intentional in
miz-oki-command-center-ui. Remove the global force-dynamic, convert
server-renderable surfaces to RSC with streaming, lazy-load heavy
visualization bundles, and enforce one freshness mechanism per resource.
Measure everything: this phase's currency is before/after numbers.
TECHNICAL SPEC:
1. MEASURE FIRST:
- Add @next/bundle-analyzer behind ANALYZE=true. Record baseline:
shared First Load JS, per-route JS for the 10 heaviest routes, and a
Lighthouse run (performance + a11y) on /, boss chat, and the KG page.
Commit the numbers to docs/reports/frontend-perf-baseline-<date>.md.
2. UN-FORCE-DYNAMIC:
- Remove `export const dynamic = 'force-dynamic'` from app/layout.tsx.
The mobile-detection headers() usage forces dynamic implicitly —
replace it: move device detection to CSS container/media queries
where it drives layout, or isolate the headers() read into a small
server component that wraps only the parts that truly need it.
- Add per-route segment config ONLY where needed:
`export const revalidate = 30` for dashboard-ish pages,
`dynamic = 'force-dynamic'` only on truly per-request pages (chat).
Every remaining force-dynamic must carry a one-line comment saying
why.
3. RSC CONVERSION (top-10 routes by traffic/weight, from the ledger):
- Pattern per page: page.tsx becomes a Server Component that fetches
initial data server-side via the service registry (server-side can
mint OIDC directly), then renders <ClientSection initialData={…}>
islands for interactivity. React Query hydrates from initialData —
no double-fetch on mount.
- Wrap independent slow sections in <Suspense fallback={<SectionSkeleton/>}>
so the shell streams immediately.
4. BUNDLE DIET:
- Convert every static import of cytoscape, d3, react-force-graph,
reactflow, jsplumb, monaco/editor-like modules to
next/dynamic(() => import(...), { ssr:false, loading: Skeleton }).
- Audit lucide-react and date libs for per-icon/function imports.
- Re-run the analyzer; record deltas. Target: shared First Load JS
down ≥30% vs baseline; graph libs absent from non-graph routes
(verify in the analyzer treemap).
5. ONE FRESHNESS MECHANISM PER RESOURCE:
- Build the inventory: for each live resource (cell status, KG stats,
boss health, run streams…) record current mechanisms (poll interval,
SSE topic, socket event). Pick ONE per resource:
* high-frequency push (tokens, run progress) → SSE via gateway
streamSSE;
* periodic snapshots (health, stats) → React Query with
refetchInterval and visibilitychange pause;
Delete the loser. providers.tsx must stop double-subscribing
(currently connects a global SSE stream AND sets interval refreshes
for overlapping data).
- Pause all polling when document.hidden; resume + refetch on focus
(React Query's refetchOnWindowFocus covers most).
VISUAL / STYLE SPEC:
- Every Suspense fallback uses the Phase 0 skeleton language — shaped
like the content it replaces (same grid, same heights). No spinners,
no "Loading…" text at route level.
- Streaming must not cause layout shift: skeletons reserve the same
min-height as the settled content (measure the settled section and
set min-h accordingly). CLS on the 10 converted routes must be < 0.1
in Lighthouse.
- Lazy-loaded viz containers show a bordered placeholder with the
library name and a subtle shimmer (border border-dashed
border-muted-foreground/30 rounded-xl) so users understand a heavy
module is arriving — perceived intent beats blank space.
NON-GOALS:
- No navigation/IA changes (Phase 4).
- No new pages (Phase 5).
- Do not migrate chat streaming off its current SSE route.
ACCEPTANCE GATES:
[ ] Root layout no longer force-dynamic; every remaining force-dynamic
is commented with a reason.
[ ] Shared First Load JS reduced ≥30% vs committed baseline.
[ ] Graph libraries absent from non-graph route bundles (analyzer
screenshot in PR).
[ ] Freshness inventory committed; zero resources with two mechanisms.
[ ] Lighthouse performance ≥ 80 on /, boss chat, KG page (report all
three before/after).
[ ] All tests + delta-tsc green.
VERIFICATION COMMANDS:
cd miz-oki-command-center-ui
ANALYZE=true npm run build # record treemap
npm run test && npm run typecheck:delta
npx lighthouse http://localhost:3000 --preset=desktop # after npm start
Phase 4 Prompt — UX Overhaul & Design System
Why this phase exists (explanation)
The product exposes 100+ navigation entries with equal visual weight, has no global search or command surface, and grew page-by-page so spacing, cards, states, and feedback differ across sections. Power users navigate by memory; new users are lost. Phase 4 rebuilds the experience around four pillars: progressive disclosure (a role-aware home + curated nav with an "everything" escape hatch), a global ⌘K command palette (navigate, run tools, search entities), a design-token pass (one spacing/color/typography scale enforced by lint), and a unified feedback kit (one toast, one inline-error, one empty-state, one confirm pattern used everywhere). This is the most visible phase — it should feel like a new product without moving any backend contract.
Experience model
flowchart TB
subgraph HOME["Role-aware Home"]
H1["'What needs my attention'<br/>alerts, pending approvals,<br/>degraded services"]
H2["Pinned surfaces<br/>(user-chosen, drag to reorder)"]
H3["Recent activity<br/>(runs, chats, decisions)"]
end
subgraph NAV["Navigation"]
N1["Curated sidebar:<br/>≤7 top groups"]
N2["'All surfaces' directory page<br/>searchable, tagged"]
end
subgraph PALETTE["⌘K Command Palette"]
K1["Go to page…"]
K2["Run MCP tool…"]
K3["Search entities<br/>(campaigns, cells, decisions)"]
K4["Recent + pinned actions"]
end
HOME --- NAV --- PALETTE
style HOME fill:#0f766e,color:#fff
style NAV fill:#1d4ed8,color:#fff
style PALETTE fill:#7c3aed,color:#fff
PROMPT — copy from here
[PASTE SHARED CONTEXT BLOCK]
MISSION: Rebuild the miz-oki-command-center-ui experience layer: design
tokens, feedback kit, navigation IA, role-aware home, and a global ⌘K
command palette. All work targets the post-Phase-2 canonical route set and
the post-Phase-3 rendering model. Backend contracts do not change.
TECHNICAL SPEC:
1. DESIGN TOKENS (foundation — do first):
- Consolidate the Tailwind theme into explicit tokens in
tailwind.config: a 4px-base spacing scale; a semantic color layer
(bg-surface, bg-surface-raised, text-primary, text-muted, border-line,
accent, success/warning/danger/info) mapped to the existing palette;
a type scale (display/h1/h2/h3/body/caption with fixed line-heights);
radius tokens (sm 6px, md 10px, lg 16px); z-index scale; motion
durations (fast 120ms, base 200ms, slow 320ms) + a single easing.
- Codify in components/ui/: Button (primary/secondary/ghost/danger ×
sm/md/lg), Card, Badge/Status-pill, Input/Select/Combobox (Radix),
Tabs, Table shell, Drawer, Modal, Tooltip. Reuse and normalize
existing Radix-based components rather than adding a new library —
the dependency set stays as-is.
- Enforcement: an ESLint rule (or stylelint-style grep check in CI)
flagging raw hex colors and arbitrary px values in className outside
components/ui/**. Existing violations get a tracked allowlist file
that must only shrink.
2. FEEDBACK KIT (one way to say each thing):
- <Toast/> (single provider, 4 intents, action slot, auto-dismiss with
hover-pause), <InlineError/> (absorb Phase 1's), <EmptyState/>
(icon + one-line explanation + primary action), <ConfirmDialog/>
(destructive actions require it; danger button on the right),
<StatusDot/> (healthy=emerald, degraded=amber, down=rose,
unknown=zinc — with accessible labels, never color-only).
- Migrate the 20 most-used surfaces to the kit; grep-and-replace ad-hoc
alert()/inline-div patterns as encountered; ledger the rest.
3. NAVIGATION IA:
- Rewrite config/navigation.ts as a typed registry where each entry has
{ id, title, href, icon, group, tags[], roles?[], pinnedByDefault? }.
- Curated sidebar: ≤7 top-level groups (e.g. Home, Boss Agent,
Intelligence, Operations, Data & KG, Governance, Settings). Groups
collapse; current section stays expanded; active item gets a left
accent bar (border-l-2 accent), not a filled background.
- "All surfaces" page (/directory): searchable, tag-filtered grid of
every registered page — the escape hatch that lets the sidebar stay
small. External links (Agent IDE) keep target=_blank + the external
icon convention.
4. COMMAND PALETTE (⌘K / Ctrl-K):
- Use cmdk (already a dependency — verify; add if absent). Sources:
(a) the navigation registry (fuzzy "go to"); (b) MCP tools via the
existing tools list endpoint — "Run tool…" opens a param form drawer,
never fire-and-forget for mutating tools (reuse ConfirmDialog);
(c) entity search endpoint if present — otherwise ship nav+tools
first and leave entities as a registered follow-up; (d) recent items
(localStorage, last 10).
- Palette is globally mounted in providers, lazy-loaded on first open,
fully keyboard navigable, and closes on route change.
5. ROLE-AWARE HOME:
- Replace the current landing with three stacked zones (see diagram):
attention (pending approvals, degraded StatusDots, error-rate
alerts — sourced from existing health/status endpoints via the
gateway), pinned surfaces (user-pinnable from the directory page;
persist in localStorage now, profile doc later), recent activity
(runs/chats/decisions from existing endpoints).
- Zones stream independently (Suspense) and each has a real
EmptyState ("Nothing needs your attention — all systems nominal").
VISUAL / STYLE SPEC:
- Overall character: calm operations console. Dense but breathable —
12/16/24px rhythm, generous section gaps (32px+), max content width
1440px with fluid gutters. Dark theme is primary (the fleet is
monitored at night); verify every token pair passes WCAG AA in dark
AND light.
- Typography: keep the existing font stack; enforce the scale. Numbers
in tables/metrics use tabular-nums.
- Color discipline: neutrals dominate; accent color is for interactive
affordances ONLY; intent colors (success/warning/danger) are for
state ONLY, never decoration. Charts get a fixed 8-color categorical
ramp that harmonizes with the tokens.
- Motion: sidebar collapse, drawer, palette open = base 200ms ease-out;
respect prefers-reduced-motion (disable non-essential transitions).
- Every interactive element: visible focus ring (ring-2 ring-accent
ring-offset-2 on dark), hit target ≥ 40px, aria labels on icon-only
buttons. Add eslint-plugin-jsx-a11y and fix new violations (existing
ones join the shrink-only allowlist).
- Empty/error/loading states are DESIGNED states, not afterthoughts:
each kit component has a Storybook-style demo page at /directory/kit
(internal) showing all variants — this doubles as the living style
guide.
NON-GOALS:
- No backend/API changes beyond consuming existing endpoints.
- No new product surfaces (Phase 5).
- No theme-system rewrite (extend the existing Tailwind setup).
ACCEPTANCE GATES:
[ ] Token layer merged; raw-hex/arbitrary-px lint active; allowlist
committed and CI-enforced as shrink-only.
[ ] Feedback kit shipped + adopted on top-20 surfaces (list them in
the PR).
[ ] Sidebar ≤7 groups; /directory lists every registered page;
zero pages reachable ONLY by memorized URL.
[ ] ⌘K palette: navigate + run-tool flows work end-to-end; mutating
tools always confirm.
[ ] New home renders attention/pinned/recent zones with live data,
streaming independently.
[ ] Keyboard-only walkthrough of home → palette → boss chat → a
dashboard succeeds; axe/jsx-a11y clean on new code.
[ ] All tests + delta-tsc green; add tests for the nav registry
(unique ids/hrefs) and palette actions.
VERIFICATION COMMANDS:
cd miz-oki-command-center-ui
npm run test && npm run typecheck:delta && npm run lint
npm run dev # manual: ⌘K flows, keyboard-only pass, /directory, /directory/kit
Phase 5 Prompt — Expansion Surfaces
Why this phase exists (explanation)
With the foundation sound (typed gateway, canonical routes, intentional rendering, design system), the frontend can finally expose platform capabilities that exist in the backend but have no UI today: the ORACLE/Intent platform (cells 33–36 with eight intent_* tools live), the governance layer (decision proposals, approvals, authorizations, audit replay), a unified realtime operations hub, and a first-class mobile experience. Each surface below is written as its own mini-prompt so they can be built in parallel by different agents once Phase 4 merges — they share the design system and gateway, not each other's code.
Expansion map
flowchart LR
GW["Typed Gateway + BFF<br/>(Phase 1)"] --> A["5A · Intent / ORACLE<br/>console"]
GW --> B["5B · Governance &<br/>Approvals console"]
GW --> C["5C · Realtime Ops hub"]
GW --> D["5D · Mobile-first pass"]
DS["Design System<br/>(Phase 4)"] --> A & B & C & D
style A fill:#7c3aed,color:#fff
style B fill:#b45309,color:#fff
style C fill:#0f766e,color:#fff
style D fill:#1d4ed8,color:#fff
PROMPT 5A — Intent / ORACLE Console
[PASTE SHARED CONTEXT BLOCK]
MISSION: Build the Intent/ORACLE console at /intelligence/intent — the
first UI over the live intent platform (cells 33–36; eight intent_* tools
served by the boss agent; consent-gated, fail-closed).
TECHNICAL SPEC:
- BFF routes under app/api/intent/* proxy the boss agent's intent_* tools
(intent_status, intent_score_get, intent_cohort_query,
intent_transitions_recent, intent_explain, intent_taxonomy_list,
intent_consent_stats, intent_incrementality_report) through the
orchestration allowlist. Zod-type every response.
- Pages: overview (pipeline status, hourly scoring-run freshness, consent
stats), cohort explorer (filterable table + score distribution chart),
identity drilldown (score history sparkline + transitions +
intent_explain panel), taxonomy viewer (topics × domains grid,
deny-list surfaced read-only).
- Poll-free: overview uses React Query refetchInterval 60s (hourly
pipeline — faster polling is theater); explorer/drilldown fetch on
demand.
VISUAL / STYLE SPEC:
- Scores are CALIBRATED PROBABILITIES: render as 0–1 with two decimals +
a horizontal micro-bar, never as percentages with hype styling. Neutral
ramp for magnitude; intent colors only for state (consent denied =
rose, backfilled provenance = amber badge).
- Every metric block carries its claim label chip ("built,
pre-benchmark") in text-caption text-muted — truth-in-UI mirrors the
platform's claim discipline, and the AUUC/ranking-gate caveat from the
Phase-A report is shown verbatim in the incrementality panel's info
popover.
- Consent-denied cohorts render as EmptyState with the fail-closed
explanation, never as zeros that look like data.
GATES:
[ ] All eight tool surfaces reachable and typed; error/empty/loading
states designed; identity IDs displayed as shp_*/em_* hashes only
(never raw PII); tests for the zod layer; a11y pass.
PROMPT 5B — Governance & Approvals Console
[PASTE SHARED CONTEXT BLOCK]
MISSION: Build /governance — a console over the governed decision pathway
(envelope → passport → policy → decision → approval → authorization →
audit replay) served by the service-* fleet through the BFF.
TECHNICAL SPEC:
- BFF routes proxy: pending approvals (tenant-scoped), decision detail
(DEL score, passport checks, rejected alternatives, policy_version),
approve/reject actions (require ConfirmDialog + typed reason), audit
replay fetch by decision_id.
- The approval action is the ONLY mutating call; everything else is
read-only. Idempotency: send a client-generated request id; disable the
button while in flight; surface 409 (already consumed) as a designed
state, not an error toast.
VISUAL / STYLE SPEC:
- Decision detail reads like a case file: header (DEL score as a large
numeral vs threshold, pass/fail pill), passport checks as a vertical
checklist (check = emerald check icon, fail = rose x, soft-fail = amber
tilde), rejected alternatives in a collapsed section, full JSON behind
a "raw" tab in a monospace scroll area.
- Approvals demand gravity: the approve dialog restates amount/action/
bounds in a bordered summary block and requires a typed confirmation
for amounts over a configured threshold. Danger-red is reserved for
reject/rollback.
- Audit replay renders as a numbered vertical timeline with monospace
hashes, each step linkable (#seq anchors).
GATES:
[ ] Approve/reject round-trip works against a mocked BFF in tests; 409
and tenant-mismatch render designed states; timeline deep-links work;
a11y pass including focus-trap in dialogs.
PROMPT 5C — Realtime Operations Hub
[PASTE SHARED CONTEXT BLOCK]
MISSION: Build /operations/live — one hub that answers "is the platform
healthy RIGHT NOW" by unifying the fleet-health, cell-status, and
run-stream data the app already receives but scatters across pages.
TECHNICAL SPEC:
- One multiplexed SSE subscription via the gateway's streamSSE (topics:
health, runs, alerts) feeding a dedicated Zustand slice; React Query
snapshots hydrate the initial state. No polling on this page.
- Panels: fleet grid (every service as a StatusDot card with revision +
latency), live run feed (streaming rows, virtualized list —
react-window or CSS content-visibility), alert lane (governance holds,
SLO breaches).
- Failure-class rule from the platform docs is ENCODED: unauth 403 =
healthy+locked (emerald with a lock glyph), authed 404 = up/wrong-path
(amber), 503 = real error (rose), timeout = cold start (zinc pulse).
Never render a locked service as an outage.
VISUAL / STYLE SPEC:
- Wall-display friendly: readable at 2m — metric numerals text-3xl
tabular-nums, high-contrast dots with shape+label redundancy, an
optional ?kiosk=1 mode that hides nav chrome.
- New feed rows enter with a 200ms fade + background flash
(accent/10 → transparent); the feed NEVER auto-scrolls while the user's
pointer is inside it (pin-to-bottom resumes on leave).
- Timestamps: relative ("32s ago") with absolute on hover; a stream-health
indicator in the page header (connected/reconnecting with backoff
countdown).
GATES:
[ ] Kill the SSE connection in devtools → reconnect with backoff and a
visible reconnecting state, no data loss on resume (snapshot refetch).
[ ] 500-row feed scrolls at 60fps (virtualized).
[ ] Failure-class rendering matches the table above (unit-test the
classifier fn).
PROMPT 5D — Mobile-First Pass
[PASTE SHARED CONTEXT BLOCK]
MISSION: Make the five surfaces an operator actually needs on a phone
work excellently at 390×844: Home, Boss chat, approvals, live ops,
directory. Everything else must be usable (no horizontal scroll, no
broken layouts) but is not optimized.
TECHNICAL SPEC:
- Replace the server-side header sniffing removed in Phase 3 with pure
responsive CSS (container queries where supported, min-width media
queries otherwise). One source of breakpoints in the Tailwind config
(sm 640 / md 768 / lg 1024 / xl 1280) — kill ad-hoc pixel checks in JS
(window.innerWidth reads move to a single useViewport hook if truly
needed).
- Navigation on <lg: sidebar becomes a bottom tab bar (5 slots: Home,
Boss, Approvals, Live, More→directory drawer). Palette gets a floating
search button (bottom-right, above the tab bar).
- Chat on mobile: full-height flex column, composer pinned above the
keyboard (dvh units + visualViewport listener), message list uses
overscroll-behavior: contain.
VISUAL / STYLE SPEC:
- Touch targets ≥44px; swipe-to-dismiss on drawers/toasts; tables
collapse to card lists (each row becomes a Card with label/value
pairs) — never pinch-zoom tables.
- Test matrix: iPhone 14 (390), small Android (360), iPad portrait (768).
Screenshot all five surfaces × three widths into the PR.
GATES:
[ ] Zero horizontal scroll at 360px on all routes (automated check via
Playwright viewport sweep if available, else manual sweep recorded).
[ ] The five target surfaces pass a thumb-only walkthrough.
[ ] Lighthouse mobile performance ≥ 75 on Home and chat.
Cross-Phase Style Guide
Every prompt above references these rules; they are the constitution for all visual work.
Voice & copy
| Rule | Do | Don't |
|---|---|---|
| Calm and specific | "Cell 28 has returned 503 for 4 minutes" | "Oops! Something broke!!" |
| State-first | "3 approvals waiting" | "You have some pending items" |
| Honest degradation | "memory-only session" amber dot | silently losing chat history |
| Claim discipline | "built, pre-benchmark" chips on metrics | implying benchmarked performance |
Color semantics (token layer)
| Token | Meaning | Never used for |
|---|---|---|
accent |
interactive affordances (links, focus, primary buttons) | status, decoration |
success / emerald |
healthy, passed, approved | "good numbers" styling |
warning / amber |
degraded, soft-fail, backfilled, memory-only | attention-grabbing marketing |
danger / rose |
down, failed, destructive actions, consent-denied | emphasis of non-destructive items |
zinc neutrals |
structure, text, unknown states | conveying state on their own |
State rendering (the "four states" rule)
Every data surface designs all four states before it ships:
stateDiagram-v2
[*] --> Loading: mount
Loading --> Loaded: data
Loading --> Error: failure
Loaded --> Empty: zero items
Error --> Loading: retry
note right of Loading: shaped skeleton,\nnever spinner at route level
note right of Error: InlineError + retry,\nhuman message
note right of Empty: EmptyState with\nexplanation + action
Accessibility floor (all phases)
- WCAG AA contrast on every token pair, dark and light.
- Full keyboard operability; visible focus rings; focus trap in modals.
- Status never conveyed by color alone (dot + label/shape).
prefers-reduced-motionrespected everywhere.- Icon-only buttons carry
aria-label.
Engineering conventions (all phases)
toSafeUrl()on every dynamic navigation — no exceptions, lint-enforced.- One commit = one logical change; deletions isolated; every push shippable (auto-merge bot).
- Measure before/after for anything claiming improvement (bundle, Lighthouse, counts) and commit the numbers to
docs/reports/. - Delta-tsc gate green on every commit; allowlists only shrink.
Prompt-Writing Conventions Used Here
For anyone extending this document with new phase prompts:
- Explanation before prompt. Each phase opens with why it exists in prose, so the executing agent (or reviewer) can challenge the spec instead of following it blindly.
- Shared context is pasted, not linked. Agents in fresh sessions have no memory; the context block travels with every prompt.
- Technical and visual specs are separate sections. They are reviewed by different people and fail in different ways.
- Non-goals are explicit. Scope creep between phases is the biggest schedule risk; each prompt names what it must not do.
- Gates are checkboxes with verification commands. "Done" is a command output, not a feeling.
- Diagrams are Mermaid, in-repo. They version with the code and render on GitHub.
End of document. Companion plan: docs/reports/FRONTEND_UI_IMPROVEMENT_PLAN.md.