feat(app): global backend-health banner across every scene (#7)
This commit was merged in pull request #7.
This commit is contained in:
+10
@@ -19,6 +19,7 @@ import CommandBar from "./components/CommandBar";
|
|||||||
import Toaster from "./components/Toaster";
|
import Toaster from "./components/Toaster";
|
||||||
import Console from "./components/Console";
|
import Console from "./components/Console";
|
||||||
import { Cmd, Home, Layers, HistoryIcon, Pulse, Refresh, Branch, Cog, User, Sun, Moon, Bot } from "./components/icons";
|
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() {
|
export default function App() {
|
||||||
@@ -120,8 +121,17 @@ export default function App() {
|
|||||||
return () => { cancelled = true; window.clearInterval(id); };
|
return () => { cancelled = true; window.clearInterval(id); };
|
||||||
}, [isAuthed, tenantId, userEmail]);
|
}, [isAuthed, tenantId, userEmail]);
|
||||||
|
|
||||||
|
const backendHealth = useBackendHealth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`shell shell-${scene}`}>
|
<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" && (
|
{scene !== "landing" && scene !== "login" && scene !== "sso-callback" && (
|
||||||
<header className="topbar" data-anchor="topbar">
|
<header className="topbar" data-anchor="topbar">
|
||||||
<button className="brand-lock brand-btn" onClick={() => setScene("landing")} aria-label="Home">
|
<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;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
.login-banner-warn { border-color: #b08a4a; background: #fff6e0; color: #5a3a00; }
|
.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