Adds a router middleware that extracts the X-Session-Tag header from
each request and decorates the request context via
project_events.WithSessionTag. Service- and model-layer publishers
then read it back via project_events.SessionTagFromContext and
attach it to outgoing CardMoved / CardLinked / CardUnlinked events.
The originating browser tab compares the incoming session_tag to
its own and skips the echo, avoiding double-application of the
optimistic local update. Other tabs see no tag match and apply the
event normally.
Wired into both the web router chain (before Contexter so the base
context inherits the tag) and the API router chain (before
APIContexter for the same reason).
The project board view now opens a SharedWorker EventSource
subscription scoped to project-board.{id} and patches the DOM in
response to incoming events:
- card.moved: relocates the card to the destination column and
refreshes both column count badges; falls back to a column refetch
when the card isn't currently rendered (filtered out / new).
- card.linked: refetches the destination column's issue list and
updates the count badge.
- card.unlinked: removes the card and updates the badge.
- column.created: page reload (rare event, simplest path).
- column.updated: in-place title + color/contrast updates.
- column.deleted: removes the column element.
- column.reordered: re-attaches columns in the new sort order.
- project.updated: updates the header title + description text.
- project.deleted: navigates up to the projects listing.
The board template now exposes data-project-id, data-project-scope
(repo/user/org), data-project-owner, and data-project-repo so the
subscriber can build the right column-issues refetch URL.
Each mutation request the page makes also carries a
crypto.randomUUID-generated X-Session-Tag header; the receiving
handler compares it against the incoming payload's session_tag to
suppress own-tab echoes (the optimistic local update is already
authoritative).
Wrap the model-layer column/project/issue mutation funcs in service-layer
helpers (CreateColumn, EditColumn, DeleteColumn, ReorderColumns,
DeleteProject, AssignOrRemoveProjects) that publish the matching SSE
event after the underlying call succeeds.
Routers (web + REST) are migrated to call these service helpers so the
publish side-effects fire uniformly across repo, user, and org scopes.
DeleteColumn snapshots the column's issues before deletion and emits
one CardMoved per affected issue (alongside the ColumnDeleted event)
so the receiving tab can patch the DOM without a full reload.
Move-issue publishing fires after the txn commits so we never emit
events for moves that get rolled back.
New services/project_events package marshals typed payloads to JSON,
wraps them in SSE events named project-board.{project_id}, and fans
them out via the eventsource manager to every connected user that
has read access to the project. Each Publish* helper runs the
broadcast in a goroutine so request handlers stay responsive.
Includes WithSessionTag / SessionTagFromContext for propagating an
X-Session-Tag value down to the publisher (so the originating browser
tab can suppress its own echo).
Unit tests cover event-name format, payload JSON shape, session-tag
propagation, the connected-uids access filter, and the broadcast
fan-out path.