Front-end stays in sync with all forge mutations (broadcast issue/PR events on /events) #493

Closed
opened 2026-04-27 21:48:28 +00:00 by claude-desktop · 0 comments
Collaborator

As an operator, I want the dashboard to reflect new issues, label changes, assignee changes, and PR state transitions immediately, so that I do not have to wait for the 30 s polling backstop or hit refresh after every Forgejo action.

Context

Today /events (SSE) broadcasts only task lifecycle events (task_started, task_finished, steer_queued, …). Forgejo webhook events that mutate the operator's view of the world — issues.opened, issues.assigned, issues.labeled, issues.closed, pull_request.opened/closed/labeled, pull_request_review.submitted — are processed server-side (dispatch / label routing) but never propagated to the SSE stream.

Result: when a new issue is created (manually or via the breakdown skill), the board / pipeline / watchdog screens only refresh on the 30 s polling backstop. Same for label retags, assignee swaps, PR state changes. The UI feels stale.

Acceptance criteria

Server

  • Webhook ingress (apps/server/src/http/webhook.ts + domain/workflows/event-handlers.ts) emits an SSE event for every state-mutating forge event after normalisation. Categories:
    • issue.opened, issue.closed, issue.reopened
    • issue.assigned, issue.unassigned
    • issue.labeled, issue.unlabeled
    • issue.milestoned, issue.demilestoned
    • pr.opened, pr.closed, pr.merged, pr.reopened, pr.draft_changed
    • pr.labeled, pr.unlabeled
    • pr.review_submitted
  • Event envelope: { kind: "<category>", repo: "owner/name", issue_number?: number, pr_number?: number, ts: <unix_ms> }. Same shape every consumer in the web app already understands (matches /events schema).
  • Broadcast happens after the server's own state writes commit (label routing, dispatch decisions, pr_dependencies updates), so a refetch triggered by the event sees the new state.
  • The broadcast is best-effort fire-and-forget — a slow / disconnected SSE consumer never delays the webhook response.
  • Inactive-forge events (per F4 dispatch gate) do NOT broadcast — they're already dropped at ingress.

Web app

  • Existing /events consumers (board, pipeline, watchdog, monitor) react to the new event categories by invalidating the matching React Query keys:
    • issue.*, pr.* → invalidate ["board"], ["pipeline", repo], ["watchdog"].
    • Granular invalidation where cheap (e.g. invalidate ["pipeline", repo, issue_number] instead of the whole pipeline list).
  • Polling backstop reduced from 30 s → 60 s now that SSE covers the common path. Keep polling as the recovery mechanism for missed reconnects.
  • Toast on first connection / reconnection so operators can tell SSE is healthy (only on transitions, not on every event).

Observability

  • [sse-broadcast] log line per event with kind, repo, and consumer count. Drop-rate metric on SSE backpressure.
  • Test coverage: integration test that POSTs a synthetic issues.opened webhook and asserts the corresponding SSE event lands within 100 ms.

Out of scope

  • Full optimistic-mutation client-side cache (e.g. apply issue.labeled to the local React Query cache without refetch). Invalidate-and-refetch is enough for now; hand-merging would create drift.
  • WebSocket upgrade. SSE is fine for unidirectional pushes.
  • Per-user SSE filtering (single-operator app).
  • Pre-existing task lifecycle events — no changes to those.

References

  • apps/server/src/main.ts:1617/events route handler
  • apps/server/src/domain/views/pipeline-sse.ts + tests
  • apps/server/src/http/webhook.ts — ingress
  • apps/server/src/http/webhook-normalize.ts — already produces normalised ForgeEvent shapes; this ticket consumes those
  • apps/web/src/components/app-shell.tsx — global SSE subscription
  • apps/web/src/routes/planner.index.tsx:140 — current 30 s poll comment

Dependencies

  • Independent. Can land before or after the F-series spec; helps regardless of which forge is active.
