diff options
| author | Adam Malczewski <[email protected]> | 2026-05-21 00:07:03 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-21 00:07:03 +0900 |
| commit | 41f681d23491f0ba090afae053f0232743703619 (patch) | |
| tree | af2fdbf8cd0f9d6de9784f1ffb74c2f0522e9d28 /packages/api/src | |
| parent | 1f4776e6891348d2dbdcbbf704c0a5901b008ecf (diff) | |
| download | dispatch-41f681d23491f0ba090afae053f0232743703619.tar.gz dispatch-41f681d23491f0ba090afae053f0232743703619.zip | |
fix: backend wake scheduler with atomic toggle API, American time display
- Replaced POST /wake-schedule (full-replace) with
POST /wake-schedule/toggle (atomic single-hour toggle)
to eliminate race conditions between frontend and scheduler
- Recursive setTimeout prevents overlapping wake executions
- HMR-safe via global timer reference
- Frontend now uses toggle endpoint instead of full schedule POST
- Display shows reset time (wake hour + 5h) in American 12h format
e.g. '8:15 → Reset at 1:00 PM' instead of European 24h
Diffstat (limited to 'packages/api/src')
| -rw-r--r-- | packages/api/src/routes/models.ts | 79 |
1 files changed, 76 insertions, 3 deletions
diff --git a/packages/api/src/routes/models.ts b/packages/api/src/routes/models.ts index 79f25cc..f29d236 100644 --- a/packages/api/src/routes/models.ts +++ b/packages/api/src/routes/models.ts @@ -336,11 +336,14 @@ modelsRoutes.get("/key-usage", async (c) => { } }); -// Wake all Claude accounts by sending "hi" to haiku -modelsRoutes.post("/wake", async (c) => { +// ─── Shared wake function ───────────────────────────────────── + +async function wakeAllClaudeAccounts(): Promise< + Array<{ label: string; ok: boolean; error?: string }> +> { const accounts = discoverClaudeAccounts(); if (accounts.length === 0) { - return c.json({ error: "no Claude accounts available" }, 502); + return [{ label: "(none)", ok: false, error: "no Claude accounts available" }]; } const results: Array<{ label: string; ok: boolean; error?: string }> = []; @@ -376,5 +379,75 @@ modelsRoutes.post("/wake", async (c) => { } } + return results; +} + +modelsRoutes.post("/wake", async (c) => { + const results = await wakeAllClaudeAccounts(); return c.json({ results }); }); + +// ─── Wake scheduler (runs on backend, survives frontend close) ─ + +type WakeSchedule = Record<number, number>; // hour → target timestamp (ms) + +let wakeSchedule: WakeSchedule = {}; + +// HMR-safe: clear previous tick before starting a new one +(globalThis as Record<string, unknown>)._dispatchWakeTimer ??= undefined; +const timerKey = "_dispatchWakeTimer"; + +async function schedulerTick(): Promise<void> { + const now = Date.now(); + const hours = Object.keys(wakeSchedule).map(Number); + + for (const hour of hours) { + const ts = wakeSchedule[hour]; + if (ts !== undefined && ts <= now) { + // Delete BEFORE wake to prevent duplicate triggers + delete wakeSchedule[hour]; + wakeAllClaudeAccounts().catch(() => {}); + } + } + + // Schedule next tick + if (Object.keys(wakeSchedule).length > 0) { + (globalThis as Record<string, unknown>)[timerKey] = setTimeout(schedulerTick, 30_000); + } +} + +export function startWakeScheduler(): void { + // Clear any previous interval (HMR-safe) + const prev = (globalThis as Record<string, unknown>)[timerKey]; + if (typeof prev === "number") clearTimeout(prev); + schedulerTick(); +} + +modelsRoutes.post("/wake-schedule/toggle", async (c) => { + const body = await c.req.json<{ hour?: number; timestamp?: number }>(); + const hour = body.hour; + if (typeof hour !== "number" || !Number.isFinite(hour) || hour < 0 || hour > 23) { + return c.json({ error: "hour must be a number 0-23" }, 400); + } + + if (wakeSchedule[hour] !== undefined) { + // Delete + delete wakeSchedule[hour]; + } else { + // Add — require a future timestamp + const ts = body.timestamp; + if (typeof ts !== "number" || ts <= Date.now()) { + return c.json({ error: "timestamp must be a future Unix ms value" }, 400); + } + wakeSchedule[hour] = ts; + } + + // Restart the tick loop (handles empty → non-empty or vice versa) + startWakeScheduler(); + + return c.json({ schedule: wakeSchedule }); +}); + +modelsRoutes.get("/wake-schedule", (c) => { + return c.json({ schedule: wakeSchedule }); +}); |
