You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

174 lines
4.3 KiB
TypeScript

/**
* 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`;
}