feat(housekeeping): compose operations workspace
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 17:23:55 +02:00
1 parent 5574e601bb
commit 85ad0452d3
22 files changed
+1240 -87

No files matched your search

@@ -44,6 +44,7 @@ export default async function HousekeepingPreviewRoutePage({
return handler.render({
context,
match,
surface: "preview",
...(searchParams ? { searchParams: await searchParams } : {}),
});
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,97 @@
import "server-only";
import { PERMS } from "@/lib/permission-slugs";
import { authorizeHousekeeping } from "../../foundation/authorization";
import { satisfiesCapability } from "../../foundation/capability-context";
import {
anyCapability,
fail,
type HousekeepingCapabilityContext,
type HousekeepingInboxSource,
ok,
} from "../../foundation/contracts";
const capability = anyCapability(PERMS.ADMIN_DASHBOARD);
export interface OperationsAccessSummary {
readonly accessibleDomains: number;
}
export interface OperationsInboxAdapters {
accessSummary(
context: HousekeepingCapabilityContext,
signal: AbortSignal,
): Promise<OperationsAccessSummary>;
}
export function createOperationsInboxSources(
adapters: OperationsInboxAdapters,
): readonly HousekeepingInboxSource[] {
return [
{
id: "operations.access-summary",
owner: "operations",
capability,
async getItems(context, signal) {
const authorization = authorizeHousekeeping(context, capability);
if (!authorization.ok) return authorization;
try {
const summary = await adapters.accessSummary(context, signal);
const occurredAt = new Date().toISOString();
return ok(
{
availability: "available" as const,
items:
summary.accessibleDomains === 0
? [
{
sourceId: "operations.access-summary",
itemId: "limited-access",
deduplicationKey:
"operations.access-summary:limited-access",
domain: "operations" as const,
capability,
severity: "warning" as const,
priority: "high" as const,
ageMs: 0,
state: "limited-access",
occurredAt,
titleKey:
"pages.housekeeping.items.operationsLimitedAccess",
href: "/ase" as const,
freshness: "fresh" as const,
actions: [],
},
]
: [],
},
authorization.correlationId,
);
} catch {
return fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
authorization.correlationId,
);
}
},
},
];
}
export const OPERATIONS_INBOX_SOURCES = createOperationsInboxSources({
async accessSummary(context, signal) {
if (signal.aborted) throw new Error("aborted operations access summary");
const { HOUSEKEEPING_MANIFESTS } = await import("../../manifests");
return {
accessibleDomains: HOUSEKEEPING_MANIFESTS.filter(
(domain) =>
domain.id !== "operations" &&
satisfiesCapability(context, domain.capability) &&
domain.routes.some((route) =>
satisfiesCapability(context, route.capability),
),
).length,
};
},
});
@@ -3,6 +3,10 @@ import {
anyCapability,
type HousekeepingDomainManifest,
} from "../../foundation/contracts";
import { OPERATIONS_INBOX_SOURCES } from "./inbox";
import { OPERATIONS_ROUTES } from "./routes";
import { OPERATIONS_SEARCH_PROVIDERS } from "./search";
import { OPERATIONS_WIDGETS } from "./widgets";
export const operationsManifest = {
id: "operations",
@@ -11,8 +15,8 @@ export const operationsManifest = {
iconId: "inbox",
canonicalHref: "/ase",
capability: anyCapability(PERMS.ADMIN_DASHBOARD),
routes: [],
searchProviders: [],
inboxSources: [],
widgets: [],
routes: OPERATIONS_ROUTES,
searchProviders: OPERATIONS_SEARCH_PROVIDERS,
inboxSources: OPERATIONS_INBOX_SOURCES,
widgets: OPERATIONS_WIDGETS,
} satisfies HousekeepingDomainManifest;
@@ -0,0 +1,78 @@
"use client";
import type {
HousekeepingRouteDefinition,
HousekeepingSurface,
HousekeepingWidgetDefinition,
} from "../../../foundation/contracts";
import type { HousekeepingPreferences } from "../../../foundation/preferences/schema";
import type { HousekeepingWidgetLoadResult } from "../../../foundation/registry";
import { Favorites } from "../../../foundation/shell/favorites";
import { WidgetGrid } from "../../../foundation/shell/widget-grid";
import {
type HousekeepingPreferenceSaver,
WidgetSettings,
} from "../../../foundation/shell/widget-settings";
const persistPreferences: HousekeepingPreferenceSaver = async (preferences) => {
const { saveHousekeepingPreferences } = await import(
"@/actions/housekeeping-preferences"
);
return saveHousekeepingPreferences(preferences);
};
export function OperationsPersonalization({
preferences,
routes,
widgets,
widgetResult,
surface,
savePreferences = persistPreferences,
}: {
readonly preferences: HousekeepingPreferences;
readonly routes: readonly HousekeepingRouteDefinition[];
readonly widgets: readonly HousekeepingWidgetDefinition[];
readonly widgetResult: HousekeepingWidgetLoadResult;
readonly surface: HousekeepingSurface;
readonly savePreferences?: HousekeepingPreferenceSaver;
}) {
return (
<>
<div className="grid gap-6 xl:grid-cols-2">
<Favorites
preferences={preferences}
routes={routes}
surface={surface}
savePreferences={savePreferences}
labels={{
title: "Favorites",
pin: "Pin",
unpin: "Unpin",
error: "Preferences could not be saved",
}}
/>
<WidgetSettings
preferences={preferences}
widgets={widgets}
savePreferences={savePreferences}
labels={{
title: "Widget settings",
mandatory: "Mandatory",
error: "Preferences could not be saved",
}}
/>
</div>
<WidgetGrid
preferences={preferences}
result={widgetResult}
savePreferences={savePreferences}
labels={{
title: "Widgets",
partial: "Some widgets are unavailable",
error: "Widget order could not be saved",
}}
/>
</>
);
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,47 @@
import { createElement } from "react";
import { satisfiesCapability } from "../../foundation/capability-context";
import { createHousekeepingRegistry } from "../../foundation/registry";
import { HOUSEKEEPING_MANIFESTS } from "../../manifests";
import type { HousekeepingRouteHandler } from "../../route-handlers";
import { OperationsWorkspacePage } from "./pages/workspace";
import { operationsWorkspaceQuery } from "./queries";
export async function renderOperationsWorkspacePage(
input: Parameters<HousekeepingRouteHandler["render"]>[0],
) {
const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS);
const accessibleDomains = registry.domains.filter(
(domain) =>
domain.id !== "operations" &&
satisfiesCapability(input.context, domain.capability),
);
const routes = accessibleDomains.flatMap((domain) =>
domain.routes.filter((route) =>
satisfiesCapability(input.context, route.capability),
),
);
const widgetDefinitions = registry.domains
.filter((domain) => satisfiesCapability(input.context, domain.capability))
.flatMap((domain) =>
domain.widgets.filter((widget) =>
satisfiesCapability(input.context, widget.capability),
),
);
return createElement(OperationsWorkspacePage, {
result: await operationsWorkspaceQuery(input.context),
routes,
widgetDefinitions,
surface: input.surface ?? "preview",
...(input.savePreferences
? { savePreferences: input.savePreferences }
: {}),
});
}
export const OPERATIONS_ROUTE_HANDLERS: readonly HousekeepingRouteHandler[] =
Object.freeze([
Object.freeze({
routeId: "operations.workspace",
render: renderOperationsWorkspacePage,
}),
]);
@@ -0,0 +1,91 @@
import { describe, expect, it } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import type { HousekeepingCapabilityContext } from "../../foundation/contracts";
import { operationsMigrationEntries } from "../../migration/operations";
import { OPERATIONS_INBOX_SOURCES } from "./inbox";
import { operationsManifest } from "./manifest";
import { OPERATIONS_ROUTES } from "./routes";
import { OPERATIONS_SEARCH_PROVIDERS } from "./search";
import { OPERATIONS_WIDGETS } from "./widgets";
const context: HousekeepingCapabilityContext = {
actor: { id: 42, username: "operator", rank: 7 },
isSuperAdmin: false,
has: (slug) => slug === PERMS.ADMIN_DASHBOARD,
hasAny: (...slugs) => slugs.includes(PERMS.ADMIN_DASHBOARD),
hasAll: (...slugs) => slugs.every((slug) => slug === PERMS.ADMIN_DASHBOARD),
};
describe("Operations workspace registration", () => {
it("maps the one legacy dashboard row to operations.workspace at canonical /ase", () => {
expect(OPERATIONS_ROUTES).toEqual([
expect.objectContaining({
id: "operations.workspace",
href: "/ase",
capability: { mode: "any", slugs: [PERMS.ADMIN_DASHBOARD] },
}),
]);
expect(operationsMigrationEntries).toEqual([
expect.objectContaining({
legacyPath: "/admin",
targetDomain: "operations",
targetPath: OPERATIONS_ROUTES[0]?.href,
}),
]);
});
it("publishes a real route, safe navigation search, inbox summary, and mandatory widget", async () => {
expect(operationsManifest.routes).toEqual(OPERATIONS_ROUTES);
expect(operationsManifest.searchProviders).toEqual(
OPERATIONS_SEARCH_PROVIDERS,
);
expect(operationsManifest.inboxSources).toEqual(OPERATIONS_INBOX_SOURCES);
expect(operationsManifest.widgets).toEqual(OPERATIONS_WIDGETS);
const search = await OPERATIONS_SEARCH_PROVIDERS[0]?.search(context, {
term: "operations",
limit: 25,
});
expect(search).toMatchObject({
ok: true,
data: [
expect.objectContaining({
id: "operations.workspace",
type: "navigation",
href: "/ase",
}),
],
});
const inbox = await OPERATIONS_INBOX_SOURCES[0]?.getItems(
context,
new AbortController().signal,
);
expect(inbox).toMatchObject({
ok: true,
data: {
availability: "available",
items: [
expect.objectContaining({
sourceId: "operations.access-summary",
href: "/ase",
}),
],
},
});
expect(OPERATIONS_WIDGETS[0]).toMatchObject({
id: "operations.workspace-summary",
kind: "mandatory",
owner: "operations",
});
const widget = await OPERATIONS_WIDGETS[0]?.load(
context,
new AbortController().signal,
);
expect(widget).toMatchObject({
ok: true,
data: { accessibleDomains: 0, accessibleRoutes: 0 },
});
});
});
@@ -0,0 +1,14 @@
import { PERMS } from "@/lib/permission-slugs";
import {
anyCapability,
type HousekeepingRouteDefinition,
} from "../../foundation/contracts";
export const OPERATIONS_ROUTES = [
{
id: "operations.workspace",
labelKey: "pages.housekeeping.routes.operations.workspace",
href: "/ase",
capability: anyCapability(PERMS.ADMIN_DASHBOARD),
},
] as const satisfies readonly HousekeepingRouteDefinition[];
@@ -0,0 +1,43 @@
import "server-only";
import { PERMS } from "@/lib/permission-slugs";
import { authorizeHousekeeping } from "../../foundation/authorization";
import {
anyCapability,
type HousekeepingSearchProvider,
ok,
} from "../../foundation/contracts";
const capability = anyCapability(PERMS.ADMIN_DASHBOARD);
export const OPERATIONS_SEARCH_PROVIDERS = [
{
id: "operations.workspace",
owner: "operations",
capability,
async search(context, input) {
const authorization = authorizeHousekeeping(context, capability);
if (!authorization.ok) return authorization;
const term = input.term.normalize("NFC").trim().toLocaleLowerCase();
const matches = ["operations", "workspace", "dashboard"].some((value) =>
value.includes(term),
);
return ok(
matches
? [
{
id: "operations.workspace",
domain: "operations" as const,
type: "navigation" as const,
title: "Operations workspace",
description: "Open the operational home",
href: "/ase" as const,
capability,
},
].slice(0, Math.max(0, Math.trunc(input.limit)))
: [],
authorization.correlationId,
);
},
},
] as const satisfies readonly HousekeepingSearchProvider[];
@@ -0,0 +1,82 @@
import "server-only";
import { PERMS } from "@/lib/permission-slugs";
import { authorizeHousekeeping } from "../../foundation/authorization";
import { satisfiesCapability } from "../../foundation/capability-context";
import {
anyCapability,
fail,
type HousekeepingCapabilityContext,
type HousekeepingWidgetDefinition,
ok,
} from "../../foundation/contracts";
const capability = anyCapability(PERMS.ADMIN_DASHBOARD);
export interface OperationsWorkspaceSummary {
readonly accessibleDomains: number;
readonly accessibleRoutes: number;
}
export interface OperationsWidgetAdapters {
workspaceSummary(
context: HousekeepingCapabilityContext,
signal: AbortSignal,
): Promise<OperationsWorkspaceSummary>;
}
export function createOperationsWidgets(
adapters: OperationsWidgetAdapters,
): readonly HousekeepingWidgetDefinition[] {
return [
{
id: "operations.workspace-summary",
owner: "operations",
capability,
kind: "mandatory",
async load(context, signal) {
const authorization = authorizeHousekeeping(context, capability);
if (!authorization.ok) return authorization;
try {
return ok(
await adapters.workspaceSummary(context, signal),
authorization.correlationId,
);
} catch {
return fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
authorization.correlationId,
);
}
},
},
];
}
export const OPERATIONS_WIDGETS = createOperationsWidgets({
async workspaceSummary(context, signal) {
if (signal.aborted) throw new Error("aborted operations workspace summary");
const { HOUSEKEEPING_MANIFESTS } = await import("../../manifests");
const domains = HOUSEKEEPING_MANIFESTS.filter(
(domain) =>
domain.id !== "operations" &&
satisfiesCapability(context, domain.capability),
);
return {
accessibleDomains: domains.filter((domain) =>
domain.routes.some((route) =>
satisfiesCapability(context, route.capability),
),
).length,
accessibleRoutes: domains.reduce(
(total, domain) =>
total +
domain.routes.filter((route) =>
satisfiesCapability(context, route.capability),
).length,
0,
),
};
},
});
@@ -7,6 +7,7 @@ import { describe, expect, it, vi } from "vitest";
import { CONTENT_ROUTE_IDS } from "../domains/content/routes";
import { ECONOMY_ROUTE_IDS } from "../domains/economy/routes";
import { HOTEL_ROUTE_IDS } from "../domains/hotel/routes";
import { OPERATIONS_ROUTES } from "../domains/operations/routes";
import { PEOPLE_ROUTE_IDS } from "../domains/people/routes";
import { SYSTEM_ROUTE_IDS } from "../domains/system/routes";
import { HOUSEKEEPING_MANIFESTS } from "../manifests";
@@ -27,6 +28,33 @@ const SERVER_CAPABILITY_CONTEXT =
const PERMISSIONS_ADAPTER = "src/lib/permissions";
const DOMAIN_MODULE_ROOT = "src/features/housekeeping/domains";
const approvedRuntimeImports = new Map<string, ReadonlySet<string>>([
[
"src/features/housekeeping/domains/operations/manifest.ts",
new Set([
"src/features/housekeeping/domains/operations/inbox",
"src/features/housekeeping/domains/operations/routes",
"src/features/housekeeping/domains/operations/search",
"src/features/housekeeping/domains/operations/widgets",
]),
],
[
"src/features/housekeeping/domains/operations/pages/personalization.tsx",
new Set(["src/actions/housekeeping-preferences"]),
],
[
"src/features/housekeeping/domains/operations/pages/workspace.tsx",
new Set([
"src/features/housekeeping/domains/operations/pages/personalization",
"src/features/housekeeping/domains/operations/queries",
]),
],
[
"src/features/housekeeping/domains/operations/route-handlers.ts",
new Set([
"src/features/housekeeping/domains/operations/pages/workspace",
"src/features/housekeeping/domains/operations/queries",
]),
],
[
"src/features/housekeeping/foundation/shell/command-deck.tsx",
new Set(["src/actions/housekeeping-search"]),
@@ -923,6 +951,7 @@ const approvedRuntimeImports = new Map<string, ReadonlySet<string>>([
"src/features/housekeeping/domains/content/route-handlers",
"src/features/housekeeping/domains/economy/route-handlers",
"src/features/housekeeping/domains/hotel/route-handlers",
"src/features/housekeeping/domains/operations/route-handlers",
"src/features/housekeeping/domains/people/route-handlers",
"src/features/housekeeping/domains/system/route-handlers",
]),
@@ -1523,7 +1552,7 @@ describe("housekeeping foundation completion contracts", () => {
}
});
it("creates the real six-domain registry with People, Content, Economy, Hotel, and System routes enabled", () => {
it("creates the real six-domain registry with every domain route enabled", () => {
const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS);
expect(registry.domains.map((domain) => domain.id)).toEqual([
@@ -1536,14 +1565,9 @@ describe("housekeeping foundation completion contracts", () => {
]);
expect(
registry.domains
.filter(
(domain) =>
!["people", "content", "economy", "hotel", "system"].includes(
domain.id,
),
)
.every((domain) => domain.routes.length === 0),
).toBe(true);
.find((domain) => domain.id === "operations")
?.routes.map((route) => route.id),
).toEqual(OPERATIONS_ROUTES.map((route) => route.id));
expect(
registry.domains
.find((domain) => domain.id === "people")
@@ -148,6 +148,7 @@ const routeMocks = vi.hoisted(() => {
"routes.hotel.studio.repair-icons": "HK::hotel-studio-repair-icons",
"routes.hotel.studio.sync": "HK::hotel-studio-sync",
"routes.hotel.studio.upload": "HK::hotel-studio-upload",
"routes.operations.workspace": "HK::operations-workspace",
"states.empty.title": "Localized empty title",
"states.empty.description": "Localized empty description",
};
@@ -276,6 +277,10 @@ vi.mock("@/features/housekeeping/manifests", async (importOriginal) => {
});
vi.mock("@/features/housekeeping/route-handlers", () => ({
HOUSEKEEPING_ROUTE_HANDLERS: [
{
routeId: "operations.workspace",
render: routeMocks.renderHousekeepingRoute,
},
{ routeId: "people.users", render: routeMocks.renderHousekeepingRoute },
{
routeId: "people.user-detail",
@@ -617,24 +622,30 @@ describe("/ase-next preview gate", () => {
);
});
describe("/ase-next first visible domain", () => {
describe("/ase-next operational home", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("redirects an administrator to the first permitted registered route", async () => {
it("renders the Operations workspace directly for an administrator", async () => {
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(
capabilityContext([PERMS.ADMIN_DASHBOARD, PERMS.USERS_VIEW]),
);
await expect(AdminNextPage()).rejects.toThrow(
"NEXT_REDIRECT:/ase-next/people/users",
const html = await renderRoute(AdminNextPage());
expect(html).toContain("Rendered operations.workspace");
expect(html).toContain("HK::operations-workspace");
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith(
expect.objectContaining({
match: expect.objectContaining({ routeId: "operations.workspace" }),
surface: "preview",
}),
);
expect(routeMocks.redirect).toHaveBeenCalledWith("/ase-next/people/users");
expect(routeMocks.redirect).not.toHaveBeenCalled();
expect(routeMocks.getHousekeepingCapabilityContext).toHaveBeenCalledTimes(
1,
);
expect(routeMocks.getTranslations).not.toHaveBeenCalled();
expect(routeMocks.getTranslations).toHaveBeenCalledTimes(1);
});
it("redirects a moderator with only an approved mod view capability to People", async () => {
@@ -790,6 +801,7 @@ describe("/ase-next/[domain]/[[...segments]] page", () => {
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledTimes(1);
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
context,
surface: "preview",
match: {
routeId: "people.users",
domain: "people",
@@ -815,6 +827,7 @@ describe("/ase-next/[domain]/[[...segments]] page", () => {
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
context,
surface: "preview",
match: {
routeId: "people.user-detail",
domain: "people",
@@ -839,6 +852,7 @@ describe("/ase-next/[domain]/[[...segments]] page", () => {
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
context,
surface: "preview",
match: {
routeId: "people.literal-tool",
domain: "people",
File diff suppressed because it is too large. Load diff
@@ -40,7 +40,10 @@ const {
};
});
vi.mock("react", () => ({ cache: wrapCache }));
vi.mock("react", async (importOriginal) => ({
...(await importOriginal<typeof import("react")>()),
cache: wrapCache,
}));
vi.mock("next/navigation", () => ({ notFound, redirect }));
vi.mock("next-intl/server", () => ({ getTranslations }));
vi.mock("@/lib/auth", () => ({ auth }));
@@ -74,17 +77,17 @@ function adminContext(
};
}
async function invokeRequestConsumers(expectedPageError = "NEXT_NOT_FOUND") {
async function invokeRequestConsumers() {
const shell = await AdminNextDomainLayout({
children: null,
params: Promise.resolve({ domain: "operations" }),
});
await expect(AdminNextPage()).rejects.toThrow(expectedPageError);
const page = await AdminNextPage();
const commandContext = await requireHousekeepingCapability(
anyCapability("admin.dashboard"),
);
return { commandContext, shell };
return { commandContext, page, shell };
}
describe("getHousekeepingCapabilityContext", () => {
@@ -102,11 +105,10 @@ describe("getHousekeepingCapabilityContext", () => {
});
it("isolates real shell, page, and command preflight consumers between logical requests", async () => {
const first = await invokeRequestConsumers(
"NEXT_REDIRECT:/ase-next/people/users",
);
const first = await invokeRequestConsumers();
expect(first.shell).toBeDefined();
expect(first.page).toBeDefined();
expect(first.commandContext.actor).toEqual({
id: 42,
username: "operator",
@@ -123,6 +125,7 @@ describe("getHousekeepingCapabilityContext", () => {
const second = await invokeRequestConsumers();
expect(second.shell).toBeDefined();
expect(second.page).toBeDefined();
expect(second.commandContext.actor).toEqual({
id: 84,
username: "second-operator",
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vitest";
import type { HousekeepingSurface } from "../contracts";
import type { HousekeepingNavigationDomain } from "../navigation";
import { HousekeepingShell } from "./housekeeping-shell";
@@ -147,6 +148,7 @@ const componentBoundaryRules = [
function renderShell(
activeDomainId: HousekeepingNavigationDomain["id"] = "operations",
availableDomains: readonly HousekeepingNavigationDomain[] = domains,
surface: HousekeepingSurface = "preview",
): string {
return renderToStaticMarkup(
<HousekeepingShell
@@ -154,6 +156,7 @@ function renderShell(
activeDomainId={activeDomainId}
domains={availableDomains}
labels={labels}
surface={surface}
>
<p>Deck body</p>
</HousekeepingShell>,
@@ -230,6 +233,13 @@ describe("HousekeepingShell", () => {
}
});
it("only marks the preview surface as preview", () => {
expect(renderShell()).toContain(">Preview<");
expect(renderShell("operations", domains, "canonical")).not.toContain(
">Preview<",
);
});
it("does not throw or render contextual items when a valid active domain is absent", () => {
const domainsWithoutSystem = domains.filter(
(domain) => domain.id !== "system",
@@ -54,9 +54,11 @@ export function HousekeepingShell({
</a>
<header className="flex flex-wrap items-center justify-between gap-3 border-b border-[var(--admin-border)] bg-[var(--admin-surface)] px-4 py-3">
<div className="flex items-center gap-3">
<span className="rounded-full border border-[var(--admin-border)] px-2 py-1 text-xs text-[var(--admin-accent)]">
{labels.preview}
</span>
{surface === "preview" ? (
<span className="rounded-full border border-[var(--admin-border)] px-2 py-1 text-xs text-[var(--admin-accent)]">
{labels.preview}
</span>
) : null}
<OperatorSummary actor={actor} />
</div>
<div className="flex items-center gap-2">
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { CONTENT_ROUTE_IDS } from "./domains/content/routes";
import { ECONOMY_ROUTE_IDS } from "./domains/economy/routes";
import { HOTEL_ROUTE_IDS } from "./domains/hotel/routes";
import { OPERATIONS_ROUTES } from "./domains/operations/routes";
import { PEOPLE_ROUTE_IDS } from "./domains/people/routes";
import { SYSTEM_ROUTE_IDS } from "./domains/system/routes";
import { createHousekeepingRegistry } from "./foundation/registry";
@@ -21,6 +22,7 @@ describe("housekeeping route handlers", () => {
expect(new Set(handlerIds).size).toBe(handlerIds.length);
expect([...handlerIds].sort()).toEqual([...routeIds].sort());
expect(handlerIds).toEqual([
...OPERATIONS_ROUTES.map((route) => route.id),
...PEOPLE_ROUTE_IDS,
...CONTENT_ROUTE_IDS,
...ECONOMY_ROUTE_IDS,
+9 -1
View File
@@ -2,10 +2,15 @@ import type { ReactNode } from "react";
import { CONTENT_ROUTE_HANDLERS } from "./domains/content/route-handlers";
import { ECONOMY_ROUTE_HANDLERS } from "./domains/economy/route-handlers";
import { HOTEL_ROUTE_HANDLERS } from "./domains/hotel/route-handlers";
import { OPERATIONS_ROUTE_HANDLERS } from "./domains/operations/route-handlers";
import { PEOPLE_ROUTE_HANDLERS } from "./domains/people/route-handlers";
import { SYSTEM_ROUTE_HANDLERS } from "./domains/system/route-handlers";
import type { HousekeepingCapabilityContext } from "./foundation/contracts";
import type {
HousekeepingCapabilityContext,
HousekeepingSurface,
} from "./foundation/contracts";
import type { HousekeepingRouteMatch } from "./foundation/routing/match-route";
import type { HousekeepingPreferenceSaver } from "./foundation/shell/widget-settings";
export interface HousekeepingPageInput {
readonly context: HousekeepingCapabilityContext;
@@ -13,6 +18,8 @@ export interface HousekeepingPageInput {
readonly searchParams?: Readonly<
Record<string, string | readonly string[] | undefined>
>;
readonly surface?: HousekeepingSurface;
readonly savePreferences?: HousekeepingPreferenceSaver;
}
export interface HousekeepingRouteHandler {
@@ -22,6 +29,7 @@ export interface HousekeepingRouteHandler {
export const HOUSEKEEPING_ROUTE_HANDLERS: readonly HousekeepingRouteHandler[] =
Object.freeze([
...OPERATIONS_ROUTE_HANDLERS,
...PEOPLE_ROUTE_HANDLERS,
...CONTENT_ROUTE_HANDLERS,
...ECONOMY_ROUTE_HANDLERS,