feat(sse): live library count badges via SSE library.counts events (bookshelf-t3z2w.4) #1231
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-t3z2w.4"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
library.countsSSE event kind, emitted per-user (multi-user isolated viaTargetUserID) whenever a library's book count changes — on scan completion (from inside theRunScanactivity, replay-safe) and on library creation (fromcreateHandler)GetLibraryUserIDssqlc query (reverse lookup: library→entitled user IDs),CountBooksInLibraryquery,sse.NotifyLibraryCountscurried helper (fail-closed: errors logged and dropped)library_counts_push_controller.jsStimulus controller mounts on the sidebar<ul>, listens to/eventsSSE, and updates.nav-countbadges in-place on matching<li data-library-id>elements; abbrev mirrors the Gotmpl.Abbrevfunction exactlyTest plan
make test— all Go packages greennpm run coverage— 100% statements + branchesgo build ./...— clean compilemake lint— zero new issues in this worktreeCloses bead bookshelf-t3z2w.4 on merge.
- Add GetLibraryUserIDs sqlc query (reverse lookup: library→user IDs) - Add CountBooksInLibrary sqlc query (per-library book count for SSE payload) - Add KindLibraryCounts event constant + LibraryCountsPayload struct to sse/event.go - Add sse.NotifyLibraryCounts: fetches per-library user IDs and book count, publishes a TargetUserID-routed event to each entitled user; fail-closed (errors logged and dropped, never fail the caller) - Wire notifyLibraryCounts into BuildScanDeps (called from activity internals, replay-safe) and into library.createHandler (library-create path) - Move sseHub creation before the wfengine block so the scan notifier can reference sseHub.Publish at wiring time - Add NotifyLibraryCounts field to appwire.Deps and library.Deps; nil-safe in both the standalone worker binary and test stubs - Add library_counts_push_controller.js Stimulus controller: subscribes to /events SSE, handles library.counts events, updates sidebar .nav-count badges live (find-by-data-library-id, create/update/remove span, abbrev number format) - Add data-controller="library-counts-push" to sidebar <ul>, data-library-id="{{.ID}}" to each library <li> - 100% Go + JS coverage; 7 Go + 17 JS specs added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>cc085d2d01aa73cda60eSecurity Review — PR #1231 (SSE slice .4: live library counts)
Adversarial focus: multi-user leak (push + reload), hub routing default-deny, DoS, PII.
Multi-user isolation — VERIFIED CLEAN.
NotifyLibraryCounts(internal/sse/library_counts.go) fans out only toGetLibraryUserIDs=SELECT user_id FROM user_library_mapping WHERE library_id=?, and publishes each event withTargetUserID: uid. The hub's unchangedmatches()routes aTargetUserID != 0event only tosub.userID == TargetUserID, default-deny for everything else.GetUserLibraryIDs=SELECT library_id FROM user_library_mapping WHERE user_id=?(internal/app/app.go:685-712), fail-closed on empty mapping. The push scope is the exact inverse of the reload scope over the sameuser_library_mappingtable, so a user receives a live count for library L iff their sidebar renders L. No cross-user leak; no broadcast of per-library counts.userIDon the SSE stream comes fromusers.ClaimsFromContext(session), never a request param (internal/sse/handler.go:31,app.gosseExtractClaims). A strayuser_id=0mapping row would route to nobody (fail-closed).Hub routing (#2):
matches()is unchanged by this diff and remains DEFAULT-DENY. No fail-open regression.DoS (#3):
NotifyLibraryCountsfires once per scan-completion (activity internals — replay-safe, correctly not in the workflow body) and once per library-create (admin-gated). It runs 2 queries + N non-blocking publishes (N bounded by users mapped to the library), not per-SSE-event or per-connection. Subscriptions are capped (512 global / 16 per-user). No cheap DoS vector.Secrets/PII (#4):
LibraryCountsPayloadcarries only{library_id, count}integers. Clean.XSS/CSP: controller writes
badge.textContent(numeric, no HTML injection);library_idis a server-controlled int64. No inlinestyle=. Clean.[MINOR] internal/sse/library_counts_test.go:52 — odd trailing actual in assertion
Expect(published, nil).To(HaveLen(2))passes a spuriousnilsecond actual (asserts nil-is-zero, meaningless here). UseExpect(published).To(HaveLen(2)). No correctness impact.REVIEW VERDICT: 0 blocker, 0 major, 1 minor
Sidebar screenshot (bookshelf-t3z2w.4)
Left-nav sidebar showing library count badges rendered from DB counts (Fiction: 7, Non-Fiction: 3). The
library-counts-pushStimulus controller connects anEventSourceto/eventsand updates these badges live when SSElibrary.countsevents arrive.Sidebar screenshot (bookshelf-t3z2w.4)
Left-nav sidebar showing library count badges rendered from DB counts (Fiction: 7, Non-Fiction: 3). The
library-counts-pushStimulus controller connects anEventSourceto/eventsand updates these badges live when SSElibrary.countsevents arrive.1eb91fa9a9c6804f7fecSidebar screenshot (bookshelf-t3z2w.4)
Left-nav sidebar showing library count badges rendered from DB counts (Fiction: 7, Non-Fiction: 3). The
library-counts-pushStimulus controller connects anEventSourceto/eventsand updates these badges live when SSElibrary.countsevents arrive.c6804f7fecfd31f1c675