feat(catalog): persist idempotent bulk operations and retryable deliveries
CI / check (push) Successful in 3m7s
CI / deploy (push) Successful in 1m37s
CI / publish-container (push) Successful in 44s

This commit is contained in:
Simo committed 2026-09-13 18:05:03 +02:00
1 parent 76e46d295c
commit 13665e0c3c
53 files changed
+1316 -76

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import Link from "@/components/link";
import { DeliveryRetry } from "@/features/operations/retry-button";
import { listEffects } from "@/features/operations/server";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function DeliveryPage() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.DEVOPS_VIEW, session.user.rank))
redirect("/admin");
const editable = canAccess(permissions, PERMS.DEVOPS_EDIT, session.user.rank);
const t = await getTranslations("pages.admin.deliveries");
const items = await listEffects();
return (
<section className="min-w-0 space-y-4">
<h1 className="text-2xl font-semibold">{t("title")}</h1>
<p className="text-muted-foreground">{t("scope")}</p>
<Link href="/admin/devops/deliveries" className="btn btn-outline">
{t("refresh")}
</Link>
{!items.length ? (
<p className="admin-card">{t("empty")}</p>
) : (
<ul className="space-y-3">
{items.map((item) => (
<li key={item.id} className="admin-card min-w-0 space-y-2">
<div className="flex flex-wrap justify-between gap-3">
<strong>
{t(item.topic === "catalog.refresh" ? "hotel" : "export")}
</strong>
<span>{t(`states.${item.status}`)}</span>
</div>
<p className="break-all text-sm">
{item.operationId} · {t("actor", { id: item.actorId })} ·{" "}
{t("attempts", { count: item.attempts })}
</p>
{item.status === "failed" && <p>{t("retryError")}</p>}
{editable && item.status === "failed" && (
<DeliveryRetry id={item.id} />
)}
</li>
))}
</ul>
)}
</section>
);
}
+4
View File
@@ -54,6 +54,7 @@ export default async function DevOpsPage() {
const t = await getTranslations("pages.admin.devops");
const data = await getDevOpsData();
const performance = await getTranslations("pages.admin.performance");
const deliveries = await getTranslations("pages.admin.deliveries");
const installation = await getTranslations("pages.admin.installation");
return (
@@ -67,6 +68,9 @@ export default async function DevOpsPage() {
<Link href="/admin/devops/performance" className="btn btn-outline">
{performance("title")}
</Link>
<Link href="/admin/devops/deliveries" className="btn btn-outline">
{deliveries("title")}
</Link>
{/* Status cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
<Card>