Give the Projects sidebar its own decision-count socket
The second review of #378 (#345) found sidebar counts still went stale in four cases. With no document open there was no socket at all. A watch the server refused because GitHub was briefly unavailable was never retried. A watch sent before the catalogue loaded never covered rows that loaded later, so a decision answered in between stayed wrong. Deleting an archived child of an active parent never published the lower project total. - The sidebar now owns a socket at /ws/sidebar, admitted on the browser session and origin alone and bound to no room. Room sockets no longer carry decision counts, subscribe to repository topics, or accept watches. - sidebar:watch is additive and bounded: 50 repositories a frame, 500 document IDs per entry, 200 repositories per socket. sidebar:unwatch removes the ones the sidebar stops showing. The server checks GitHub read access and the repository node ID before subscribing, shares one check across overlapping frames, and skips the check for a repository already watched under the same name. That lets the client watch again whenever rows load, so every loaded document gets a snapshot. - The reply separates refused from unavailable. Unavailable subscribes nothing; the client watches those repositories again with every loaded document after a backoff from two seconds to a minute. A recheck that cannot reach GitHub keeps an existing subscription, and a denied one drops it. - Permanent deletion publishes sidebar:decisions with the deleted document at zero and the repository total read after the commit. - New coverage: decision-watch and client planner unit tests for limits, chunking, outage recovery, overlapping and stale replies; a service test for the deletion announcement; and browser tests for a viewer with no document open, a row loaded after the first snapshot, and deleting an archived child while a viewer stays in the parent. The reconnect test now drops the sidebar socket. - docs/channels.md, docs/authentication.md and AGENTS.md describe the sidebar socket. The archive and restore wording now matches the client: archive applies the returned total, and restore reloads the active catalogue. Assistant-model: Claude Opus 5.5 (fast) Assistant-workflow: issue-to-pr Assistant-duration: 18m converged, estimated 45m Assistant-verification: bun run fix passed: dprint fmt and oxlint --fix, 10 existing warnings, 0 errors; changes inspected Assistant-verification: bun test failed: 4077 pass, 3 skip, 29 fail, all in scripts/pr-maintenance because this host requires commit signing; GIT_CONFIG_GLOBAL=/dev/null bun test scripts/pr-maintenance passed 166/166 Assistant-verification: bun unit passed: decision-watch (frame and socket limits, outage recovery, shared checks, unwatch during a check, rename, recheck, release), sidebar-decision-watch planner (chunking, late-loaded rows, refused and unavailable, stale replies, backoff), deletion announcement Assistant-verification: bun run types passed: every workspace package and e2e Assistant-verification: bun run ci passed: dprint, oxlint, tokens, design contract after renewing the navigation-shell owner hash, design record, impeccable Assistant-verification: postgres contract passed: TEST_DATABASE_URL against a disposable postgres:17 on 127.0.0.1:55346, apps/server/src/storage/postgres 74/74 Assistant-verification: playwright e2e failed: sidebar-decision-counts, smoke and document-navigation passed 52/53, including all nine sidebar-count tests; document-navigation:322 failed at 412.125 > 412, the same result the reviewer got on origin/main Assistant-verification: playwright e2e passed: without the deletion announcement, the archived-child deletion test fails because the project total never drops User-preference: keep sidebar counts fresh across repositories with authorized socket subscriptions Co-authored-by: Alex Lavaee <lavaman131@github.com>
Parentb95c7a2
24 files changed+1384-477
StackedSplit