feat(app): global backend-health banner across every scene
The login screen already shows a yellow banner when /api/* is in proxy-down (502/504) or auth-down (503) state. Extend the same signal to every authenticated scene so a regional store manager seeing a broken Mission Control gets a one-line explanation, not silent failure. - New src/lib/useBackendHealth.ts polls /api/v1/auth/me every 30s (cheap, returns 401 when healthy + unauth'd so it doubles as auth probe). Status maps: 200/401/403 -> up, 502/504 -> proxy-down, 503 -> auth-down. - App.tsx renders .global-banner above the topbar whenever the proxy or auth service is down. Hidden on /login (Login.tsx already has its own banner) and during SSO callback. - Dark-theme banner colours added. 31/31 vitest pass. tsc + vite build green.
This commit is contained in:
+10
@@ -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">
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user