import type { SyncSchedulerSettings } from "../lib/settings"; import { buildScheduleView, type ChannelEntry, type ChannelScheduleView, } from "../jobs/syncScheduler"; import type { SchedulerRun, SchedulerState, } from "../jobs/syncSchedulerState"; import type { ChannelPriority } from "../lib/channelPriority"; // THE READ-ONLY "SYNC SCHEDULE" VIEW. // // Shared by the SSR page and the /api/scheduler/status poll so the two can // never drift. Everything it needs arrives as an argument: the settings, the // scheduler state read off disk, the channel list, the clock and the effective // heartbeat cadence. `buildScheduleView` is a pure projection in jobs/ and is // the one definition of "when is this channel next due" — the console asks the // scheduler rather than re-deriving it. export type SchedulerStatusPayload = { now: number; scheduler: SyncSchedulerSettings; channels: ChannelScheduleView[]; runs: SchedulerRun[]; // Effective internal-heartbeat cadence in seconds (env override applied), so // the UI can show whether ticks are internally driven. 0 = no internal timer // (awaiting an external cron heartbeat). heartbeatSeconds: number; }; export type SchedulerStatusInputs = { settings: { syncScheduler: SyncSchedulerSettings; channelPriority: ChannelPriority; }; now: number; state: SchedulerState; channels: ReadonlyArray; // Resolved by the shell: it reads the env override and imports the heartbeat // module, which owns a RUNNER. A view never names a runner. heartbeatSeconds: number; }; export function buildSchedulerStatusPayload( inputs: SchedulerStatusInputs, ): SchedulerStatusPayload { const { settings, now, state, channels, heartbeatSeconds } = inputs; const view = buildScheduleView({ channels, scheduler: settings.syncScheduler, state, now, priority: settings.channelPriority, }); return { now, scheduler: settings.syncScheduler, channels: view, runs: state.runs, heartbeatSeconds, }; }