feat(cms): add recovery history and operational reliability tools
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user