|
|
|
@@ -166,8 +166,8 @@ function displayMetadata(value?: string | null) {
|
|
|
|
|
return value && value.trim().length > 0 ? value : "-";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDate(value?: string | null) {
|
|
|
|
|
return value ? new Date(value).toLocaleString() : "-";
|
|
|
|
|
function formatDate(value?: string | null, locale?: string) {
|
|
|
|
|
return value ? new Date(value).toLocaleString(locale) : "-";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatPercent(value?: number | null) {
|
|
|
|
@@ -206,7 +206,7 @@ function DetailRow({ label, value }: { label: string; value: React.ReactNode })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function AdminSystemPage() {
|
|
|
|
|
const { t } = useI18n();
|
|
|
|
|
const { language, t } = useI18n();
|
|
|
|
|
const [tab, setTab] = useState(0);
|
|
|
|
|
const [status, setStatus] = useState<SystemStatus | null>(null);
|
|
|
|
|
const [emailSettings, setEmailSettings] = useState<EditableEmailSettings | null>(null);
|
|
|
|
@@ -237,7 +237,7 @@ export default function AdminSystemPage() {
|
|
|
|
|
setSmtpPassword("");
|
|
|
|
|
setClearPassword(false);
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
setError(getApiErrorMessage(e, "Failed to load system status."));
|
|
|
|
|
setError(getApiErrorMessage(e, t("adminSystemLoadFailed")));
|
|
|
|
|
setStatus(null);
|
|
|
|
|
setEmailSettings(null);
|
|
|
|
|
setBenchmarkStatus(null);
|
|
|
|
@@ -277,20 +277,20 @@ export default function AdminSystemPage() {
|
|
|
|
|
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: `Suspicious contact location: ${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: "Education qualification looks over-captured." });
|
|
|
|
|
findings.push({ file: entry.FileName, issue: t("adminBenchmarkEducationOvercaptured") });
|
|
|
|
|
}
|
|
|
|
|
if ((entry.FirstJob ?? "").length > 120) {
|
|
|
|
|
findings.push({ file: entry.FileName, issue: "Work title looks over-captured." });
|
|
|
|
|
findings.push({ file: entry.FileName, issue: t("adminBenchmarkWorkTitleOvercaptured") });
|
|
|
|
|
}
|
|
|
|
|
if ((entry.QualificationLevels ?? []).includes("Other")) {
|
|
|
|
|
findings.push({ file: entry.FileName, issue: "Qualification level fell back to Other." });
|
|
|
|
|
findings.push({ file: entry.FileName, issue: t("adminBenchmarkQualificationOther") });
|
|
|
|
|
}
|
|
|
|
|
return findings;
|
|
|
|
|
}).slice(0, 10);
|
|
|
|
|
}, [benchmarkIndex]);
|
|
|
|
|
}, [benchmarkIndex, language]);
|
|
|
|
|
|
|
|
|
|
const sendTestEmail = async () => {
|
|
|
|
|
setSendingTestEmail(true);
|
|
|
|
@@ -302,7 +302,7 @@ export default function AdminSystemPage() {
|
|
|
|
|
});
|
|
|
|
|
setError(null);
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
setError(getApiErrorMessage(e, "Failed to send test email."));
|
|
|
|
|
setError(getApiErrorMessage(e, t("adminSystemTestEmailFailed")));
|
|
|
|
|
} finally {
|
|
|
|
|
setSendingTestEmail(false);
|
|
|
|
|
}
|
|
|
|
@@ -329,7 +329,7 @@ export default function AdminSystemPage() {
|
|
|
|
|
setClearPassword(false);
|
|
|
|
|
await load();
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
setError(getApiErrorMessage(e, "Failed to save email settings."));
|
|
|
|
|
setError(getApiErrorMessage(e, t("adminSystemEmailSaveFailed")));
|
|
|
|
|
} finally {
|
|
|
|
|
setSavingSettings(false);
|
|
|
|
|
}
|
|
|
|
@@ -352,7 +352,7 @@ export default function AdminSystemPage() {
|
|
|
|
|
await api.post("/admin/system/ai/probe");
|
|
|
|
|
await load();
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
setError(getApiErrorMessage(e, "Failed to run AI service probe."));
|
|
|
|
|
setError(getApiErrorMessage(e, t("adminSystemProbeFailed")));
|
|
|
|
|
} finally {
|
|
|
|
|
setRunningProbe(false);
|
|
|
|
|
}
|
|
|
|
@@ -382,12 +382,12 @@ export default function AdminSystemPage() {
|
|
|
|
|
<SummaryCard
|
|
|
|
|
title={t("adminSystemEnvironment")}
|
|
|
|
|
value={status?.environment ?? "-"}
|
|
|
|
|
subtitle={`Version ${displayMetadata(status?.version)} · Commit ${displayMetadata(status?.commitSha)}`}
|
|
|
|
|
subtitle={t("adminSystemVersionCommit", { version: displayMetadata(status?.version), commit: displayMetadata(status?.commitSha) })}
|
|
|
|
|
/>
|
|
|
|
|
<SummaryCard
|
|
|
|
|
title={t("adminSystemDatabase")}
|
|
|
|
|
value={status ? (status.database.canConnect ? t("adminSystemConnected") : t("adminSystemOffline")) : "-"}
|
|
|
|
|
subtitle={status ? `${status.database.provider} · ${status.database.target || "No target"}` : "-"}
|
|
|
|
|
subtitle={status ? `${status.database.provider} · ${status.database.target || t("adminSystemNoTarget")}` : "-"}
|
|
|
|
|
tone={dbTone}
|
|
|
|
|
/>
|
|
|
|
|
<SummaryCard
|
|
|
|
@@ -400,9 +400,9 @@ export default function AdminSystemPage() {
|
|
|
|
|
title={t("adminSystemSummarizer")}
|
|
|
|
|
value={status?.ai.healthy ? t("adminSystemHealthy") : t("adminSystemOffline")}
|
|
|
|
|
subtitle={status?.ai.probeLatencyMs != null
|
|
|
|
|
? `${status.ai.probeLatencyMs} ms probe · ${status.ai.device || "unknown device"}`
|
|
|
|
|
? t("adminSystemProbeDevice", { latency: status.ai.probeLatencyMs, device: status.ai.device || t("adminSystemUnknownDevice") })
|
|
|
|
|
: status?.ai.healthLatencyMs != null
|
|
|
|
|
? `${status.ai.healthLatencyMs} ms health · ${status.ai.device || "unknown device"}`
|
|
|
|
|
? t("adminSystemHealthDevice", { latency: status.ai.healthLatencyMs, device: status.ai.device || t("adminSystemUnknownDevice") })
|
|
|
|
|
: t("adminSystemNoLatencyData")}
|
|
|
|
|
tone={aiTone}
|
|
|
|
|
/>
|
|
|
|
@@ -468,13 +468,13 @@ export default function AdminSystemPage() {
|
|
|
|
|
<DetailRow label={t("adminSystemOllamaReachable")} value={status?.ai.ollamaReachable ? t("yes") : t("noWord")} />
|
|
|
|
|
<DetailRow label={t("adminSystemOllamaModel")} value={status?.ai.ollamaModel || "-"} />
|
|
|
|
|
<DetailRow label={t("adminSystemOllamaModelAvailable")} value={status?.ai.ollamaModelAvailable ? t("yes") : t("noWord")} />
|
|
|
|
|
<DetailRow label="Ollama version" value={status?.ai.ollamaVersion || "-"} />
|
|
|
|
|
<DetailRow label="Loaded models" value={status?.ai.ollamaLoadedCount ?? 0} />
|
|
|
|
|
<DetailRow label={t("adminSystemOllamaVersion")} value={status?.ai.ollamaVersion || "-"} />
|
|
|
|
|
<DetailRow label={t("adminSystemLoadedModels")} value={status?.ai.ollamaLoadedCount ?? 0} />
|
|
|
|
|
<DetailRow label={t("adminSystemHealthLatency")} value={status?.ai.healthLatencyMs != null ? `${status.ai.healthLatencyMs} ms` : "-"} />
|
|
|
|
|
<DetailRow label={t("adminSystemProbeLatency")} value={status?.ai.probeLatencyMs != null ? `${status.ai.probeLatencyMs} ms` : "-"} />
|
|
|
|
|
<DetailRow label={t("adminSystemLastProbe")} value={formatDate(status?.ai.lastProbeAt)} />
|
|
|
|
|
<DetailRow label={t("adminSystemLastSuccessfulProbe")} value={formatDate(status?.ai.lastProbeSuccessAt)} />
|
|
|
|
|
<DetailRow label={t("adminSystemLastSummarizationSuccess")} value={formatDate(status?.ai.lastSuccessAt)} />
|
|
|
|
|
<DetailRow label={t("adminSystemLastProbe")} value={formatDate(status?.ai.lastProbeAt, language === "nb" ? "nb-NO" : "en")} />
|
|
|
|
|
<DetailRow label={t("adminSystemLastSuccessfulProbe")} value={formatDate(status?.ai.lastProbeSuccessAt, language === "nb" ? "nb-NO" : "en")} />
|
|
|
|
|
<DetailRow label={t("adminSystemLastSummarizationSuccess")} value={formatDate(status?.ai.lastSuccessAt, language === "nb" ? "nb-NO" : "en")} />
|
|
|
|
|
</Stack>
|
|
|
|
|
</Paper>
|
|
|
|
|
</Box>
|
|
|
|
@@ -497,57 +497,57 @@ export default function AdminSystemPage() {
|
|
|
|
|
<Chip label={status?.auth.googleConfigured ? t("adminSystemGoogleReady") : t("adminSystemGoogleOff")} variant="outlined" size="small" />
|
|
|
|
|
<Chip label={status?.auth.gmailConfigured ? t("adminSystemGmailReady") : t("adminSystemGmailIncomplete")} variant="outlined" size="small" />
|
|
|
|
|
<Chip label={status?.ai.gpuAvailable ? t("adminSystemGpuVisible") : t("adminSystemCpuMode")} color={status?.ai.gpuAvailable ? "success" : "default"} size="small" />
|
|
|
|
|
<Chip label={status?.ai.ocrAvailable ? `OCR ${status.ai.ocrLanguages || "enabled"}` : t("adminSystemOcrUnavailable")} variant="outlined" size="small" />
|
|
|
|
|
<Chip label={status?.ai.ocrAvailable ? t("adminSystemOcrEnabled", { languages: status.ai.ocrLanguages || t("adminSystemEnabled") }) : t("adminSystemOcrUnavailable")} variant="outlined" size="small" />
|
|
|
|
|
{(status?.ai.ollamaInstalledModels ?? []).slice(0, 4).map((model) => (
|
|
|
|
|
<Chip key={model} label={`Model · ${model}`} variant="outlined" size="small" />
|
|
|
|
|
<Chip key={model} label={t("adminSystemModelName", { model })} variant="outlined" size="small" />
|
|
|
|
|
))}
|
|
|
|
|
{(status?.ai.ollamaLoadedModels ?? []).slice(0, 3).map((model) => (
|
|
|
|
|
<Chip key={`loaded-${model}`} label={`Loaded · ${model}`} color="primary" variant="outlined" size="small" />
|
|
|
|
|
<Chip key={`loaded-${model}`} label={t("adminSystemLoadedModel", { model })} color="primary" variant="outlined" size="small" />
|
|
|
|
|
))}
|
|
|
|
|
</Box>
|
|
|
|
|
</Paper>
|
|
|
|
|
|
|
|
|
|
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
|
|
|
|
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>CV benchmark review</Typography>
|
|
|
|
|
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkTitle")}</Typography>
|
|
|
|
|
<Stack spacing={0.75}>
|
|
|
|
|
<DetailRow label="Benchmark root" value={benchmarkStatus?.rootPath || "-"} />
|
|
|
|
|
<DetailRow label="Last benchmark update" value={formatDate(benchmarkStatus?.lastUpdatedAtUtc)} />
|
|
|
|
|
<DetailRow label="Corpus root" value={benchmarkIndex?.CorpusRoot || "-"} />
|
|
|
|
|
<DetailRow label={t("adminBenchmarkRoot")} value={benchmarkStatus?.rootPath || "-"} />
|
|
|
|
|
<DetailRow label={t("adminBenchmarkLastUpdate")} value={formatDate(benchmarkStatus?.lastUpdatedAtUtc, language === "nb" ? "nb-NO" : "en")} />
|
|
|
|
|
<DetailRow label={t("adminBenchmarkCorpusRoot")} value={benchmarkIndex?.CorpusRoot || "-"} />
|
|
|
|
|
</Stack>
|
|
|
|
|
|
|
|
|
|
{benchmarkIndex ? (
|
|
|
|
|
<>
|
|
|
|
|
<Box sx={{ mt: 2, display: "grid", gridTemplateColumns: { xs: "1fr 1fr", md: "repeat(5, 1fr)" }, gap: 1.25 }}>
|
|
|
|
|
<SummaryCard title="Files" value={String(benchmarkIndex.TotalFiles)} subtitle="Corpus inputs" />
|
|
|
|
|
<SummaryCard title="Coverage" value={formatPercent(benchmarkIndex.AverageCoverage)} subtitle="Structured field coverage" tone={benchmarkTone(benchmarkIndex.AverageCoverage)} />
|
|
|
|
|
<SummaryCard title="Confidence" value={formatPercent(benchmarkIndex.AverageConfidence)} subtitle="Field metadata confidence" tone={benchmarkTone(benchmarkIndex.AverageConfidence)} />
|
|
|
|
|
<SummaryCard title="Consistency" value={formatPercent(benchmarkIndex.AverageConsistency)} subtitle="Normalization consistency" tone={benchmarkTone(benchmarkIndex.AverageConsistency)} />
|
|
|
|
|
<SummaryCard title="Missing approved" value={String(benchmarkIndex.MissingApprovedFixtures)} subtitle="Needs fixture review" tone={benchmarkIndex.MissingApprovedFixtures > 0 ? "warning" : "success"} />
|
|
|
|
|
<SummaryCard title={t("adminBenchmarkFiles")} value={String(benchmarkIndex.TotalFiles)} subtitle={t("adminBenchmarkCorpusInputs")} />
|
|
|
|
|
<SummaryCard title={t("adminBenchmarkCoverage")} value={formatPercent(benchmarkIndex.AverageCoverage)} subtitle={t("adminBenchmarkCoverageHelp")} tone={benchmarkTone(benchmarkIndex.AverageCoverage)} />
|
|
|
|
|
<SummaryCard title={t("adminBenchmarkConfidence")} value={formatPercent(benchmarkIndex.AverageConfidence)} subtitle={t("adminBenchmarkConfidenceHelp")} tone={benchmarkTone(benchmarkIndex.AverageConfidence)} />
|
|
|
|
|
<SummaryCard title={t("adminBenchmarkConsistency")} value={formatPercent(benchmarkIndex.AverageConsistency)} subtitle={t("adminBenchmarkConsistencyHelp")} tone={benchmarkTone(benchmarkIndex.AverageConsistency)} />
|
|
|
|
|
<SummaryCard title={t("adminBenchmarkMissingApproved")} value={String(benchmarkIndex.MissingApprovedFixtures)} subtitle={t("adminBenchmarkNeedsReview")} tone={benchmarkIndex.MissingApprovedFixtures > 0 ? "warning" : "success"} />
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
<Box sx={{ mt: 2.5, display: "grid", gridTemplateColumns: { xs: "1fr", xl: "1.1fr 0.9fr" }, gap: 2 }}>
|
|
|
|
|
<Paper variant="outlined" sx={{ p: 1.5, borderRadius: 3 }}>
|
|
|
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>Top parser findings</Typography>
|
|
|
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkTopFindings")}</Typography>
|
|
|
|
|
<Stack spacing={1}>
|
|
|
|
|
{benchmarkFindings.length > 0 ? benchmarkFindings.map((finding) => (
|
|
|
|
|
<Box key={`${finding.file}:${finding.issue}`} sx={{ p: 1.25, borderRadius: 2, backgroundColor: "background.default", border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
|
|
|
|
|
<Typography variant="caption" sx={{ color: "text.secondary" }}>{finding.file}</Typography>
|
|
|
|
|
<Typography variant="body2">{finding.issue}</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
)) : <Typography variant="body2" sx={{ color: "text.secondary" }}>No standout benchmark anomalies in the current run.</Typography>}
|
|
|
|
|
)) : <Typography variant="body2" sx={{ color: "text.secondary" }}>{t("adminBenchmarkNoAnomalies")}</Typography>}
|
|
|
|
|
</Stack>
|
|
|
|
|
</Paper>
|
|
|
|
|
|
|
|
|
|
<Paper variant="outlined" sx={{ p: 1.5, borderRadius: 3 }}>
|
|
|
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>Weakest files in current run</Typography>
|
|
|
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkWeakest")}</Typography>
|
|
|
|
|
<Stack spacing={1}>
|
|
|
|
|
{weakestEntries.map((entry) => (
|
|
|
|
|
<Box key={entry.Slug} sx={{ p: 1.25, borderRadius: 2, backgroundColor: "background.default", border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
|
|
|
|
|
<Typography variant="body2" sx={{ fontWeight: 800 }}>{entry.FileName}</Typography>
|
|
|
|
|
<Box sx={{ display: "flex", gap: 0.75, flexWrap: "wrap", mt: 0.75 }}>
|
|
|
|
|
<Chip size="small" label={`Coverage ${formatPercent(entry.CoverageScore)}`} color={benchmarkTone(entry.CoverageScore)} />
|
|
|
|
|
<Chip size="small" label={`Confidence ${formatPercent(entry.ConfidenceScore)}`} color={benchmarkTone(entry.ConfidenceScore)} />
|
|
|
|
|
<Chip size="small" label={`Consistency ${formatPercent(entry.ConsistencyScore)}`} color={benchmarkTone(entry.ConsistencyScore)} />
|
|
|
|
|
<Chip size="small" label={t("adminBenchmarkCoverageValue", { value: formatPercent(entry.CoverageScore) })} color={benchmarkTone(entry.CoverageScore)} />
|
|
|
|
|
<Chip size="small" label={t("adminBenchmarkConfidenceValue", { value: formatPercent(entry.ConfidenceScore) })} color={benchmarkTone(entry.ConfidenceScore)} />
|
|
|
|
|
<Chip size="small" label={t("adminBenchmarkConsistencyValue", { value: formatPercent(entry.ConsistencyScore) })} color={benchmarkTone(entry.ConsistencyScore)} />
|
|
|
|
|
</Box>
|
|
|
|
|
<Typography variant="caption" sx={{ display: "block", color: "text.secondary", mt: 0.75 }}>{entry.DiffSummary || "-"}</Typography>
|
|
|
|
|
</Box>
|
|
|
|
@@ -557,7 +557,7 @@ export default function AdminSystemPage() {
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
<Box sx={{ mt: 2, p: 1.5, borderRadius: 2, backgroundColor: "background.default", border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)", maxHeight: 280, overflow: "auto" }}>
|
|
|
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 1 }}>Latest markdown summary</Typography>
|
|
|
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 800, mb: 1 }}>{t("adminBenchmarkLatestSummary")}</Typography>
|
|
|
|
|
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap", fontFamily: "ui-monospace, SFMono-Regular, monospace" }}>
|
|
|
|
|
{benchmarkStatus?.reportMarkdown || "-"}
|
|
|
|
|
</Typography>
|
|
|
|
@@ -566,7 +566,7 @@ export default function AdminSystemPage() {
|
|
|
|
|
) : (
|
|
|
|
|
<Box sx={{ mt: 1.5, p: 1.5, borderRadius: 2, backgroundColor: "background.default", border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
|
|
|
|
|
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap", fontFamily: "ui-monospace, SFMono-Regular, monospace" }}>
|
|
|
|
|
{benchmarkStatus?.reportMarkdown || "Run scripts/run-cv-benchmark.sh to generate the latest corpus report and fixture candidates."}
|
|
|
|
|
{benchmarkStatus?.reportMarkdown || t("adminBenchmarkRunScript")}
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
)}
|
|
|
|
|