/** * Formatting: provider status → widget segments and /peak report lines. * Pure string/Date work; theming happens in the extension (index.ts). */ import type { PeakSchedule } from "./schedules.ts"; import { isPeakAt, nextChangeAt, parseHm } from "./windows.ts"; export interface ProviderStatus { schedule: PeakSchedule; peak: boolean; /** Next phase flip, or null when none within the scan horizon. */ nextChange: Date | null; } export function summarize(schedules: PeakSchedule[], now: Date): ProviderStatus[] { return schedules.map((schedule) => ({ schedule, peak: isPeakAt(schedule, now), nextChange: nextChangeAt(schedule, now)?.at ?? null, })); } /** "auto" → system zone; "UTC" → UTC; otherwise an IANA name (falls back to auto when unknown). */ export function resolveTimezone(setting: string): string { if (!setting || setting === "auto") { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; } catch { return "UTC"; } } if (setting.toUpperCase() === "UTC") return "UTC"; try { new Intl.DateTimeFormat("en", { timeZone: setting }); return setting; } catch { return resolveTimezone("auto"); } } /** Offset of a timezone at the given instant, in minutes (east positive). */ export function tzOffsetMinutes(instant: Date, tz: string): number { const parts = new Intl.DateTimeFormat("en-US", { timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23", }).formatToParts(instant); const get = (type: string): string => parts.find((p) => p.type === type)?.value ?? "00"; const asUtc = Date.parse( `${get("year")}-${get("month")}-${get("day")}T${get("hour")}:${get("minute")}:${get("second")}Z`, ); return Math.round((asUtc - instant.getTime()) / 60_000); } function dayKey(instant: Date, tz: string): string { return new Intl.DateTimeFormat("en-CA", { timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit", }).format(instant); } /** "09:00", or "Mon 09:00" when the target is not the same local day as `now`. */ export function fmtWhen(target: Date, now: Date, tz: string): string { const clock = new Intl.DateTimeFormat("en-GB", { timeZone: tz, hour: "2-digit", minute: "2-digit", hourCycle: "h23", }).format(target); if (dayKey(target, tz) === dayKey(now, tz)) return clock; const weekday = new Intl.DateTimeFormat("en-GB", { timeZone: tz, weekday: "short" }).format(target); return `${weekday} ${clock}`; } export type Tone = "warning" | "dim" | "success"; export interface Segment { text: string; tone: Tone; } /** * One compact widget line: peak providers first, e.g. * `⚡ DS until 09:00 · GLM until 15:00`. With show="always" off-peak * providers are appended dimmed. Returns null when the line should be * hidden (show="peak" and nobody is in peak). */ export function widgetSegments( statuses: ProviderStatus[], now: Date, tz: string, show: "peak" | "always", ): Segment[] | null { const segments: Segment[] = []; for (const st of statuses) { if (st.peak) { const until = st.nextChange ? ` until ${fmtWhen(st.nextChange, now, tz)}` : ""; if (segments.length > 0) segments.push({ text: " · ", tone: "dim" }); segments.push({ text: `⚡ ${st.schedule.shortLabel}${until}`, tone: "warning" }); } else if (show === "always") { if (segments.length > 0) segments.push({ text: " · ", tone: "dim" }); segments.push({ text: `${st.schedule.shortLabel} off-peak`, tone: "dim" }); } } return segments.length > 0 ? segments : null; } /** Convert a billing-calendar "HH:mm" edge to display-tz "HH:mm". */ function edgeText(calendarMinutes: number, schedule: PeakSchedule, now: Date, tz: string): string { const raw = calendarMinutes - schedule.calendarOffsetMinutes + tzOffsetMinutes(now, tz); const dayShift = Math.floor(raw / 1440); const normalized = raw - dayShift * 1440; const h = String(Math.floor(normalized / 60)).padStart(2, "0"); const m = String(normalized % 60).padStart(2, "0"); return dayShift === 0 ? `${h}:${m}` : `${h}:${m}(${dayShift > 0 ? "+" : ""}${dayShift}d)`; } function windowText(schedule: PeakSchedule, now: Date, tz: string): string { return schedule.windows .map((w) => `${edgeText(parseHm(w.start), schedule, now, tz)}-${edgeText(parseHm(w.end), schedule, now, tz)}`) .join(", "); } /** Full multi-line report for /peak. */ export function reportLines(statuses: ProviderStatus[], now: Date, tz: string): string[] { const width = Math.max(...statuses.map((s) => s.schedule.label.length)); const lines: string[] = [`LLM peak/off-peak — local time ${tz}`]; for (const st of statuses) { const until = st.nextChange ? ` until ${fmtWhen(st.nextChange, now, tz)}` : ""; lines.push( `${st.peak ? "⚡" : " "} ${st.schedule.label.padEnd(width)} ` + `${st.peak ? "PEAK" : "off-peak"}${until} · peak ${windowText(st.schedule, now, tz)} ` + `Mon-Fri on ${utcOffsetText(st.schedule)}`, ); } for (const st of statuses) { lines.push(`${st.schedule.label}: ${st.schedule.offPeakNote}. ${st.schedule.source}`); } return lines; } function utcOffsetText(schedule: PeakSchedule): string { const sign = schedule.calendarOffsetMinutes >= 0 ? "+" : "-"; const abs = Math.abs(schedule.calendarOffsetMinutes); return `UTC${sign}${String(Math.floor(abs / 60)).padStart(2, "0")}:${String(abs % 60).padStart(2, "0")}`; }