// ── Pre-Warning Cascade System ───────────────────────────────── // Cascade presets aligned with PRD: Aggressive, Standard, Light, Minimal, None, Custom export type CascadePreset = 'aggressive' | 'standard' | 'light' | 'minimal' | 'none' | 'custom'; export interface CascadeWarning { id: string; minutesBefore: number; fired: boolean; firedAt: number | null; scheduledTime: number; // epoch ms } export interface CascadeConfig { preset: CascadePreset; intervals: number[]; // minutes before target time } // Preset definitions (minutes before target) export const CASCADE_PRESETS: Record = { aggressive: [240, 180, 120, 90, 60, 30, 15, 5, 1], standard: [120, 60, 30, 15, 5], light: [60, 15, 5], minimal: [15], none: [], custom: [], }; export const CASCADE_PRESET_LABELS: Record = { aggressive: 'Aggressive', standard: 'Standard', light: 'Light', minimal: 'Minimal', none: 'None (fire only)', custom: 'Custom', }; /** * Calculate all warning timestamps from target time and cascade intervals. * Filters out warnings that would be in the past relative to `now`. */ export function calculateCascadeWarnings( targetTime: number, intervals: number[], now: number = Date.now() ): CascadeWarning[] { return intervals .sort((a, b) => b - a) // largest first (earliest warning) .map((minutesBefore, idx) => { const scheduledTime = targetTime - minutesBefore * 60 * 1000; return { id: `w-${idx}-${minutesBefore}m`, minutesBefore, fired: scheduledTime <= now, firedAt: scheduledTime <= now ? scheduledTime : null, scheduledTime, }; }); } /** * Get the next unfired warning from a cascade. */ export function getNextWarning(warnings: CascadeWarning[]): CascadeWarning | null { return warnings.find((w) => !w.fired) ?? null; } /** * Check which warnings should fire given the current time. * Returns newly-fired warning IDs. */ export function checkWarnings( warnings: CascadeWarning[], now: number = Date.now() ): string[] { const newlyFired: string[] = []; for (const warning of warnings) { if (!warning.fired && warning.scheduledTime <= now) { warning.fired = true; warning.firedAt = now; newlyFired.push(warning.id); } } return newlyFired; } /** * Get intervals for a preset, or custom intervals if preset is 'custom'. */ export function getCascadeIntervals(config: CascadeConfig): number[] { if (config.preset === 'custom') { return [...config.intervals].sort((a, b) => b - a); } return CASCADE_PRESETS[config.preset]; } /** * Format minutes into human-readable string. */ export function formatMinutesBefore(minutes: number): string { if (minutes >= 60) { const hours = Math.floor(minutes / 60); const remaining = minutes % 60; if (remaining === 0) return `${hours}h`; return `${hours}h ${remaining}m`; } return `${minutes}m`; }