Files
EpicNext-Cms/src/components/admin/operations-inbox.tsx
T
Simo 1ef405be0a
CI / check (push) Failing after 22s
CI / deploy (push) Skipped
CI / publish-container (push) Skipped
feat(cms): add recovery history and operational reliability tools
2026-09-09 21:57:56 +02:00

153 lines
3.9 KiB
TypeScript

import { count, eq, ne } from "drizzle-orm";
import { getTranslations } from "next-intl/server";
import Link from "@/components/link";
import { collectOperations } from "@/lib/admin/operations-inbox";
import {
db,
WebsiteArticles,
WebsiteHelpCenterTickets,
WebsiteTicket,
} from "@/lib/db";
import { ErrorStore } from "@/lib/error-monitor";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { ImportJobStore } from "@/lib/services/furni-job-store";
export async function OperationsInbox() {
const { session, permissions } = await getAdminContext();
const allowed = (permission: string) =>
canAccess(permissions, permission, session.user.rank);
const t = await getTranslations("pages.admin.operations");
const result = await collectOperations([
{
source: "errors",
allowed: allowed(PERMS.DEVOPS_VIEW),
load: async () => {
const { records } = await new ErrorStore().read();
const total = new Set(
records
.filter((record) => !record.resolved)
.map((record) => record.fingerprint),
).size;
return [
{
id: "unresolved",
source: "errors",
count: total,
severity: 3,
href: "/admin/devops/cms-errors?status=open",
},
];
},
},
{
source: "imports",
allowed: allowed(PERMS.ASSETS_IMPORT),
load: async () => {
const jobs = await new ImportJobStore().list({
userId: Number(session.user.id),
limit: 30,
});
const total = jobs.filter(
(job) =>
job.state === "interrupted" ||
job.items.some((item) => item.state === "failed"),
).length;
return [
{
id: "attention",
source: "imports",
count: total,
severity: 2,
href: "/admin/operations",
},
];
},
},
{
source: "tickets",
allowed: allowed(PERMS.TICKETS_VIEW),
load: async () => {
const [rows, helpRows] = await Promise.all([
db
.select({ total: count() })
.from(WebsiteTicket)
.where(ne(WebsiteTicket.status, "closed")),
db
.select({ total: count() })
.from(WebsiteHelpCenterTickets)
.where(eq(WebsiteHelpCenterTickets.open, true)),
]);
return [
{
id: "open",
source: "tickets",
count:
Number(rows[0]?.total ?? 0) + Number(helpRows[0]?.total ?? 0),
severity: 1,
href: "/admin/tickets",
},
];
},
},
{
source: "publications",
allowed: allowed(PERMS.NEWS_VIEW),
load: async () => {
const rows = await db
.select({ total: count() })
.from(WebsiteArticles)
.where(eq(WebsiteArticles.status, "draft"));
return [
{
id: "drafts",
source: "publications",
count: Number(rows[0]?.total ?? 0),
severity: 0,
href: "/admin/articles?status=draft",
},
];
},
},
]);
return (
<section
className="admin-card mb-6 space-y-3"
aria-labelledby="operations-title"
>
<h2 id="operations-title" className="text-lg font-semibold">
{t("title")}
</h2>
<p className="text-sm text-[var(--admin-text-muted)]">{t("hint")}</p>
{result.items.length === 0 && result.unavailable.length === 0 ? (
<p>{t("empty")}</p>
) : null}
<div className="grid gap-3 sm:grid-cols-2">
{result.items.map((item) => (
<Link
key={`${item.source}:${item.id}`}
href={item.href}
className="rounded-lg border border-[var(--admin-border)] p-3 focus-visible:outline-2"
>
<span>{t(item.source)}</span>
<strong className="float-right">{item.count}</strong>
</Link>
))}
</div>
{result.unavailable.map((source) => (
<p
key={source}
role="status"
className="text-sm text-[var(--admin-warning)]"
>
{t("unavailable", { source: t(source) })}
</p>
))}
{allowed(PERMS.DEVOPS_VIEW) ? (
<Link href="/admin/devops/installation" className="btn btn-outline">
{t("installation")}
</Link>
) : null}
</section>
);
}