/** * Server-stamped phase timer math for the grand-finale ceremony. * * The cursor stores `phaseStartedAt` + `phaseDurationSeconds` plus a pause * accumulator; every client derives the countdown locally from those stamps, * so all screens agree and overtime is just a negative remainder. */ export type PhaseTimerState = { phaseStartedAt: Date | string | null phaseDurationSeconds: number | null phasePausedAt: Date | string | null phasePausedAccumMs: number } export function elapsedMs(t: PhaseTimerState, now: Date = new Date()): number { if (!t.phaseStartedAt) return 0 const start = new Date(t.phaseStartedAt).getTime() const end = t.phasePausedAt ? new Date(t.phasePausedAt).getTime() : now.getTime() return Math.max(0, end - start - t.phasePausedAccumMs) } /** Seconds left on the phase timer; negative = overtime; null = no timer running. */ export function remainingSeconds(t: PhaseTimerState, now: Date = new Date()): number | null { if (!t.phaseStartedAt || t.phaseDurationSeconds == null) return null return t.phaseDurationSeconds - Math.floor(elapsedMs(t, now) / 1000) } /** `5:05` for positive seconds, `+1:23` for overtime. */ export function formatClock(seconds: number): string { const over = seconds < 0 const abs = Math.abs(seconds) const m = Math.floor(abs / 60) const s = abs % 60 return `${over ? '+' : ''}${m}:${s.toString().padStart(2, '0')}` }