export const activityTypes = { create: 'Create server', start: 'Start server', restart: 'Restart server', rebuild: 'Rebuild server', stop: 'Stop server', remove: 'Remove server', update: 'Game update', files: 'File changes', health: 'Health finding', drift: 'File drift', host: 'Host operation', beta: 'Beta test' } as const; export type ActivityType = keyof typeof activityTypes; export const activitySources = { provisioning: 'Server operations', maintenance: 'Maintenance & recovery', recovery: 'Recovery', campaign: 'Campaigns', beta: 'Betas', host: 'Hosts', autoscaling: 'Autoscaling' } as const; export type ActivitySource = keyof typeof activitySources; export const activityStates = { queued: 'Queued', running: 'In progress', succeeded: 'Completed', warning: 'Completed with warnings', failed: 'Failed', cancelled: 'Cancelled', attention: 'Needs attention', unknown: 'Unknown' } as const; export type ActivityState = keyof typeof activityStates; export interface ActivityRow { id: string; at: string; type: ActivityType; source: ActivitySource; /** A shared operation can have more than one caller; filters match any membership. */ sources: ActivitySource[]; state: ActivityState; rawState: string; title: string; detail: string | null; server: string | null; serverDeletedAt: string | null; hostId: string | null; hostName: string | null; href: string; } export interface ActivityQuery { server?: string; host?: string; type?: ActivityType; source?: ActivitySource; state?: ActivityState; days?: number; q?: string; limit?: number; offset?: number; } export interface ActivityPage { rows: ActivityRow[]; total: number; limit: number; offset: number; query: ActivityQuery; } function selection(value: string | null, labels: Record): T | undefined { return value !== null && Object.hasOwn(labels, value) ? value as T : undefined; } function integer(value: string | null, fallback: number, min: number, max: number): number { if (value === null || !/^\d+$/.test(value)) return fallback; const parsed = Number(value); return Number.isSafeInteger(parsed) ? Math.min(max, Math.max(min, parsed)) : fallback; } /** Server and host are exact identities; q searches friendly names and safe summaries. */ export function parseActivityQuery(params: URLSearchParams): ActivityQuery { const query: ActivityQuery = { limit: integer(params.get('limit'), 25, 1, 100), offset: integer(params.get('offset'), 0, 0, 1_000_000_000) }; for (const key of ['server', 'host', 'q'] as const) { const value = params.get(key)?.trim().slice(0, key === 'q' ? 200 : 255); if (value) query[key] = value; } query.type = selection(params.get('type'), activityTypes); query.source = selection(params.get('source'), activitySources); query.state = selection(params.get('state'), activityStates); // Missing or zero days means all retained history, including removed servers. const days = integer(params.get('days'), 0, 0, 36_500); if (days) query.days = days; return query; } export function normalizeActivityQuery(query: ActivityQuery): ActivityQuery { const params = new URLSearchParams(); for (const [key, value] of Object.entries(query)) if (value !== undefined) params.set(key, String(value)); return parseActivityQuery(params); }