diff --git a/src/App.tsx b/src/App.tsx
index 277fd19..c8cf1df 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -19,6 +19,7 @@ import CommandBar from "./components/CommandBar";
import Toaster from "./components/Toaster";
import Console from "./components/Console";
import { Cmd, Home, Layers, HistoryIcon, Pulse, Refresh, Branch, Cog, User, Sun, Moon, Bot } from "./components/icons";
+import { useBackendHealth } from "./lib/useBackendHealth";
export default function App() {
@@ -120,8 +121,17 @@ export default function App() {
return () => { cancelled = true; window.clearInterval(id); };
}, [isAuthed, tenantId, userEmail]);
+ const backendHealth = useBackendHealth();
+
return (
+ {(backendHealth === "proxy-down" || backendHealth === "auth-down") && scene !== "login" && scene !== "sso-callback" && (
+
+ {backendHealth === "proxy-down"
+ ? "The FlowMaster backend is not reachable from this environment right now. Some actions will fail until it is restored."
+ : "The authentication service is temporarily down. Some actions will fail until it is restored."}
+
+ )}
{scene !== "landing" && scene !== "login" && scene !== "sso-callback" && (
setScene("landing")} aria-label="Home">
diff --git a/src/index.css b/src/index.css
index e349f5a..c8ac383 100644
--- a/src/index.css
+++ b/src/index.css
@@ -2249,3 +2249,14 @@ select.studio-input { background: var(--bp-paper); }
letter-spacing: 0.02em;
}
.login-banner-warn { border-color: #b08a4a; background: #fff6e0; color: #5a3a00; }
+
+.global-banner {
+ padding: 8px 16px;
+ font-size: 11.5px;
+ line-height: 1.45;
+ letter-spacing: 0.02em;
+ text-align: center;
+ border-bottom: 1px solid var(--bp-line, #b08a4a);
+}
+.global-banner-warn { background: #fff6e0; color: #5a3a00; border-color: #b08a4a; }
+[data-theme="dark"] .global-banner-warn { background: #3a2a0a; color: #ffd690; border-color: #6b4a14; }
diff --git a/src/lib/useBackendHealth.ts b/src/lib/useBackendHealth.ts
new file mode 100644
index 0000000..620e2e2
--- /dev/null
+++ b/src/lib/useBackendHealth.ts
@@ -0,0 +1,43 @@
+// Background poll of /api/v1/auth/me — the cheapest endpoint that reflects
+// whole-stack health. 200/401/403 means the proxy AND auth-service are up.
+// 502/504 means the canvas nginx → demo proxy is down. 503 means the auth
+// service is down upstream. Anything else is treated as "up" so we don't
+// false-positive when EA2 returns a non-standard code.
+
+import { useEffect, useState } from "react";
+
+export type BackendHealth = "unknown" | "up" | "auth-down" | "proxy-down";
+
+const POLL_MS = 30_000;
+
+async function probeOnce(signal: AbortSignal): Promise {
+ try {
+ const r = await fetch("/api/v1/auth/me", { method: "GET", signal });
+ if (r.status === 200 || r.status === 401 || r.status === 403) return "up";
+ if (r.status === 502 || r.status === 504) return "proxy-down";
+ if (r.status === 503) return "auth-down";
+ return "up";
+ } catch {
+ return "proxy-down";
+ }
+}
+
+export function useBackendHealth(): BackendHealth {
+ const [state, setState] = useState("unknown");
+ useEffect(() => {
+ const ctrl = new AbortController();
+ let cancelled = false;
+ const run = async () => {
+ const next = await probeOnce(ctrl.signal);
+ if (!cancelled) setState(next);
+ };
+ void run();
+ const id = window.setInterval(() => { void run(); }, POLL_MS);
+ return () => {
+ cancelled = true;
+ ctrl.abort();
+ window.clearInterval(id);
+ };
+ }, []);
+ return state;
+}