276 lines
8.3 KiB
TypeScript
276 lines
8.3 KiB
TypeScript
import "server-only";
|
|
|
|
import { PERMS } from "@/lib/permission-slugs";
|
|
import { authorizeHousekeeping } from "../../foundation/authorization";
|
|
import { satisfiesCapability } from "../../foundation/capability-context";
|
|
import {
|
|
anyCapability,
|
|
type CapabilityRequirement,
|
|
fail,
|
|
type HousekeepingCapabilityContext,
|
|
type HousekeepingInboxSource,
|
|
type HousekeepingWorkItem,
|
|
ok,
|
|
} from "../../foundation/contracts";
|
|
import { isSafeHousekeepingHref } from "../../foundation/housekeeping-href";
|
|
import type {
|
|
SystemHealthSnapshot,
|
|
SystemObservationRow,
|
|
} from "./queries/observability";
|
|
import { systemObservabilityQuery } from "./queries/observability";
|
|
import type { SystemAlertRow } from "./queries/operations";
|
|
import { systemOperationsQuery } from "./queries/operations";
|
|
|
|
export const SYSTEM_INBOX_SOURCE_IDS = [
|
|
"system.alerts",
|
|
"system.emulator-errors",
|
|
"system.operational-anomalies",
|
|
] as const;
|
|
|
|
export interface SystemInboxAdapters {
|
|
readonly alerts: (
|
|
context: HousekeepingCapabilityContext,
|
|
signal: AbortSignal,
|
|
) => Promise<readonly SystemAlertRow[]>;
|
|
readonly errors: (
|
|
context: HousekeepingCapabilityContext,
|
|
signal: AbortSignal,
|
|
) => Promise<readonly SystemObservationRow[]>;
|
|
readonly health: (
|
|
context: HousekeepingCapabilityContext,
|
|
signal: AbortSignal,
|
|
) => Promise<SystemHealthSnapshot>;
|
|
readonly now?: () => Date;
|
|
}
|
|
|
|
type SystemInboxLoader = (
|
|
context: HousekeepingCapabilityContext,
|
|
signal: AbortSignal,
|
|
) => Promise<readonly HousekeepingWorkItem[]>;
|
|
|
|
function createSource(
|
|
id: (typeof SYSTEM_INBOX_SOURCE_IDS)[number],
|
|
capability: CapabilityRequirement,
|
|
load: SystemInboxLoader,
|
|
): HousekeepingInboxSource {
|
|
return {
|
|
id,
|
|
owner: "system",
|
|
capability,
|
|
async getItems(context, signal) {
|
|
const authorization = authorizeHousekeeping(context, capability);
|
|
if (!authorization.ok) return authorization;
|
|
try {
|
|
const items = await load(context, signal);
|
|
return ok(
|
|
{
|
|
availability: "available" as const,
|
|
items: items
|
|
.filter(
|
|
(item) =>
|
|
isSafeHousekeepingHref(item.href) &&
|
|
satisfiesCapability(context, item.capability),
|
|
)
|
|
.slice(0, 25),
|
|
},
|
|
authorization.correlationId,
|
|
);
|
|
} catch {
|
|
return fail(
|
|
"DEPENDENCY_UNAVAILABLE",
|
|
"errors.housekeeping.dependencyUnavailable",
|
|
authorization.correlationId,
|
|
);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
function dateValue(
|
|
value: string | number | Date | null | undefined,
|
|
): Date | null {
|
|
if (value instanceof Date) {
|
|
return Number.isFinite(value.getTime()) ? value : null;
|
|
}
|
|
if (typeof value === "number") {
|
|
const millis = Math.abs(value) < 10_000_000_000 ? value * 1_000 : value;
|
|
const date = new Date(millis);
|
|
return Number.isFinite(date.getTime()) ? date : null;
|
|
}
|
|
if (typeof value !== "string" || value.trim().length === 0) return null;
|
|
if (/^\d+$/u.test(value.trim())) return dateValue(Number(value));
|
|
const date = new Date(value);
|
|
return Number.isFinite(date.getTime()) ? date : null;
|
|
}
|
|
|
|
function temporal(date: Date, now: Date) {
|
|
const ageMs = Math.max(0, now.getTime() - date.getTime());
|
|
return {
|
|
occurredAt: date.toISOString(),
|
|
ageMs,
|
|
freshness: ageMs > 86_400_000 ? ("stale" as const) : ("fresh" as const),
|
|
};
|
|
}
|
|
|
|
function alertPriority(severity: string) {
|
|
const normalized = severity.toLocaleLowerCase();
|
|
if (["critical", "fatal", "error"].includes(normalized)) {
|
|
return { severity: "critical" as const, priority: "critical" as const };
|
|
}
|
|
if (["warning", "warn"].includes(normalized)) {
|
|
return { severity: "warning" as const, priority: "high" as const };
|
|
}
|
|
return { severity: "info" as const, priority: "normal" as const };
|
|
}
|
|
|
|
export function createSystemInboxSources(
|
|
adapters: SystemInboxAdapters,
|
|
): readonly HousekeepingInboxSource[] {
|
|
const now = adapters.now ?? (() => new Date());
|
|
const alertsCapability = anyCapability(PERMS.NOTIFICATIONS_VIEW);
|
|
const devopsCapability = anyCapability(PERMS.DEVOPS_VIEW);
|
|
return [
|
|
createSource("system.alerts", alertsCapability, async (context, signal) => {
|
|
if (signal.aborted) throw new Error("aborted System alerts");
|
|
const current = now();
|
|
return (await adapters.alerts(context, signal)).flatMap((alert) => {
|
|
if (alert.isRead) return [];
|
|
const date = dateValue(alert.createdAt);
|
|
if (!date) return [];
|
|
return [
|
|
{
|
|
sourceId: "system.alerts",
|
|
itemId: String(alert.id),
|
|
deduplicationKey: `system.alert:${alert.id}`,
|
|
domain: "system" as const,
|
|
capability: alertsCapability,
|
|
...alertPriority(alert.severity),
|
|
...temporal(date, current),
|
|
state: "unread",
|
|
titleKey: "pages.housekeeping.items.systemAlert",
|
|
context: { type: alert.type, message: alert.message },
|
|
href: "/ase/system/operations/alerts" as const,
|
|
actions: [],
|
|
},
|
|
];
|
|
});
|
|
}),
|
|
createSource(
|
|
"system.emulator-errors",
|
|
devopsCapability,
|
|
async (context, signal) => {
|
|
if (signal.aborted) throw new Error("aborted emulator errors");
|
|
const current = now();
|
|
return (await adapters.errors(context, signal)).flatMap((error) => {
|
|
const date = dateValue(error.timestamp);
|
|
if (!date) return [];
|
|
return [
|
|
{
|
|
sourceId: "system.emulator-errors",
|
|
itemId: error.id,
|
|
deduplicationKey: `system.emulator-error:${error.id}`,
|
|
domain: "system" as const,
|
|
capability: devopsCapability,
|
|
severity: "critical" as const,
|
|
priority: "high" as const,
|
|
...temporal(date, current),
|
|
state: "open",
|
|
titleKey: "pages.housekeeping.items.emulatorError",
|
|
context: {
|
|
title: error.primary,
|
|
detail: error.secondary,
|
|
},
|
|
href: "/ase/system/observability/devops/errors" as const,
|
|
actions: [],
|
|
},
|
|
];
|
|
});
|
|
},
|
|
),
|
|
createSource(
|
|
"system.operational-anomalies",
|
|
devopsCapability,
|
|
async (context, signal) => {
|
|
if (signal.aborted) throw new Error("aborted operational health");
|
|
const health = await adapters.health(context, signal);
|
|
const current = now();
|
|
const anomalies = [
|
|
{
|
|
id: "database",
|
|
active: !health.dbOk,
|
|
severity: "critical" as const,
|
|
priority: "critical" as const,
|
|
context: { latencyMs: health.dbLatencyMs },
|
|
},
|
|
{
|
|
id: "redis",
|
|
active: health.redisOk === false,
|
|
severity: "warning" as const,
|
|
priority: "high" as const,
|
|
context: {},
|
|
},
|
|
{
|
|
id: "emulator",
|
|
active: !health.emulatorOk,
|
|
severity: "critical" as const,
|
|
priority: "critical" as const,
|
|
context: { onlineUsers: health.onlineUsers },
|
|
},
|
|
];
|
|
return anomalies
|
|
.filter((anomaly) => anomaly.active)
|
|
.map((anomaly) => ({
|
|
sourceId: "system.operational-anomalies",
|
|
itemId: anomaly.id,
|
|
deduplicationKey: `system.operational-anomaly:${anomaly.id}`,
|
|
domain: "system" as const,
|
|
capability: devopsCapability,
|
|
severity: anomaly.severity,
|
|
priority: anomaly.priority,
|
|
ageMs: 0,
|
|
state: "degraded",
|
|
occurredAt: current.toISOString(),
|
|
titleKey: `pages.housekeeping.items.${anomaly.id}Anomaly`,
|
|
context: anomaly.context,
|
|
href: "/ase/system/observability/devops" as const,
|
|
freshness: "fresh" as const,
|
|
actions: [],
|
|
}));
|
|
},
|
|
),
|
|
];
|
|
}
|
|
|
|
export const SYSTEM_INBOX_SOURCES = createSystemInboxSources({
|
|
async alerts(context, signal) {
|
|
if (signal.aborted) throw new Error("aborted System alerts");
|
|
const result = await systemOperationsQuery.run(context, {
|
|
routeId: "system.operations.alerts",
|
|
});
|
|
if (!result.ok || result.data.kind !== "alerts") {
|
|
throw new Error("System alerts unavailable");
|
|
}
|
|
return result.data.alerts;
|
|
},
|
|
async errors(context, signal) {
|
|
if (signal.aborted) throw new Error("aborted emulator errors");
|
|
const result = await systemObservabilityQuery.run(context, {
|
|
routeId: "system.observability.devops-errors",
|
|
});
|
|
if (!result.ok || result.data.kind !== "devops-errors") {
|
|
throw new Error("System emulator errors unavailable");
|
|
}
|
|
return result.data.errors;
|
|
},
|
|
async health(context, signal) {
|
|
if (signal.aborted) throw new Error("aborted operational health");
|
|
const result = await systemObservabilityQuery.run(context, {
|
|
routeId: "system.observability.devops",
|
|
});
|
|
if (!result.ok || result.data.kind !== "devops" || !result.data.health) {
|
|
throw new Error("System health unavailable");
|
|
}
|
|
return result.data.health;
|
|
},
|
|
});
|