feat(cms): add recovery history and operational reliability tools

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

+44 -29
View File
@@ -4,6 +4,7 @@ import { getTranslations } from "next-intl/server";
import Link from "@/components/link";
import { queryErrorGroups } from "@/lib/error-group-query";
import { summarizeErrorGroup } from "@/lib/error-groups";
import { ErrorStore } from "@/lib/error-monitor";
@@ -26,8 +27,6 @@ export default async function CmsErrorsPage({
const params = await searchParams;
const query = (params.q ?? "").trim().slice(0, 200).toLowerCase();
let result: Awaited<ReturnType<ErrorStore["read"]>>;
try {
@@ -40,30 +39,13 @@ export default async function CmsErrorsPage({
);
}
const allGroups = new Map<string, typeof result.records>();
for (const record of result.records) {
const events = allGroups.get(record.fingerprint) ?? [];
events.push(record);
allGroups.set(record.fingerprint, events);
}
const releases = [...new Set(result.records.map((r) => r.release))].sort();
const queried = queryErrorGroups(result.records, params);
const groups = new Map(
[...allGroups.entries()].filter(([, events]) => {
const latest = summarizeErrorGroup(events).latest;
return (
latest &&
(!params.source || latest.source === params.source) &&
(params.status !== "open" || !latest.resolved) &&
(params.status !== "resolved" || latest.resolved) &&
(!query ||
events.some((record) =>
JSON.stringify(record).toLowerCase().includes(query),
))
);
}),
queried.map((group) => [group.fingerprint, group.events]),
);
const latestByGroup = new Map(
queried.map((group) => [group.fingerprint, group.latest]),
);
const filtered = [...groups.values()].flat();
@@ -72,14 +54,14 @@ export default async function CmsErrorsPage({
1,
Math.min(
Number(params.page) || 1,
Math.floor(Number(params.page)) || 1,
Math.max(1, Math.ceil(groups.size / 25)),
),
);
const pageHref = (next: number) =>
`?${new URLSearchParams({ q: params.q ?? "", source: params.source ?? "", status: params.status ?? "", page: String(next) })}`;
`?${new URLSearchParams({ q: params.q ?? "", source: params.source ?? "", status: params.status ?? "", release: params.release ?? "", period: params.period ?? "", sort: params.sort ?? "", page: String(next) })}`;
return (
<div className="space-y-5">
@@ -126,13 +108,46 @@ export default async function CmsErrorsPage({
<option value="open">{t("open")}</option>
<option value="resolved">{t("resolved")}</option>
</select>
<select
name="release"
aria-label={t("releaseFilter")}
defaultValue={params.release ?? ""}
className="input max-w-full"
>
<option value="">{t("allReleases")}</option>
{releases.map((release) => (
<option key={release} value={release}>
{release}
</option>
))}
</select>
<select
name="period"
aria-label={t("period")}
defaultValue={params.period ?? ""}
className="input"
>
<option value="">{t("allRetained")}</option>
<option value="24h">{t("last24h")}</option>
<option value="7d">{t("last7d")}</option>
</select>
<select
name="sort"
aria-label={t("sort")}
defaultValue={params.sort ?? ""}
className="input"
>
<option value="">{t("recentFirst")}</option>
<option value="frequency">{t("frequentFirst")}</option>
</select>
<button type="submit" className="btn btn-primary">
{t("search")}
</button>
</form>
<p className="text-sm text-muted-foreground">
{t("counts", { count: filtered.length, groups: groups.size })}{" "}
{result.truncated ? t("truncated") : ""} {t("retention")}
{result.truncated ? t("truncated") : ""} {t("retention")}{" "}
{t("filterScope")}
</p>
{groups.size === 0 ? (
<div className="admin-card">{t("empty")}</div>
@@ -144,7 +159,7 @@ export default async function CmsErrorsPage({
.map(([fingerprint, events]) => {
const metrics = summarizeErrorGroup(events);
const r = metrics.latest;
const r = latestByGroup.get(fingerprint);
if (!r) return null;