As an operator, I want the dashboard to reflect new issues, label changes, assignee changes, and PR state transitions immediately, so that I do not have to wait for the 30 s polling backstop or hit refresh after every Forgejo action. ## Context Today `/events` (SSE) broadcasts only **task lifecycle** events (`task_started`, `task_finished`, `steer_queued`, …). Forgejo webhook events that mutate the operator's view of the world — `issues.opened`, `issues.assigned`, `issues.labeled`, `issues.closed`, `pull_request.opened/closed/labeled`, `pull_request_review.submitted` — are processed server-side (dispatch / label routing) but never propagated to the SSE stream. Result: when a new issue is created (manually or via the breakdown skill), the board / pipeline / watchdog screens only refresh on the 30 s polling backstop. Same for label retags, assignee swaps, PR state changes. The UI feels stale. ## Acceptance criteria ### Server - [ ] Webhook ingress (`apps/server/src/http/webhook.ts` + `domain/workflows/event-handlers.ts`) emits an SSE event for every state-mutating forge event after normalisation. Categories: - `issue.opened`, `issue.closed`, `issue.reopened` - `issue.assigned`, `issue.unassigned` - `issue.labeled`, `issue.unlabeled` - `issue.milestoned`, `issue.demilestoned` - `pr.opened`, `pr.closed`, `pr.merged`, `pr.reopened`, `pr.draft_changed` - `pr.labeled`, `pr.unlabeled` - `pr.review_submitted` - [ ] Event envelope: `{ kind: "<category>", repo: "owner/name", issue_number?: number, pr_number?: number, ts: <unix_ms> }`. Same shape every consumer in the web app already understands (matches `/events` schema). - [ ] Broadcast happens **after** the server's own state writes commit (label routing, dispatch decisions, `pr_dependencies` updates), so a refetch triggered by the event sees the new state. - [ ] The broadcast is **best-effort fire-and-forget** — a slow / disconnected SSE consumer never delays the webhook response. - [ ] Inactive-forge events (per F4 dispatch gate) do NOT broadcast — they're already dropped at ingress. ### Web app - [ ] Existing `/events` consumers (board, pipeline, watchdog, monitor) react to the new event categories by invalidating the matching React Query keys: - `issue.*`, `pr.*` → invalidate `["board"]`, `["pipeline", repo]`, `["watchdog"]`. - Granular invalidation where cheap (e.g. invalidate `["pipeline", repo, issue_number]` instead of the whole pipeline list). - [ ] Polling backstop reduced from 30 s → 60 s now that SSE covers the common path. Keep polling as the recovery mechanism for missed reconnects. - [ ] Toast on first connection / reconnection so operators can tell SSE is healthy (only on transitions, not on every event). ### Observability - [ ] `[sse-broadcast]` log line per event with `kind`, `repo`, and consumer count. Drop-rate metric on SSE backpressure. - [ ] Test coverage: integration test that POSTs a synthetic `issues.opened` webhook and asserts the corresponding SSE event lands within 100 ms. ## Out of scope - Full optimistic-mutation client-side cache (e.g. apply `issue.labeled` to the local React Query cache without refetch). Invalidate-and-refetch is enough for now; hand-merging would create drift. - WebSocket upgrade. SSE is fine for unidirectional pushes. - Per-user SSE filtering (single-operator app). - Pre-existing task lifecycle events — no changes to those. ## References - `apps/server/src/main.ts:1617` — `/events` route handler - `apps/server/src/domain/views/pipeline-sse.ts` + tests - `apps/server/src/http/webhook.ts` — ingress - `apps/server/src/http/webhook-normalize.ts` — already produces normalised `ForgeEvent` shapes; this ticket consumes those - `apps/web/src/components/app-shell.tsx` — global SSE subscription - `apps/web/src/routes/planner.index.tsx:140` — current 30 s poll comment ## Dependencies - Independent. Can land before or after the F-series spec; helps regardless of which forge is active.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks#493
No description provided.