v1.10.0: top-bar Log out (ISS-157) + Admin button gated to admins (ISS-158)
CI / build + unit + e2e (pull_request) Successful in 6m18s

This commit is contained in:
2026-09-27 22:31:11 +00:00
parent 127d8f97f0
commit cde4334946
5 changed files with 53 additions and 12 deletions
+14 -6
View File
@@ -19,6 +19,11 @@ export default function Home() {
// the sign-in screen or the game shell. The shell only mounts once authenticated, so its state
// feed never polls an unauthenticated /api/state.
const [authed, setAuthed] = useState<boolean | null>(null);
// v1.10.0 (ISS-158): /api/me's `admin` flag — gates the top-bar Admin button in Map.
const [isAdmin, setIsAdmin] = useState(false);
// Bumped after a form sign-in so the probe below re-runs for the NEW session — isAdmin must
// track whoever just signed in, not the user probed on mount (or before the last logout).
const [probeKey, setProbeKey] = useState(0);
useEffect(() => {
const sid = storedSession();
@@ -28,9 +33,10 @@ export default function Home() {
}
let cancelled = false;
fetch('/api/me', { headers: sessionHeaders(sid), cache: 'no-store' })
.then((res) => {
.then(async (res) => {
if (cancelled) return;
if (!res.ok) clearStoredSession(); // stale/expired id — drop it and show sign-in
else setIsAdmin(Boolean(((await res.json()) as { admin?: boolean }).admin)); // v1.10.0 (ISS-158)
setAuthed(res.ok);
})
.catch(() => {
@@ -39,7 +45,7 @@ export default function Home() {
return () => {
cancelled = true;
};
}, []);
}, [probeKey]);
if (authed === null) {
return (
@@ -51,14 +57,16 @@ export default function Home() {
);
}
if (!authed) {
return <SignInScreen onAuthed={() => setAuthed(true)} />;
// v1.10.0 (ISS-158): re-probe /api/me for the new session instead of blindly flipping authed —
// isAdmin has to come from the user who just signed in.
return <SignInScreen onAuthed={() => setProbeKey((k) => k + 1)} />;
}
// The admin panel's Log out cleared the session — drop back to sign-in (ISS-107).
return <AppShell onLogout={() => setAuthed(false)} />;
return <AppShell onLogout={() => setAuthed(false)} isAdmin={isAdmin} />;
}
function AppShell({ onLogout }: { onLogout: () => void }) {
function AppShell({ onLogout, isAdmin }: { onLogout: () => void; isAdmin: boolean }) {
// Single polling source for the whole shell (v0.9.0): one /api/state loop, snapshot passed
// down as props so later panes can share it without extra poll loops. applySnapshot (v0.10.0)
// lets a pane push an action endpoint's full response snapshot in immediately.
@@ -95,7 +103,7 @@ function AppShell({ onLogout }: { onLogout: () => void }) {
{/* v0.11.0 — the same polled snapshot drives the map's plane markers + route lines;
ISS-010 (v0.18.0) — applySnapshot lets the airport popup's Relocate row push its
action response into the shared state, exactly like RightPane does */}
<Map state={state} hovered={hovered} applySnapshot={applySnapshot} onLogout={onLogout} />
<Map state={state} hovered={hovered} applySnapshot={applySnapshot} onLogout={onLogout} isAdmin={isAdmin} />
<RightPane state={state} loading={loading} applySnapshot={applySnapshot} onHover={setHovered} onError={onError} />
<ToastStack toasts={toasts} onDismiss={dismiss} />
</div>