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:
+1
-2
@@ -12,9 +12,8 @@ export interface ApiConfig {
|
||||
// In dev, route via vite proxy (same-origin) to dodge CORS; in production
|
||||
// the build is deployed at the same origin as the backend, so an empty
|
||||
// baseUrl is correct.
|
||||
const isDev = import.meta.env.DEV;
|
||||
const DEFAULT_CONFIG: ApiConfig = {
|
||||
baseUrl: import.meta.env.VITE_FM_BASE || (isDev ? "" : "https://demo.flow-master.ai"),
|
||||
baseUrl: import.meta.env.VITE_FM_BASE ?? "",
|
||||
email: import.meta.env.VITE_FM_EMAIL || "dev@flow-master.ai",
|
||||
};
|
||||
|
||||
|
||||
+23
-22
@@ -243,29 +243,30 @@ export async function buildLiveScenariosFromApi(signal?: AbortSignal): Promise<{
|
||||
const candidates = [...byDef.values()].filter((d) => d.cases.length >= 2 || (d.statuses.running ?? 0) >= 1);
|
||||
candidates.sort((a, b) => b.cases.length - a.cases.length);
|
||||
|
||||
// For each candidate fetch graph + a couple runtimes.
|
||||
interface Enriched { bucket: CandidateBucket; graph: ProcessGraph; headlineRt: RuntimeTransaction | null; recent: RuntimeTransaction[] }
|
||||
const enriched: Enriched[] = [];
|
||||
for (const c of candidates.slice(0, 20)) {
|
||||
if (signal?.aborted) break;
|
||||
const graph = await api.graph(c.key, signal);
|
||||
if (!graph?.process_definition?.config?.nodes?.length) continue;
|
||||
const headlineCase =
|
||||
c.cases.find((w) => w.status === "running") ||
|
||||
c.cases.find((w) => w.status === "waiting_for_user") ||
|
||||
c.cases.find((w) => w.status === "errored" || w.status === "failed") ||
|
||||
c.cases[0];
|
||||
const headlineRt = headlineCase?.transaction_id ? await api.transaction(headlineCase.transaction_id, signal) : null;
|
||||
const recent: RuntimeTransaction[] = [];
|
||||
for (const w of c.cases.slice(0, 6)) {
|
||||
if (signal?.aborted) break;
|
||||
if (!w.transaction_id || w.transaction_id === headlineCase?.transaction_id) continue;
|
||||
const r = await api.transaction(w.transaction_id, signal);
|
||||
if (r) recent.push(r);
|
||||
if (recent.length >= 3) break;
|
||||
}
|
||||
enriched.push({ bucket: c, graph, headlineRt, recent });
|
||||
}
|
||||
const TOP_N = 6;
|
||||
const enrichedRaw = await Promise.all(
|
||||
candidates.slice(0, TOP_N).map(async (c): Promise<Enriched | null> => {
|
||||
if (signal?.aborted) return null;
|
||||
const graph = await api.graph(c.key, signal);
|
||||
if (!graph?.process_definition?.config?.nodes?.length) return null;
|
||||
const headlineCase =
|
||||
c.cases.find((w) => w.status === "running") ||
|
||||
c.cases.find((w) => w.status === "waiting_for_user") ||
|
||||
c.cases.find((w) => w.status === "errored" || w.status === "failed") ||
|
||||
c.cases[0];
|
||||
const recentCandidates = c.cases
|
||||
.filter((w) => w.transaction_id && w.transaction_id !== headlineCase?.transaction_id)
|
||||
.slice(0, 3);
|
||||
const [headlineRt, ...recentResults] = await Promise.all([
|
||||
headlineCase?.transaction_id ? api.transaction(headlineCase.transaction_id, signal) : Promise.resolve(null),
|
||||
...recentCandidates.map((w) => api.transaction(w.transaction_id, signal)),
|
||||
]);
|
||||
const recent = recentResults.filter((r): r is RuntimeTransaction => r != null);
|
||||
return { bucket: c, graph, headlineRt, recent };
|
||||
}),
|
||||
);
|
||||
const enriched: Enriched[] = enrichedRaw.filter((e): e is Enriched => e != null);
|
||||
|
||||
// Classify into families.
|
||||
const used = new Set<string>();
|
||||
|
||||
Reference in New Issue
Block a user