feat: add housekeeping domain registry

This commit is contained in:
Simo committed 2026-08-25 19:39:08 +02:00
1 parent 7edca410fa
commit 2d5f03048a
14 files changed
+694

No files matched your search

@@ -0,0 +1,22 @@
import { PERMS } from "@/lib/permission-slugs";
import {
anyCapability,
type HousekeepingDomainManifest,
} from "../../foundation/contracts";
export const contentManifest = {
id: "content",
labelKey: "pages.housekeeping.domains.content.title",
descriptionKey: "pages.housekeeping.domains.content.description",
iconId: "file-text",
previewHref: "/admin-next/content",
capability: anyCapability(
PERMS.NEWS_VIEW,
PERMS.PAGES_VIEW,
PERMS.BANNERS_VIEW,
PERMS.EVENTS_VIEW,
PERMS.POLLS_VIEW,
PERMS.PREFIXES_VIEW,
),
routes: [],
} satisfies HousekeepingDomainManifest;
@@ -0,0 +1,15 @@
import { PERMS } from "@/lib/permission-slugs";
import {
anyCapability,
type HousekeepingDomainManifest,
} from "../../foundation/contracts";
export const economyManifest = {
id: "economy",
labelKey: "pages.housekeeping.domains.economy.title",
descriptionKey: "pages.housekeeping.domains.economy.description",
iconId: "gem",
previewHref: "/admin-next/economy",
capability: anyCapability(PERMS.CATALOG_VIEW, PERMS.SHOP_VIEW),
routes: [],
} satisfies HousekeepingDomainManifest;
@@ -0,0 +1,19 @@
import { PERMS } from "@/lib/permission-slugs";
import {
anyCapability,
type HousekeepingDomainManifest,
} from "../../foundation/contracts";
export const hotelManifest = {
id: "hotel",
labelKey: "pages.housekeeping.domains.hotel.title",
descriptionKey: "pages.housekeeping.domains.hotel.description",
iconId: "hotel",
previewHref: "/admin-next/hotel",
capability: anyCapability(
PERMS.ROOMS_VIEW,
PERMS.RADIO_VIEW,
PERMS.ASSETS_IMPORT,
),
routes: [],
} satisfies HousekeepingDomainManifest;
@@ -0,0 +1,15 @@
import { PERMS } from "@/lib/permission-slugs";
import {
anyCapability,
type HousekeepingDomainManifest,
} from "../../foundation/contracts";
export const operationsManifest = {
id: "operations",
labelKey: "pages.housekeeping.domains.operations.title",
descriptionKey: "pages.housekeeping.domains.operations.description",
iconId: "inbox",
previewHref: "/admin-next/operations",
capability: anyCapability(PERMS.ADMIN_DASHBOARD),
routes: [],
} satisfies HousekeepingDomainManifest;
@@ -0,0 +1,26 @@
import { PERMS } from "@/lib/permission-slugs";
import {
anyCapability,
type HousekeepingDomainManifest,
} from "../../foundation/contracts";
export const peopleManifest = {
id: "people",
labelKey: "pages.housekeeping.domains.people.title",
descriptionKey: "pages.housekeeping.domains.people.description",
iconId: "users",
previewHref: "/admin-next/people",
capability: anyCapability(
PERMS.USERS_VIEW,
PERMS.MODERATION_VIEW,
PERMS.TICKETS_VIEW,
PERMS.BANS_VIEW,
PERMS.MOD_DASHBOARD,
PERMS.MOD_CFH_VIEW,
PERMS.MOD_TEAM_VIEW,
PERMS.MOD_TICKETS_VIEW,
PERMS.MOD_USERS_VIEW,
PERMS.MOD_BANS_VIEW,
),
routes: [],
} satisfies HousekeepingDomainManifest;
@@ -0,0 +1,23 @@
import { PERMS } from "@/lib/permission-slugs";
import {
anyCapability,
type HousekeepingDomainManifest,
} from "../../foundation/contracts";
export const systemManifest = {
id: "system",
labelKey: "pages.housekeeping.domains.system.title",
descriptionKey: "pages.housekeeping.domains.system.description",
iconId: "settings",
previewHref: "/admin-next/system",
capability: anyCapability(
PERMS.SETTINGS_VIEW,
PERMS.LOGS_VIEW,
PERMS.ANALYTICS_VIEW,
PERMS.DEVOPS_VIEW,
PERMS.NOTIFICATIONS_VIEW,
PERMS.PERMISSIONS_MANAGE,
PERMS.RCON_EXECUTE,
),
routes: [],
} satisfies HousekeepingDomainManifest;
@@ -0,0 +1,57 @@
import { describe, expect, it } from "vitest";
import en from "@/messages/en.json";
import itMessages from "@/messages/it.json";
import { HOUSEKEEPING_MANIFESTS } from "../manifests";
const requiredKeys = [
"pages.housekeeping.preview.badge",
"pages.housekeeping.preview.commandDisabled",
"pages.housekeeping.preview.backToSite",
"pages.housekeeping.navigation.skipToContent",
"pages.housekeeping.navigation.primary",
"pages.housekeeping.navigation.contextual",
"pages.housekeeping.states.loading.title",
"pages.housekeeping.states.loading.description",
"pages.housekeeping.states.empty.title",
"pages.housekeeping.states.empty.description",
"pages.housekeeping.states.partial.label",
"pages.housekeeping.states.partial.title",
"pages.housekeeping.states.partial.description",
"pages.housekeeping.states.error.title",
"pages.housekeeping.states.error.description",
];
function resolveMessage(messages: unknown, key: string): unknown {
return key.split(".").reduce<unknown>((value, segment) => {
if (value === null || typeof value !== "object") return undefined;
return (value as Record<string, unknown>)[segment];
}, messages);
}
describe("housekeeping localization contract", () => {
it.each([
["English", en],
["Italian", itMessages],
])("provides the planned housekeeping subtree in %s", (_locale, messages) => {
const housekeeping = resolveMessage(messages, "pages.housekeeping");
expect(housekeeping).toEqual(expect.any(Object));
expect(Object.keys(housekeeping as object)).toEqual([
"preview",
"navigation",
"domains",
"states",
]);
for (const key of [
...HOUSEKEEPING_MANIFESTS.flatMap((manifest) => [
manifest.labelKey,
manifest.descriptionKey,
]),
...requiredKeys,
]) {
expect(resolveMessage(messages, key), key).toEqual(expect.any(String));
}
});
});
@@ -0,0 +1,120 @@
import { describe, expect, it, vi } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import { anyCapability, type HousekeepingCapabilityContext } from "./contracts";
import { buildHousekeepingNavigation } from "./navigation";
import { createHousekeepingRegistry } from "./registry";
const context = (
granted: readonly string[],
): HousekeepingCapabilityContext => ({
actor: { id: 42, username: "moderator", rank: 3 },
isSuperAdmin: false,
has: (slug) => granted.includes(slug),
hasAny: (...slugs) => slugs.some((slug) => granted.includes(slug)),
hasAll: (...slugs) => slugs.every((slug) => granted.includes(slug)),
});
describe("housekeeping navigation", () => {
it("shows People to a moderator with a mod view capability while hiding Economy", () => {
const registry = createHousekeepingRegistry([
{
id: "people",
labelKey: "pages.housekeeping.domains.people.title",
descriptionKey: "pages.housekeeping.domains.people.description",
iconId: "users",
previewHref: "/admin-next/people",
capability: anyCapability(PERMS.MOD_CFH_VIEW),
routes: [],
},
{
id: "economy",
labelKey: "pages.housekeeping.domains.economy.title",
descriptionKey: "pages.housekeeping.domains.economy.description",
iconId: "gem",
previewHref: "/admin-next/economy",
capability: anyCapability(PERMS.CATALOG_VIEW),
routes: [],
},
]);
const navigation = buildHousekeepingNavigation(
registry,
context([PERMS.MOD_CFH_VIEW]),
(key) => key,
);
expect(navigation).toEqual([
{
id: "people",
href: "/admin-next/people",
iconId: "users",
label: "pages.housekeeping.domains.people.title",
description: "pages.housekeeping.domains.people.description",
items: [],
},
]);
});
it("filters unauthorized domains and routes before translation", () => {
const registry = createHousekeepingRegistry([
{
id: "people",
labelKey: "people.title",
descriptionKey: "people.description",
iconId: "users",
previewHref: "/admin-next/people",
capability: anyCapability(PERMS.USERS_VIEW),
routes: [
{
id: "users",
labelKey: "people.users",
href: "/admin-next/people/users",
capability: anyCapability(PERMS.USERS_VIEW),
},
{
id: "bans",
labelKey: "people.bans",
href: "/admin-next/people/bans",
capability: anyCapability(PERMS.BANS_VIEW),
},
],
},
{
id: "system",
labelKey: "system.title",
descriptionKey: "system.description",
iconId: "settings",
previewHref: "/admin-next/system",
capability: anyCapability(PERMS.SETTINGS_VIEW),
routes: [],
},
]);
const translate = vi.fn((key: string) => `translated:${key}`);
const navigation = buildHousekeepingNavigation(
registry,
context([PERMS.USERS_VIEW]),
translate,
);
expect(navigation).toEqual([
{
id: "people",
href: "/admin-next/people",
iconId: "users",
label: "translated:people.title",
description: "translated:people.description",
items: [
{
id: "users",
href: "/admin-next/people/users",
label: "translated:people.users",
},
],
},
]);
expect(translate).not.toHaveBeenCalledWith("people.bans");
expect(translate).not.toHaveBeenCalledWith("system.title");
expect(translate).not.toHaveBeenCalledWith("system.description");
});
});
@@ -0,0 +1,39 @@
import type { HousekeepingDomainId } from "../migration/types";
import { satisfiesCapability } from "./capability-context";
import type {
HousekeepingCapabilityContext,
HousekeepingDomainManifest,
} from "./contracts";
import type { HousekeepingRegistry } from "./registry";
export interface HousekeepingNavigationDomain {
id: HousekeepingDomainId;
href: string;
iconId: HousekeepingDomainManifest["iconId"];
label: string;
description: string;
items: readonly { id: string; href: string; label: string }[];
}
export function buildHousekeepingNavigation(
registry: HousekeepingRegistry,
context: HousekeepingCapabilityContext,
translate: (key: string) => string,
): readonly HousekeepingNavigationDomain[] {
return registry.domains
.filter((domain) => satisfiesCapability(context, domain.capability))
.map((domain) => ({
id: domain.id,
href: domain.previewHref,
iconId: domain.iconId,
label: translate(domain.labelKey),
description: translate(domain.descriptionKey),
items: domain.routes
.filter((route) => satisfiesCapability(context, route.capability))
.map((route) => ({
id: route.id,
href: route.href,
label: translate(route.labelKey),
})),
}));
}
@@ -0,0 +1,153 @@
import { describe, expect, it } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import { HOUSEKEEPING_MANIFESTS } from "../manifests";
import {
HOUSEKEEPING_DOMAIN_IDS,
type HousekeepingDomainId,
} from "../migration/types";
import { anyCapability, type HousekeepingDomainManifest } from "./contracts";
import { createHousekeepingRegistry } from "./registry";
const manifest = (
id: HousekeepingDomainId,
capabilitySlug = PERMS.ADMIN_DASHBOARD,
): HousekeepingDomainManifest => ({
id,
labelKey: `pages.housekeeping.domains.${id}.title`,
descriptionKey: `pages.housekeeping.domains.${id}.description`,
iconId: "settings",
previewHref: `/admin-next/${id}`,
capability: anyCapability(capabilitySlug),
routes: [],
});
describe("housekeeping registry", () => {
it("registers the six approved domains in their locked order", () => {
const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS);
expect(registry.domains.map((domain) => domain.id)).toEqual(
HOUSEKEEPING_DOMAIN_IDS,
);
expect(Object.isFrozen(registry.domains)).toBe(true);
});
it("rejects duplicate and unknown domain identifiers", () => {
expect(() =>
createHousekeepingRegistry([
manifest("operations"),
manifest("operations"),
]),
).toThrow("duplicate domain id");
expect(() =>
createHousekeepingRegistry([
{ ...manifest("operations"), id: "future" as HousekeepingDomainId },
]),
).toThrow("unknown domain id");
});
it("rejects invalid preview and empty translation keys", () => {
expect(() =>
createHousekeepingRegistry([
{ ...manifest("people"), previewHref: "/admin-next/operations" },
]),
).toThrow("invalid preview href");
expect(() =>
createHousekeepingRegistry([{ ...manifest("people"), labelKey: " " }]),
).toThrow("empty label key");
expect(() =>
createHousekeepingRegistry([
{ ...manifest("people"), descriptionKey: "\t" },
]),
).toThrow("empty description key");
});
it("rejects duplicate route identities and hrefs", () => {
const base = manifest("people");
expect(() =>
createHousekeepingRegistry([
{
...base,
routes: [
{
id: "users",
labelKey: "pages.housekeeping.domains.people.title",
href: "/admin-next/people/users",
capability: anyCapability(PERMS.USERS_VIEW),
},
{
id: "users",
labelKey: "pages.housekeeping.domains.people.title",
href: "/admin-next/people/staff",
capability: anyCapability(PERMS.USERS_VIEW),
},
],
},
]),
).toThrow("duplicate route id");
expect(() =>
createHousekeepingRegistry([
{
...base,
routes: [
{
id: "users",
labelKey: "pages.housekeeping.domains.people.title",
href: "/admin-next/people/users",
capability: anyCapability(PERMS.USERS_VIEW),
},
{
id: "staff",
labelKey: "pages.housekeeping.domains.people.title",
href: "/admin-next/people/users",
capability: anyCapability(PERMS.USERS_VIEW),
},
],
},
]),
).toThrow("duplicate route href");
});
it("rejects empty route fields and unknown capability slugs", () => {
const route = {
id: "users",
labelKey: "pages.housekeeping.domains.people.title",
href: "/admin-next/people/users",
capability: anyCapability(PERMS.USERS_VIEW),
};
expect(() =>
createHousekeepingRegistry([
{ ...manifest("people"), routes: [{ ...route, id: " " }] },
]),
).toThrow("empty route id");
expect(() =>
createHousekeepingRegistry([
{ ...manifest("people"), routes: [{ ...route, href: " " }] },
]),
).toThrow("empty route href");
expect(() =>
createHousekeepingRegistry([
{ ...manifest("people"), routes: [{ ...route, labelKey: " " }] },
]),
).toThrow("empty route label key");
expect(() =>
createHousekeepingRegistry([
{
...manifest("system"),
capability: anyCapability("admin.ghost.view"),
},
]),
).toThrow("unknown capability slug: admin.ghost.view");
expect(() =>
createHousekeepingRegistry([
{
...manifest("people"),
routes: [
{ ...route, capability: anyCapability("admin.ghost.route") },
],
},
]),
).toThrow("unknown capability slug: admin.ghost.route");
});
});
@@ -0,0 +1,75 @@
import { PERMS } from "@/lib/permission-slugs";
import { HOUSEKEEPING_DOMAIN_IDS } from "../migration/types";
import type {
CapabilityRequirement,
HousekeepingDomainManifest,
} from "./contracts";
const approvedDomainIds = new Set<string>(HOUSEKEEPING_DOMAIN_IDS);
const knownCapabilitySlugs = new Set<string>(Object.values(PERMS));
export interface HousekeepingRegistry {
domains: readonly HousekeepingDomainManifest[];
}
export function createHousekeepingRegistry(
manifests: readonly HousekeepingDomainManifest[],
): HousekeepingRegistry {
const domainIds = new Set<string>();
const routeIds = new Set<string>();
const routeHrefs = new Set<string>();
for (const manifest of manifests) {
if (!approvedDomainIds.has(manifest.id)) {
throw new Error(`unknown domain id: ${manifest.id}`);
}
if (domainIds.has(manifest.id)) {
throw new Error(`duplicate domain id: ${manifest.id}`);
}
domainIds.add(manifest.id);
if (manifest.previewHref !== `/admin-next/${manifest.id}`) {
throw new Error(`invalid preview href: ${manifest.previewHref}`);
}
validateNonEmpty(manifest.labelKey, "label key");
validateNonEmpty(manifest.descriptionKey, "description key");
validateCapability(manifest.capability);
for (const route of manifest.routes) {
validateNonEmpty(route.id, "route id");
if (routeIds.has(route.id)) {
throw new Error(`duplicate route id: ${route.id}`);
}
routeIds.add(route.id);
validateNonEmpty(route.href, "route href");
if (routeHrefs.has(route.href)) {
throw new Error(`duplicate route href: ${route.href}`);
}
routeHrefs.add(route.href);
validateNonEmpty(route.labelKey, "route label key");
validateCapability(route.capability);
}
}
return { domains: Object.freeze([...manifests]) };
}
function validateNonEmpty(value: string, name: string): void {
if (!value.trim()) {
throw new Error(`empty ${name}`);
}
}
function validateCapability(requirement: CapabilityRequirement): void {
if (requirement.slugs.length === 0) {
throw new Error("empty capability requirement");
}
for (const slug of requirement.slugs) {
if (!knownCapabilitySlugs.has(slug)) {
throw new Error(`unknown capability slug: ${slug}`);
}
}
}
+16
View File
@@ -0,0 +1,16 @@
import { contentManifest } from "./domains/content/manifest";
import { economyManifest } from "./domains/economy/manifest";
import { hotelManifest } from "./domains/hotel/manifest";
import { operationsManifest } from "./domains/operations/manifest";
import { peopleManifest } from "./domains/people/manifest";
import { systemManifest } from "./domains/system/manifest";
import type { HousekeepingDomainManifest } from "./foundation/contracts";
export const HOUSEKEEPING_MANIFESTS = [
operationsManifest,
peopleManifest,
contentManifest,
economyManifest,
hotelManifest,
systemManifest,
] as const satisfies readonly HousekeepingDomainManifest[];
+57
View File
@@ -3255,6 +3255,63 @@
"failed": "Failed"
}
}
},
"housekeeping": {
"preview": {
"badge": "Foundation preview",
"commandDisabled": "Search and commands are enabled in a later subproject.",
"backToSite": "Back to site"
},
"navigation": {
"skipToContent": "Skip to content",
"primary": "Housekeeping domains",
"contextual": "Domain navigation"
},
"domains": {
"operations": {
"title": "Operations",
"description": "Operational overview and daily queues"
},
"people": {
"title": "People",
"description": "Users, moderation, and support"
},
"content": {
"title": "Content",
"description": "Editorial content and community engagement"
},
"economy": {
"title": "Economy",
"description": "Catalog and shop operations"
},
"hotel": {
"title": "Hotel",
"description": "Rooms, radio, and asset tools"
},
"system": {
"title": "System",
"description": "Configuration, observability, and access"
}
},
"states": {
"loading": {
"title": "Loading housekeeping",
"description": "Preparing the available housekeeping tools."
},
"empty": {
"title": "Nothing available",
"description": "No housekeeping content is available for this domain."
},
"partial": {
"label": "Partial data",
"title": "Some information is unavailable",
"description": "Review the available information and try again later."
},
"error": {
"title": "Unable to load housekeeping",
"description": "Try again later or contact an administrator."
}
}
}
}
}
+57
View File
@@ -3260,6 +3260,63 @@
"failed": "Failed"
}
}
},
"housekeeping": {
"preview": {
"badge": "Anteprima della base",
"commandDisabled": "Ricerca e comandi saranno abilitati in un sottoprogetto successivo.",
"backToSite": "Torna al sito"
},
"navigation": {
"skipToContent": "Vai al contenuto",
"primary": "Domini housekeeping",
"contextual": "Navigazione del dominio"
},
"domains": {
"operations": {
"title": "Operazioni",
"description": "Panoramica operativa e code giornaliere"
},
"people": {
"title": "Persone",
"description": "Utenti, moderazione e supporto"
},
"content": {
"title": "Contenuti",
"description": "Contenuti editoriali e coinvolgimento della community"
},
"economy": {
"title": "Economia",
"description": "Operazioni di catalogo e shop"
},
"hotel": {
"title": "Hotel",
"description": "Stanze, radio e strumenti per gli asset"
},
"system": {
"title": "Sistema",
"description": "Configurazione, osservabilità e accesso"
}
},
"states": {
"loading": {
"title": "Caricamento housekeeping",
"description": "Preparazione degli strumenti housekeeping disponibili."
},
"empty": {
"title": "Nessun contenuto disponibile",
"description": "Non ci sono contenuti housekeeping disponibili per questo dominio."
},
"partial": {
"label": "Dati parziali",
"title": "Alcune informazioni non sono disponibili",
"description": "Controlla le informazioni disponibili e riprova più tardi."
},
"error": {
"title": "Impossibile caricare housekeeping",
"description": "Riprova più tardi o contatta un amministratore."
}
}
}
}
}