import { Navigate, Route, Routes } from "react-router-dom"; import { lazy, Suspense, useEffect, useMemo, useState } from "react"; import { fetchMe } from "./api/auth"; import { fetchPlatformModules } from "./api/platform"; import { AUTH_REQUIRED_EVENT, loadApiSettings, saveApiSettings, type AuthRequiredEventDetail } from "./api/client"; import type { ApiSettings, AuthInfo, LoginResponse, PlatformModuleInfo } from "./types"; import AppShell from "./layout/AppShell"; import PublicLandingPage from "./features/auth/PublicLandingPage"; import LoginModal from "./features/auth/LoginModal"; import { PermissionBoundary } from "./components/AccessBoundary"; import { adminReadScopes } from "./utils/permissions"; import { firstAccessibleRoute, navItemsForModules, resolveInstalledWebModules, routeContributionsForModules } from "./platform/modules"; import { PlatformModulesProvider } from "./platform/ModuleContext"; import { UnsavedChangesProvider } from "./components/UnsavedChangesGuard"; const DashboardPage = lazy(() => import("./features/dashboard/DashboardPage")); const SettingsPage = lazy(() => import("./features/settings/SettingsPage")); const AdminPage = lazy(() => import("./features/admin/AdminPage")); export default function App() { const [settings, setSettings] = useState(() => loadApiSettings()); const [auth, setAuth] = useState(null); const [checkingSession, setCheckingSession] = useState(true); const [platformModules, setPlatformModules] = useState(null); const [reloginMessage, setReloginMessage] = useState(""); const webModules = useMemo(() => resolveInstalledWebModules(platformModules), [platformModules]); const navItems = useMemo(() => navItemsForModules(webModules), [webModules]); const moduleRoutes = useMemo(() => routeContributionsForModules(webModules), [webModules]); function updateSettings(next: ApiSettings) { setSettings(next); saveApiSettings(next); } function updateAuth(next: AuthInfo | null, accessToken?: string) { const nextSettings = accessToken !== undefined ? { ...settings, accessToken } : settings; setAuth(next); if (accessToken !== undefined) { setSettings(nextSettings); saveApiSettings(nextSettings); } } function authFromLoginResponse(response: LoginResponse): AuthInfo { const active = response.active_tenant ?? response.tenant; return { user: response.user, tenant: active, active_tenant: active, tenants: response.tenants ?? [active], scopes: response.scopes, roles: response.roles, groups: response.groups }; } function handlePublicLogin(response: LoginResponse) { updateAuth(authFromLoginResponse(response), ""); } function handleRelogin(response: LoginResponse) { updateAuth(authFromLoginResponse(response), ""); setReloginMessage(""); } useEffect(() => { function handleAuthRequired(event: Event) { if (!auth) return; const detail = (event as CustomEvent).detail; setReloginMessage(detail?.message || "Your session has expired. Sign in again to continue."); } window.addEventListener(AUTH_REQUIRED_EVENT, handleAuthRequired); return () => window.removeEventListener(AUTH_REQUIRED_EVENT, handleAuthRequired); }, [auth]); useEffect(() => { let cancelled = false; setCheckingSession(true); fetchMe(settings) .then((me) => { if (!cancelled) setAuth(me); }) .catch(() => { if (!cancelled) { const cleared = { ...settings, accessToken: "" }; setSettings(cleared); saveApiSettings(cleared); setAuth(null); setPlatformModules(null); } }) .finally(() => { if (!cancelled) setCheckingSession(false); }); return () => { cancelled = true; }; }, [settings.apiBaseUrl, settings.apiKey]); useEffect(() => { if (!auth) return; let cancelled = false; fetchPlatformModules(settings) .then((response) => { if (!cancelled) setPlatformModules(response.modules); }) .catch(() => { if (!cancelled) setPlatformModules(null); }); return () => { cancelled = true; }; }, [auth?.user.id, auth?.active_tenant?.id, auth?.tenant.id, settings.apiBaseUrl, settings.apiKey]); useEffect(() => { if (!auth) return; let inFlight = false; let lastRefreshAt = 0; async function refreshVisibleSession() { if (document.visibilityState === "hidden" || inFlight) return; const now = Date.now(); if (now - lastRefreshAt < 5_000) return; inFlight = true; lastRefreshAt = now; try { setAuth(await fetchMe(settings)); } catch { // A background refresh must not log the user out on a transient network error. } finally { inFlight = false; } } window.addEventListener("focus", refreshVisibleSession); document.addEventListener("visibilitychange", refreshVisibleSession); return () => { window.removeEventListener("focus", refreshVisibleSession); document.removeEventListener("visibilitychange", refreshVisibleSession); }; }, [auth?.user.id, auth?.active_tenant?.id, auth?.tenant.id, settings.apiBaseUrl, settings.apiKey]); if (checkingSession) { return (
GovOPlaN

Checking session...

Please wait while the local session is verified.

); } if (!auth) { return ( ); } const defaultRoute = firstAccessibleRoute(auth, webModules); return (

Loading module...

}> } /> } /> {moduleRoutes.map((route) => ( {route.render({ settings, auth })} } /> ))} } /> } /> } />
{reloginMessage && ( setReloginMessage("")} onLogin={handleRelogin} /> )}
); }