diff --git a/src/settings.py b/src/settings.py index 09a53c9..ef8c162 100644 --- a/src/settings.py +++ b/src/settings.py @@ -159,6 +159,12 @@ DEFAULT_SETTINGS = { "admin_panel": "ctrl+shift+u", "cancel": "escape", }, + # Cookbook scheduler (calendar-driven serve windows). Off by default; + # the inline card in the Cookbook tab flips this. When true, the + # reconciler at src/cookbook_scheduler.py reads events from the + # designated calendar and auto-launches/kills serves. + "cookbook_scheduler_enabled": False, + "cookbook_schedule_calendar_href": "", } DEFAULT_FEATURES = { diff --git a/static/js/cookbookSchedule.js b/static/js/cookbookSchedule.js index 6a5cd82..c0f612a 100644 --- a/static/js/cookbookSchedule.js +++ b/static/js/cookbookSchedule.js @@ -241,4 +241,175 @@ // Also re-check whenever a serve panel expands. const obs = new MutationObserver(() => refreshScheduleButtonVisibility()); obs.observe(document.body, { childList: true, subtree: true }); + + // ── Settings card injected at the top of the Cookbook tab ───────────── + // Lives here (not in settings.js) so the whole feature is in one file. + // When you delete cookbookSchedule.js, this UI vanishes with it. + + async function fetchSettings() { + try { + const r = await fetch("/api/auth/settings", { credentials: "same-origin" }); + if (!r.ok) return {}; + return await r.json(); + } catch (_) { return {}; } + } + + async function saveSettings(body) { + try { + const r = await fetch("/api/auth/settings", { + method: "POST", credentials: "same-origin", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + return r.ok; + } catch (_) { return false; } + } + + async function fetchCalendars() { + try { + const r = await fetch("/api/calendar/calendars", { credentials: "same-origin" }); + if (!r.ok) return []; + const d = await r.json(); + // Endpoint shape varies — accept either an array or { calendars: [...] }. + const list = Array.isArray(d) ? d : (d.calendars || []); + return list.map(c => ({ + href: c.href || c.url || c.id || "", + name: c.display_name || c.name || c.summary || c.href || "Calendar", + })).filter(c => c.href); + } catch (_) { return []; } + } + + async function fetchUpcoming() { + try { + const r = await fetch("/api/cookbook/schedule/upcoming?hours=24", { credentials: "same-origin" }); + if (!r.ok) return null; + return await r.json(); + } catch (_) { return null; } + } + + function buildCardHtml(s, calendars, upcoming) { + const enabled = !!s.cookbook_scheduler_enabled; + const calHref = s.cookbook_schedule_calendar_href || ""; + const events = (upcoming && upcoming.events) || []; + const running = events.filter(e => e.status === "running" || e.status === "adopted").length; + const skipped = events.filter(e => e.status === "skipped" || e.status === "failed").length; + let statusLine = ""; + if (!enabled) { + statusLine = "Scheduler is off. Toggle on to start launching models on a schedule."; + } else if (!calHref) { + statusLine = "Pick a calendar — events on it become serve windows."; + } else if (events.length === 0) { + statusLine = "Enabled. No scheduled windows in the next 24h."; + } else { + const parts = [`${events.length} scheduled in next 24h`]; + if (running) parts.push(`${running} running now`); + if (skipped) parts.push(`${skipped} skipped`); + statusLine = parts.join(" · "); + } + const calOptions = [''] + .concat(calendars.map(c => ``)) + .join(""); + return ` +