summaryrefslogtreecommitdiffhomepage
path: root/packages/api/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-21 00:07:03 +0900
committerAdam Malczewski <[email protected]>2026-05-21 00:07:03 +0900
commit41f681d23491f0ba090afae053f0232743703619 (patch)
treeaf2fdbf8cd0f9d6de9784f1ffb74c2f0522e9d28 /packages/api/src
parent1f4776e6891348d2dbdcbbf704c0a5901b008ecf (diff)
downloaddispatch-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.ts79
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 });
+});