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
+22 -5
View File
@@ -14,7 +14,7 @@ import { approxLocalTime } from '../lib/localTime';
import Modal from './Modal';
import AdminPanel from './AdminPanel';
import Leaderboard from './Leaderboard';
import { sessionHeaders } from '../lib/authClient';
import { clearStoredSession, sessionHeaders } from '../lib/authClient';
// v0.70.0 default view: null island [0,0] at zoom 2 — the world is 2048 px wide there and the
// Africa+Europe bounding box (lat −35..+71, lon −18..+52) spans ~800 px vertically in Web Mercator,
@@ -74,11 +74,13 @@ interface MapProps {
// v1.1.0 (ISS-107): the admin panel's Log out cleared the session — tell the shell to drop back
// to the sign-in screen instead of leaving a signed-out user staring at a live dashboard.
onLogout?: () => void;
// v1.10.0 (ISS-158): /api/me says this session is an admin → show the top-bar Admin button.
isAdmin?: boolean;
}
// Named FlightMap (not Map): a component called `Map` would shadow the global
// Map constructor inside its own body, breaking `new Map(...)` for the airport index.
export default function FlightMap({ state, hovered, applySnapshot, onLogout }: MapProps) {
export default function FlightMap({ state, hovered, applySnapshot, onLogout, isAdmin = false }: MapProps) {
const containerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<maplibregl.Map | null>(null);
// /api/airports is fetched at most once per page life: every styledata event
@@ -569,6 +571,14 @@ export default function FlightMap({ state, hovered, applySnapshot, onLogout }: M
void localAssetsAvailable().then((available) => mapRef.current?.setStyle(styleUrlFor(id, available)));
};
// v1.10.0 (ISS-157): top-bar Log out — same three steps as the admin panel's button: best-effort
// server-side session kill, drop the stored id, tell the shell to show sign-in again.
const onTopBarLogout = async () => {
await fetch('/api/logout', { method: 'POST', headers: sessionHeaders() }).catch(() => undefined);
clearStoredSession();
onLogout?.();
};
return (
<div className="map-wrap">
<header data-testid="app-header" className="app-header">
@@ -596,9 +606,16 @@ export default function FlightMap({ state, hovered, applySnapshot, onLogout }: M
<button type="button" data-testid="leaderboard-btn" className="pane-btn" onClick={() => setLeaderboardOpen(true)}>
Leaderboard
</button>
{/* v0.71.0: admin panel entry point — right side of the top bar, opens a modal (no URL). */}
<button type="button" data-testid="admin-btn" className="pane-btn" onClick={() => setAdminOpen(true)}>
Admin
{/* v0.71.0: admin panel entry point — right side of the top bar, opens a modal (no URL).
v1.10.0 (ISS-158): only for admins (/api/me `admin` flag) — hidden entirely otherwise. */}
{isAdmin && (
<button type="button" data-testid="admin-btn" className="pane-btn" onClick={() => setAdminOpen(true)}>
Admin
</button>
)}
{/* v1.10.0 (ISS-157): Log out — last child, so the flex row puts it top-right of the bar. */}
<button type="button" data-testid="logout-btn" className="pane-btn" onClick={() => void onTopBarLogout()}>
Log out
</button>
</header>
<div ref={containerRef} data-testid="map" className="map-container" />