feat: consolidate shared UI and harden browser authority for release

This commit is contained in:
2026-09-08 01:35:05 +02:00
parent ac40774785
commit b75ca34295
143 changed files with 8664 additions and 752 deletions
+30 -7
View File
@@ -2,7 +2,7 @@ import { Navigate, Route, Routes, useLocation } from "react-router";
import { lazy, useEffect, useMemo, useState } from "react";
import { fetchSession, fetchShellAuth, updateProfile } from "./api/auth";
import { fetchPlatformModules, fetchPlatformPublicModules, fetchPlatformStatus } from "./api/platform";
import { AUTH_REQUIRED_EVENT, isApiError, loadApiSettings, saveApiSettings, type AuthRequiredEventDetail } from "./api/client";
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";
@@ -20,13 +20,16 @@ import {
type WorkflowViewChangedEventDetail
} from "./platform/views";
import { PLATFORM_MODULES_CHANGED_EVENT } from "./platform/moduleEvents";
import { UnsavedChangesProvider } from "./components/UnsavedChangesGuard";
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/AppearanceOverridesEditor";
import { applyAppearanceOverrides } from "./components/appearanceOverrides";
const DashboardPage = lazy(() => import("./features/dashboard/DashboardPage"));
const SettingsPage = lazy(() => import("./features/settings/SettingsPage"));
@@ -54,6 +57,7 @@ export default function App() {
const [remoteWebModules, setRemoteWebModules] = useState<PlatformWebModule[]>([]);
const [remotePublicWebModules, setRemotePublicWebModules] = useState<PlatformWebModule[]>([]);
const [webModulesLoading, setWebModulesLoading] = useState(true);
const [webModuleLoadFailures, setWebModuleLoadFailures] = useState<string[]>([]);
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);
@@ -172,9 +176,10 @@ export default function App() {
}
function updateAuth(next: AuthUpdate | null, accessToken?: string) {
const nextSettings = accessToken !== undefined ? { ...settings, accessToken } : settings;
clearApiReadCache();
const nextSettings = apiSettingsForAuthUpdate(settings, next, accessToken);
setAuth((current) => next ? normalizeAuthInfo(mergeAuthPayload(current, next)) : null);
if (accessToken !== undefined) {
if (nextSettings !== settings) {
setSettings(nextSettings);
saveApiSettings(nextSettings);
}
@@ -328,6 +333,7 @@ export default function App() {
useEffect(() => {
let cancelled = false;
setWebModuleLoadFailures([]);
if (!auth) {
setLocalWebModules([]);
setRemoteWebModules([]);
@@ -345,12 +351,18 @@ export default function App() {
}
async function loadWebModules() {
const local = await loadInstalledWebModules(platformModules);
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);
if (!cancelled) {
setRemoteWebModules(remote);
const recovered = new Set(remote.map((module) => module.id));
setWebModuleLoadFailures(failedIds.filter((moduleId) => !recovered.has(moduleId)));
}
}
void loadWebModules().catch((error) => {
@@ -451,8 +463,10 @@ export default function App() {
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))
@@ -555,6 +569,7 @@ export default function App() {
<PlatformActiveObjectProvider>
<UnsavedChangesProvider>
<AppShell settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} navItems={navItems} allToolItems={allToolItems} maintenanceMode={maintenanceMode} backendReachable={backendReachable}>
{webModuleLoadFailures.length > 0 && <WebModuleLoadFailureNotice moduleIds={webModuleLoadFailures} />}
<ModuleLoadBoundary resetKey={`${location.pathname}:${temporalRevision}`} loading={webModulesLoading}>
<Routes key={`${(auth.active_tenant ?? auth.tenant).id}:${temporalRevision}`}>
<Route path="/" element={<Navigate to={defaultRoute} replace />} />
@@ -607,6 +622,14 @@ export default function App() {
}
function WebModuleLoadFailureNotice({ moduleIds }: { moduleIds: string[] }) {
const { requestNavigation } = useUnsavedChanges();
return <DismissibleAlert tone="warning" dismissible={false} floating>
<p>{i18nMessage("i18n:govoplan-core.optional_module_load_failed", { value0: moduleIds.join(", ") })}</p>
<Button onClick={() => requestNavigation(() => window.location.reload())}>i18n:govoplan-core.reload.cce71553</Button>
</DismissibleAlert>;
}
type AuthPayload = AuthUpdate;
function mergeAuthPayload(current: AuthInfo | null, next: AuthPayload): AuthPayload {