/** * Materialisation Status Types * * Types for slot materialisation job status and manual trigger endpoints. * Supports status polling, rate limiting, and idempotency. */ export type MaterialisationJobStatus = 'idle' | 'running' | 'completed' | 'failed'; export type MaterialisationPolicyProfile = 'SAFE' | 'CLEANUP'; export interface MaterialisationPolicy { name: MaterialisationPolicyProfile; description: string; icon: string; color: string; badge?: string; } export const MATERIALISATION_POLICIES: MaterialisationPolicy[] = [ { name: 'SAFE', description: 'Normal mode - skips overlapping slots to protect existing schedule', icon: '🛡️', color: 'blue', }, { name: 'CLEANUP', description: 'Cleanup mode - removes conflicting slots (keeps bookings safe)', icon: '🧹', color: 'orange', badge: 'Deletes old slots', }, ]; export interface MaterialisationRateLimit { can_trigger: boolean; next_available_at: string | null; // ISO 8601 timestamp cooldown_seconds: number; } export interface MaterialisationJobInfo { job_id: string; status: 'queued' | 'running' | 'completed' | 'failed'; accepted_at: string; // ISO 8601 timestamp started_at?: string; // ISO 8601 timestamp completed_at?: string; // ISO 8601 timestamp failed_at?: string; // ISO 8601 timestamp policy_profile: MaterialisationPolicyProfile; horizon_days: number; slots_generated?: number; actor_user_id?: number; error?: string; result?: { created: number; updated: number; moved: number; cancelled: number; skipped: number; errors: string[]; }; } export interface MaterialisationStatus { status: MaterialisationJobStatus; last_run_at: string | null; // ISO 8601 timestamp last_success_at: string | null; // ISO 8601 timestamp last_error: string | null; slots_generated: number | null; rate_limit: MaterialisationRateLimit; last_job?: MaterialisationJobInfo; // Real-time job tracking (BUILD 353) } export interface MaterialisationTriggerRequest { idempotency_key: string; // UUID v4 horizon_days?: number; // 1-28, default 7 policy_profile?: MaterialisationPolicyProfile; // default 'SAFE' } export interface MaterialisationTriggerResponse { status: 'accepted' | 'running' | 'completed' | 'failed'; job_id: string; message: string; duplicate?: boolean; } export interface MaterialisationError { type: string; title: string; status: number; detail: string; code: string; retry_after?: number; // seconds (for 429 responses) next_available_at?: string; // ISO 8601 timestamp (for 429 responses) } /** * Helper to calculate remaining cooldown seconds */ export function calculateRemainingCooldown(nextAvailableAt: string | null): number { if (!nextAvailableAt) return 0; const now = new Date(); const next = new Date(nextAvailableAt); const diffMs = next.getTime() - now.getTime(); return Math.max(0, Math.ceil(diffMs / 1000)); } /** * Helper to format countdown display (e.g., "2m 34s", "45s") */ export function formatCountdown(seconds: number): string { if (seconds <= 0) return '0s'; const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; if (minutes > 0) { return `${minutes}m ${remainingSeconds}s`; } return `${seconds}s`; } /** * Helper to format timestamp for display */ export function formatTimestamp(isoString: string): string { const date = new Date(isoString); return date.toLocaleString('en-GB', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); } /** * Generate UUID v4 for idempotency keys */ export function generateIdempotencyKey(): string { return crypto.randomUUID(); } /** * Calculate elapsed time in seconds since job started */ export function calculateElapsedTime(startTime: string): number { const now = new Date(); const start = new Date(startTime); const diffMs = now.getTime() - start.getTime(); return Math.floor(diffMs / 1000); } /** * Format elapsed time for display (e.g., "2m 34s", "45s") */ export function formatElapsedTime(seconds: number): string { if (seconds <= 0) return '0s'; const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; if (minutes > 0) { return `${minutes}m ${remainingSeconds}s`; } return `${seconds}s`; }