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"; import { type StudioOperationRecord, studioOperationRepository, } from "./queries/studio"; export const HOTEL_INBOX_SOURCE_IDS = ["hotel.studio.operations"] as const; type HotelOperationLoader = ( context: HousekeepingCapabilityContext, signal: AbortSignal, ) => Promise; export interface HotelInboxAdapters { readonly operations: HotelOperationLoader; } export function createHotelInboxSources( adapters: HotelInboxAdapters, ): readonly HousekeepingInboxSource[] { const capability = anyCapability(PERMS.ASSETS_IMPORT, PERMS.CATALOG_EDIT); return [ { id: "hotel.studio.operations", owner: "hotel", capability, async getItems(context, signal) { const authorization = authorizeHousekeeping(context, capability); if (!authorization.ok) return authorization; try { if (signal.aborted) throw new Error("aborted Hotel inbox"); const operations = await adapters.operations(context, signal); return ok( { availability: "available" as const, items: operations .filter((operation) => operation.phase !== "completed") .filter((operation) => satisfiesCapability( context, operation.kind === "maintenance" ? anyCapability(PERMS.CATALOG_EDIT) : anyCapability(PERMS.ASSETS_IMPORT), ), ) .slice(0, 25) .map((operation) => { const timestamp = Date.parse(operation.updatedAt); const ageMs = Number.isFinite(timestamp) ? Math.max(0, Date.now() - timestamp) : 0; return { sourceId: "hotel.studio.operations", itemId: operation.operationId, deduplicationKey: `hotel.studio.operations:${operation.operationId}`, domain: "hotel" as const, capability: operation.kind === "maintenance" ? anyCapability(PERMS.CATALOG_EDIT) : anyCapability(PERMS.ASSETS_IMPORT), severity: operation.phase === "failed" ? ("critical" as const) : operation.phase === "partial" ? ("warning" as const) : ("info" as const), priority: operation.phase === "failed" ? ("high" as const) : ("normal" as const), ageMs, state: operation.phase, occurredAt: operation.updatedAt, titleKey: "pages.housekeeping.items.hotelStudioOperation", context: { kind: operation.kind, completed: operation.completed, total: operation.total, }, href: `/ase/hotel/studio/${ operation.kind === "badge" ? "badges" : operation.kind === "effect" ? "effects" : operation.kind === "pet" ? "pets" : operation.kind }` as `/ase/${string}`, freshness: ageMs > 86_400_000 ? ("stale" as const) : ("fresh" as const), actions: [], }; }), }, authorization.correlationId, ); } catch { return fail( "DEPENDENCY_UNAVAILABLE", "errors.housekeeping.dependencyUnavailable", authorization.correlationId, ); } }, }, ]; } export const HOTEL_INBOX_SOURCES = createHotelInboxSources({ async operations(_context, signal) { if (signal.aborted) throw new Error("aborted Hotel operations"); return (await studioOperationRepository.list({ pageSize: 25, offset: 0 })) .items; }, });