/** * peak-pricing-board — today's billing schedule, as DATA. * * The 2×2 峰谷定价 card answers one question ("is it peak RIGHT NOW?"); this module * answers the wide card's question ("how does the REST OF TODAY bill?") as a pure * function of (clock, config) — no React, no DOM, no i18n. Every label the card * prints is built in index.ts; here we only produce minutes and i18n KEYS, so the * schedule can be read (and probed) without a locale. * * THE RULE IS NOT OURS, AND NO LONGER NEXT DOOR. The peak/off-peak verdict, the * timezone clock, the window parser, the config reader and the holiday lookup are * read from the SHARED layer — `src/client/lib/peak-schedule.ts` + * `src/client/lib/peak-holidays.ts` — because the 2×2 峰谷定价 card must answer * exactly the same question the same way: if the two ever disagreed, the rail * would be lying on one of them. They used to be imported FROM the 2×2 unit (a * real unit-to-unit coupling: deleting that directory broke this card, and the * +8 offset was mirrored here because `clockAt` kept it inline); both moved into * the shared layer on 2026-09-28, and `ZONE_OFFSET_MINS` is now read from there * instead of being copied. */ import { type Clock, type PeakPricingConfig, type PeakStatus } from '../../client/lib/peak-schedule'; /** A day's worth of PeakPricingConfig (what the schedule is computed from). */ export type ScheduleConfig = PeakPricingConfig; /** The shipped timetable, as the shared config string. Used by the unit's * example/probe so neither has to re-type it. */ export declare const DEFAULT_BILLING_WINDOWS = "09:00-12:00, 14:00-18:00"; /** Minutes → `42m` / `1h 05m` — the countdown in the head. */ export declare function fmtRemaining(mins: number): string; /** * ONE block of the day's clock: a stretch that bills the same way. * * - `peak: true` — inside one of the configured windows (`windowIndex`); * - `peak: false` — an off-peak stretch. */ export interface DayBlock { start: number; /** Exclusive; `DAY` for the last block of the day. */ end: number; peak: boolean; /** Which configured window this block belongs to (peak blocks only). */ windowIndex?: number; } /** ONE row the CARD draws: a configured peak window, or the day's off-peak * remainder — plus whether THIS is the row in effect right now. */ export interface ScheduleRow { /** `09:00–12:00` — always real clock text, so the table reads as a timetable. */ range: string; peak: boolean; /** The row now in effect. At most one row is ever `now` (see `boardRows`). */ now: boolean; /** Whole-day-off days only, on the remainder row: the i18n key naming WHY. * On such a day the peak windows are still LISTED (the timetable is what the * card is) but they are not in effect — the reason rides this one row. */ dayOffKey?: string; } /** Everything the wide card needs about today. */ export interface DayTrack { /** `YYYY-MM-DD` in the configured zone. */ dateKey: string; /** The zone-local clock the verdicts below were computed in. */ clock: Clock; /** The verdict from the SHARED implementation (never re-derived here). */ status: PeakStatus; /** True when the whole day bills off-peak by rule (holiday / weekend). */ wholeDayOff: boolean; /** The i18n key naming WHY, when `wholeDayOff` — e.g. `card.peak.holiday.spring` * or `card.peak.weekend`. Undefined on an ordinary day. */ reasonKey?: string; /** Today's rows in clock order: one per peak window, then the off-peak * remainder (three rows for the shipped two-window timetable). */ rows: ScheduleRow[]; /** The blocks the timetable is made of, in clock order. */ blocks: DayBlock[]; /** The block in effect right now. */ current: DayBlock; /** Start (minutes) of the next block — when the rate flips. `DAY` when nothing * follows inside today (i.e. the next change is the day rollover). */ nextAt: number; /** `nextAt - now`, in minutes. */ nextInMins: number; /** True when the next flip is the day rollover rather than another block today. */ nextTomorrow: boolean; /** Whole-day-off only: when the expensive rate can next come back — the START OF * THE FIRST PEAK WINDOW on the next day whose windows apply (`09:00`), the * CALENDAR-DAY distance to that day, its weekday, and the honest wait from now. * The wait is measured against the wall clock rather than assembled from the * weekday, so the two can never disagree. */ offUntil?: { label: string; mins: number; days: number; dow: number; }; /** False when the holiday table does not cover a year this caption depends on, * so neither card may promise what tomorrow costs. Kept as a FIELD rather than * a silent fallback: the card prints no "next" caption and says why on hover. */ nextConfident: boolean; } /** * Read an instance's merged config record into a schedule config. * * Reuses the 2×2 card's reader verbatim (`peakConfigOf`), so every key — * `peakWindows`, `weekendOff`, `holidayOff`, `timeZone`, `extraHolidays` — means * exactly what it means over there. `billingWindows` is this card's own OPTIONAL * override: when set (non-empty) it replaces `peakWindows`, so the wide card can * carry its own timetable without editing the narrow card. */ export declare function resolveScheduleConfig(config: Record | null | undefined): ScheduleConfig; /** The zone-local clock for `now` — the contrived-`Date` rule lives in the 2×2 * card's `clockAt`, so this is only here to name the intent. */ export declare function clockInZone(now: Date, tz: string): Clock; /** * The whole day, as data. * * `config` is required (not defaulted): every caller already holds one — the * shell's merged instance config, or this unit's own `example.stats` — and a * silent default here would hide a card that lost its config. * * `now` is the live-path seam: the card passes the real clock reading, and the * function asks the SHARED verdict about that exact instant rather than * reconstructing it (a reconstruction is only needed when the clock was pinned by * a preview, and `instantOf` does it there). Tests and probes therefore drive the * whole card through one honest parameter instead of stubbing the global `Date`. */ export declare function trackDay(clock: Clock, config: ScheduleConfig, now?: Date): DayTrack;