Files
netfelix-audio-fix/server/services/scheduler.ts
Felix Förtsch c5ea37aab9
All checks were successful
Build and Push Docker Image / build (push) Successful in 58s
address audit findings: schedule validation, settings json guard, pipeline types, a11y labels
2026-04-13 15:48:55 +02:00

120 lines
4.4 KiB
TypeScript

import { getConfig, setConfig } from "../db";
export interface ScheduleWindow {
enabled: boolean;
start: string; // "HH:MM" — 24h
end: string; // "HH:MM" — 24h
}
export interface ScheduleConfig {
job_sleep_seconds: number;
scan: ScheduleWindow;
process: ScheduleWindow;
}
type WindowKind = "scan" | "process";
const DEFAULTS: Record<WindowKind, ScheduleWindow> = {
scan: { enabled: false, start: "01:00", end: "07:00" },
process: { enabled: false, start: "01:00", end: "07:00" },
};
function readWindow(kind: WindowKind): ScheduleWindow {
return {
enabled: getConfig(`${kind}_schedule_enabled`) === "1",
start: getConfig(`${kind}_schedule_start`) ?? DEFAULTS[kind].start,
end: getConfig(`${kind}_schedule_end`) ?? DEFAULTS[kind].end,
};
}
function writeWindow(kind: WindowKind, w: Partial<ScheduleWindow>): void {
if (w.enabled != null) setConfig(`${kind}_schedule_enabled`, w.enabled ? "1" : "0");
if (w.start != null) setConfig(`${kind}_schedule_start`, w.start);
if (w.end != null) setConfig(`${kind}_schedule_end`, w.end);
}
export function getScheduleConfig(): ScheduleConfig {
return {
job_sleep_seconds: Number.parseInt(getConfig("job_sleep_seconds") ?? "0", 10),
scan: readWindow("scan"),
process: readWindow("process"),
};
}
const HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
function validateWindow(kind: WindowKind, w: Partial<ScheduleWindow>): void {
if (w.start != null && !HHMM.test(w.start)) {
throw new Error(`${kind}.start must be HH:MM 24h, got ${JSON.stringify(w.start)}`);
}
if (w.end != null && !HHMM.test(w.end)) {
throw new Error(`${kind}.end must be HH:MM 24h, got ${JSON.stringify(w.end)}`);
}
}
export function updateScheduleConfig(updates: Partial<ScheduleConfig>): void {
if (updates.job_sleep_seconds != null) {
const n = updates.job_sleep_seconds;
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0 || n > 86_400) {
throw new Error(`job_sleep_seconds must be an integer in [0, 86400], got ${JSON.stringify(n)}`);
}
setConfig("job_sleep_seconds", String(n));
}
if (updates.scan) {
validateWindow("scan", updates.scan);
writeWindow("scan", updates.scan);
}
if (updates.process) {
validateWindow("process", updates.process);
writeWindow("process", updates.process);
}
}
function parseTime(hhmm: string): number {
const [h, m] = hhmm.split(":").map(Number);
return h * 60 + m;
}
function isInWindow(w: ScheduleWindow): boolean {
if (!w.enabled) return true;
const now = new Date();
const minutes = now.getHours() * 60 + now.getMinutes();
const start = parseTime(w.start);
const end = parseTime(w.end);
// Overnight windows (e.g. 23:00 → 07:00) wrap midnight.
if (start <= end) return minutes >= start && minutes <= end;
return minutes >= start || minutes <= end;
}
function msUntilWindow(w: ScheduleWindow): number {
const now = new Date();
const minutes = now.getHours() * 60 + now.getMinutes();
const start = parseTime(w.start);
if (minutes < start) return (start - minutes) * 60_000;
return (24 * 60 - minutes + start) * 60_000;
}
function waitForWindow(w: ScheduleWindow): Promise<void> {
if (isInWindow(w)) return Promise.resolve();
return new Promise((resolve) => setTimeout(resolve, msUntilWindow(w)));
}
// ─── Scan window ─────────────────────────────────────────────────────────────
export const isInScanWindow = () => isInWindow(readWindow("scan"));
export const msUntilScanWindow = () => msUntilWindow(readWindow("scan"));
export const nextScanWindowTime = () => readWindow("scan").start;
export const waitForScanWindow = () => waitForWindow(readWindow("scan"));
// ─── Process window ──────────────────────────────────────────────────────────
export const isInProcessWindow = () => isInWindow(readWindow("process"));
export const msUntilProcessWindow = () => msUntilWindow(readWindow("process"));
export const nextProcessWindowTime = () => readWindow("process").start;
export const waitForProcessWindow = () => waitForWindow(readWindow("process"));
/** Sleep for the configured duration between jobs. */
export function sleepBetweenJobs(): Promise<void> {
const seconds = Number.parseInt(getConfig("job_sleep_seconds") ?? "0", 10);
if (seconds <= 0) return Promise.resolve();
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}