The N3W SCH3DUL3 vertical form is gone. The schedules table now
carries an always-visible inline create row at the bottom — fill the
cells, click + to POST /api/schedules, the new schedule appears
above on the next refresh.
Per-column inputs match the display semantics:
- `next` → `<input type="datetime-local">` (defaults to now+5min)
- `every` → 4 mini d/h/m/s number inputs (blank/all-zero = one-shot)
- `body` → `<textarea rows=1>` expanding to 4em on focus-within
so multi-line prompts still fit (closes mara's "prompt may still be
multi-line" requirement)
- per-agent columns → checkbox (whole cell clickable via padded label)
- actions → + submit + ⌫ reset (clears the carry without POSTing)
`newScheduleCarry` module-scope object survives the paintAtomic
re-render so mid-typed values stick across schedule-list refreshes
(same pattern as `scheduleEditCarry` from #474).
H2 collapses from "N3W SCH3DUL3" + "QU3U3D SCH3DUL3S" down to a
single "SCH3DUL3S" since the section now does both. Empty-state
branch on `renderSchedulesList` is gone — the table always renders
because the create row is always there.
`#schedule-new-section` div + `renderScheduleNewForm` /
`readScheduleFormCarry` / `submitNewSchedule` and the
`.schedule-new-form` CSS selector all removed. The edit-form path
(#474 `renderScheduleEditForm` colspan'd row on `✎` toggle) is
unchanged — "edit mode is inline in the table" already shipped
there, this PR just adds the same mental model for create.
Validations match the old form: empty targets / empty body /
invalid datetime / non-integer interval each surface an `alert()`.
The + button shows a spinner while the POST is in flight, then
clears the carry + triggers `refreshSchedules()`.
The sticky bulk-action bar (#443) was visible on every tab whenever the
selection was non-empty. On Y3R C4LL / SYST3M / SCH3DUL3S the operator
sees a floating bar without the agent cards next to it for cross-
reference — which is what mara called out in #596.
Fix: gate the bar on `document.body.dataset.activeTab === 'swarm'` in
addition to the existing non-empty-selection check. Selection state
itself stays in memory, so the bar reappears on return to SW4RM if any
agents are still ticked. The `activateTab` hook now sets the data
attribute and re-runs `renderSelectionBar` so the toggle takes effect
on hashchange without waiting for the next SSE update.
Backend route POST /api/rebuild-queue/{id}/cancel already exists
(refuses Running / terminal entries with {cancelled: false}). This
adds the operator-facing affordance:
- small circular X button on the right edge of each row whose
state === 'queued'. Running / done / failed rows don't render
it, so the operator never clicks a button that the backend
would refuse.
- uses the same data-async + data-confirm pattern as the
reminder cancel form — global submit handler does POST +
spinner + error toast for free.
- successful cancel flips the row queued -> cancelled via the
live RebuildQueueChanged snapshot, so the button disappears
on the next paint without an explicit refresh.
CSS keeps it quiet by default (muted border, transparent
background) and lights red on hover / focus, matching the
.btn-deny family without claiming a full button-width slot
that would push the row layout around.
The journald viewer opens in the side panel, but the <pre> only takes
the height of its content — so a short log fetch leaves the bottom 80%
of the panel empty, and a long one pushes the controls past the
viewport. Wrap the body in a column-flex container that fills the
side-panel-body, keep controls fixed at the top, and let the <pre>
flex-grow into the rest of the panel with its own overflow:
.journal-body { display: flex; flex-direction: column; height: 100%; }
.journal-controls { flex: 0 0 auto; }
.journal-output { flex: 1 1 0; min-height: 0; overflow: auto; }
`min-height: 0` is the canonical "let me actually flex-shrink for
overflow" escape hatch on flex children. Also moved the auto-scroll
target from side-panel-body onto the <pre> itself — the panel-body no
longer overflows now that journal-body fills it; the <pre> is the new
scroll surface.
Pure CSS + one JS line; no DOM shape change.
Replaces the per-schedule card layout with one table:
| # | src | next | every | owner | body | …agents… | actions |
|---|-----|------|-------|-------|------|----------|---------|
| 5 | op | 5m | 10m | mara | "…" | ✓ ✓ . ✓ | ↯ ✎ ✕ |
Each schedule is one <tr> in the tbody. Agent columns are dynamic —
operator + manager + live containers + any "extra" name that appears
as a target on some schedule but isn't a current container (same
membership rule buildTargetChips uses, so the table + new/edit forms
agree on what's addressable). Per-agent cells:
- active target → <button>✓</button> that cancels just that target
on click (replaces the per-row ✕ from the old targets sub-table)
- cancelled target → muted ✕ glyph (no button; re-add flows through
the edit form's targets multi-select)
- not a target → empty cell
Agent column headers tilt -45° via CSS so each column reads as 28px
of horizontal real estate instead of the full word width. Standard
rotated-header pattern: 95px-tall <th> with position:relative, inner
<div> positioned absolute at bottom-left, transform rotates about
left-bottom.
Body cell truncates with ellipsis + full-text title. Description used
to be a separate visible block on the card layout; the table folds
it into the body cell's title to keep row height tight. If mara wants
description visible in-table it's a small follow-up — easier to
iterate on a rejection.
Edit form expands inline into a colspan'd row underneath the schedule
row it edits (instead of inside the card). Wrapper drops the form's
background so it reads as a row extension.
No backend changes; everything renders from existing schedulesState
+ containersState.
Every field on the new- and edit-schedule forms (plus buildIntervalComposer
and buildTargetChips) builds the same wrapper shape:
el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, '…'))
then appends an input. Eight identical-modulo-text sites. Extracting one
small helper:
function scheduleField(labelText, ...children) {
return el('label', { class: 'schedule-field' },
el('span', { class: 'schedule-field-label' }, labelText),
...children);
}
drops the per-site cost to a single `form_.append(scheduleField('caption',
input));` line. -10 LOC net; no behaviour change.
mara: \"look through the code for dedups, structural improvements
and so on\". Two near-identical blocks across the new-schedule and
edit-schedule forms folded into shared helpers.
## `buildTargetChips({ idPrefix, fieldName, checked, extraNames })`
Was inlined twice in 18-line blocks that built the same
`<label class="schedule-field">` + `<div class="schedule-targets">`
+ candidate-list logic (containers + operator + manager). Now one
function, two callers; `extraNames` lets the edit form keep
showing already-active targets that have vanished from the live
container list so the operator can still uncheck them
intentionally.
## `intervalSecondsFromFormData(fd, namePrefix)`
Both submit handlers had the same ~13-line d/h/m/s → total-seconds
parser (with `NaN` propagation on bad input). Pulled into one
helper next to `buildIntervalComposer`; call sites become 3 lines:
const intervalTotal = intervalSecondsFromFormData(fd, 'interval_');
if (Number.isNaN(intervalTotal)) { alert(...); return; }
const interval_seconds = intervalTotal > 0 ? intervalTotal : null;
Net -13 lines, but the bigger win is shape — when (not if) a new
schedule field surfaces, there's one chip-render path + one
interval-parser to thread it through instead of two.
Zero behaviour change. Built clean.
Frontend half of #437 — paired with damocles's #501 backend which
adds `step: Option<String>` to `QueueEntry` and annotates 9
worker phases across ApplyCommit / Spawn / MetaUpdate / Rebuild
pipelines.
When `entry.step` is present, render a cyan `↳ <step>` sub-line
below the main row in `renderQueueEntry`. Wraps to its own flex
line via `flex-basis: 100%`, indented 1.8em to align under the
state glyph + kind. Terminal transitions clear `step` on the
backend, so the sub-line just disappears on Done / Failed rows —
no client-side staleness handling needed.
CSS: `.rqe-step` keys off cyan to visually group with the
running-spinner color cue (vs the muted `.rqe-when` /
`.rqe-reason` chips). `tabular-nums` keeps the indented arrow
column stable as step strings change length mid-build.
docs/web-ui.md updated to describe the new sub-line behaviour
in the R3BU1LD QU3U3 section.
#406 was the JS-split refactor: pull shared helpers into
common.js (step 1), pull the flow-only IIFEs into flow.js
(step 2), then rename the legacy combined entry from app.js
to tabs.js (step 3 — this commit) to reflect that the bundle
now owns the dashboard tabs surface only.
What moved:
- `frontend/packages/dashboard/src/app.js` → `tabs.js`
- `build.mjs` entry: `src('app.js')` → `src('tabs.js')`;
output is now `static/tabs.js`
- `index.html` `<script>` src: `/static/app.js` →
`/static/tabs.js`; the FL0W-section comment retouched
- `flow.html` reference from `/static/app.js`-as-tabs to
`/static/tabs.js`; notif + offscreen-inbox comments rewired
to point at the correct owners (common.js for NOTIF,
flow.js for renderInbox)
- `common.js`, `flow.js`, `tabs.js` headers: scrub stale
"app.js" references; document that #406 steps 2 + 3 are
done and both pages import directly from common.js
- `dashboard.css` comments: every "by app.js" → "by tabs.js"
- `docs/web-ui.md`, `docs/conventions.md`: legacy
`assets/app.js` → `assets/tabs.js` (the path prefix is
itself stale from a much-earlier rename, but consistent
with the rest of those docs)
- `CLAUDE.md` file map: refresh the dashboard package's
src/ and dist/ layouts to reflect the post-split shape
(tabs.js, flow.js, common.js, stream-worker.js)
What did NOT move:
- `frontend/packages/agent/src/app.js` (per-agent UI entry)
stays named `app.js` — it's a separate package, has only
one entry point, no split happened there
- The two "no-op when target absent" guards in renderContainers
and renderQuestions are kept as belt-and-suspenders for any
future page that adds tabs.js without the corresponding
sections; comments updated to note this rather than the
pre-split flow.html reason
Functional behaviour identical; this is a pure rename + comment
sweep.
2026-05-26 19:39:27 +02:00
Renamed from frontend/packages/dashboard/src/app.js (Browse further)