import { Navigate, Route, Routes, useLocation } from "react-router"; import { lazy, useEffect, useMemo, useState } from "react"; import { fetchSession, fetchShellAuth, logout, updateProfile } from "./api/auth"; import { createModuleRefresh } from "./platform/moduleRefresh"; import type { AuthActionUiCapability } from "./types"; import { fetchPlatformModules, fetchPlatformPublicModules, fetchPlatformStatus } from "./api/platform"; import { AUTH_REQUIRED_EVENT, apiSettingsForAuthUpdate, clearApiReadCache, isApiError, loadApiSettings, saveApiSettings, type AuthRequiredEventDetail } from "./api/client"; import type { ApiSettings, AuthInfo, AuthSessionInfo, AuthUpdate, AuthUser, EffectiveViewProjection, LoginResponse, PlatformModuleInfo, PlatformPublicModuleInfo, PlatformWebModule, UserUiPalette, UserUiPreferences, ViewsRuntimeUiCapability } 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 { configurableNavigationItemsForModules, firstAccessibleRoute, loadInstalledPublicWebModules, loadInstalledWebModules, loadRemotePublicWebModules, loadRemoteWebModules, moduleInstalled, navItemsForModules, publicRouteContributionsForModules, routeContributionsForModules, uiCapability } from "./platform/modules"; import { PlatformModulesProvider } from "./platform/ModuleContext"; import { PlatformViewProvider } from "./platform/ViewContext"; import { PlatformTemporalProvider } from "./platform/TemporalContext"; import { PlatformActiveObjectProvider } from "./platform/ActiveObjectContext"; import { PLATFORM_TEMPORAL_CONTEXT_CHANGED_EVENT } from "./platform/temporal"; import { PLATFORM_VIEW_CHANGED_EVENT, PLATFORM_WORKFLOW_VIEW_CHANGED_EVENT, type WorkflowViewChangedEventDetail } from "./platform/views"; import { PLATFORM_MODULES_CHANGED_EVENT } from "./platform/moduleEvents"; import { UnsavedChangesProvider, useUnsavedChanges } from "./components/UnsavedChangesGuard"; import { PlatformLanguageProvider, type PlatformLanguage } from "./i18n/LanguageContext"; import ViewSurfaceRouteBoundary from "./components/ViewSurfaceRouteBoundary"; import ModuleLoadBoundary from "./components/ModuleLoadBoundary"; import DismissibleAlert from "./components/DismissibleAlert"; import Button from "./components/Button"; import { i18nMessage } from "./i18n/LanguageContext"; import { DocumentationHelpProvider } from "./components/help/DocumentationHelpLink"; import { hasAnyScope } from "./utils/permissions"; import { applyAppearanceOverrides } from "./components/appearanceOverrides"; const DashboardPage = lazy(() => import("./features/dashboard/DashboardPage")); const SettingsPage = lazy(() => import("./features/settings/SettingsPage")); const ProductSurfaceRoute = lazy(() => import("./components/ProductSurfaceRoute")); const AuthActionGate = lazy(() => import("./features/auth/AuthActionGate")); const DEFAULT_UI_PREFERENCES: UserUiPreferences = { compact_tables: false, show_inline_help_hints: true, reduce_motion: false, sticky_section_sidebars: true, theme: "system", palette: null, appearance_overrides: null }; export default function App() { const location = useLocation(); const [settings, setSettings] = useState(() => loadApiSettings()); const [auth, setAuth] = useState(null); const [checkingSession, setCheckingSession] = useState(true); const [platformModules, setPlatformModules] = useState(null); const [platformPublicModules, setPlatformPublicModules] = useState(null); const [localWebModules, setLocalWebModules] = useState([]); const [localPublicWebModules, setLocalPublicWebModules] = useState([]); const [remoteWebModules, setRemoteWebModules] = useState([]); const [remotePublicWebModules, setRemotePublicWebModules] = useState([]); const [webModulesLoading, setWebModulesLoading] = useState(true); const [webModuleLoadFailures, setWebModuleLoadFailures] = useState([]); const [maintenanceMode, setMaintenanceMode] = useState<{enabled: boolean;message?: string | null;}>({ enabled: false, message: null }); const [backendReachable, setBackendReachable] = useState(true); const [systemLanguages, setSystemLanguages] = useState<{available: PlatformLanguage[];enabled: string[];defaultLanguage: string;} | null>(null); const [reloginMessage, setReloginMessage] = useState(""); const [baseViewProjection, setBaseViewProjection] = useState(null); const [workflowViewProjection, setWorkflowViewProjection] = useState(null); const [temporalRevision, setTemporalRevision] = useState(0); const viewProjection = workflowViewProjection ?? baseViewProjection; const webModules = useMemo(() => mergeWebModules(localWebModules, remoteWebModules), [localWebModules, remoteWebModules]); const publicWebModules = useMemo(() => mergeWebModules(localPublicWebModules, remotePublicWebModules), [localPublicWebModules, remotePublicWebModules]); const requiredAuthAction = auth?.user.required_auth_action ?? null; const authActions = useMemo( () => uiCapability("auth.actions", publicWebModules), [publicWebModules] ); const viewsRuntime = useMemo( () => uiCapability("views.runtime", webModules), [webModules] ); const navItems = useMemo( () => navItemsForModules(webModules, viewProjection), [viewProjection, webModules] ); const allToolItems = useMemo( () => configurableNavigationItemsForModules(webModules), [webModules] ); const moduleRoutes = useMemo(() => routeContributionsForModules(webModules), [webModules]); const publicRoutes = useMemo(() => publicRouteContributionsForModules(publicWebModules), [publicWebModules]); const contextModules = auth && !requiredAuthAction ? webModules : publicWebModules; const moduleTranslations = useMemo(() => contextModules.map((module) => module.translations).filter(Boolean), [contextModules]); const dashboardModuleInstalled = useMemo(() => moduleInstalled("dashboard", webModules), [webModules]); useEffect(() => { function reloadTemporalData() { setTemporalRevision((current) => current + 1); } window.addEventListener( PLATFORM_TEMPORAL_CONTEXT_CHANGED_EVENT, reloadTemporalData ); return () => window.removeEventListener( PLATFORM_TEMPORAL_CONTEXT_CHANGED_EVENT, reloadTemporalData ); }, []); useEffect(() => { if (!auth || requiredAuthAction || !viewsRuntime) { setBaseViewProjection(null); setWorkflowViewProjection(null); return; } const currentAuth = auth; const currentRuntime = viewsRuntime; let cancelled = false; async function loadEffectiveView() { try { const projection = await currentRuntime.loadEffectiveView(settings, currentAuth); if (!cancelled) setBaseViewProjection(projection); } catch (error) { if (!cancelled) setBaseViewProjection(null); console.error("Failed to load the effective View", error); } } function reloadNormalView() { setWorkflowViewProjection(null); clearStoredWorkflowView(currentAuth); void loadEffectiveView(); } void loadEffectiveView(); window.addEventListener(PLATFORM_VIEW_CHANGED_EVENT, reloadNormalView); return () => { cancelled = true; window.removeEventListener(PLATFORM_VIEW_CHANGED_EVENT, reloadNormalView); }; }, [ auth?.user?.id, auth?.active_tenant?.id, auth?.tenant.id, settings.accessToken, settings.apiBaseUrl, settings.apiKey, requiredAuthAction, viewsRuntime ]); useEffect(() => { if (!auth || requiredAuthAction || !viewsRuntime) { setWorkflowViewProjection(null); return; } const currentAuth = auth; setWorkflowViewProjection(loadStoredWorkflowView(currentAuth)); function applyWorkflowView(event: Event) { const detail = (event as CustomEvent).detail; const projection = detail?.projection ?? null; setWorkflowViewProjection(projection); storeWorkflowView(currentAuth, projection, detail?.contextId); } window.addEventListener( PLATFORM_WORKFLOW_VIEW_CHANGED_EVENT, applyWorkflowView ); return () => { window.removeEventListener( PLATFORM_WORKFLOW_VIEW_CHANGED_EVENT, applyWorkflowView ); }; }, [ auth?.user?.id, auth?.active_tenant?.id, auth?.tenant.id, requiredAuthAction, viewsRuntime ]); function updateSettings(next: ApiSettings) { setSettings(next); saveApiSettings(next); } function updateAuth(next: AuthUpdate | null, accessToken?: string) { clearApiReadCache(); const nextSettings = apiSettingsForAuthUpdate(settings, next, accessToken); setAuth((current) => next ? normalizeAuthInfo(mergeAuthPayload(current, next)) : null); if (nextSettings !== settings) { setSettings(nextSettings); saveApiSettings(nextSettings); } } function authFromLoginResponse(response: LoginResponse): AuthInfo { return normalizeAuthInfo(response); } function handlePublicLogin(response: LoginResponse) { setWebModulesLoading(true); 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 || "i18n:govoplan-core.your_session_has_expired_sign_in_again_to_contin.4cc6a1b3"); } window.addEventListener(AUTH_REQUIRED_EVENT, handleAuthRequired); return () => window.removeEventListener(AUTH_REQUIRED_EVENT, handleAuthRequired); }, [auth]); useEffect(() => { let cancelled = false; async function refreshPlatformStatus() { try { const response = await fetchPlatformStatus(settings); if (cancelled) return; setBackendReachable(true); setMaintenanceMode(response.maintenance_mode); if (response.i18n) { setSystemLanguages({ available: response.i18n.available_languages.map((item) => ({ code: item.code, label: item.label, nativeLabel: item.native_label ?? undefined })), enabled: response.i18n.enabled_languages, defaultLanguage: response.i18n.default_language }); } } catch { if (!cancelled) { setBackendReachable(false); setMaintenanceMode({ enabled: false, message: null }); } } } void refreshPlatformStatus(); const interval = window.setInterval(() => {void refreshPlatformStatus();}, 30_000); window.addEventListener("focus", refreshPlatformStatus); return () => { cancelled = true; window.clearInterval(interval); window.removeEventListener("focus", refreshPlatformStatus); }; }, [settings.apiBaseUrl]); useEffect(() => { let cancelled = false; fetchPlatformPublicModules(settings). then((response) => {if (!cancelled) setPlatformPublicModules(response.modules);}). catch(() => {if (!cancelled) setPlatformPublicModules(null);}); return () => {cancelled = true;}; }, [settings.apiBaseUrl, settings.apiKey]); useEffect(() => { let cancelled = false; setCheckingSession(true); async function bootstrapAuth() { try { const shellAuth = await fetchShellAuth(settings); if (!cancelled) { setBackendReachable(true); setWebModulesLoading(true); setAuth(normalizeAuthInfo(shellAuth)); } } catch (error) { if (!cancelled) { setBackendReachable(isApiError(error)); const cleared = { ...settings, accessToken: "" }; setSettings(cleared); saveApiSettings(cleared); setAuth(null); setPlatformModules(null); setWebModulesLoading(false); setRemoteWebModules([]); } } finally { if (!cancelled) setCheckingSession(false); } } void bootstrapAuth(); return () => {cancelled = true;}; }, [settings.apiBaseUrl, settings.apiKey]); useEffect(() => { if (!auth || requiredAuthAction) { setPlatformModules(null); return; } const refresh = createModuleRefresh({ load: () => fetchPlatformModules(settings), accept: (response) => setPlatformModules(response.modules), reject: () => { setPlatformModules(null); setWebModulesLoading(false); } }); refresh.invalidate(); function handleModulesChanged() { refresh.invalidate(); } function refreshVisibleModules() { refresh.refreshVisible(document.visibilityState !== "hidden"); } window.addEventListener(PLATFORM_MODULES_CHANGED_EVENT, handleModulesChanged); window.addEventListener("focus", refreshVisibleModules); document.addEventListener("visibilitychange", refreshVisibleModules); return () => { refresh.dispose(); window.removeEventListener(PLATFORM_MODULES_CHANGED_EVENT, handleModulesChanged); window.removeEventListener("focus", refreshVisibleModules); document.removeEventListener("visibilitychange", refreshVisibleModules); }; }, [auth, requiredAuthAction, settings.apiBaseUrl, settings.apiKey, settings.accessToken]); useEffect(() => { let cancelled = false; setWebModuleLoadFailures([]); if (!auth || requiredAuthAction) { setLocalWebModules([]); setRemoteWebModules([]); setWebModulesLoading(false); return () => {cancelled = true;}; } if (platformModules === null) { return () => {cancelled = true;}; } if (platformModules.length === 0) { setLocalWebModules([]); setRemoteWebModules([]); setWebModulesLoading(false); return () => {cancelled = true;}; } async function loadWebModules() { const failedIds: string[] = []; const local = await loadInstalledWebModules(platformModules, (moduleId) => failedIds.push(moduleId)); if (cancelled) return; setLocalWebModules(local); setWebModuleLoadFailures(failedIds); setWebModulesLoading(false); const remote = await loadRemoteWebModules(platformModules, local); if (!cancelled) { setRemoteWebModules(remote); const recovered = new Set(remote.map((module) => module.id)); setWebModuleLoadFailures(failedIds.filter((moduleId) => !recovered.has(moduleId))); } } void loadWebModules().catch((error) => { console.error("Failed to load platform WebUI modules", error); if (!cancelled) { setLocalWebModules([]); setRemoteWebModules([]); setWebModulesLoading(false); } }); return () => {cancelled = true;}; }, [auth?.user?.id, auth?.active_tenant?.id, auth?.tenant.id, requiredAuthAction, platformModules]); useEffect(() => { let cancelled = false; if (!platformPublicModules?.length) { setLocalPublicWebModules([]); setRemotePublicWebModules([]); return () => {cancelled = true;}; } async function loadPublicWebModules() { const local = await loadInstalledPublicWebModules(platformPublicModules); if (cancelled) return; setLocalPublicWebModules(local); const remote = await loadRemotePublicWebModules(platformPublicModules, local); if (!cancelled) setRemotePublicWebModules(remote); } void loadPublicWebModules().catch((error) => { console.error("Failed to load public WebUI modules", error); if (!cancelled) { setLocalPublicWebModules([]); setRemotePublicWebModules([]); } }); return () => {cancelled = true;}; }, [platformPublicModules]); useEffect(() => { const preferences = auth?.user.ui_preferences ?? DEFAULT_UI_PREFERENCES; const root = document.documentElement; root.classList.toggle("ui-compact-tables", preferences.compact_tables); root.classList.toggle("ui-hide-help-hints", !preferences.show_inline_help_hints); root.classList.toggle("ui-reduce-motion", preferences.reduce_motion); root.classList.toggle("ui-no-sticky-section-sidebars", !preferences.sticky_section_sidebars); root.dataset.palette = normalizeUiPalette(auth?.user.appearance?.palette ?? preferences.palette); const systemDarkQuery = window.matchMedia?.("(prefers-color-scheme: dark)") ?? null; const applyTheme = () => { const resolvedTheme = preferences.theme === "system" ? systemDarkQuery?.matches ? "dark" : "light" : preferences.theme; root.dataset.theme = resolvedTheme; root.dataset.themePreference = preferences.theme; applyAppearanceOverrides(root, auth?.user.appearance?.custom_overrides ?? null, resolvedTheme); }; applyTheme(); if (preferences.theme !== "system" || !systemDarkQuery) { return undefined; } systemDarkQuery.addEventListener("change", applyTheme); return () => { systemDarkQuery.removeEventListener("change", applyTheme); }; }, [ auth?.user.ui_preferences?.compact_tables, auth?.user.ui_preferences?.show_inline_help_hints, auth?.user.ui_preferences?.reduce_motion, auth?.user.ui_preferences?.sticky_section_sidebars, auth?.user.ui_preferences?.theme, auth?.user.ui_preferences?.palette, auth?.user.appearance?.palette, auth?.user.appearance?.custom_overrides ]); useEffect(() => { if (!auth) return; const currentAuth = auth; let cancelled = false; let inFlight = false; let lastRefreshAt = 0; let lastShellRefreshAt = Date.now(); async function refreshVisibleSession() { if (document.visibilityState === "hidden" || inFlight) return; const now = Date.now(); if (now - lastRefreshAt < 5_000) return; inFlight = true; lastRefreshAt = now; try { const sessionInfo = await fetchSession(settings); if (cancelled) return; setBackendReachable(true); const shellRefreshDue = now - lastShellRefreshAt >= 60_000; if (!sessionMatchesAuth(sessionInfo, currentAuth) || shellRefreshDue) { clearApiReadCache(); const shellAuth = await fetchShellAuth(settings); if (cancelled) return; clearApiReadCache(); lastShellRefreshAt = Date.now(); setAuth((current) => current && sessionMatchesAuth(sessionInfo, current) ? normalizeAuthInfo(mergeAuthPayload(current, shellAuth)) : normalizeAuthInfo(shellAuth)); } } catch (error) { if (!cancelled && !isApiError(error)) { setBackendReachable(false); } // 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 () => { cancelled = true; window.removeEventListener("focus", refreshVisibleSession); document.removeEventListener("visibilitychange", refreshVisibleSession); }; }, [auth?.user?.id, auth?.active_tenant?.id, auth?.tenant.id, auth?.principal?.session_id, requiredAuthAction, settings.apiBaseUrl, settings.apiKey]); if (checkingSession) { return (
i18n:govoplan-core.govoplan.a84c0a85

i18n:govoplan-core.checking_session.e7d81968

i18n:govoplan-core.please_wait_while_the_local_session_is_verified.1dbd8fd3

); } if (!auth) { return ( {publicRoutes.map((route) => )} } /> ); } if (requiredAuthAction) { return { void logout(settings).catch(() => undefined).finally(() => updateAuth(null, "")); }} /> {reloginMessage && setReloginMessage("")} onLogin={handleRelogin} />} ; } const defaultRoute = firstAccessibleRoute(auth, webModules, viewProjection); const localDocsAvailable = hasAnyScope(auth, ["docs:documentation:read", "docs:documentation:admin", "system:settings:read", "admin:settings:read"]) && webModules.some((module) => module.id === "docs" && module.routes?.some((route) => route.path === "/docs")); const authAvailableLanguages = auth.available_languages?.map((item) => ({ code: item.code, label: item.label, nativeLabel: item.native_label ?? undefined })); function persistLanguagePreference(code: string) { void updateProfile(settings, { preferred_language: code }).then((next) => updateAuth(next)).catch(() => undefined); } return ( {webModuleLoadFailures.length > 0 && } } /> {!dashboardModuleInstalled && } />} {publicRoutes.map((route) => )} {moduleRoutes.map((route) => {route.render({ settings, auth, onAuthChange: updateAuth })} } /> )} } /> } /> {reloginMessage && setReloginMessage("")} onLogin={handleRelogin} /> } ); } function WebModuleLoadFailureNotice({ moduleIds }: { moduleIds: string[] }) { const { requestNavigation } = useUnsavedChanges(); return

{i18nMessage("i18n:govoplan-core.optional_module_load_failed", { value0: moduleIds.join(", ") })}

; } type AuthPayload = AuthUpdate; function mergeAuthPayload(current: AuthInfo | null, next: AuthPayload): AuthPayload { if (!current) return next; const nextActiveTenant = next.active_tenant ?? next.tenant ?? null; const currentActiveTenant = current.active_tenant ?? current.tenant; const tenantChanged = Boolean(nextActiveTenant && nextActiveTenant.id !== currentActiveTenant.id); return { ...current, ...next, user: next.user ? { ...current.user, ...next.user } : current.user, tenant: nextActiveTenant ?? current.tenant, active_tenant: nextActiveTenant ?? currentActiveTenant, tenants: next.tenants ?? current.tenants, scopes: next.scopes ?? current.scopes, roles: next.roles ?? (next.roles_loaded === false || tenantChanged ? [] : current.roles), groups: next.groups ?? (next.groups_loaded === false || tenantChanged ? [] : current.groups), principal: next.principal === undefined ? current.principal : next.principal, available_languages: next.available_languages ?? (tenantChanged ? undefined : current.available_languages), enabled_language_codes: next.enabled_language_codes ?? (tenantChanged ? undefined : current.enabled_language_codes), default_language: next.default_language ?? (tenantChanged ? undefined : current.default_language), profile_loaded: next.profile_loaded ?? (tenantChanged ? false : current.profile_loaded), roles_loaded: next.roles_loaded ?? (tenantChanged ? false : current.roles_loaded), groups_loaded: next.groups_loaded ?? (tenantChanged ? false : current.groups_loaded) }; } export function normalizeAuthInfo(response: AuthPayload): AuthInfo { const principal = response.principal ?? null; const activeTenant = response.active_tenant ?? response.tenant ?? response.tenants?.[0] ?? null; const user = normalizeAuthUser(response.user, principal); const profileLoaded = response.profile_loaded ?? hasFullProfilePayload(response); const rolesLoaded = response.roles_loaded ?? response.roles !== undefined; const groupsLoaded = response.groups_loaded ?? response.groups !== undefined; if (!activeTenant) { throw new Error("Authentication response did not include an active tenant."); } if (!user) { throw new Error("Authentication response did not include a user."); } return { user, tenant: activeTenant, active_tenant: activeTenant, tenants: response.tenants ?? [activeTenant], scopes: response.scopes ?? principal?.scopes ?? [], roles: response.roles ?? [], groups: response.groups ?? [], principal, available_languages: response.available_languages, enabled_language_codes: profileLoaded ? response.enabled_language_codes ?? activeTenant.enabled_language_codes ?? [] : undefined, default_language: profileLoaded ? response.default_language ?? user.preferred_language ?? activeTenant.default_locale ?? "de" : undefined, profile_loaded: profileLoaded, roles_loaded: rolesLoaded, groups_loaded: groupsLoaded }; } function hasFullProfilePayload(response: AuthPayload): boolean { return Boolean( response.default_language || response.available_languages || response.enabled_language_codes || response.roles || response.groups ); } function normalizeAuthUser(user: Partial | null | undefined, principal: AuthInfo["principal"]): AuthUser | null { if (user?.id && user.account_id) { return { id: user.id, account_id: user.account_id, email: user.email ?? principal?.email ?? "", display_name: user.display_name ?? principal?.display_name ?? principal?.email ?? null, tenant_display_name: user.tenant_display_name ?? null, is_tenant_admin: user.is_tenant_admin ?? false, password_reset_required: user.password_reset_required ?? false, required_auth_action: user.required_auth_action ?? null, local_password: user.local_password ?? false, preferred_language: user.preferred_language ?? null, enabled_language_codes: user.enabled_language_codes ?? [], ui_preferences: normalizeUiPreferences(user.ui_preferences) }; } if (!principal?.membership_id || !principal.account_id) { return null; } return { id: principal.membership_id, account_id: principal.account_id, email: principal.email ?? "", display_name: principal.display_name ?? principal.email ?? null, tenant_display_name: principal.display_name ?? null, is_tenant_admin: false, password_reset_required: false, required_auth_action: null, local_password: false, preferred_language: null, enabled_language_codes: [], ui_preferences: DEFAULT_UI_PREFERENCES }; } export function sessionMatchesAuth(sessionInfo: AuthSessionInfo, auth: AuthInfo): boolean { const activeTenant = auth.active_tenant ?? auth.tenant; if (sessionInfo.user.id !== auth.user.id) return false; if (sessionInfo.user.account_id !== auth.user.account_id) return false; if ((sessionInfo.user.required_auth_action ?? null) !== (auth.user.required_auth_action ?? null)) return false; if (Boolean(sessionInfo.user.local_password) !== Boolean(auth.user.local_password)) return false; if ((sessionInfo.active_tenant ?? sessionInfo.tenant).id !== activeTenant.id) return false; if (auth.principal?.auth_method && sessionInfo.auth_method !== auth.principal.auth_method) return false; if (auth.principal?.session_id && sessionInfo.session_id && auth.principal.session_id !== sessionInfo.session_id) return false; if (auth.principal?.api_key_id && sessionInfo.api_key_id && auth.principal.api_key_id !== sessionInfo.api_key_id) return false; return true; } function normalizeUiPreferences(value: Partial | null | undefined): UserUiPreferences { const theme = value?.theme === "light" || value?.theme === "dark" || value?.theme === "system" ? value.theme : "system"; return { compact_tables: Boolean(value?.compact_tables ?? DEFAULT_UI_PREFERENCES.compact_tables), show_inline_help_hints: Boolean(value?.show_inline_help_hints ?? DEFAULT_UI_PREFERENCES.show_inline_help_hints), reduce_motion: Boolean(value?.reduce_motion ?? DEFAULT_UI_PREFERENCES.reduce_motion), sticky_section_sidebars: Boolean(value?.sticky_section_sidebars ?? DEFAULT_UI_PREFERENCES.sticky_section_sidebars), theme, palette: normalizeOptionalUiPalette(value?.palette), appearance_overrides: value?.appearance_overrides ?? null }; } function normalizeUiPalette(value: UserUiPalette | string | null | undefined): UserUiPalette { return value === "civic_blue" || value === "forest" || value === "plum" ? value : "default"; } function normalizeOptionalUiPalette(value: UserUiPalette | string | null | undefined): UserUiPalette | null { return value == null ? null : normalizeUiPalette(value); } function mergeWebModules(localModules: PlatformWebModule[], remoteModules: PlatformWebModule[]): PlatformWebModule[] { if (remoteModules.length === 0) return localModules; const seen = new Set(localModules.map((module) => module.id)); return [ ...localModules, ...remoteModules.filter((module) => { if (seen.has(module.id)) return false; seen.add(module.id); return true; })]; } const WORKFLOW_VIEW_SESSION_TTL_MS = 12 * 60 * 60 * 1000; type StoredWorkflowView = { projection: EffectiveViewProjection; contextId?: string | null; expiresAt: number; }; function workflowViewStorageKey(auth: AuthInfo): string { const tenant = auth.active_tenant ?? auth.tenant; return `govoplan:workflow-view:${tenant.id}:${auth.user.account_id}`; } function loadStoredWorkflowView( auth: AuthInfo ): EffectiveViewProjection | null { try { const key = workflowViewStorageKey(auth); const raw = window.sessionStorage.getItem(key); if (!raw) return null; const stored = JSON.parse(raw) as Partial; if ( typeof stored.expiresAt !== "number" || stored.expiresAt <= Date.now() || !stored.projection || !Array.isArray(stored.projection.visibleSurfaceIds) ) { window.sessionStorage.removeItem(key); return null; } return stored.projection; } catch { return null; } } function storeWorkflowView( auth: AuthInfo, projection: EffectiveViewProjection | null, contextId?: string | null ): void { try { const key = workflowViewStorageKey(auth); if (!projection) { window.sessionStorage.removeItem(key); return; } const stored: StoredWorkflowView = { projection, contextId, expiresAt: Date.now() + WORKFLOW_VIEW_SESSION_TTL_MS }; window.sessionStorage.setItem(key, JSON.stringify(stored)); } catch { // Session persistence is optional; the in-memory projection still applies. } } function clearStoredWorkflowView(auth: AuthInfo): void { storeWorkflowView(auth, null); }