feat(housekeeping): compose operational inbox
CI / check (pull_request) Successful in 37s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped

This commit is contained in:
Simo committed 2026-08-30 16:26:20 +02:00
1 parent bcb0ca977f
commit 300ac0ef95
13 files changed
+1594 -7

No files matched your search

@@ -614,6 +614,27 @@ const approvedRuntimeImports = new Map<string, ReadonlySet<string>>([
"src/features/housekeeping/domains/system/commands/system-commands.ts",
new Set(["src/features/housekeeping/domains/system/services/mutations"]),
],
[
"src/features/housekeeping/domains/system/search.ts",
new Set([
"src/features/housekeeping/domains/system/queries/observability",
"src/features/housekeeping/domains/system/queries/operations",
]),
],
[
"src/features/housekeeping/domains/system/inbox.ts",
new Set([
"src/features/housekeeping/domains/system/queries/observability",
"src/features/housekeeping/domains/system/queries/operations",
]),
],
[
"src/features/housekeeping/domains/system/widgets.ts",
new Set([
"src/features/housekeeping/domains/system/queries/observability",
"src/features/housekeeping/domains/system/queries/operations",
]),
],
[
"src/features/housekeeping/domains/system/pages/access.tsx",
new Set(["src/features/housekeeping/domains/system/queries/access"]),
@@ -703,7 +724,12 @@ const approvedRuntimeImports = new Map<string, ReadonlySet<string>>([
],
[
"src/features/housekeeping/domains/system/manifest.ts",
new Set(["src/features/housekeeping/domains/system/routes"]),
new Set([
"src/features/housekeeping/domains/system/inbox",
"src/features/housekeeping/domains/system/routes",
"src/features/housekeeping/domains/system/search",
"src/features/housekeeping/domains/system/widgets",
]),
],
[
"src/features/housekeeping/domains/system/route-handlers.ts",
@@ -0,0 +1,221 @@
import { describe, expect, it, vi } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import {
anyCapability,
fail,
type HousekeepingCapabilityContext,
type HousekeepingDomainManifest,
type HousekeepingInboxSource,
type HousekeepingWorkItem,
ok,
} from "../contracts";
import type { HousekeepingRegistry } from "../registry";
import { createHousekeepingInboxService, INBOX_POLICY } from "./inbox-service";
function context(granted: readonly string[]): HousekeepingCapabilityContext {
const permissions = new Set(granted);
return {
actor: { id: 42, username: "operator", rank: 7 },
isSuperAdmin: false,
has: (slug) => permissions.has(slug),
hasAny: (...slugs) => slugs.some((slug) => permissions.has(slug)),
hasAll: (...slugs) => slugs.every((slug) => permissions.has(slug)),
};
}
function item(
itemId: string,
options: Partial<HousekeepingWorkItem> = {},
): HousekeepingWorkItem {
return {
sourceId: "people.queue",
itemId,
deduplicationKey: `people.queue:${itemId}`,
domain: "people",
capability: anyCapability(PERMS.USERS_VIEW),
severity: "info",
priority: "normal",
ageMs: 1_000,
state: "open",
occurredAt: "2026-08-30T10:00:00.000Z",
titleKey: "pages.housekeeping.items.test",
href: "/ase/people/users",
freshness: "fresh",
actions: [],
...options,
};
}
function source(
id: string,
permission: string,
getItems: HousekeepingInboxSource["getItems"],
owner: "people" | "content" = "people",
): HousekeepingInboxSource {
return {
id,
owner,
capability: anyCapability(permission),
getItems,
};
}
function manifest(
id: "people" | "content",
permission: string,
inboxSources: readonly HousekeepingInboxSource[],
): HousekeepingDomainManifest {
return {
id,
labelKey: `pages.housekeeping.domains.${id}.title`,
descriptionKey: `pages.housekeeping.domains.${id}.description`,
iconId: id === "people" ? "users" : "file-text",
canonicalHref: `/ase/${id}`,
capability: anyCapability(permission),
routes: [],
searchProviders: [],
inboxSources,
widgets: [],
};
}
function registry(
...domains: readonly HousekeepingDomainManifest[]
): HousekeepingRegistry {
return { domains };
}
describe("Housekeeping inbox service", () => {
it("locks timeout, cap, and deduplication policy", () => {
expect(INBOX_POLICY).toEqual({
timeoutMs: 2_000,
combinedLimit: 200,
dedupe: "sourceId:itemId",
});
});
it("filters domain and source capabilities before invoking sources", async () => {
const permitted = vi.fn(async () =>
ok({ availability: "available" as const, items: [] }, "permitted"),
);
const forbiddenSource = vi.fn(async () =>
ok({ availability: "available" as const, items: [] }, "source"),
);
const forbiddenDomain = vi.fn(async () =>
ok({ availability: "available" as const, items: [] }, "domain"),
);
const service = createHousekeepingInboxService({
registry: registry(
manifest("people", PERMS.USERS_VIEW, [
source("people.ready", PERMS.USERS_VIEW, permitted),
source("people.bans", PERMS.BANS_VIEW, forbiddenSource),
]),
manifest("content", PERMS.NEWS_VIEW, [
source("content.users", PERMS.USERS_VIEW, forbiddenDomain, "content"),
]),
),
createCorrelationId: () => "inbox-capabilities",
});
await service(context([PERMS.USERS_VIEW]));
expect(permitted).toHaveBeenCalledOnce();
expect(forbiddenSource).not.toHaveBeenCalled();
expect(forbiddenDomain).not.toHaveBeenCalled();
});
it("deduplicates by source/item and orders by priority then oldest age", async () => {
const items = [
item("normal", { priority: "normal", ageMs: 900 }),
item("high-new", { priority: "high", ageMs: 100 }),
item("critical", { priority: "critical", ageMs: 10 }),
item("high-old", { priority: "high", ageMs: 1_000 }),
item("low", { priority: "low", ageMs: 5_000 }),
item("high-old", { priority: "high", ageMs: 999 }),
];
const service = createHousekeepingInboxService({
registry: registry(
manifest("people", PERMS.USERS_VIEW, [
source("people.queue", PERMS.USERS_VIEW, async () =>
ok({ availability: "available", items }, "ordered"),
),
]),
),
createCorrelationId: () => "inbox-order",
});
const response = await service(context([PERMS.USERS_VIEW]));
expect(response.items.map((entry) => entry.itemId)).toEqual([
"critical",
"high-old",
"high-new",
"normal",
"low",
]);
});
it("filters item capabilities before enforcing the 200-item cap", async () => {
const items = Array.from({ length: 230 }, (_, index) =>
item(String(index), {
capability: anyCapability(
index < 20 ? PERMS.BANS_VIEW : PERMS.USERS_VIEW,
),
}),
);
const service = createHousekeepingInboxService({
registry: registry(
manifest("people", PERMS.USERS_VIEW, [
source("people.queue", PERMS.USERS_VIEW, async () =>
ok({ availability: "available", items }, "capped"),
),
]),
),
createCorrelationId: () => "inbox-cap",
});
const response = await service(context([PERMS.USERS_VIEW]));
expect(response.items).toHaveLength(200);
expect(response.items[0]?.itemId).toBe("20");
expect(response.items.at(-1)?.itemId).toBe("219");
});
it("returns successful items with stable timeout and source errors", async () => {
let timeoutSignal: AbortSignal | undefined;
const service = createHousekeepingInboxService({
registry: registry(
manifest("people", PERMS.USERS_VIEW, [
source("people.timeout", PERMS.USERS_VIEW, (_context, signal) => {
timeoutSignal = signal;
return new Promise(() => undefined);
}),
source("people.failed", PERMS.USERS_VIEW, async () =>
fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
"failed",
),
),
source("people.ready", PERMS.USERS_VIEW, async () =>
ok(
{
availability: "available",
items: [item("ready", { sourceId: "people.ready" })],
},
"ready",
),
),
]),
),
createCorrelationId: () => "inbox-partial",
timeoutMs: 5,
});
const response = await service(context([PERMS.USERS_VIEW]));
expect(response.items.map((entry) => entry.itemId)).toEqual(["ready"]);
expect(response.errors).toEqual([
{ sourceId: "people.timeout", code: "TIMEOUT" },
{ sourceId: "people.failed", code: "DEPENDENCY_UNAVAILABLE" },
]);
expect(timeoutSignal?.aborted).toBe(true);
expect(response.correlationId).toBe("inbox-partial");
});
});
@@ -0,0 +1,206 @@
import "server-only";
import { satisfiesCapability } from "../capability-context";
import {
fail,
type HousekeepingCapabilityContext,
type HousekeepingErrorCode,
type HousekeepingInboxSource,
type HousekeepingWorkItem,
ok,
} from "../contracts";
import { createCorrelationId } from "../correlation";
import { isSafeHousekeepingHref } from "../housekeeping-href";
import {
type HousekeepingProvider,
runProvider,
} from "../providers/run-provider";
import {
createHousekeepingRegistry,
type HousekeepingRegistry,
} from "../registry";
export const INBOX_POLICY = Object.freeze({
timeoutMs: 2_000,
combinedLimit: 200,
dedupe: "sourceId:itemId",
} as const);
export interface HousekeepingInboxResponse {
readonly items: readonly HousekeepingWorkItem[];
readonly errors: readonly {
readonly sourceId: string;
readonly code: HousekeepingErrorCode;
}[];
readonly correlationId: string;
}
export interface HousekeepingInboxServiceDependencies {
readonly registry: HousekeepingRegistry;
readonly createCorrelationId?: () => string;
readonly timeoutMs?: number;
}
const priorityRank = {
critical: 0,
high: 1,
normal: 2,
low: 3,
} as const;
const severities = new Set(["info", "warning", "critical"]);
const priorities = new Set(Object.keys(priorityRank));
const freshnessValues = new Set(["fresh", "stale"]);
function hasUsableCapability(
value: unknown,
): value is HousekeepingWorkItem["capability"] {
if (value === null || typeof value !== "object") return false;
const capability = value as { mode?: unknown; slugs?: unknown };
return (
(capability.mode === "any" || capability.mode === "all") &&
Array.isArray(capability.slugs) &&
capability.slugs.length > 0 &&
capability.slugs.every((slug) => typeof slug === "string")
);
}
function isValidWorkItem(
item: HousekeepingWorkItem,
source: HousekeepingInboxSource,
context: HousekeepingCapabilityContext,
): boolean {
return (
item !== null &&
typeof item === "object" &&
item.sourceId === source.id &&
item.domain === source.owner &&
typeof item.itemId === "string" &&
item.itemId.trim().length > 0 &&
typeof item.deduplicationKey === "string" &&
item.deduplicationKey.trim().length > 0 &&
severities.has(item.severity) &&
priorities.has(item.priority) &&
Number.isFinite(item.ageMs) &&
item.ageMs >= 0 &&
typeof item.state === "string" &&
item.state.trim().length > 0 &&
typeof item.occurredAt === "string" &&
Number.isFinite(Date.parse(item.occurredAt)) &&
typeof item.titleKey === "string" &&
item.titleKey.trim().length > 0 &&
isSafeHousekeepingHref(item.href) &&
freshnessValues.has(item.freshness) &&
Array.isArray(item.actions) &&
hasUsableCapability(item.capability) &&
satisfiesCapability(context, item.capability)
);
}
function isPermittedAction(
action: HousekeepingWorkItem["actions"][number],
context: HousekeepingCapabilityContext,
): boolean {
return (
action !== null &&
typeof action === "object" &&
typeof action.id === "string" &&
action.id.trim().length > 0 &&
typeof action.labelKey === "string" &&
action.labelKey.trim().length > 0 &&
typeof action.commandId === "string" &&
action.commandId.trim().length > 0 &&
hasUsableCapability(action.capability) &&
satisfiesCapability(context, action.capability)
);
}
function sourceProvider(
source: HousekeepingInboxSource,
): HousekeepingProvider<HousekeepingWorkItem> {
return {
id: source.id,
capability: source.capability,
async run(context, signal) {
const result = await source.getItems(context, signal);
if (!result.ok) return result;
if (result.data.availability !== "available") {
return fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
result.correlationId,
);
}
return ok(result.data.items, result.correlationId);
},
};
}
export function createHousekeepingInboxService({
registry,
createCorrelationId: correlationIdFactory = createCorrelationId,
timeoutMs = INBOX_POLICY.timeoutMs,
}: HousekeepingInboxServiceDependencies) {
return async function loadInbox(
context: HousekeepingCapabilityContext,
): Promise<HousekeepingInboxResponse> {
const sources = registry.domains.flatMap((domain) => {
if (!satisfiesCapability(context, domain.capability)) return [];
return domain.inboxSources.filter((source) =>
satisfiesCapability(context, source.capability),
);
});
const outcomes = await Promise.all(
sources.map((source) =>
runProvider(sourceProvider(source), context, timeoutMs),
),
);
const sourceById = new Map(sources.map((source) => [source.id, source]));
const seen = new Set<string>();
const items: HousekeepingWorkItem[] = [];
for (const outcome of outcomes) {
const source = sourceById.get(outcome.providerId);
if (!source) continue;
for (const item of outcome.items) {
if (!isValidWorkItem(item, source, context)) continue;
const key = JSON.stringify([item.sourceId, item.itemId]);
if (seen.has(key)) continue;
seen.add(key);
items.push({
...item,
actions: item.actions.filter((action) =>
isPermittedAction(action, context),
),
});
}
}
items.sort((left, right) => {
const priority =
priorityRank[left.priority] - priorityRank[right.priority];
if (priority !== 0) return priority;
return right.ageMs - left.ageMs;
});
return {
items: items.slice(0, INBOX_POLICY.combinedLimit),
errors: outcomes.flatMap((outcome) =>
outcome.error
? [{ sourceId: outcome.providerId, code: outcome.error }]
: [],
),
correlationId: correlationIdFactory(),
};
};
}
export async function loadHousekeepingInbox(
context: HousekeepingCapabilityContext,
): Promise<HousekeepingInboxResponse> {
const { HOUSEKEEPING_MANIFESTS } = await import("../../manifests");
const service = createHousekeepingInboxService({
registry: createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS),
});
return service(context);
}
@@ -16,7 +16,10 @@ import { PEOPLE_INBOX_SOURCES } from "../domains/people/inbox";
import { PEOPLE_ROUTES } from "../domains/people/routes";
import { PEOPLE_SEARCH_PROVIDERS } from "../domains/people/search";
import { PEOPLE_WIDGETS } from "../domains/people/widgets";
import { SYSTEM_INBOX_SOURCES } from "../domains/system/inbox";
import { SYSTEM_ROUTES } from "../domains/system/routes";
import { SYSTEM_SEARCH_PROVIDERS } from "../domains/system/search";
import { SYSTEM_WIDGETS } from "../domains/system/widgets";
import { HOUSEKEEPING_MANIFESTS } from "../manifests";
import { HOUSEKEEPING_MIGRATION_MATRIX } from "../migration/matrix";
import {
@@ -392,7 +395,9 @@ describe("housekeeping registry", () => {
? ECONOMY_SEARCH_PROVIDERS
: expected.id === "hotel"
? HOTEL_SEARCH_PROVIDERS
: [],
: expected.id === "system"
? SYSTEM_SEARCH_PROVIDERS
: [],
);
expect(actual.inboxSources).toEqual(
expected.id === "people"
@@ -403,7 +408,9 @@ describe("housekeeping registry", () => {
? ECONOMY_INBOX_SOURCES
: expected.id === "hotel"
? HOTEL_INBOX_SOURCES
: [],
: expected.id === "system"
? SYSTEM_INBOX_SOURCES
: [],
);
expect(actual.widgets).toEqual(
expected.id === "people"
@@ -414,7 +421,9 @@ describe("housekeeping registry", () => {
? ECONOMY_WIDGETS
: expected.id === "hotel"
? HOTEL_WIDGETS
: [],
: expected.id === "system"
? SYSTEM_WIDGETS
: [],
);
expect(actual.capability).toEqual({ mode: "any", slugs: expected.slugs });
}
@@ -0,0 +1,128 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { anyCapability, type HousekeepingWorkItem } from "../contracts";
import {
filterOperationalInboxItems,
OperationalInbox,
projectOperationalInboxHref,
} from "./operational-inbox";
const labels = {
title: "Operational inbox",
domainFilter: "Filter by domain",
stateFilter: "Filter by state",
priorityFilter: "Filter by priority",
all: "All",
loading: "Loading operational work",
empty: "No operational work",
partial: "Some sources are unavailable",
open: "Open workflow",
};
function item(
itemId: string,
options: Partial<HousekeepingWorkItem> = {},
): HousekeepingWorkItem {
return {
sourceId: "people.queue",
itemId,
deduplicationKey: `people.queue:${itemId}`,
domain: "people",
capability: anyCapability("admin.users.view"),
severity: "warning",
priority: "high",
ageMs: 1_000,
state: "open",
occurredAt: "2026-08-30T10:00:00.000Z",
titleKey: `Item ${itemId}`,
href: "/ase/people/users",
freshness: "fresh",
actions: [],
...options,
};
}
describe("OperationalInbox", () => {
it("filters deterministically by domain, state, and priority", () => {
const items = [
item("people-open"),
item("content-draft", {
domain: "content",
state: "draft",
priority: "normal",
href: "/ase/content/editorial/articles",
}),
];
expect(
filterOperationalInboxItems(items, {
domain: "people",
state: "open",
priority: "high",
}).map((entry) => entry.itemId),
).toEqual(["people-open"]);
expect(
filterOperationalInboxItems(items, {
domain: "all",
state: "draft",
priority: "all",
}).map((entry) => entry.itemId),
).toEqual(["content-draft"]);
});
it("renders accessible filters, partial errors, and preview links", () => {
const html = renderToStaticMarkup(
<OperationalInbox
labels={labels}
surface="preview"
response={{
items: [item("ticket-1")],
errors: [{ sourceId: "content.publication", code: "TIMEOUT" }],
correlationId: "inbox-ui",
}}
/>,
);
expect(html).toContain('aria-label="Filter by domain"');
expect(html).toContain('aria-label="Filter by state"');
expect(html).toContain('aria-label="Filter by priority"');
expect(html).toContain("Some sources are unavailable");
expect(html).toContain("content.publication");
expect(html).toContain('href="/ase-next/people/users"');
expect(html).not.toContain("Dismiss");
expect(html).not.toContain("Assign");
});
it("renders explicit loading and empty states", () => {
const response = {
items: [],
errors: [],
correlationId: "inbox-empty",
} as const;
const loading = renderToStaticMarkup(
<OperationalInbox
labels={labels}
surface="preview"
response={response}
loading
/>,
);
const empty = renderToStaticMarkup(
<OperationalInbox
labels={labels}
surface="preview"
response={response}
/>,
);
expect(loading).toContain('role="status"');
expect(loading).toContain("Loading operational work");
expect(empty).toContain("No operational work");
});
it("projects canonical hrefs without changing production destinations", () => {
expect(
projectOperationalInboxHref("/ase/system/operations/alerts", "preview"),
).toBe("/ase-next/system/operations/alerts");
expect(
projectOperationalInboxHref("/ase/system/operations/alerts", "canonical"),
).toBe("/ase/system/operations/alerts");
});
});
@@ -0,0 +1,191 @@
"use client";
import { useMemo, useState } from "react";
import Link from "@/components/link";
import type {
CanonicalHousekeepingHref,
HousekeepingSurface,
HousekeepingWorkItem,
} from "../contracts";
import type { HousekeepingInboxResponse } from "../inbox/inbox-service";
import { toHousekeepingHref } from "../routing/href";
export interface OperationalInboxLabels {
readonly title: string;
readonly domainFilter: string;
readonly stateFilter: string;
readonly priorityFilter: string;
readonly all: string;
readonly loading: string;
readonly empty: string;
readonly partial: string;
readonly open: string;
}
export interface OperationalInboxFilters {
readonly domain: string;
readonly state: string;
readonly priority: string;
}
export function filterOperationalInboxItems(
items: readonly HousekeepingWorkItem[],
filters: OperationalInboxFilters,
): readonly HousekeepingWorkItem[] {
return items.filter(
(item) =>
(filters.domain === "all" || item.domain === filters.domain) &&
(filters.state === "all" || item.state === filters.state) &&
(filters.priority === "all" || item.priority === filters.priority),
);
}
export function projectOperationalInboxHref(
href: CanonicalHousekeepingHref,
surface: HousekeepingSurface,
) {
return toHousekeepingHref(href, surface);
}
function unique(values: readonly string[]): readonly string[] {
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
}
export function OperationalInbox({
response,
labels,
surface,
loading = false,
titles = {},
}: {
readonly response: HousekeepingInboxResponse;
readonly labels: OperationalInboxLabels;
readonly surface: HousekeepingSurface;
readonly loading?: boolean;
readonly titles?: Readonly<Record<string, string>>;
}) {
const [domain, setDomain] = useState("all");
const [state, setState] = useState("all");
const [priority, setPriority] = useState("all");
const domains = useMemo(
() => unique(response.items.map((item) => item.domain)),
[response.items],
);
const states = useMemo(
() => unique(response.items.map((item) => item.state)),
[response.items],
);
const priorities = useMemo(
() => unique(response.items.map((item) => item.priority)),
[response.items],
);
const items = useMemo(
() =>
filterOperationalInboxItems(response.items, {
domain,
state,
priority,
}),
[domain, priority, response.items, state],
);
return (
<section aria-labelledby="operational-inbox-title" className="space-y-4">
<h2 id="operational-inbox-title" className="text-lg font-semibold">
{labels.title}
</h2>
{loading ? <p role="status">{labels.loading}</p> : null}
{!loading ? (
<>
<div className="grid gap-3 sm:grid-cols-3">
<select
aria-label={labels.domainFilter}
value={domain}
onChange={(event) => setDomain(event.target.value)}
className="rounded-md border border-[var(--admin-border)] bg-[var(--admin-surface)] px-3 py-2"
>
<option value="all">{labels.all}</option>
{domains.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
<select
aria-label={labels.stateFilter}
value={state}
onChange={(event) => setState(event.target.value)}
className="rounded-md border border-[var(--admin-border)] bg-[var(--admin-surface)] px-3 py-2"
>
<option value="all">{labels.all}</option>
{states.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
<select
aria-label={labels.priorityFilter}
value={priority}
onChange={(event) => setPriority(event.target.value)}
className="rounded-md border border-[var(--admin-border)] bg-[var(--admin-surface)] px-3 py-2"
>
<option value="all">{labels.all}</option>
{priorities.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
</div>
{response.errors.length > 0 ? (
<div
role="status"
className="rounded-md border border-[var(--admin-warning-border)] bg-[var(--admin-warning-subtle)] p-3"
>
<p className="text-[var(--admin-warning)]">{labels.partial}</p>
<ul className="mt-2 list-disc pl-5 text-sm">
{response.errors.map((error) => (
<li key={error.sourceId}>
{error.sourceId}: {error.code}
</li>
))}
</ul>
</div>
) : null}
{items.length === 0 ? <p>{labels.empty}</p> : null}
<ul className="space-y-3">
{items.map((item) => (
<li
key={`${item.sourceId}:${item.itemId}`}
className="rounded-md border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4"
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<p className="font-medium">
{titles[item.titleKey] ?? item.titleKey}
</p>
<p className="text-sm text-[var(--admin-text-muted)]">
{item.domain} · {item.state} · {item.priority}
</p>
<time dateTime={item.occurredAt} className="text-xs">
{item.occurredAt}
</time>
</div>
<Link
href={projectOperationalInboxHref(item.href, surface)}
className="rounded-md border border-[var(--admin-border)] px-3 py-2 text-sm"
>
{labels.open}
</Link>
</div>
</li>
))}
</ul>
</>
) : null}
</section>
);
}