fix(oracle-r2): same-origin baseUrl + refresh actually re-fetches

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)
This commit is contained in:
2026-06-14 00:26:38 +04:00
parent 2b83e3ad0e
commit e3b4ed62c0
10 changed files with 237 additions and 81 deletions
+38 -23
View File
@@ -31,15 +31,15 @@ on Gitea.
## Scenarios in the catalog
| id | mode | source |
| ----------- | --------- | ------------------------------------------------------------------- |
| procurement | live | Purchase Requisition → PO (`pr_to_po_def` on demo.flow-master.ai) |
| extra-1 | live | Atlas F1 Fresh (procurement variant) |
| extra-2 | live | Atlas F1 Fresh (procurement variant) |
| ar | blueprint | AR · Customer Refund Approval |
| hcm | blueprint | HCM · New Hire Onboarding |
| gl | blueprint | GL · Period-End Close |
| service | blueprint | Service Ops · Customer Incident |
| id | mode | source |
|-------------|-----------|-------------------------------------------------------------------|
| procurement | live | Purchase Requisition → PO (`pr_to_po_def` on demo.flow-master.ai) |
| extra-1 | live | Atlas F1 Fresh (procurement variant) |
| extra-2 | live | Atlas F1 Fresh (procurement variant) |
| ar | blueprint | AR · Customer Refund Approval |
| hcm | blueprint | HCM · New Hire Onboarding |
| gl | blueprint | GL · Period-End Close |
| service | blueprint | Service Ops · Customer Incident |
**Live** = backed by a real EA2 process definition currently in the demo
backend, with real runtime transactions and a real work-item queue.
@@ -68,15 +68,20 @@ prior version. Safeguards now in place:
## Live-mode mechanics (the CORS gotcha)
`demo.flow-master.ai` does not advertise CORS headers for arbitrary origins.
`src/lib/api.ts` therefore uses an **empty `baseUrl` everywhere** (both dev
and prod). All `/api/*` requests are same-origin from the browser's
perspective; whatever is serving the page is responsible for proxying them
to the backend.
- In **dev** (`pnpm dev`): Vite proxies `/api/*` to
`${VITE_FM_BASE:-https://demo.flow-master.ai}` (see `vite.config.ts`). The
browser sees a same-origin request, no CORS check. `src/lib/api.ts` uses an
empty `baseUrl` in dev for this.
- In **production**: deploy the build at the same origin as the backend (or
behind a reverse proxy that passes `/api/*` through). Cross-origin
deployments fail gracefully — `setMode("live")` catches the error, surfaces
it in the topbar banner and a toast, and falls back to snapshot mode.
- **Dev** (`pnpm dev`): `vite.config.ts` proxies `/api/*` to
`${VITE_FM_BASE:-https://demo.flow-master.ai}`.
- **Prod** (Docker image): the bundled `nginx.conf` reverse-proxies `/api/*`
to `https://demo.flow-master.ai`. The image is intended to sit behind the
`mc.flow-master.ai` ingress (see `FM06/flowmaster-ops` overlay).
- **Anywhere else**: set `VITE_FM_BASE=https://your-backend` at build time
and accept that browsers will reject the cross-origin call. Live mode then
fails gracefully — `setMode("live")` catches the error, raises an
`mc-banner-err` banner + error toast, and falls back to snapshot.
## Run, test, build
@@ -129,15 +134,25 @@ fetch_scenarios.mjs # Node script to refresh src/scenarios.json
## Deploy
Production deployment is tracked in `FM06/flowmaster-ops` under
`manifests/overlays/mc.flow-master.ai/`. A static nginx serves `dist/`;
ingress fronts it at `mc.flow-master.ai`. `/api/*` is reverse-proxied to the
same backend `demo.flow-master.ai` talks to — that's what makes live mode
work in production without CORS.
Production deployment is tracked in
[FM06/flowmaster-ops PR #1164](https://gitea.flow-master.ai/FM06/flowmaster-ops/pulls/1164),
which adds three resources to `manifests/overlays/demo/`:
- `mc-deployment.yaml` — 2-replica nginx Deployment in the `demo` namespace
- `mc-service.yaml` — ClusterIP service on port 80
- `mc-ingress.yaml` — Traefik ingress at `mc.flow-master.ai` with cert-manager DNS-01 cert
**Status:** the PR is open and mergeable. The live URL `https://mc.flow-master.ai` is **not yet serving** — two pre-merge action items remain for the trusted updater:
1. Cloudflare A record `mc.flow-master.ai → 65.21.71.186, 91.98.159.56` (same as `hakeem.flow-master.ai`).
2. Gitea Actions must have published the first image tag (`gitea.flow-master.ai/shad/mission-control-demo:sha-…`) — `mc-deployment.yaml` pins that explicit SHA, not `:latest`.
Until the PR merges + DNS is added, the artifact is this repo + the open PR.
```bash
pnpm build
# dist/ is the static site → ship to nginx referenced by the ops overlay
# dist/ is the static site. The Dockerfile bakes it into a tiny nginx
# image. CI in .gitea/workflows/build.yml builds + publishes on push to main.
```
## What's intentionally not here