Files
pad/internal/events
xarmian dfd3811eee feat(api): bulk-mutation endpoint + single SSE batch event (TASK-1668) (#669)
* feat(api): bulk-mutation endpoint + single SSE batch event (TASK-1668)

Add POST /workspaces/{ws}/items/bulk accepting item IDs + a verb
(archive, move, tag, untag, set-priority, assign). The lane-header
bulk actions operate on a whole filtered lane, so the endpoint emits
ONE items_bulk_updated SSE event and ONE item.bulk_updated webhook for
the batch instead of per-item fan-out.

Reuses the existing store paths (UpdateItemWithPreCheck / MoveItem /
DeleteItem) rather than re-implementing writes; the open-children
guard runs per status-bearing move exactly as the single PATCH path
does (force-overridable). Per-row failures are collected into the
response envelope (updated/failed/total) rather than aborting the
batch. Editor/owner gated.

Frontend client + TS types follow in TASK-1669; UI wiring in TASK-1672.

Parent: PLAN-1667.

* fix(api): per-item visibility + collection-move guard on bulk endpoint per Codex review (round 1)

- Enforce per-item collection visibility (checkItemVisible) in the bulk
  loop so a member with collection_access="specific" can't bulk-mutate
  items in hidden collections by guessing refs; report invisible rows as
  not-found. Also gate the move target collection on visibility.
- Route bulk collection moves through MoveItemWithPreCheck with the
  open-children guard (destination schema), closing the bypass where a
  collection move + terminal status could mark a parent terminal with
  open children. Status-only moves already ran the guard.
- Tests: status-move + collection-move guard coverage (reject + force
  override + mutation-safety).

* fix(web): consume items_bulk_updated SSE event per Codex review (round 2)

The bulk endpoint emits one items_bulk_updated event, but the SSE
service only listened for the fixed ITEM_EVENTS list — so a bulk
mutation left other tabs/sessions stale until an unrelated sync fired.
Route the batch event through the existing sync_required path: it
carries item_ids + a max seq but no per-item field payload, so an
incremental /items-changes delta reconciles every affected row by seq.
Broadcast so peer tabs reconcile too.

* fix(api): scope bulk SSE event per-collection, drop item_ids per Codex review (round 3)

The batch event published with an empty Collection, which the SSE
filter treats as workspace-level: restricted members received bulk
events for hidden collections (leaking item_ids/op/count) while guests
with grants were dropped entirely and stayed stale.

Emit one items_bulk_updated event per affected collection with
Collection set, so the existing visibility filter routes it like any
collection-scoped event. Drop per-item IDs from the SSE payload — a
batch can't be item-grant-filtered for guests on a broadcast bus, so
IDs would leak; recipients reconcile via the /items-changes delta,
which is visibility-filtered server-side (Seq carries the cursor). The
webhook (a trusted workspace integration) keeps the full id list.

Test asserts the event is collection-scoped and carries no item_ids.

* fix(api): bulk collection move notifies both source and target scopes per Codex review (round 4)

A cross-collection move only emitted a batch event for the target
collection, so a restricted member watching the source lane wouldn't
reconcile the item leaving it. Notify both the source and target
collection scopes for moves (still no per-item IDs). Test asserts both
events fire.

* fix(api): suppress itemless batch SSE events for item-grant-only subscribers per Codex review (round 5)

A guest/restricted member with only item-level grants in a collection
could still receive the collection-scoped items_bulk_updated event
(itemless), learning op/count/timing for items they can't see. Extract
the SSE visibility filter into sseEventVisibleFor and add a rule:
itemless collection-scoped events go only to subscribers with FULL
collection access; item-grant-only subscribers reconcile their granted
items via the next resume/reconnect /items-changes sync instead.

Adds a unit test covering the visibility matrix.

* fix(api): validate status override against target schema on bulk collection move per Codex review (round 6)

A status override on a collection move was applied after MigrateFields
but never validated against the target schema, so an out-of-options
value (e.g. status=bogus) could be written. Run ValidateFields on the
final field map before the move. Test asserts the invalid value is
rejected per-row and the item stays put.
2026-05-30 18:41:55 -04:00
..