dashboard: edit-form target multi-select (#474 follow-up)

mara confirmed target editing on q #195 ("also let me edit targets").
Damocles wired `targets_add` / `targets_remove` onto PATCH
/api/schedules/{id} in #478. UI side: the schedule edit form's
read-only targets callout becomes a multi-select checkbox box
(same chip styling as the new-schedule form) pre-checked for
currently-active targets.

Submit logic:

- diff new selection against `originalActiveTargets` to populate
  `targets_add` (checked, not originally active) and
  `targets_remove` (originally active, now unchecked)
- only include each key in the PATCH body when non-empty
- guard against zero-target submission with a clear alert
  pointing the operator at `✕ cancel all` as the intended path
- already-active targets that aren't in the live candidate list
  (e.g. a since-destroyed container) still surface so the
  operator can intentionally drop them

Note (re-add semantics): backend's replace-on-conflict drops
the cancelled-target tombstone + history on re-add — per the
design discussion with damocles, operator intent on re-adding
reads as "fresh start, target is active again." UI copy below
the chip row reflects this.

Carry persistence extended to the `targets` array so the
checkbox state survives a state-poll re-render mid-edit;
listens on `change` in addition to `input` for checkbox events.
This commit is contained in:
iris 2026-05-26 16:18:01 +02:00
commit befe5c0523
2 changed files with 87 additions and 24 deletions

View file

@ -232,8 +232,11 @@ time, and per-target last-result. Per-row controls: a
every active target (#467 — recurring schedules keep their
cadence; one-shots are consumed after the manual fire), an
`✎ edit` button opens an inline edit form (#474 — body /
description / interval / next-fire editable, targets stay
immutable; submit PATCHes `/api/schedules/{id}`), and a
description / interval / next-fire / targets all editable;
targets are a multi-select diff'd against the original active
set so unchecked-was-active = `targets_remove`, checked-not-
originally-active = `targets_add`; submit PATCHes
`/api/schedules/{id}`), and a
`CANC3L` button cancels the whole schedule
(`POST /api/schedules/{id}/cancel`). Individual target chips
have their own cancel links. An inline creation form lets
@ -498,18 +501,19 @@ not ours.
Agent-initiated schedules go through the approval queue instead
(manager MCP `request_schedule_prompt`).
- `PATCH /api/schedules/{id}` — partial edit (#474). JSON body
`{ body?, description?, interval_seconds?, next_fire_at_unix? }`.
`{ body?, description?, interval_seconds?, next_fire_at_unix?,
targets_add?, targets_remove? }`.
Missing key = "leave alone"; explicit `null` on
`description` / `interval_seconds` clears the field (so a
recurring schedule flips to one-shot when `interval_seconds`
is sent as `null`). Targets stay immutable — cancel + new
schedule is the retarget workaround. Refuses cancelled rows;
returns the updated `WireSchedule` on success.
- `POST /api/schedules/{id}/fire-now` — operator-initiated manual
fire (#467). Runs the per-target fan-out once immediately.
Recurring schedules keep their cadence (manual fire is additive);
one-shot schedules are consumed (cancelled afterwards). Manager
surface: `fire_schedule_now(id)` MCP tool.
is sent as `null`). `targets_add` is replace-on-conflict:
re-adding a previously-cancelled target drops the tombstone
and the target starts fresh (operator intent on re-add =
"this target is active again"). `targets_remove` delegates
to the same path as `cancel_targets` — tombstones preserve
audit, parent schedule auto-cancels when no active targets
remain. Refuses cancelled rows; returns the updated
`WireSchedule` on success.
- `POST /api/schedules/{id}/cancel` — cancel a schedule. Body
`{ targets?: ["name", …] }` cancels just those recipients;
absent or empty body cancels the whole schedule.