109 lines
3.0 KiB
TypeScript
109 lines
3.0 KiB
TypeScript
// ── 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<CascadePreset, number[]> = {
|
|
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<CascadePreset, string> = {
|
|
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`;
|
|
}
|