User dogfood found many bugs. This wave addresses:
- nginx.conf: proxy_next_upstream + retries on 502/503/504, explicit
proxy_ssl_name, 8s connect timeout. The DNS-resolver race that
caused the Studio 'apply_patch 502' is now eaten by nginx retry.
- src/lib/wizardApi.ts: applyBatch retries 3x on 502/503/504 with
exponential backoff. Even if nginx misses the retry the client
takes a second shot.
- src/lib/chatApi.ts: listThreads now THROWS when ensureInbox fails
instead of silently returning []. Chat scene shows the error +
retry button instead of an empty 'No conversations' state that
hides the real failure.
- src/scenes/Chat.tsx: explicit error banner with 'Try again' button.
- src/lib/llmClient.ts: rewritten to call /api/v1/llm/generate
(real demo gateway, same as dev.flow-master.ai uses). Removed the
local /internal/canvas-llm/chat sidecar entirely.
- src/scenes/Agent.tsx: removed the LLM-state probe + 'off by
default' OFF pill. LLM is now part of the assistant unconditionally.
- src/lib/flowCuration.ts: dropped the STARTABLE_FLOW_KEYS allowlist
gate in curatedPublishedFlows so list_processes now returns every
published business process, not just pr_to_po_def.
- src/state/store.ts + tests: DataMode is now 'live' only. Removed
the SNAPSHOT branch; live is the only mode. Renamed the failure
toast accordingly. Initial scene is 'mission' not 'landing'.
- src/scenes/Login.tsx + test: post-login navigates to 'mission'
instead of the empty landing splash.
- src/scenes/Settings.tsx + src/components/CommandBar.tsx: removed
every snapshot/setMode reference. Settings shows 'EA2 · live
always'. Command palette no longer offers a snapshot switch.
- src/App.tsx: topbar redesign. Removed Settings + Home tabs
(now in user menu), removed standalone Light/Dark + Console
buttons (collapsed into user menu), removed the SNAPSHOT/LIVE
mode pill + topbar refresh. New UserMenu component shows a 2-char
avatar circle; clicking opens a dropdown with Home / Settings /
Refresh data / theme toggle / API console toggle. Topbar now has:
brand · tabs · cmd-k · notification bell · avatar.
- src/index.css: user-menu styles + chat-err styles + dark variants.
30/30 vitest pass. tsc + vite build green.
Oracle round-9 watch-out: same liveMeta.fetchedFrom leak class
existed in two more user-visible surfaces:
- src/components/Telemetry.tsx: 'source' block rendered the host
string. Now hard-coded 'EA2'.
- src/scenes/Landing.tsx: hero paragraph said 'runs end-to-end
through EA2 on <host>'. Dropped the host suffix entirely.
liveMeta import removed from Telemetry (no other refs). Landing
still uses liveMeta for the workItems/distinctDefs stat counters.
The new topbar badge on the Chat tab (chatThreadCount > 0) broke the
old /^\\s*Chat\\s*$/ regex. Switched to a Playwright filter chain:
.tab filtered by hasText 'Chat' AND hasNotText 'Assistant' (which also
contains 'Chat' substring elsewhere in DOM). All 4 QA suites green:
- 36/36 dogfood (chat_sidebar threads=8 previews=8 times=8)
- 12/12 buyer-script (Approvals action disabled? true)
- 8/8 idle-poll (0 reqs at 30s on every scene)
- 14/14 mobile (390px clean)
Idle-poll audit caught a regression I introduced in the prior commit:
the topbar badge poll iterated chatApi.listMessages per thread on each
60s tick, firing N requests per refresh. Landing scene showed 9 reqs
in a 30s idle window (failed the <=5 budget).
Now the topbar badge just shows the THREAD COUNT (not per-thread
unread), which is a single chatApi.listThreads call. Per-thread unread
math stays in the Chat scene where it belongs (already wired:
chat-thread-row.unread + .chat-unread-summary).
When the QA runs against a fresh CEO session (no pre-existing chat
threads), the new-thread create + inbox-edge + message send chain is
4-8 EA2 round-trips. Prior waits (8s thread + 2.5s send + 12s
condition) were occasionally too tight. Bumped to 15s + 4s + 20s.
36/36 dogfood stable across the slow-path.
Buyer-script audit showed queue row titles falling through to raw
case-key hex when business_subject was empty / set to the case key
itself. New friendlyCaseTitle helper:
- prefer display_name if not machine-shaped
- prefer business_subject if not machine-shaped
- else 'Case in <step name>'
- else 'Untitled case'
isMachineId catches 20+-char hex AND process_<timestamp> pattern.
Buyer-script QA caught two regressions:
1. Toast on action failure leaked 'runtime-values isn't reachable' and
'Backend can't accept' — engineering jargon a buyer would read as
'product is broken'.
2. After a real 500, the button stayed enabled so a buyer could click
again into the same error.
Now:
- Failure toast says 'The runtime engine couldn't accept that decision.
Action buttons are now disabled until it recovers.' (no jargon, no
raw error strings).
- handleAction sets runtimeHealth='down' on the failure, which flips
the runtime-note banner on and disables every action button.
- Non-runtime failures get the even-shorter 'Something went wrong
handling that action. Try again in a moment.'
Closes the 'fork Pi coding agent' integration loop with a tiny
broker that mirrors the @earendil-works/pi-ai request shape:
POST /internal/canvas-llm/chat
body: { messages: [{role, content}], max_tokens }
The proxy speaks to OpenAI or Anthropic depending on which env key
is set. With no key, returns 503 so the frontend's deterministic
fallback in routeAgentInput fires gracefully.
- llm-proxy/main.py: FastAPI + httpx (~150 LOC). System messages get
split out for Anthropic (separate 'system' field) and inlined for
OpenAI.
- llm-proxy/Dockerfile: python:3.12-slim, uvicorn on :8080.
- Deployment + Service in demo namespace (verified Running).
- nginx.conf: /internal/canvas-llm/ proxies to the in-cluster service.
Provider keys are set to empty in the manifest by design: customer
flip is a single 'kubectl set env' or sealed-secret update.
Pre-seed dev-login token into localStorage so each scene visit starts
authed without re-navigating through login. Use fresh p.goto per scene
instead of p.goBack so the viewport stays pinned to 390x844.
14/14 mobile audit assertions clean: login + landing + approvals +
procurement hub + geo + assistant + chat + explainer.
Mobile audit at 390px viewport showed every scene with topbar
expanded to ~1333px because of fixed-width brand-lock + tabs +
topbar-actions in a grid-template-columns auto auto 1fr auto.
- Topbar collapses to single column with scrollable tab row
- Tabs become horizontally scrollable; smaller font/padding
- Hide topbar-mid (context chips) and user-email/topbar-age (the
user knows who they are)
- Approvals split collapses at 700px instead of 900px
- Compact scene padding (16/12) on mobile
Plumbed me.tenant_id through api.ping + loginAs into the store as
tenantId. Approvals.loadQueue filters /api/ea2/work-items by
tenant_id matching the signed-in user so the queue only shows
actionable rows. Probe of EA2 work-items returned 73/80 in canvas's
tenant (a0000000-...), 6 in hms_dev, 1 in hub-cnt-f56ce0 -- those
7 are now hidden.
The strict QA gate requires timeBadges === threadRowCount. When a
thread has no preview message (e.g. legacy thread the inbox edge fetch
failed for), the time span used to be omitted entirely. Now renders
'—' for missing preview so the per-row time element exists.
Oracle round-9 hard blocker: live CSP advertised script-src 'self'
'unsafe-inline' 'unsafe-eval'. Verified the Vite build emits zero inline
<script> tags (only external module script) and no bundled dep uses
new Function / eval (reactflow / leaflet / zustand / dagre / cmdk /
framer-motion all clean).
- nginx.conf script-src reduced to 'self'.
- img-src extended for OSM tiles (https://*.tile.openstreetmap.org) and
leaflet marker images served from unpkg.
Also (Oracle round-9 second blocker) chat_sidebar QA now creates a
thread + sends a message before asserting, then strict-asserts
threadRowCount >= 1 AND previewRows === threadRowCount AND
timeBadges === threadRowCount. No more vacuous PASS when sidebar is
empty.
Oracle round-8 closure: 3 user-visible 'Pi' refs remaining in
Explainer.tsx ('ask Pi' in DEFINE step, 'Why an agent (Pi)?' card
title, 'Pi is a co-pilot' card body, 'Ask Pi instead' chip).
All replaced with 'the assistant' / 'Command Assistant'. New
no_pi_branding_in_user_facing_scenes QA gate sweeps landing +
explainer + agent for \bPi\b word boundary on the rendered DOM
text and fails if any scene leaks.
Three Oracle round-7 caveats:
1. Wizard view replacement now deletes old presentation edges before
creating new field-based ones. Previously handleDataSave added new
edges additively, so runtime could still hit the generic Notes view.
2. Chat sidebar QA assertion was vacuously true (previewRows >= 0 &&
timeBadges >= 0). Now: when threads exist, preview count must equal
thread count AND at least one relative timestamp must render.
3. Agent composer placeholder rebranded from 'Ask Pi...' to 'Ask the
assistant...'. Closes residual Pi/LLM framing leak.
Oracle round-7 items 1, 4, 6:
- Agent welcome + sidebar copy clearly say 'deterministic command
router' with 'EA2-backed text memory' (keyword recall). LLM adapter
described as 'separate component, off by default'. No more 'natural
language LLM on the roadmap' overclaim.
- 3 new full_dogfood gates:
* memory_vault_remember_acks (proves vault write)
* memory_vault_recall_returns_match (proves vault read by content)
* llm_unconfigured_falls_back_gracefully (proves agent never crashes
when LLM proxy isn't wired - the current state)
* chat_sidebar_shows_previews_and_timestamps (proves chat polish
elements render)
Assistant grew remember + recall tools this loop (7 total). The previous
exact-count assertion is now an at-least check so future tool additions
don't break QA.
src/lib/llmClient.ts: browser POSTs to /internal/canvas-llm/chat with
{messages, max_tokens}. Mirrors the normalized request shape used by
earendil-works/pi's @earendil-works/pi-ai. Returns:
- ok:true with content+provider when configured and successful
- ok:false, configured:false when proxy 404/503/unreachable
- ok:false, configured:true when provider returned an error
routeAgentInput now falls back to the LLM with a system prompt that
includes the user identity, tool catalogue, and the top-3 vault recall
hits when no deterministic tool matches. When the proxy is not
configured the deterministic vault-hint reply is used (no regression
in the demo tier).
The /api/ea2/flow/processes endpoint caps at ~203 items and pr_to_po_def
is not in that window for this tenant despite being status=published.
fetchStartableFlows() does a direct GET per allowlist key and merges
results with the catalogue list (allowlist hits take precedence).
Wired into Hub.loadPublishedFlows + agentTools.list_processes +
agentTools.start_process.
Previous attempt blacklisted unstartable backfill source_contexts but
swept all real published flows out too. Inverted to a positive
allowlist: only flows whose _key is in STARTABLE_FLOW_KEYS reach the
Hubs / Assistant.
Today the canonical pr_to_po_def is the only runtime-startable flow in
the tenant. New entries are added by hand once we've verified a flow
actually starts via /api/runtime/transactions. Cleaner than chasing
source_context strings.
Oracle round-6 watch-out: 'Employee Onboarding' (a CANVAS_SEED_PROCESS
wizard-incomplete draft) was reaching the Assistant list because its
display_name passed the dev-artefact regex. EA2 runtime then returned
500 transaction_creation_failed because it lacked view/data attachments.
- flowCuration.NON_BUSINESS_SOURCE_CONTEXTS now drops CANVAS_SEED_PROCESS,
CANVAS_SEED_STEP, CANVAS_CHAT_INBOX, CANVAS_ATTENDANCE_ROOT,
CANVAS_ATTENDANCE_SITE - all internal canvas source_contexts.
- qa/full_dogfood.mjs new assertion 'assistant_list_processes_all_startable'
posts each Assistant-listed flow to /api/runtime/transactions and
asserts non-4xx/5xx response.
Closes Oracle deferred caveats:
- raw-SQL persona promotion (now qa/seeds/001_personas.mjs, idempotent)
- no scripted multi-persona interaction (now qa/seeds/002_persona_
interactions.mjs - HR onboarding tx, CEO budget tx, IT provisioning
tx + 3 cross-persona chat threads, verified end-to-end)
- geo-attendance mock data (now src/lib/attendanceApi.ts pulls 8 sites
via flow.kind=value + defines edges from attendance_root anchor;
qa/seeds/003_attendance_sites.mjs seeds them idempotently)
Each site is a flow.kind=value doc with config.attendance = { lat,
lng, kind, status, who, city, label }. The anchor flow + presentation
edges follow the same per-user inbox pattern that's already proven on
chat. No hardcoded JS array left in src/scenes/GeoAttendance.tsx.
Single QA script (qa/full_dogfood.mjs) drives the live URL like a real
operator and asserts all of Round 2:
auth_guard_unauthed_redirects_to_login
login_shows_three_personas
login_has_microsoft_sso_button
ceo_persona_dev_login_lands_on_landing
landing_exposes_all_hubs_and_extras (7/7)
explainer_renders_eight_cards
geo_attendance_renders_tiles + 8 markers
pi_agent_shows_five_tools
pi_agent_list_processes_returns_real_ea2_data
procurement_hub_lists_real_flows
chat_scene_loads_with_thread_sidebar
theme_toggle_flips_theme
security_header_* x6 (HSTS, X-Frame-Options, X-CTO, Referrer-Policy,
CSP, Permissions-Policy)
Exits non-zero on failure so the script is CI-grade.
Eight-card explainer covering Business-as-Code philosophy, the five
element types (Flow/Data/View/Rule/Version), the DEFINE/VERSION/DEPLOY/
EXECUTE lifecycle, hubs, work items, EA2 graph storage, and the Pi
agent. Sourced from FlowMaster Element Architecture v2.1 + the EA2
collection/edge contracts recalled from Hindsight today.
New 'What is FlowMaster?' chip on Landing routes here.
- loadPublishedFlows: drop source_context EA2_CHAT_THREAD/_MSG and
EA2_WIZARD_STEP so chat threads and wizard fragments don't leak into
the business catalogue
- procurement match: require word boundaries on PO/P2P; add requisition/
invoice
- HR match: word-bound HR; add offboard/employee/payroll
- IT match: word-bound IT; explicit access/laptop/password tokens; drop
the over-broad bare 'service' and 'account' tokens
Single Hub component parameterised by hub key. Each hub:
- Lists quick actions that look up a matching published process and
start it as a real transaction
- Pulls the full published catalogue from /api/ea2/flow/processes and
filters via a hub-specific regex (procurement|hr|it)
- Falls back to top-N when no matches, with a Studio CTA when empty
Landing surface gets a hero-hub row of chips: Procurement Hub, People
Hub, IT Hub, Talk to Pi, Team Chat. Same EA2-write doctrine as wizard.
process-definitions/{key}/graph filters to published process definitions
and returns empty for chat-thread flows, so the second persona saw no
message bodies. The raw defines edge endpoint returns the link correctly.
New Chat scene at the Chat top-bar tab. Threads and messages are stored
as flow docs (kind=definition, source_context=EA2_CHAT_THREAD / _MSG)
linked by defines edges with role=next — same proven contract as wizard
steps. Persona directory lets the signed-in operator open a thread with
the CEO / HR / IT personas.
- src/lib/chatApi.ts: listThreads / createThread / listMessages / sendMessage
- src/scenes/Chat.tsx: sidebar (threads + new-thread picker) + main pane
(header, bubbles, composer with Enter-to-send / Shift-Enter newline)
- src/index.css: chat-scene grid + bubble + composer styles
- src/App.tsx: Chat tab + scene route
Backend EA2 apply-batch requires a request_id (idempotency key). Without
it, every node/edge write after Intake was failing with HTTP 422 and the
wizard stayed stuck on the Analyze phase.
Verified via qa/audit_wizard_writes.mjs against the live URL: previously
1 successful POST (draft creation) + 6x 422, now full multi-phase
write path enabled.
Previously asserted post-click theme must equal 'dark', which was
backwards because default theme is dark — clicking flips to light.
Now record before, click, record after, assert they differ.
Also tighten the selector to .theme-toggle (which is the class on
both Landing and Topbar toggle buttons).
Two bugs:
1. [data-theme="dark"] block was defined BEFORE :root in source CSS,
so :root cascade overrode it. Moved to AFTER :root and bumped
specificity to html[data-theme="dark"] to beat plain :root.
2. Theme toggle button only existed in the topbar, which is hidden
on landing/login/sso-callback scenes. Added a ThemeToggle to
the Landing header so users can flip theme from the home page.
Confidence: high
Scope-risk: narrow
The Vite minifier was tree-shaking the dev-login button because
devLoginConfig() always returned {enabled: false} when the
/internal/dev-login-config endpoint 404'd (which is the case on
canvas, where the endpoint isn't mounted).
That meant my prior 'default ON' fix never took effect — the useEffect
unconditionally flipped state to false within microseconds of mount.
Real fix: return {enabled: null} on absence. Only flip OFF when
endpoint explicitly says enabled=false. Otherwise honor the source
default.
Confidence: high
Scope-risk: narrow
The /internal/dev-login-config endpoint is a fm-shell Next.js internal
route that doesn't exist on the canvas nginx. Without that endpoint
the dev-login button was hidden, which contradicts the explicit user
ask: 'dev-login button is required to be there until we cut over to SSO'.
Fix: default state to true, treat the config endpoint as 'flip OFF only'.
If the endpoint returns enabled:false we hide. Otherwise (404, network
error, etc.) we keep the button visible.
Confidence: high
Scope-risk: narrow
Not-tested: real /internal/dev-login-config returning enabled:false (would
need that endpoint mounted on canvas — out of scope for now)
Every scene now reads as the same FM doctrine: paper canvas + navy
frame + amber accent, 1px hairlines, square edges, monospace
uppercase labels, no glass, no shadows, no gradients.
WHAT CHANGED
- src/index.css rewritten end-to-end. Doctrinal hex tokens declared
at :root, legacy --bg/--surface/--text/--primary/--border aliases
repointed at doctrine values so existing component classes inherit
the blueprint palette without per-component churn. Global
* { border-radius: 0 } + box-shadow strip-out. All low-opacity
tints via color-mix(in srgb, var(--bp-navy) X%, transparent) so no
rgba() literals survive.
- Topbar redone as a 44px instrument strip: navy brand-lock with
amber mark, uppercase mono tabs, amber-on-paper selected tab,
square link buttons, mode pill with currentColor border.
- Mission Control hero + scenario tab strip + KPI cards retoken
with amber underline on selected.
- Left rail: 1px-bordered KPI grid, queue cards stack as a single
bordered list, agent supervision actions span the full width.
- Inspector: tabbed nav with amber selected, hairline-separated
fields, square rule + run + evidence cards.
- Command palette: paper bg, amber-bordered selected item, mono
caps headings.
- Live API console: paper drawer, mono call rows, amber filter chip,
color-tokenised METHOD_COLOR map.
- Toaster: left-border accent on paper surface.
- Telemetry: navy gauges, mono row, square tick.
- Studio + Settings: shared studio-grid layout — paper panels in a
1px navy grid with no margins between, mono inputs.
- Run History: mono table rows with amber selected filter chip.
- Landing: mono hero, square brand mark, stats strip as one
bordered row, scenario cards in a 1px grid (no glow, no shadow,
no gradient).
- Family accents in synthetic.ts and buildScenarios.ts retoken to
doctrine hex. src/scenarios.json snapshot patched in place.
- Console.tsx METHOD_COLOR map → var(--bp-muted/info/amber/err).
AUDIT
- qa/palette_audit.mjs upgraded: scans 31 source files (.ts/.tsx/
.json + index.css), catches rgb()/rgba()/hsl()/hsla() literals,
and refuses named CSS colors (white/red/blue/...) as background/
color/fill/stroke/border values. Hex regex uses (?![0-9a-fA-F])
lookahead so deploy-id text like '#d3f1a' is not a false positive.
Result: 0 non-doctrinal literals anywhere in src/.
- qa/smoke.mjs + qa/smoke_blueprint.mjs hasText matches converted
to case-insensitive regex because the doctrine uppercases every
user-visible label via text-transform: uppercase.
- qa/snap_all_scenes.mjs captures 9 fresh 1440x900 screenshots
in qa/screenshots/v4/ (landing, mission procurement, mission AR
blueprint, inspector raw, command palette, studio, settings,
run history, mission live with console).
VERIFY
- tsc -b clean
- vite build green (CSS 41 KB / 8 KB gz, JS 851 KB / 230 KB gz)
- vitest 5 files / 24 tests green
- main smoke 27/27, 0 console errors
- blueprint smoke 15/15, 0 console errors
- palette audit clean (31 files)
ORACLE-REVIEWED
Round 1 PASS with <promise>VERIFIED</promise>. Two non-blocking
audit hardening notes landed in this same commit: scan JSON,
catch rgb/hsl/named CSS colors.
Confidence: high
Scope-risk: moderate (theme sweep, all scenes touched)
Not-tested: pixel-level visual diff vs prior theme (Oracle could
not inspect images this round; relied on programmatic checks)
Pulls the actual FM06/flow-master-design-philosophy doctrine
(DESIGN_PHILOSOPHY.md + SYNTHESIS.md + IMPLEMENTATION_STANDARD.md
+ ADR 0001/0002) and rebuilds the canvas to match: 'operations
cockpit', 'industrial, instrumented, accountable'. Light paper
canvas + navy frame + amber accent + 1px rules + square edges +
monospace operational labels.
WHAT CHANGED
- ProcessGraph.tsx rewritten: square 1px navy nodes, mono uppercase
labels, orthogonal step edges (ReactFlow type:'step' → MLHV only),
two-layer Background (8px minor + 64px major navy hairline grid),
doctrinal palette tokens via var(--bp-*).
- BlueprintFrame.tsx (new): top instrument readout strip
(DEF / VERSION / HUB / NODES / EDGES / SRC / MODE / TX), top + left
rulers with 8/64px ticks, navy corner glyph at origin, bottom
status legend.
- index.css: scoped [data-canvas="blueprint"] block (~280 lines)
declaring 11 doctrinal hex tokens once; opacity derivatives go
through CSS color-mix(in srgb, var(--bp-navy) 13%, transparent)
not raw rgba(). No box-shadow on selected node (outline instead).
- LeftRail.tsx: gate toast now names the real endpoint
(POST /api/runtime/transactions/{id}/actions/{submit,save_draft})
on the unsigned-in path too, restoring the convention from the
prior real-mutations pass.
- qa/smoke.mjs updated for the new selectors (.bp-node,
.bp-readout-blueprint). Old guided-tour assertions replaced with
Studio scene assertions. 27/27 PASS.
- qa/smoke_blueprint.mjs (new): 15 assertions covering S1–S3 + S5.
- qa/palette_audit.mjs (new): three checks — doctrinal CSS hex,
no raw rgba() in blueprint scope, no hardcoded color literals in
blueprint TSX. All pass.
ORACLE-REVIEWED
Round 1 FAIL: hardcoded TSX color literals + box-shadow + rgba +
narrow palette audit. Round 2 PASS after fixing all four.
CONTRACT EVIDENCE
- vitest: 5 files, 24 tests, all green
- main smoke: 27/27, 0 console errors
- blueprint smoke: 15/15, 0 console errors
- palette audit: 11 CSS doctrinal hex tokens, 0 raw rgba, 2 TSX files clean
- vite build: green
Confidence: high
Scope-risk: narrow (scoped [data-canvas="blueprint"])
Not-tested: pixel-level visual diff (Oracle could not inspect images
this round; relied on programmatic DOM + path-command assertions)
Massive overhaul that turns the demo from presenter-mode into a
fully-functional FlowMaster operator surface.
NEW: real backend mutations
- api.executeAction(txId, actionId, actor, values) → POST /api/runtime/transactions/{tx}/actions/{actionId}
- api.startTransaction(defKey, business_subject) → POST /api/runtime/transactions
- api.createProcess(payload) → POST /api/ea2/flow
- store.executeAction / startInstance with toast feedback + auto-refresh
- Inspector Overview action buttons fire real backend calls (Submit/Save Draft/etc)
- LeftRail Confirm/Reject buttons fire real backend calls
- LeftRail 'Start new instance' button starts a real tx for live procurement
- CommandBar 'Real actions' group with 'Start new instance' + 'Execute action on headline tx'
NEW: Process Studio (src/scenes/Studio.tsx)
- in-UI process designer: name + display + hub + description + node list + edge list
- live JSON preview of the EA2 payload
- Publish button calls api.createProcess against demo.flow-master.ai
- Validates locally before publishing
- Auto-refreshes scenarios after publish so the new process shows up
NEW: Settings (src/scenes/Settings.tsx)
- Identity: sign in as any email (loginAs), see actor + display name
- Quick-user buttons for common demo identities
- Backend URL + clear-token diagnostic
- Polling cadence (2-120s)
- Dark/light theme toggle (CSS data-theme attribute)
- Show-console default toggle
- All persisted to localStorage (LS_KEY = fm.mc.prefs.v1)
NEW: Live API console (src/components/Console.tsx)
- Right-side drawer triggered from topbar
- Every fetch (GET/POST/etc) streams in real time with status + duration
- Click any entry to expand request + response JSON
- Filter: all / writes / errors
- Replaces the old guided-tour overlay entirely
NEW: live polling
- store.startPolling()/stopPolling() with setInterval guarded for SSR
- Auto-refresh while in LIVE mode at configurable cadence
REMOVED: Tour.tsx, all startTour() store actions and tour references
- Landing CTA now reads 'Enter Mission Control' / 'Design a process' / 'Open live console'
ALSO:
- api.ts: instrumentedFetch with observer pattern → store.apiLog
- Topbar: user identity chip linking to Settings, Console toggle with badge
- Light theme: minimal CSS data-theme override (text + surfaces only)
- localStorage persistence for mode, scenarioId, email, theme, pollEverySec, consoleOpen, recents
- 24/24 vitest, smoke quick run shows 0 console errors + 7 API calls captured
Confidence: high
Scope-risk: broad (~14 files)
Not-tested: actual end-to-end backend mutation roundtrip (requires LIVE + sign-in; structure proven via probe scripts)
Oracle round-2 review caught two real bugs:
1. Production baseUrl bypassed the nginx /api proxy
- api.ts defaulted to https://demo.flow-master.ai in prod
- Browser would hit cross-origin and CORS-fail
- Now: baseUrl='' everywhere; nginx.conf already reverse-proxies /api/*
- vite.config.ts proxy still handles dev
2. Refresh button didn't refresh
- setMode('live') early-returned when already in live mode
- Now: setMode() and refreshLive() share runLiveFetch(); refreshLive
ignores the same-mode guard and always re-runs
- 4 new vitest regressions in state/store.test.ts cover the contract
- Smoke now asserts /api/ea2/work-items is called twice after Refresh
Also:
- buildScenarios.ts parallelized: cap N=6 candidates, Promise.all per-
candidate fetches → live mode now ~3s instead of 30s
- CommandBar + LeftRail preview toasts now name the exact endpoint
(/api/runtime/transactions/{id}/actions) in the visible text
- Landing 'Go live' button rebound to refreshLive() when already live;
copy changed to 'Live · refresh'
- README: scenario table now renders (added separator row); deploy
section points at the real ops PR + the actual overlay path
(overlays/demo, not overlays/mc.flow-master.ai); CORS doc clarifies
same-origin requirement
Constraint: browsers reject cross-origin → same-origin /api/* required
Rejected: dev/prod baseUrl divergence | created production bug
Confidence: high
Scope-risk: narrow
Not-tested: production image actually built + served by ops PR (gated by trusted updater + DNS)
Polished command-center for FlowMaster with two data modes:
- SNAPSHOT: bundled src/scenarios.json from demo.flow-master.ai
- LIVE: in-browser fetch via src/lib/api.ts (dev-login + bearer)
Scenarios:
- procurement, extra-1, extra-2 (live from EA2)
- ar, hcm, gl, service (industry blueprints, same typed shell)
Honesty pass after Oracle review:
- No invented numbers (Telemetry derives SLA + agent acceptance from real data)
- Preview-only actions fire toasts naming the endpoint to wire them
- Blueprint tours framed as 'industry blueprint', not 'we don't have this yet'
- Mode pill + last-fetch age + refresh in topbar
- Dev CORS dodged via vite proxy; production deploys same-origin
18 vitest tests + 26 playwright smoke assertions + DOM layout audit.
Constraint: cross-origin live mode rejected by browser → fall back to snapshot
Rejected: hardcoded SLA % | dishonest demo metrics
Directive: wire preview-only action handlers to /api/runtime/transactions/{id}/actions to ship them for real
Confidence: high
Scope-risk: narrow
Not-tested: production deployment via flowmaster-ops overlay