feat(admin): dispatch allowlisted maintenance jobs
CI and Deploy / test (push) Successful in 7m8s
CI and Deploy / deploy (push) Successful in 1m42s

This commit is contained in:
cesnimda
2026-09-01 16:53:54 +02:00
parent 3ec9ac1dad
commit 3b055b01f5
11 changed files with 328 additions and 8 deletions
+54 -2
View File
@@ -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")} />