feat(admin): dispatch allowlisted maintenance jobs
This commit is contained in:
@@ -133,6 +133,17 @@ describe('AdminSystemPage', () => {
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
if (url === '/admin/system/maintenance-jobs') {
|
||||
return Promise.resolve({
|
||||
data: [{
|
||||
id: 'cv-benchmark',
|
||||
title: 'CV extraction benchmark',
|
||||
description: 'Runs the CV corpus benchmark.',
|
||||
available: true,
|
||||
runsUrl: 'https://git.example.test/actions',
|
||||
}],
|
||||
} as any);
|
||||
}
|
||||
return Promise.resolve({ data: {} } as any);
|
||||
});
|
||||
mockedApi.put.mockResolvedValue({
|
||||
@@ -201,12 +212,25 @@ describe('AdminSystemPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches the allowlisted CV benchmark from the system panel', async () => {
|
||||
mockedApi.post.mockResolvedValueOnce({ data: { jobId: 'cv-benchmark', status: 'queued' } } as any);
|
||||
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /run benchmark/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedApi.post).toHaveBeenCalledWith('/admin/system/maintenance-jobs/cv-benchmark/runs');
|
||||
});
|
||||
expect(await screen.findByText(/queued in Gitea Actions/i)).toBeTruthy();
|
||||
expect(screen.getByRole('link', { name: /view runs/i }).getAttribute('href')).toBe('https://git.example.test/actions');
|
||||
});
|
||||
|
||||
it('localizes benchmark diagnostics without changing runtime values', async () => {
|
||||
window.localStorage.setItem('uiLanguage', 'nb');
|
||||
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
|
||||
|
||||
expect(await screen.findByText('Gjennomgang av CV-referansetest')).toBeTruthy();
|
||||
expect(screen.getByText(/Mistenkelig kontaktsted: san francisco, hobbies/i)).toBeTruthy();
|
||||
expect(await screen.findByText(/Mistenkelig kontaktsted: san francisco, hobbies/i)).toBeTruthy();
|
||||
expect(screen.getByText('Production')).toBeTruthy();
|
||||
expect(screen.getByText(/Modell · qwen2.5:7b/i)).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole('tab', { name: /innstillinger/i }));
|
||||
|
||||
@@ -1569,6 +1569,12 @@ export const translations = {
|
||||
adminBenchmarkConsistencyValue: "Consistency {value}",
|
||||
adminBenchmarkLatestSummary: "Latest markdown summary",
|
||||
adminBenchmarkRunScript: "Run scripts/run-cv-benchmark.sh to generate the latest corpus report and fixture candidates.",
|
||||
adminMaintenanceRunBenchmark: "Run benchmark",
|
||||
adminMaintenanceStarting: "Starting…",
|
||||
adminMaintenanceQueued: "CV benchmark queued in Gitea Actions. The report and fixture candidates will be retained with the workflow run.",
|
||||
adminMaintenanceFailed: "The maintenance job could not be started.",
|
||||
adminMaintenanceUnavailable: "Configure a scoped Gitea Actions token to run this job from the system panel.",
|
||||
adminMaintenanceViewRuns: "View runs",
|
||||
adminBenchmarkSuspiciousLocation: "Suspicious contact location: {value}",
|
||||
adminBenchmarkEducationOvercaptured: "Education qualification looks over-captured.",
|
||||
adminBenchmarkWorkTitleOvercaptured: "Work title looks over-captured.",
|
||||
@@ -3998,6 +4004,12 @@ export const translations = {
|
||||
adminBenchmarkConsistencyValue: "Konsistens {value}",
|
||||
adminBenchmarkLatestSummary: "Siste markdown-sammendrag",
|
||||
adminBenchmarkRunScript: "Kjør scripts/run-cv-benchmark.sh for å generere den nyeste korpusrapporten og kandidater til testdata.",
|
||||
adminMaintenanceRunBenchmark: "Kjør referansetest",
|
||||
adminMaintenanceStarting: "Starter…",
|
||||
adminMaintenanceQueued: "CV-referansetesten er satt i kø i Gitea Actions. Rapporten og kandidatene til testdata lagres med arbeidsflytkjøringen.",
|
||||
adminMaintenanceFailed: "Vedlikeholdsjobben kunne ikke startes.",
|
||||
adminMaintenanceUnavailable: "Konfigurer et avgrenset Gitea Actions-token for å kjøre denne jobben fra systempanelet.",
|
||||
adminMaintenanceViewRuns: "Vis kjøringer",
|
||||
adminBenchmarkSuspiciousLocation: "Mistenkelig kontaktsted: {value}",
|
||||
adminBenchmarkEducationOvercaptured: "Utdanningskvalifikasjonen ser ut til å inneholde for mye tekst.",
|
||||
adminBenchmarkWorkTitleOvercaptured: "Jobbtittelen ser ut til å inneholde for mye tekst.",
|
||||
|
||||
@@ -109,6 +109,14 @@ type CvBenchmarkStatus = {
|
||||
lastUpdatedAtUtc?: string | null;
|
||||
};
|
||||
|
||||
type MaintenanceJob = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
available: boolean;
|
||||
runsUrl?: string | null;
|
||||
};
|
||||
|
||||
type SystemStatus = {
|
||||
environment: string;
|
||||
contentRoot: string;
|
||||
@@ -211,6 +219,9 @@ export default function AdminSystemPage() {
|
||||
const [status, setStatus] = useState<SystemStatus | null>(null);
|
||||
const [emailSettings, setEmailSettings] = useState<EditableEmailSettings | null>(null);
|
||||
const [benchmarkStatus, setBenchmarkStatus] = useState<CvBenchmarkStatus | null>(null);
|
||||
const [maintenanceJobs, setMaintenanceJobs] = useState<MaintenanceJob[]>([]);
|
||||
const [runningMaintenanceJob, setRunningMaintenanceJob] = useState<string | null>(null);
|
||||
const [maintenanceMessage, setMaintenanceMessage] = useState<string | null>(null);
|
||||
const [smtpPassword, setSmtpPassword] = useState("");
|
||||
const [clearPassword, setClearPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -226,14 +237,16 @@ export default function AdminSystemPage() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [statusRes, emailRes, benchmarkRes] = await Promise.all([
|
||||
const [statusRes, emailRes, benchmarkRes, maintenanceRes] = await Promise.all([
|
||||
api.get<SystemStatus>("/admin/system"),
|
||||
api.get<EditableEmailSettings>("/admin/system/email-settings"),
|
||||
api.get<CvBenchmarkStatus>("/admin/system/cv-benchmark").catch(() => ({ data: null } as any)),
|
||||
api.get<MaintenanceJob[]>("/admin/system/maintenance-jobs").catch(() => ({ data: [] } as any)),
|
||||
]);
|
||||
setStatus(statusRes.data);
|
||||
setEmailSettings(emailRes.data);
|
||||
setBenchmarkStatus(benchmarkRes.data ?? null);
|
||||
setMaintenanceJobs(maintenanceRes.data ?? []);
|
||||
setSmtpPassword("");
|
||||
setClearPassword(false);
|
||||
} catch (e: any) {
|
||||
@@ -241,6 +254,7 @@ export default function AdminSystemPage() {
|
||||
setStatus(null);
|
||||
setEmailSettings(null);
|
||||
setBenchmarkStatus(null);
|
||||
setMaintenanceJobs([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -265,6 +279,7 @@ export default function AdminSystemPage() {
|
||||
}, [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]
|
||||
@@ -335,6 +350,20 @@ export default function AdminSystemPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const runMaintenanceJob = async (jobId: string) => {
|
||||
setRunningMaintenanceJob(jobId);
|
||||
setMaintenanceMessage(null);
|
||||
setError(null);
|
||||
try {
|
||||
await api.post(`/admin/system/maintenance-jobs/${encodeURIComponent(jobId)}/runs`);
|
||||
setMaintenanceMessage(t("adminMaintenanceQueued"));
|
||||
} catch (e: any) {
|
||||
setError(getApiErrorMessage(e, t("adminMaintenanceFailed")));
|
||||
} finally {
|
||||
setRunningMaintenanceJob(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}>
|
||||
@@ -508,7 +537,30 @@ export default function AdminSystemPage() {
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{ p: 2, borderRadius: 3 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 900, mb: 1 }}>{t("adminBenchmarkTitle")}</Typography>
|
||||
<Box sx={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 2, flexWrap: "wrap", mb: 1 }}>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 900 }}>{t("adminBenchmarkTitle")}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mt: 0.25 }}>
|
||||
{cvBenchmarkJob?.description || t("adminBenchmarkRunScript")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack direction="row" spacing={1} sx={{ flexWrap: "wrap" }}>
|
||||
{cvBenchmarkJob?.runsUrl ? (
|
||||
<Button component="a" href={cvBenchmarkJob.runsUrl} target="_blank" rel="noreferrer" variant="outlined">
|
||||
{t("adminMaintenanceViewRuns")}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!cvBenchmarkJob?.available || runningMaintenanceJob === "cv-benchmark"}
|
||||
onClick={() => void runMaintenanceJob("cv-benchmark")}
|
||||
>
|
||||
{runningMaintenanceJob === "cv-benchmark" ? t("adminMaintenanceStarting") : t("adminMaintenanceRunBenchmark")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
{!cvBenchmarkJob?.available ? <Alert severity="info" sx={{ mb: 1.5 }}>{t("adminMaintenanceUnavailable")}</Alert> : null}
|
||||
{maintenanceMessage ? <Alert severity="success" sx={{ mb: 1.5 }}>{maintenanceMessage}</Alert> : null}
|
||||
<Stack spacing={0.75}>
|
||||
<DetailRow label={t("adminBenchmarkRoot")} value={benchmarkStatus?.rootPath || "-"} />
|
||||
<DetailRow label={t("adminBenchmarkLastUpdate")} value={formatDate(benchmarkStatus?.lastUpdatedAtUtc, language === "nb" ? "nb-NO" : "en")} />
|
||||
|
||||
Reference in New Issue
Block a user