Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c43ff5d80b | ||
|
|
9d9cac2f61 |
2 changed files with 44 additions and 54 deletions
|
|
@ -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 (#460 —
|
reminders surface at the bottom as a sibling list — they
|
||||||
they share enough conceptual ground to live together).
|
share enough conceptual ground to live together.
|
||||||
|
|
||||||
**N3W SCH3DUL3 / QU3U3D SCH3DUL3S** — operator-managed
|
**N3W SCH3DUL3 / QU3U3D SCH3DUL3S** — operator-managed
|
||||||
scheduled prompts (#444 / #459). **Single-table layout**
|
scheduled prompts. **Single-table layout**: each schedule is
|
||||||
(#535): each schedule is one `<tr>`; columns are
|
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,26 +308,24 @@ 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 (replaces the per-row ✕ from the
|
that one target on click
|
||||||
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 (#467 —
|
out-of-band manual pulse to every active target (recurring
|
||||||
recurring schedules keep their cadence; one-shots are
|
schedules keep their cadence; one-shots are consumed after
|
||||||
consumed after the manual fire), an `✎ edit` button expands
|
the manual fire), an `✎ edit` button expands an inline edit
|
||||||
an inline edit form as a colspan'd row directly under the
|
form as a colspan'd row directly under the schedule's row
|
||||||
schedule's row (#474 — body / description / interval /
|
(body / description / interval / next-fire / targets all
|
||||||
next-fire / targets all editable; targets are a multi-select
|
editable; targets are a multi-select diff'd against the
|
||||||
diff'd against the original active set so unchecked-was-
|
original active set so unchecked-was-active = `targets_remove`,
|
||||||
active = `targets_remove`, checked-not-originally-active =
|
checked-not-originally-active = `targets_add`; submit PATCHes
|
||||||
`targets_add`; submit PATCHes `/api/schedules/{id}`), and a
|
`/api/schedules/{id}`), and a `✕` button cancels the whole
|
||||||
`✕` button cancels the whole schedule
|
schedule (`POST /api/schedules/{id}/cancel`).
|
||||||
(`POST /api/schedules/{id}/cancel`).
|
|
||||||
|
|
||||||
The table's last row is a permanent inline creation row (#564):
|
The table's last row is a permanent inline creation row:
|
||||||
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
|
||||||
|
|
@ -347,7 +345,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 (#460).
|
operator has one place for everything time-fired.
|
||||||
|
|
||||||
### M4TR1X page (`/matrix/`, optional)
|
### M4TR1X page (`/matrix/`, optional)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -407,14 +407,9 @@ 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 (#406 step 3 — /flow.html loads
|
// bundle for that page only (flow.html loads flow.js instead).
|
||||||
// flow.js instead), but historical context: pre-split the
|
// Belt-and-suspenders for any future page that adds tabs.js
|
||||||
// `container_state_changed` SSE handler routed through
|
// without a #containers-section — matches the
|
||||||
// `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;
|
||||||
|
|
@ -1078,7 +1073,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 (#399).
|
// bundle without the dashboard's tab panes.
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
root.innerHTML = '';
|
root.innerHTML = '';
|
||||||
if (!s.tombstones || !s.tombstones.length) {
|
if (!s.tombstones || !s.tombstones.length) {
|
||||||
|
|
@ -1218,7 +1213,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 (#399).
|
// `question_resolved` SSE events route through here.
|
||||||
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);
|
||||||
|
|
@ -1272,8 +1267,7 @@ 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),
|
||||||
});
|
});
|
||||||
|
|
@ -1416,7 +1410,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 (issue #335). Renderers stamp `data-deadline` on the
|
// each second. 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.
|
||||||
|
|
@ -1553,8 +1547,7 @@ 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 = '';
|
||||||
|
|
||||||
|
|
@ -1982,7 +1975,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 from #335).
|
// (mirrors the question-TTL ticker pattern above).
|
||||||
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);
|
||||||
|
|
@ -2102,8 +2095,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 = [];
|
||||||
// #474 — schedule ids whose inline edit form is currently open.
|
// Schedule ids whose inline edit form is currently open. The set
|
||||||
// The set survives across refreshSchedules() calls so a state
|
// 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.
|
||||||
|
|
@ -2151,8 +2144,8 @@ window.marked = marked;
|
||||||
...children);
|
...children);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interval composer (#466) — shared between the new-schedule form
|
// Interval composer — shared between the new-schedule form and
|
||||||
// and the edit-schedule form (#474). Builds the preset chip row +
|
// the edit-schedule form. 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.
|
||||||
//
|
//
|
||||||
|
|
@ -2301,8 +2294,8 @@ window.marked = marked;
|
||||||
return scheduleField('targets', box);
|
return scheduleField('targets', box);
|
||||||
}
|
}
|
||||||
|
|
||||||
// #564: inline-create state — survives paintAtomic re-renders. We
|
// Inline-create carry-state — survives paintAtomic re-renders.
|
||||||
// refresh the schedules table on tab activate + after every mutation,
|
// The schedules table refreshes 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
|
||||||
|
|
@ -2362,9 +2355,9 @@ window.marked = marked;
|
||||||
function renderSchedulesList() {
|
function renderSchedulesList() {
|
||||||
const liveRoot = $('schedules-section');
|
const liveRoot = $('schedules-section');
|
||||||
if (!liveRoot) return;
|
if (!liveRoot) return;
|
||||||
// #564: capture any mid-typed inline-create state BEFORE
|
// Capture any mid-typed inline-create state BEFORE paintAtomic
|
||||||
// paintAtomic blows the row away so a refresh doesn't yank the
|
// blows the row away so a refresh doesn't yank the operator's
|
||||||
// operator's half-filled form. The carry is replayed by
|
// 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) => {
|
||||||
|
|
@ -2384,17 +2377,16 @@ window.marked = marked;
|
||||||
tbody.append(renderScheduleEditRow(s, agents));
|
tbody.append(renderScheduleEditRow(s, agents));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// #564: always-visible inline create row at the bottom of the
|
// Always-visible inline create row at the bottom of the table
|
||||||
// table — fill cells + click + to POST. Folds the old
|
// — fill cells + click + to POST. See
|
||||||
// `#schedule-new-section` form into the same surface as the
|
// docs/web-ui.md::SCH3DUL3S tab for the layout rationale.
|
||||||
// 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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// #564: inline create row. Each column carries an input matching
|
// Inline create row. Each column carries an input matching its
|
||||||
// its display semantics (datetime-local for next-fire, mini d/h/m/s
|
// 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
|
||||||
|
|
@ -2761,7 +2753,7 @@ window.marked = marked;
|
||||||
return tr;
|
return tr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// #474 — inline edit form. Renders inside the schedule row when the
|
// 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
|
||||||
|
|
@ -2832,8 +2824,8 @@ window.marked = marked;
|
||||||
form_.addEventListener('input', saveCarry);
|
form_.addEventListener('input', saveCarry);
|
||||||
form_.addEventListener('change', saveCarry);
|
form_.addEventListener('change', saveCarry);
|
||||||
|
|
||||||
// Targets multi-select (#474 fast-follow). Shares the chip-box
|
// Targets multi-select. Shares the chip-box pattern with the
|
||||||
// pattern with the new-schedule form via `buildTargetChips`;
|
// 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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue