diff --git a/webui/src/App.tsx b/webui/src/App.tsx index ca416aa..cfd19f9 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -2,7 +2,7 @@ import { Navigate, Route, Routes } from "react-router-dom"; import { lazy, Suspense, useEffect, useMemo, useState } from "react"; import { fetchSession, fetchShellAuth, updateProfile } from "./api/auth"; import { fetchPlatformModules, fetchPlatformStatus } from "./api/platform"; -import { AUTH_REQUIRED_EVENT, loadApiSettings, saveApiSettings, type AuthRequiredEventDetail } from "./api/client"; +import { AUTH_REQUIRED_EVENT, isApiError, loadApiSettings, saveApiSettings, type AuthRequiredEventDetail } from "./api/client"; import type { ApiSettings, AuthInfo, AuthSessionInfo, AuthUpdate, AuthUser, LoginResponse, PlatformModuleInfo, PlatformWebModule, UserUiPreferences } from "./types"; import AppShell from "./layout/AppShell"; import PublicLandingPage from "./features/auth/PublicLandingPage"; @@ -32,6 +32,7 @@ export default function App() { const [platformModules, setPlatformModules] = useState(null); const [remoteWebModules, setRemoteWebModules] = 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(""); @@ -87,6 +88,7 @@ export default function App() { try { const response = await fetchPlatformStatus(settings); if (cancelled) return; + setBackendReachable(true); setMaintenanceMode(response.maintenance_mode); if (response.i18n) { setSystemLanguages({ @@ -100,7 +102,10 @@ export default function App() { }); } } catch { - if (!cancelled) setMaintenanceMode({ enabled: false, message: null }); + if (!cancelled) { + setBackendReachable(false); + setMaintenanceMode({ enabled: false, message: null }); + } } } @@ -121,9 +126,13 @@ export default function App() { async function bootstrapAuth() { try { const shellAuth = await fetchShellAuth(settings); - if (!cancelled) setAuth(normalizeAuthInfo(shellAuth)); - } catch { if (!cancelled) { + setBackendReachable(true); + setAuth(normalizeAuthInfo(shellAuth)); + } + } catch (error) { + if (!cancelled) { + setBackendReachable(isApiError(error)); const cleared = { ...settings, accessToken: "" }; setSettings(cleared); saveApiSettings(cleared); @@ -243,6 +252,7 @@ export default function App() { try { const sessionInfo = await fetchSession(settings); if (cancelled) return; + setBackendReachable(true); const shellRefreshDue = now - lastShellRefreshAt >= 60_000; if (!sessionMatchesAuth(sessionInfo, currentAuth) || shellRefreshDue) { const shellAuth = await fetchShellAuth(settings); @@ -252,7 +262,10 @@ export default function App() { ? normalizeAuthInfo(mergeAuthPayload(current, shellAuth)) : normalizeAuthInfo(shellAuth)); } - } catch { + } catch (error) { + if (!cancelled && !isApiError(error)) { + setBackendReachable(false); + } // A background refresh must not log the user out on a transient network error. @@ -273,7 +286,7 @@ export default function App() { - +
i18n:govoplan-core.govoplan.a84c0a85
@@ -293,7 +306,7 @@ export default function App() { - + @@ -324,7 +337,7 @@ export default function App() { moduleTranslations={moduleTranslations}> - +

i18n:govoplan-core.loading_module.50161f3c

}> } /> diff --git a/webui/src/components/MessageDisplayPanel.tsx b/webui/src/components/MessageDisplayPanel.tsx index b255e9a..abc1680 100644 --- a/webui/src/components/MessageDisplayPanel.tsx +++ b/webui/src/components/MessageDisplayPanel.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState, type ReactNode } from "react"; -import { Archive, LockKeyhole, Paperclip } from "lucide-react"; +import { Archive, Link2, LockKeyhole, Paperclip, X } from "lucide-react"; import { i18nMessage, usePlatformLanguage } from "../i18n/LanguageContext"; import SegmentedControl from "./SegmentedControl"; @@ -14,6 +14,7 @@ export type MessageDisplayAttachment = { contentType?: string | null; sizeBytes?: number | null; detail?: string | null; + linkedToCampaign?: boolean | null; archiveGroup?: string | null; archiveLabel?: string | null; protected?: boolean | null; @@ -159,9 +160,15 @@ function AttachmentRow({ attachment, index }: {attachment: MessageDisplayAttachm const contentType = formatContentType(attachment.contentType); const size = formatBytes(attachment.sizeBytes); const hasMeta = Boolean(contentType || size); + const LinkIcon = attachment.linkedToCampaign === true ? Link2 : attachment.linkedToCampaign === false ? X : Paperclip; + const linkStateClass = attachment.linkedToCampaign === true ? + " is-linked" : + attachment.linkedToCampaign === false ? + " is-unlinked" : + ""; return ( -
-
); @@ -437,20 +380,18 @@ export function MailServerActionResult({ result, floating = false }: {result: Ma export function MailServerFolderLookupResultView({ result, disabled = false, - onUseDetected - - - - -}: {result: MailServerFolderLookupResult | null | undefined;disabled?: boolean;onUseDetected?: () => void;}) { + onUseDetected, + compact = true, + floatingFailures = false +}: {result: MailServerFolderLookupResult | null | undefined;disabled?: boolean;onUseDetected?: () => void;compact?: boolean;floatingFailures?: boolean;}) { if (!result) return null; if (!result.ok) { - return {result.message}; + return {result.message}; } const folders = result.folders ?? []; return ( - +

{result.message}

i18n:govoplan-core.detected_sent_folder.cbf8ec8d {result.detected_sent_folder || "-"}

{result.detected_sent_folder && onUseDetected && } diff --git a/webui/src/layout/AppShell.tsx b/webui/src/layout/AppShell.tsx index cf3f309..00fcd51 100644 --- a/webui/src/layout/AppShell.tsx +++ b/webui/src/layout/AppShell.tsx @@ -13,6 +13,7 @@ type Props = { publicMode?: boolean; navItems?: PlatformNavItem[]; maintenanceMode?: { enabled: boolean; message?: string | null }; + backendReachable?: boolean; }; export default function AppShell({ @@ -23,7 +24,8 @@ export default function AppShell({ onAuthChange, publicMode = false, navItems = [], - maintenanceMode + maintenanceMode, + backendReachable = true }: Props) { const location = useLocation(); @@ -32,7 +34,7 @@ export default function AppShell({
- +
{children}
@@ -43,7 +45,7 @@ export default function AppShell({
- +
{children}
diff --git a/webui/src/layout/Titlebar.tsx b/webui/src/layout/Titlebar.tsx index 99ddc0b..7b50a54 100644 --- a/webui/src/layout/Titlebar.tsx +++ b/webui/src/layout/Titlebar.tsx @@ -23,9 +23,10 @@ type Props = { onSettingsChange: (settings: ApiSettings) => void; onAuthChange: (auth: AuthUpdate | null, accessToken?: string) => void; maintenanceMode?: {enabled: boolean;message?: string | null;}; + backendReachable?: boolean; }; -export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode }: Props) { +export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode, backendReachable = true }: Props) { const navigate = useGuardedNavigate(); const { requestNavigation } = useUnsavedChanges(); const [accountOpen, setAccountOpen] = useState(false); @@ -161,7 +162,15 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode return (
- {maintenanceMode?.enabled && + {!backendReachable ? +
+ System not reachable / offline! +
: + maintenanceMode?.enabled &&