+ ⚠ this page is slated for removal. the hive no longer fires these
+ schedules — they live at swarm level now: recreate each one with
+ swarmctl schedule add (see
+ docs/swarm/schedules.md).
+
- all schedules currently in the table. fill the bottom row to queue a
- new schedule (operator-direct, no approval gate; recurring when an
- interval is set). cancel a single target with the row button or the
- whole schedule with ✕ cancel all.
+ read-only: every schedule still in the hive's table, kept so it can
+ be copied over.
loading…
diff --git a/frontend/packages/dashboard/src/schedules.js b/frontend/packages/dashboard/src/schedules.js
index 611133a1..375da1bf 100644
--- a/frontend/packages/dashboard/src/schedules.js
+++ b/frontend/packages/dashboard/src/schedules.js
@@ -1,42 +1,23 @@
-// Dashboard SCH3DUL3S tab — scheduled-prompts.
+// Dashboard SCH3DUL3S tab — read-only list of the scheduled prompts left
+// in the hive's store, so the operator can recreate them with
+// `swarmctl schedule`. Nothing on the hive fires or edits them; the list
+// is fetched from `/api/schedules` on tab activation and state loads.
//
-// The operator's scheduled prompts: the list, the inline create row,
-// the per-row edit form, and the fire-now / cancel actions. Live updates
-// arrive via the `schedules_changed` dashboard event (wired into the
-// entry's mutation dispatch table); the tab-activation re-fetch is the
-// cold-load / reconnect recovery path.
-//
-// Owns its own module state (the schedules list + edit-in-progress
-// tracking); reads the shared agent roster from state.js for the
-// target-chip pickers. Render + format helpers come from util.js.
+// Owns its own module state (the schedules list); reads the shared agent
+// roster from state.js for the per-agent columns. Render + format helpers
+// come from util.js.
//
// Note: per-agent reminders live in the in-container store, not here —
// they surface via get_loose_ends / the todos pill on the agent page.
import { $ } from "./common.js";
import { el } from "@hive/shared/dom.js";
-import { themedConfirm, themedToast } from "@hive/shared/modal.js";
import { paintAtomic, epochSec, fmtAgo, fmtDuration } from "./util.js";
import { containersState } from "./state.js";
-import { asyncBtn } from "@hive/shared/forms.js";
-// ─── scheduled prompts ─────────────────────────────────────────────────
-// Backend exposes `/api/schedules` (snapshot), `/api/schedules`
-// (POST, operator-direct submit), `/api/schedules/{id}/cancel`
-// (whole or per-target), `/api/schedules/{id}` (PATCH edit),
-// `/api/schedules/{id}/fire-now` (POST). Mutations now emit a
-// `schedules_changed` SSE event so the list updates live;
-// `applySchedulesChanged` handles it. Tab-activation re-fetch kept as
-// a safety net for approval-path inserts and disconnect windows.
+
// Local cache lets `refreshTabCounts` show the active count without
// re-fetching every second.
let schedulesState = [];
-// Schedule ids whose inline edit form is currently open. The set
-// survives across refreshSchedules() calls so a state
-// poll doesn't yank the form out from under the operator. Per-id
-// mid-edit carry sits in `scheduleEditCarry` so unsaved typing
-// also rides the refresh.
-const editingSchedules = new Set();
-const scheduleEditCarry = new Map();
export async function refreshSchedules() {
const listRoot = $("schedules-section");
if (!listRoot) return;
@@ -75,283 +56,17 @@ export function activeScheduleCount() {
}
return n;
}
-// Label + caption wrapper shared by every field on the schedule
-// forms — `
`. Variadic
-// children land inside the label after the caption span, which is
-// what every caller wants (the input + any preset/parts/preview
-// sub-rows live next to the caption).
-function scheduleField(labelText, ...children) {
- return el(
- "label",
- { class: "schedule-field" },
- el("span", { class: "schedule-field-label" }, labelText),
- ...children,
- );
-}
-
-// Interval composer — shared between the new-schedule form and
-// the edit-schedule form. Builds the preset chip row +
-// d/h/m/s sub-fields + live preview, and returns helpers to read
-// and write the value.
-//
-// The composer carries no opinion on what "0 / blank" means at the
-// semantic layer: it just reports the integer total. Callers decide
-// whether to treat 0 as `null` (one-shot) for their own submit path.
-function buildIntervalComposer({
- label = "interval (blank / all-zero = one-shot)",
- namePrefix = "interval_",
- initialSeconds = 0,
-} = {}) {
- const wrapper = scheduleField(label);
- const presets = [
- ["1m", 60],
- ["5m", 300],
- ["15m", 900],
- ["30m", 1800],
- ["1h", 3600],
- ["6h", 21600],
- ["12h", 43200],
- ["1d", 86400],
- ["7d", 604800],
- ];
- const presetsRow = el("div", { class: "schedule-interval-presets" });
- for (const [presetLabel, secs] of presets) {
- presetsRow.append(
- el(
- "button",
- {
- type: "button",
- class: "btn btn-interval-preset",
- "data-secs": String(secs),
- },
- presetLabel,
- ),
- );
- }
- presetsRow.append(
- el(
- "button",
- {
- type: "button",
- class: "btn btn-interval-preset btn-interval-oneshot",
- "data-secs": "0",
- },
- "one-shot",
- ),
- );
- wrapper.append(presetsRow);
-
- const partsRow = el("div", { class: "schedule-interval-parts" });
- const mkPart = (suffix, unit) => {
- const inp = el("input", {
- type: "number",
- name: namePrefix + suffix,
- min: "0",
- step: "1",
- placeholder: "0",
- class: "schedule-interval-num",
- });
- partsRow.append(inp, el("span", { class: "schedule-interval-unit" }, unit));
- return inp;
- };
- const dInp = mkPart("d", "d");
- const hInp = mkPart("h", "h");
- const mInp = mkPart("m", "m");
- const sInp = mkPart("s", "s");
- wrapper.append(partsRow);
-
- const preview = el("div", {
- class: "schedule-interval-preview",
- "aria-live": "polite",
- });
- wrapper.append(preview);
-
- function getSeconds() {
- const n = (v) => {
- const x = parseInt(v, 10);
- return Number.isFinite(x) && x > 0 ? x : 0;
- };
- return (
- n(dInp.value) * 86400 +
- n(hInp.value) * 3600 +
- n(mInp.value) * 60 +
- n(sInp.value)
- );
- }
- function fillFromSeconds(total) {
- const t = Math.max(0, Math.floor(total));
- const d = Math.floor(t / 86400);
- const h = Math.floor((t % 86400) / 3600);
- const m = Math.floor((t % 3600) / 60);
- const s = t % 60;
- dInp.value = d > 0 ? String(d) : "";
- hInp.value = h > 0 ? String(h) : "";
- mInp.value = m > 0 ? String(m) : "";
- sInp.value = s > 0 ? String(s) : "";
- }
- function setParts({ d, h, m, s }) {
- if (d !== undefined) dInp.value = d;
- if (h !== undefined) hInp.value = h;
- if (m !== undefined) mInp.value = m;
- if (s !== undefined) sInp.value = s;
- }
- function updatePreview() {
- const total = getSeconds();
- preview.textContent =
- total > 0
- ? "↻ every " + fmtDuration(total)
- : "one-shot (fires once at first-fire time)";
- preview.classList.toggle("schedule-interval-preview-oneshot", total === 0);
- }
- for (const inp of [dInp, hInp, mInp, sInp]) {
- inp.addEventListener("input", updatePreview);
- }
- for (const b of presetsRow.querySelectorAll("button[data-secs]")) {
- b.addEventListener("click", () => {
- fillFromSeconds(parseInt(b.getAttribute("data-secs"), 10) || 0);
- updatePreview();
- });
- }
- if (initialSeconds > 0) fillFromSeconds(initialSeconds);
- updatePreview();
-
- return { wrapper, getSeconds, fillFromSeconds, setParts, updatePreview };
-}
-
-// Parse the d/h/m/s sub-fields of a `buildIntervalComposer` instance
-// out of submitted FormData. Returns total seconds, or NaN if any
-// field carries a non-integer / negative value — the caller alerts
-// and bails. Total `0` means "one-shot" at the call site (wire is
-// `interval_seconds: null`). `namePrefix` matches the prefix passed
-// to `buildIntervalComposer` (e.g. `interval_` or `edit_interval_`).
-function intervalSecondsFromFormData(fd, namePrefix) {
- const part = (suffix, mult) => {
- const raw = String(fd.get(namePrefix + suffix) || "").trim();
- if (!raw) return 0;
- const n = parseInt(raw, 10);
- if (!Number.isFinite(n) || n < 0) return NaN;
- return n * mult;
- };
- return part("d", 86400) + part("h", 3600) + part("m", 60) + part("s", 1);
-}
-
-// Targets multi-select chip box — shared between the new-schedule
-// form and the edit-schedule form. Same DOM shape, same candidate
-// list (containers + root); only the chip element id
-// prefix and checkbox field name vary. Used to be inlined twice in
-// near-identical 18-line blocks; consolidated here so a future
-// change (new chip kind, candidate-list source swap, etc.) lives in
-// one place. Returns the wrapping `