feat(cms): add recovery history and operational reliability tools
CI / check (push) Failing after 22s
CI / deploy (push) Skipped
CI / publish-container (push) Skipped

This commit is contained in:
Simo committed 2026-09-09 21:57:56 +02:00
1 parent 89526af344
commit 1ef405be0a
92 files changed
+6384 -267

No files matched your search

@@ -1,5 +1,6 @@
"use client";
import { useTranslations } from "next-intl";
import { useCallback, useRef, useState } from "react";
import { useVisiblePolling } from "@/hooks/use-visible-polling";
import { adminFetch } from "@/lib/admin-fetch";
@@ -20,9 +21,13 @@ type ExportStatus = {
export function CatalogExportClient({
canConfigure = false,
monitorOnly = false,
}: {
canConfigure?: boolean;
monitorOnly?: boolean;
}) {
const t = useTranslations("pages.admin.operationsExport");
const submitting = useRef(false);
const [status, setStatus] = useState<ExportStatus | null>(null);
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
@@ -34,37 +39,37 @@ export function CatalogExportClient({
const response = await adminFetch("/api/admin/catalog-export", {
cache: "no-store",
});
if (!response.ok) throw new Error("Unable to load export status");
if (!response.ok) throw new Error(t("loadError"));
setStatus(await response.json());
setError("");
} catch {
setError("Unable to load export status");
setError(t("loadError"));
} finally {
refreshing.current = false;
}
}, []);
}, [t]);
useVisiblePolling(refresh, 15000);
async function retry() {
if (submitting.current) return;
submitting.current = true;
setBusy(true);
try {
const response = await adminFetch("/api/admin/catalog-export", {
method: "POST",
});
if (!response.ok) throw new Error("Unable to queue export");
if (!response.ok) throw new Error(t("queueError"));
await refresh();
} catch {
setError("Unable to queue export");
setError(t("queueError"));
} finally {
setBusy(false);
submitting.current = false;
}
}
return (
<section className="space-y-3 rounded-lg border border-[var(--admin-border)] p-4">
<h2 className="text-lg font-semibold">Catalog repository export</h2>
<p className="text-sm text-muted-foreground">
Automatically publishes bundles, icons, gamedata, translations and
catalog SQL to your configured Gitea repository.
</p>
<h2 className="text-lg font-semibold">{t("title")}</h2>
<p className="text-sm text-muted-foreground">{t("hint")}</p>
{canConfigure && (
<CatalogGitConfiguration onSaved={() => void refresh()} />
)}
@@ -75,19 +80,19 @@ export function CatalogExportClient({
)}
<div role="status" className="text-sm" aria-live="polite">
{!status
? "Loading…"
? t("loading")
: !status.enabled
? "Export disabled. Configure the Gitea connection below."
? t("disabled")
: status.active
? `Waiting for ${status.active} operation(s) to finish.`
? t("active", { count: status.active })
: status.running
? "Export in progress…"
? t("running")
: status.pending
? `${status.pending} update(s) queued for export.`
: "No pending updates."}
? t("pending", { count: status.pending })
: t("idle")}
{status?.commit && (
<p>
Last published commit:{" "}
{t("lastCommit")}{" "}
<a
className="underline"
href={`${status.remote?.replace(/\.git$/, "")}/commit/${status.commit}`}
@@ -98,21 +103,30 @@ export function CatalogExportClient({
</a>
</p>
)}
{status?.finishedAt && <p>Last attempt: {status.finishedAt}</p>}
{status?.finishedAt && (
<p>{t("lastAttempt", { date: status.finishedAt })}</p>
)}
</div>
{(error || status?.error) && (
<p role="alert" className="text-sm">
{error || status?.error}
</p>
)}
<button
type="button"
disabled={!status?.enabled || busy}
onClick={retry}
className="rounded-md border border-[var(--admin-border)] px-3 py-2 text-sm disabled:opacity-50"
>
{busy ? "Queuing…" : "Export now / retry"}
</button>
{!monitorOnly && (
<button
type="button"
disabled={
!status?.enabled ||
busy ||
Boolean(status.running) ||
status.active > 0
}
onClick={retry}
className="rounded-md border border-[var(--admin-border)] px-3 py-2 text-sm disabled:opacity-50"
>
{busy ? "Queuing…" : "Export now / retry"}
</button>
)}
</section>
);
}