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