Compare commits

..
2 changed files with 54 additions and 44 deletions

View file

@ -292,12 +292,12 @@ state + applied dirs; `POST /purge-tombstone/{name}`).
Anything that fires at a future time. Operator-set schedules Anything that fires at a future time. Operator-set schedules
are created inline in the table (last row); agent self-paced are created inline in the table (last row); agent self-paced
reminders surface at the bottom as a sibling list — they reminders surface at the bottom as a sibling list (#460
share enough conceptual ground to live together. they share enough conceptual ground to live together).
**N3W SCH3DUL3 / QU3U3D SCH3DUL3S** — operator-managed **N3W SCH3DUL3 / QU3U3D SCH3DUL3S** — operator-managed
scheduled prompts. **Single-table layout**: each schedule is scheduled prompts (#444 / #459). **Single-table layout**
one `<tr>`; columns are (#535): each schedule is one `<tr>`; columns are
`# | src | next | every | owner | body | …agents… | actions`. `# | src | next | every | owner | body | …agents… | actions`.
Agent columns are dynamic — `operator` + `manager` + every Agent columns are dynamic — `operator` + `manager` + every
live container + any extra name that appears as a target on live container + any extra name that appears as a target on
@ -308,24 +308,26 @@ headers tilt -45° via CSS so each column reads as a narrow
~28px strip; per-agent cells render as: ~28px strip; per-agent cells render as:
- **active target**`<button>✓</button>` that cancels just - **active target**`<button>✓</button>` that cancels just
that one target on click that one target on click (replaces the per-row ✕ from the
pre-#535 targets sub-table)
- **cancelled target** → muted `✕` glyph (no button — - **cancelled target** → muted `✕` glyph (no button —
re-adding goes through the edit form's targets multi-select) re-adding goes through the edit form's targets multi-select)
- **not a target** → empty cell - **not a target** → empty cell
Per-schedule action column: a `↯ fire now` button sends an Per-schedule action column: a `↯ fire now` button sends an
out-of-band manual pulse to every active target (recurring out-of-band manual pulse to every active target (#467
schedules keep their cadence; one-shots are consumed after recurring schedules keep their cadence; one-shots are
the manual fire), an `✎ edit` button expands an inline edit consumed after the manual fire), an `✎ edit` button expands
form as a colspan'd row directly under the schedule's row an inline edit form as a colspan'd row directly under the
(body / description / interval / next-fire / targets all schedule's row (#474 — body / description / interval /
editable; targets are a multi-select diff'd against the next-fire / targets all editable; targets are a multi-select
original active set so unchecked-was-active = `targets_remove`, diff'd against the original active set so unchecked-was-
checked-not-originally-active = `targets_add`; submit PATCHes active = `targets_remove`, checked-not-originally-active =
`/api/schedules/{id}`), and a `✕` button cancels the whole `targets_add`; submit PATCHes `/api/schedules/{id}`), and a
schedule (`POST /api/schedules/{id}/cancel`). `✕` button cancels the whole schedule
(`POST /api/schedules/{id}/cancel`).
The table's last row is a permanent inline creation row: The table's last row is a permanent inline creation row (#564):
inputs live directly in table cells (targets as checkboxes, inputs live directly in table cells (targets as checkboxes,
body textarea that expands on focus, datetime-local pre-filled body textarea that expands on focus, datetime-local pre-filled
to 5 minutes from now, mini d/h/m/s number inputs (blank or to 5 minutes from now, mini d/h/m/s number inputs (blank or
@ -345,7 +347,7 @@ button hard-deletes (`POST /cancel-reminder/{id}`) and a
`R3TRY` button re-arms one whose delivery failed `R3TRY` button re-arms one whose delivery failed
(`POST /retry-reminder/{id}`). Backed by `GET /api/reminders`. (`POST /retry-reminder/{id}`). Backed by `GET /api/reminders`.
Lives in the SCH3DUL3S tab alongside operator schedules so the Lives in the SCH3DUL3S tab alongside operator schedules so the
operator has one place for everything time-fired. operator has one place for everything time-fired (#460).
### M4TR1X page (`/matrix/`, optional) ### M4TR1X page (`/matrix/`, optional)

View file

@ -407,9 +407,14 @@ window.marked = marked;
function renderContainers(s) { function renderContainers(s) {
const root = $('containers-section'); const root = $('containers-section');
// #containers-section only exists on /index.html. tabs.js is the // #containers-section only exists on /index.html. tabs.js is the
// bundle for that page only (flow.html loads flow.js instead). // bundle for that page only (#406 step 3 — /flow.html loads
// Belt-and-suspenders for any future page that adds tabs.js // flow.js instead), but historical context: pre-split the
// without a #containers-section — matches the // `container_state_changed` SSE handler routed through
// `applyContainerStateChanged → renderContainersFromState` on
// every page that loaded the single combined bundle, and the
// guard prevented a `root is null` throw on /flow.html (#399).
// Today it's belt-and-suspenders for any future page that adds
// tabs.js without a #containers-section. Matches the
// no-op-when-target-absent convention the other renderers // no-op-when-target-absent convention the other renderers
// (renderTombstones, etc.) follow. // (renderTombstones, etc.) follow.
if (!root) return; if (!root) return;
@ -1073,7 +1078,7 @@ window.marked = marked;
const root = $('tombstones-section'); const root = $('tombstones-section');
// #tombstones-section only lives on /index.html (SYST3M tab); // #tombstones-section only lives on /index.html (SYST3M tab);
// no-op on /flow.html and any other page that loads the shared // no-op on /flow.html and any other page that loads the shared
// bundle without the dashboard's tab panes. // bundle without the dashboard's tab panes (#399).
if (!root) return; if (!root) return;
root.innerHTML = ''; root.innerHTML = '';
if (!s.tombstones || !s.tombstones.length) { if (!s.tombstones || !s.tombstones.length) {
@ -1213,7 +1218,7 @@ window.marked = marked;
// no-op when missing (#406 step 3 — only /index.html loads // no-op when missing (#406 step 3 — only /index.html loads
// tabs.js, but kept as belt-and-suspenders for any future page // tabs.js, but kept as belt-and-suspenders for any future page
// adding it without that section). `question_added` / // adding it without that section). `question_added` /
// `question_resolved` SSE events route through here. // `question_resolved` SSE events route through here (#399).
if (!root) return; if (!root) return;
root.innerHTML = ''; root.innerHTML = '';
const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19); const fmt = (n) => new Date(n * 1000).toISOString().replace('T', ' ').slice(0, 19);
@ -1267,7 +1272,8 @@ window.marked = marked;
if (q.deadline_at) { if (q.deadline_at) {
// Tag the chip with its deadline so the global 1s ticker // Tag the chip with its deadline so the global 1s ticker
// (set up just below this function) can refresh the text // (set up just below this function) can refresh the text
// without re-rendering the whole questions section. // without re-rendering the whole questions section
// (issue #335).
const ttlEl = el('span', { const ttlEl = el('span', {
class: 'q-ttl', 'data-deadline': String(q.deadline_at), class: 'q-ttl', 'data-deadline': String(q.deadline_at),
}); });
@ -1410,7 +1416,7 @@ window.marked = marked;
} }
// Single page-wide ticker that refreshes every TTL chip in place // Single page-wide ticker that refreshes every TTL chip in place
// each second. Renderers stamp `data-deadline` on the // each second (issue #335). Renderers stamp `data-deadline` on the
// chip; this just updates `textContent`, no re-render of the // chip; this just updates `textContent`, no re-render of the
// questions section. No-op when no chips are on screen, so the // questions section. No-op when no chips are on screen, so the
// cost is negligible. // cost is negligible.
@ -1547,7 +1553,8 @@ window.marked = marked;
const root = $('approvals-section'); const root = $('approvals-section');
// #approvals-section only lives on /index.html (Y3R C4LL tab); // #approvals-section only lives on /index.html (Y3R C4LL tab);
// no-op elsewhere — `approval_added` / `approval_resolved` SSE // no-op elsewhere — `approval_added` / `approval_resolved` SSE
// events route through here on every page that loads the bundle. // events route through here on every page that loads the bundle
// (#399).
if (!root) return; if (!root) return;
root.innerHTML = ''; root.innerHTML = '';
@ -1975,7 +1982,7 @@ window.marked = marked;
} }
// Tick once per second to refresh "running Xs" badges in place // Tick once per second to refresh "running Xs" badges in place
// (mirrors the question-TTL ticker pattern above). // (mirrors the question-TTL ticker pattern from #335).
setInterval(() => { setInterval(() => {
for (const span of document.querySelectorAll('.rqe-when[data-rqe-elapsed]')) { for (const span of document.querySelectorAll('.rqe-when[data-rqe-elapsed]')) {
const started = parseInt(span.dataset.rqeElapsed, 10); const started = parseInt(span.dataset.rqeElapsed, 10);
@ -2095,8 +2102,8 @@ window.marked = marked;
// Local cache lets `refreshTabCounts` show the active count without // Local cache lets `refreshTabCounts` show the active count without
// re-fetching every second. // re-fetching every second.
let schedulesState = []; let schedulesState = [];
// Schedule ids whose inline edit form is currently open. The set // #474 — schedule ids whose inline edit form is currently open.
// survives across refreshSchedules() calls so a state // The set survives across refreshSchedules() calls so a state
// poll doesn't yank the form out from under the operator. Per-id // poll doesn't yank the form out from under the operator. Per-id
// mid-edit carry sits in `scheduleEditCarry` so unsaved typing // mid-edit carry sits in `scheduleEditCarry` so unsaved typing
// also rides the refresh. // also rides the refresh.
@ -2144,8 +2151,8 @@ window.marked = marked;
...children); ...children);
} }
// Interval composer — shared between the new-schedule form and // Interval composer (#466) — shared between the new-schedule form
// the edit-schedule form. Builds the preset chip row + // and the edit-schedule form (#474). Builds the preset chip row +
// d/h/m/s sub-fields + live preview, and returns helpers to read // d/h/m/s sub-fields + live preview, and returns helpers to read
// and write the value. // and write the value.
// //
@ -2294,8 +2301,8 @@ window.marked = marked;
return scheduleField('targets', box); return scheduleField('targets', box);
} }
// Inline-create carry-state — survives paintAtomic re-renders. // #564: inline-create state — survives paintAtomic re-renders. We
// The schedules table refreshes on tab activate + after every mutation, // refresh the schedules table on tab activate + after every mutation,
// and each refresh rebuilds the DOM via `paintAtomic`. The bottom // and each refresh rebuilds the DOM via `paintAtomic`. The bottom
// create row's inputs would lose mid-typing values without this // create row's inputs would lose mid-typing values without this
// carry. `readNewScheduleCarryFromDOM` is called BEFORE every // carry. `readNewScheduleCarryFromDOM` is called BEFORE every
@ -2355,9 +2362,9 @@ window.marked = marked;
function renderSchedulesList() { function renderSchedulesList() {
const liveRoot = $('schedules-section'); const liveRoot = $('schedules-section');
if (!liveRoot) return; if (!liveRoot) return;
// Capture any mid-typed inline-create state BEFORE paintAtomic // #564: capture any mid-typed inline-create state BEFORE
// blows the row away so a refresh doesn't yank the operator's // paintAtomic blows the row away so a refresh doesn't yank the
// half-filled form. The carry is replayed by // operator's half-filled form. The carry is replayed by
// `renderInlineCreateRow` below. // `renderInlineCreateRow` below.
readNewScheduleCarryFromDOM(liveRoot.querySelector('.schedules-table-create-row')); readNewScheduleCarryFromDOM(liveRoot.querySelector('.schedules-table-create-row'));
paintAtomic(liveRoot, (root) => { paintAtomic(liveRoot, (root) => {
@ -2377,16 +2384,17 @@ window.marked = marked;
tbody.append(renderScheduleEditRow(s, agents)); tbody.append(renderScheduleEditRow(s, agents));
} }
} }
// Always-visible inline create row at the bottom of the table // #564: always-visible inline create row at the bottom of the
// — fill cells + click to POST. See // table — fill cells + click to POST. Folds the old
// docs/web-ui.md::SCH3DUL3S tab for the layout rationale. // `#schedule-new-section` form into the same surface as the
// schedules list so creation and display share one mental model.
tbody.append(renderInlineCreateRow(agents)); tbody.append(renderInlineCreateRow(agents));
table.append(tbody); table.append(tbody);
root.append(table); root.append(table);
}); });
} }
// Inline create row. Each column carries an input matching its // #564: inline create row. Each column carries an input matching
// display semantics (datetime-local for next-fire, mini d/h/m/s // its display semantics (datetime-local for next-fire, mini d/h/m/s
// number inputs for every, textarea for body, checkbox per agent // number inputs for every, textarea for body, checkbox per agent
// column for targets). Submitting POSTs `/api/schedules` and clears // column for targets). Submitting POSTs `/api/schedules` and clears
// the carry on success; the next `refreshSchedules` redraws the // the carry on success; the next `refreshSchedules` redraws the
@ -2753,7 +2761,7 @@ window.marked = marked;
return tr; return tr;
} }
// Inline edit form. Renders inside the schedule row when the // #474 — inline edit form. Renders inside the schedule row when the
// row's `✎ edit` button is toggled on. Pre-filled with current // row's `✎ edit` button is toggled on. Pre-filled with current
// values; submit PATCHes /api/schedules/{id}. Targets stay // values; submit PATCHes /api/schedules/{id}. Targets stay
// immutable (per damocles's backend; the workaround for retargeting // immutable (per damocles's backend; the workaround for retargeting
@ -2824,8 +2832,8 @@ window.marked = marked;
form_.addEventListener('input', saveCarry); form_.addEventListener('input', saveCarry);
form_.addEventListener('change', saveCarry); form_.addEventListener('change', saveCarry);
// Targets multi-select. Shares the chip-box pattern with the // Targets multi-select (#474 fast-follow). Shares the chip-box
// new-schedule form via `buildTargetChips`; // pattern with the new-schedule form via `buildTargetChips`;
// cancelled tombstones aren't listed (re-adding them flows // cancelled tombstones aren't listed (re-adding them flows
// through `targets_add`, which the backend replace-on-conflict // through `targets_add`, which the backend replace-on-conflict
// drops the tombstone for). Submit diffs against the original // drops the tombstone for). Submit diffs against the original