import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; import { Alert, Box, Button, Chip, Paper, Stack, Tab, Tabs, TextField, Typography, Checkbox, FormControlLabel, LinearProgress, } from "@mui/material"; import { api, getApiErrorMessage } from "../api"; import { useI18n } from "../i18n/I18nProvider"; import { PageHeader, PageSection } from "../layout/PageLayout"; import type { TranslationKey } from "../i18n/translations"; import { checkSessionCookie, getSessionContext, isSessionInvalidated } from "../sessionBoundary"; type AiServiceMetrics = { healthy: boolean; model?: string | null; device?: string | null; gpuAvailable?: boolean; gpuName?: string | null; ocrAvailable?: boolean | null; ocrLanguages?: string | null; ollamaConfigured?: boolean | null; ollamaReachable?: boolean | null; ollamaModel?: string | null; ollamaModelAvailable?: boolean | null; ollamaVersion?: string | null; ollamaInstalledModels?: string[] | null; ollamaLoadedModels?: string[] | null; ollamaLoadedCount?: number | null; healthLatencyMs?: number | null; probeLatencyMs?: number | null; lastProbeAt?: string | null; lastProbeSuccessAt?: string | null; lastProbeFailureAt?: string | null; probeFailures: number; requests: number; cacheHits: number; cacheMisses: number; failures: number; averageLatencyMs?: number | null; ocrRequests: number; ocrFailures: number; averageOcrLatencyMs?: number | null; lastOcrSuccessAt?: string | null; lastOcrFailureAt?: string | null; lastSuccessAt?: string | null; lastFailureAt?: string | null; lastError?: string | null; }; type EditableEmailSettings = { enabled: boolean; host: string; port: number; user: string; from: string; fromName: string; enableSsl: boolean; timeoutMs: number; usesOverrides: boolean; hasPassword: boolean; }; type CvBenchmarkEntry = { FileName: string; Slug: string; Extension: string; Characters: number; OutputPath: string; ApprovedFixturePath?: string | null; CandidateFixturePath?: string | null; ContactLocation?: string | null; FirstJob?: string | null; FirstJobLocation?: string | null; FirstEducation?: string | null; FirstEducationLocation?: string | null; QualificationLevels: string[]; SuspiciousLocations: string[]; CoverageScore: number; ConfidenceScore: number; ConsistencyScore: number; DiffSummary?: string | null; }; type CvBenchmarkIndex = { CorpusRoot: string; OutputRoot: string; GeneratedAtUtc: string; TotalFiles: number; AverageCoverage: number; AverageConfidence: number; AverageConsistency: number; FilesWithSuspiciousLocations: number; MissingApprovedFixtures: number; Entries: CvBenchmarkEntry[]; }; type CvBenchmarkStatus = { indexJson?: string | null; reportMarkdown?: string | null; rootPath: string; lastUpdatedAtUtc?: string | null; }; type MaintenanceJob = { id: string; title: string; description: string; available: boolean; runsUrl?: string | null; }; type SystemStatus = { environment: string; contentRoot: string; version: string; commitSha?: string | null; buildStamp?: string | null; storage: { dataRoot: string; dbPath: string; dbExists: boolean; dbSizeBytes?: number | null; companyCount: number; jobCount: number; deletedCount: number; }; email: { enabled: boolean; host?: string | null; port: number; enableSsl: boolean; from?: string | null; fromName?: string | null; }; database: { provider: string; looksConfigured: boolean; canConnect: boolean; target?: string | null; usesFileStorage: boolean; warning?: string | null; }; runtime: { framework: string; osDescription: string; processArchitecture: string; machineName?: string | null; }; auth: { required: boolean; hasJwtKey: boolean; googleConfigured: boolean; gmailConfigured: boolean; }; ai: AiServiceMetrics; }; function formatBytes(bytes?: number | null) { if (bytes == null) return "-"; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function displayMetadata(value?: string | null) { return value && value.trim().length > 0 ? value : "-"; } function formatDate(value?: string | null, locale?: string) { return value ? new Date(value).toLocaleString(locale) : "-"; } function formatPercent(value?: number | null) { return typeof value === "number" ? `${Math.round(value * 100)}%` : "-"; } function parseBenchmarkIndex(indexJson?: string | null): CvBenchmarkIndex | null { if (!indexJson?.trim()) return null; try { return JSON.parse(indexJson) as CvBenchmarkIndex; } catch { return null; } } function benchmarkTone(value?: number | null) { if (typeof value !== "number") return "default" as const; if (value >= 0.8) return "success" as const; if (value >= 0.6) return "warning" as const; return "error" as const; } function SummaryCard({ title, value, subtitle, tone = "default" }: { title: string; value: string; subtitle?: string; tone?: "default" | "success" | "warning" | "error" }) { const color = tone === "success" ? "success.main" : tone === "warning" ? "warning.main" : tone === "error" ? "error.main" : "text.primary"; return ( {title} {value} {subtitle || "-"} ); } function DetailRow({ label, value }: { label: string; value: React.ReactNode }) { return {label}: {value}; } type ReadKind = "status" | "email" | "benchmark" | "maintenance"; type ActionKind = "save" | "test" | "probe" | "maintenance"; const readPaths: Record = { status: "/admin/system", email: "/admin/system/email-settings", benchmark: "/admin/system/cv-benchmark", maintenance: "/admin/system/maintenance-jobs" }; const readErrorKeys: Record = { status: "adminSystemLoadFailed", email: "adminSystemEmailSettingsLoadFailed", benchmark: "adminSystemBenchmarkLoadFailed", maintenance: "adminSystemMaintenanceLoadFailed" }; const actionErrorKeys: Record = { save: "adminSystemEmailSaveFailed", test: "adminSystemTestEmailFailed", probe: "adminSystemProbeFailed", maintenance: "adminMaintenanceFailed" }; export default function AdminSystemPage() { const { language, t } = useI18n(); const [tab, setTab] = useState(0); const [status, setStatus] = useState(null); const [emailSettings, setEmailSettings] = useState(null); const [benchmarkStatus, setBenchmarkStatus] = useState(null); const [maintenanceJobs, setMaintenanceJobs] = useState([]); const [smtpPassword, setSmtpPassword] = useState(""); const [clearPassword, setClearPassword] = useState(false); const [loading, setLoading] = useState(true); const [readErrors, setReadErrors] = useState>>({}); const [accessDenied, setAccessDenied] = useState(false); const [action, setAction] = useState(null); const [actionError, setActionError] = useState<{ kind: ActionKind; cause: unknown } | null>(null); const [message, setMessage] = useState(null); const [testEmailTo, setTestEmailTo] = useState(""); const [testEmailSubject, setTestEmailSubject] = useState("Jobbjakt SMTP test"); const [testEmailMessage, setTestEmailMessage] = useState(() => t("adminSystemDefaultTestMessage")); const mounted = useRef(false); const generation = useRef(0); const readPending = useRef(false); const actionPending = useRef(false); const denied = useRef(false); const controller = useRef(null); const draftRevision = useRef(0); const passwordRevision = useRef(0); const savedRevision = useRef(0); const bodyRef = useRef(null); const headingRef = useRef(null); const restoreFocus = useRef(null); const navigationId = useId(); const load = useCallback(async (kind?: ReadKind, afterWrite = false) => { checkSessionCookie(); if (!mounted.current || denied.current || isSessionInvalidated() || readPending.current || actionPending.current && !afterWrite) return; const context = getSessionContext(); const lifetime = generation.current; const active = () => { checkSessionCookie(); return mounted.current && lifetime === generation.current && !isSessionInvalidated() && getSessionContext() === context; }; readPending.current = true; const abort = new AbortController(); controller.current = abort; setLoading(true); const revision = draftRevision.current; await Promise.all((kind ? [kind] : Object.keys(readPaths) as ReadKind[]).map(async (part) => { try { const response = await api.get(readPaths[part], { signal: abort.signal }); if (!active() || abort.signal.aborted || denied.current) return; if (part === "status") setStatus(response.data); if (part === "email" && revision === draftRevision.current && revision === savedRevision.current) setEmailSettings(response.data); if (part === "benchmark") setBenchmarkStatus(response.data ?? null); if (part === "maintenance") setMaintenanceJobs(response.data ?? []); setReadErrors(errors => { const next = { ...errors }; delete next[part]; return next; }); } catch (cause) { if (active() && !abort.signal.aborted) { const responseStatus = (cause as { response?: { status?: number } })?.response?.status; if (responseStatus === 401 || responseStatus === 403) { denied.current = true; setAccessDenied(true); setStatus(null); setEmailSettings(null); setBenchmarkStatus(null); setMaintenanceJobs([]); setSmtpPassword(""); setClearPassword(false); } setReadErrors(errors => ({ ...errors, [part]: cause })); } } })); if (active() && !abort.signal.aborted) { readPending.current = false; setLoading(false); } }, []); useEffect(() => { mounted.current = true; generation.current += 1; void load(); return () => { mounted.current = false; generation.current += 1; controller.current?.abort(); readPending.current = false; }; }, [load]); useEffect(() => { if (loading || action || !restoreFocus.current) return; const target = restoreFocus.current; restoreFocus.current = null; if (document.activeElement !== bodyRef.current) return; if (target.isConnected && !target.matches(":disabled")) target.focus(); else headingRef.current?.focus(); }, [loading, action]); const rememberFocus = () => { const target = document.activeElement; if (target instanceof HTMLElement && bodyRef.current?.contains(target)) { restoreFocus.current = target; bodyRef.current.focus(); } }; const refresh = (kind?: ReadKind) => { if (!readPending.current && !actionPending.current) { rememberFocus(); void load(kind); } }; const updateSettings = (update: Partial) => { draftRevision.current += 1; setEmailSettings(previous => previous ? { ...previous, ...update } : previous); }; const locked = loading || action !== null || accessDenied; const runAction = async (kind: ActionKind, write: (active: () => boolean) => Promise, success: TranslationKey) => { checkSessionCookie(); if (!mounted.current || denied.current || isSessionInvalidated() || readPending.current || actionPending.current) return; const context = getSessionContext(); const lifetime = generation.current; const active = () => { checkSessionCookie(); return mounted.current && lifetime === generation.current && !isSessionInvalidated() && getSessionContext() === context; }; actionPending.current = true; rememberFocus(); setAction(kind); setActionError(null); setMessage(null); try { await write(active); if (active()) setMessage(success); } catch (cause) { if (active()) setActionError({ kind, cause }); } finally { actionPending.current = false; if (active()) setAction(null); } }; const readNotice = (kind: ReadKind) => { if (!readErrors[kind]) return null; const title = t(kind === "status" && status ? "adminSystemRefreshFailed" : readErrorKeys[kind]); const detail = getApiErrorMessage(readErrors[kind], title); return refresh(kind)}>{t("retry")} : undefined}> {title} {detail !== title ? {detail} : null} ; }; const dbTone = useMemo(() => { if (!status) return "default" as const; if (!status.database.looksConfigured || !status.database.canConnect) return "error" as const; if (status.database.warning) return "warning" as const; return "success" as const; }, [status]); const aiTone = useMemo(() => { if (!status) return "default" as const; if (!status.ai.healthy) return "error" as const; if (status.ai.probeFailures > 0 || status.ai.failures > 0 || (status.ai.ocrFailures ?? 0) > 0) return "warning" as const; return "success" as const; }, [status]); const benchmarkIndex = useMemo(() => parseBenchmarkIndex(benchmarkStatus?.indexJson), [benchmarkStatus?.indexJson]); const cvBenchmarkJob = useMemo(() => maintenanceJobs.find((job) => job.id === "cv-benchmark"), [maintenanceJobs]); const weakestEntries = useMemo(() => { if (!benchmarkIndex) return [] as CvBenchmarkEntry[]; return [...benchmarkIndex.Entries] .sort((a, b) => (a.CoverageScore + a.ConfidenceScore + a.ConsistencyScore) - (b.CoverageScore + b.ConfidenceScore + b.ConsistencyScore)) .slice(0, 6); }, [benchmarkIndex]); const benchmarkFindings = useMemo(() => { if (!benchmarkIndex) return [] as Array<{ file: string; issue: string }>; return benchmarkIndex.Entries.flatMap((entry) => { const findings: Array<{ file: string; issue: string }> = []; if (entry.ContactLocation && /(culture|education|arial|hobbies|cooperate|ag, ni|bold)/i.test(entry.ContactLocation)) { findings.push({ file: entry.FileName, issue: t("adminBenchmarkSuspiciousLocation", { value: entry.ContactLocation }) }); } if (entry.FirstEducation && entry.FirstEducation.length > 120) { findings.push({ file: entry.FileName, issue: t("adminBenchmarkEducationOvercaptured") }); } if ((entry.FirstJob ?? "").length > 120) { findings.push({ file: entry.FileName, issue: t("adminBenchmarkWorkTitleOvercaptured") }); } if ((entry.QualificationLevels ?? []).includes("Other")) { findings.push({ file: entry.FileName, issue: t("adminBenchmarkQualificationOther") }); } return findings; }).slice(0, 10); }, [benchmarkIndex, t]); const sendTestEmail = () => runAction("test", async () => { await api.post("/users/send-test-email", { toEmail: testEmailTo.trim() || null, subject: testEmailSubject.trim() || null, message: testEmailMessage.trim() || null, }); }, "adminSystemTestEmailSent"); const saveEmailSettings = async () => { if (!emailSettings) return; const revision = draftRevision.current; const secretRevision = passwordRevision.current; const snapshot = emailSettings; await runAction("save", async (active) => { const res = await api.put("/admin/system/email-settings", { enabled: snapshot.enabled, host: snapshot.host, port: Number(snapshot.port) || 587, user: snapshot.user, password: smtpPassword.trim() || null, clearPassword, from: snapshot.from, fromName: snapshot.fromName, enableSsl: snapshot.enableSsl, timeoutMs: Number(snapshot.timeoutMs) || 15000, }); if (!active()) return; if (draftRevision.current === revision) { setEmailSettings(res.data); savedRevision.current = revision; } if (passwordRevision.current === secretRevision) { setSmtpPassword(""); setClearPassword(false); } setReadErrors(errors => { const next = { ...errors }; delete next.email; return next; }); }, "adminSystemSettingsSaved"); }; const runMaintenanceJob = async (jobId: string) => { if (jobId !== "cv-benchmark" || !cvBenchmarkJob?.available || readErrors.maintenance) return; await runAction("maintenance", async () => { await api.post(`/admin/system/maintenance-jobs/${encodeURIComponent(jobId)}/runs`); }, "adminMaintenanceQueued"); }; return ( } /> setTab(value)} variant="scrollable" scrollButtons="auto" allowScrollButtonsMobile> {loading ? : null} {actionError ? {getApiErrorMessage(actionError.cause, t(actionErrorKeys[actionError.kind]))} : null} {message ? {t(message)} : null} {readNotice("status")} {status?.database.warning && tab === 0 ? {status.database.warning} : null} {status?.ai.lastError && tab === 0 ? {status.ai.lastError} : null} ); }