feat(app): global backend-health banner across every scene (#7)
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled

This commit was merged in pull request #7.
This commit is contained in:
2026-06-14 21:49:37 +00:00
parent 968273bdcb
commit 50b42c7110
3 changed files with 64 additions and 0 deletions
+10
View File
@@ -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 (
<div className={`shell shell-${scene}`}>
{(backendHealth === "proxy-down" || backendHealth === "auth-down") && scene !== "login" && scene !== "sso-callback" && (
<div className="global-banner global-banner-warn" role="status">
{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."}
</div>
)}
{scene !== "landing" && scene !== "login" && scene !== "sso-callback" && (
<header className="topbar" data-anchor="topbar">
<button className="brand-lock brand-btn" onClick={() => setScene("landing")} aria-label="Home">
+11
View File
@@ -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; }
+43
View File
@@ -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<BackendHealth> {
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<BackendHealth>("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;
}