Org audit/activity log: member, team, and repo events in one place #364

Closed
opened 2026-09-12 00:49:04 +00:00 by crueber · 3 comments
Owner

Survey: crueber/walhub#349 candidate 6.

Evidence

  • Activity events exist but are repo-scoped: repos///collab-events/.json (internal/notify/activity.go) keyed per repo for webhook/tray fan-out. No org-level aggregation exists — an org owner cannot see member adds/removals, team changes, repo creates, or invite grants in one place.
  • GitHub orgs have an audit log; the collab-events machinery + SSE envelope is the natural substrate.

Design

  • Org activity surface (API + UI, e.g. org settings tab or /:org/activity) aggregating member/team/repo/invite events across the org's repos plus org-object events (PutOrg, SetMember, team CRUD). Decide retention/caps up front (orgs with many repos x per-repo streams).

Acceptance criteria

  • Org owners can page recent org-level events (member, team, repo-create, invite) via API + UI.
  • Retention/cap policy documented; no LIST-on-hot-path (law 4/6 respected).
  • SSE/live update if cheap via the existing envelope, else documented as polling.
Survey: crueber/walhub#349 candidate 6. ## Evidence - Activity events exist but are repo-scoped: repos/<o>/<r>/collab-events/<seq>.json (internal/notify/activity.go) keyed per repo for webhook/tray fan-out. No org-level aggregation exists — an org owner cannot see member adds/removals, team changes, repo creates, or invite grants in one place. - GitHub orgs have an audit log; the collab-events machinery + SSE envelope is the natural substrate. ## Design - Org activity surface (API + UI, e.g. org settings tab or /:org/activity) aggregating member/team/repo/invite events across the org's repos plus org-object events (PutOrg, SetMember, team CRUD). Decide retention/caps up front (orgs with many repos x per-repo streams). ## Acceptance criteria - [ ] Org owners can page recent org-level events (member, team, repo-create, invite) via API + UI. - [ ] Retention/cap policy documented; no LIST-on-hot-path (law 4/6 respected). - [ ] SSE/live update if cheap via the existing envelope, else documented as polling.
crueber added this to the v1 milestone 2026-09-12 00:49:04 +00:00
Author
Owner

Fix landed as PR #377 (#377): org activity surface over the #363 substrate — owner-gated API paging (GET …/activity) + live stream (…/activity/stream via the existing frame bus) + owner-only Activity tab, with the missing org-object emissions (org_created/org_updated/org_deleted/team_updated/repo_created via the wal birth hook). Ready for review; not merging per workflow.

Fix landed as PR #377 (https://git.packden.us/crueber/walhub/pulls/377): org activity surface over the #363 substrate — owner-gated API paging (`GET …/activity`) + live stream (`…/activity/stream` via the existing frame bus) + owner-only Activity tab, with the missing org-object emissions (org_created/org_updated/org_deleted/team_updated/repo_created via the wal birth hook). Ready for review; not merging per workflow.
Author
Owner

REVIEW PR #377 (fix/issue-364, commit c3ee385) — verified in scratch worktree, all green. No browser drive (API+logic change; tests + reasoning per efficiency rules).

VERIFIED (file:line on branch):

  • Substrate reuse: reads/writes the #363 orgevents/ log (internal/notify/orghooks.go:138-147), reuses repo frame bus verbatim (orghooks.go:529-532, OrgActivityFrameKind), same retention floor contract, tray page conventions. No duplication.
  • Emissions: org_created fresh-CreateOrg-only (internal/identity/orgs.go:236-240; resume/confirm return earlier, pinned by TestOrgEventOrgLifecycle); org_updated post-PutOrg-CAS (orgs.go:365); org_deleted pre-delete with orgevents/ prefix surviving DeleteOrg (orgs.go:920-933; 409-while-owning-repos guard intact); team_updated post-PutTeam-CAS (orgs.go:770); repo_created via nil-safe wal OnCreate hook (internal/wal/registry.go:270-277; composition injects observer in cmd/walhub/collab.go:244,249-272 — law 8 clean: no new wal imports, func-type seam, nil-checked both ends). User births silent (ValidOrg pre-check skips probe; GetOrg miss skips emit — TestOrgBirthObserverEmitsForOrgsOnly). Warm pushes never birth (Create-only choke; registry test pins 412-losers/open/invalid-id silence).
  • Paging (internal/notify/orgactivity.go:78-117): newest-first, exclusive after cursor, default 50/max 200, []-never-nil, gaps skipped, n+64 probe cap with short-page + more:true degrade — honest, client converges.
  • SSE: SubscribeRepo on bare org key; repo keys always carry '/' so namespaces disjoint (orgactivity.go:240). Ring replay filters org_activity only + live tail; foreign kinds dropped (TestOrgActivityStreamReplayAndLive).
  • Gating: owner-gate in handleOrg before dispatch, both lanes; 401 anon / 403 non-owner / 404 bad org / 400 bad n/after (TestOrgActivityHandlerGating). Stream POST->405.
  • Retention (orgactivity.go:126-165, wired tasks.go:720-727): min-active-org-cursor + 7d floor, 500-delete/600-scan caps, hookless compacts past floor, head survives (TestRetainOrgEvents x5 + LIST-failure-keeps-log).
  • No LIST on hot path: read = 1 head GET + exact probes; LISTs only in daily maintainer pass.
  • Budgets: sim green; push-budget test sanctions exactly the +1 cold-birth org.json GET (cmd/walhub/push_budget_test.go:265-287, isOrgBirthProbe).
  • UI/SDK: owner-only Activity tab (Org.jsx, canManage-gated like Danger), mergeOrgActivity seq-dedupe shared by prepend+append (tested), one SSE conn per mount w/ capped reconnect; SDK activity.list/stream follow the notifications.stream contract (client._controller/_request/_send + readSse, tested).
  • Docs: 06 §5.5 + §9 floor + Decisions entry appended; #363 out-of-scope clause amended (not silently overridden). Law 12 satisfied.

TESTS (scratch worktree /tmp/pr377, since removed): go test -race notify/identity/wal OK; cmd/walhub OrgBirth+push-budget OK; cover notify 95.1% / identity 95.6% / wal 95.4% (all >=95%); sim OK (12.4s); go build ./... OK; gofmt/vet clean; node --test orgs+sdk-notifications 14/14 pass; go.mod/web deps unchanged.

NOTES (non-blocking): (a) POST /activity falls through to 404 while stream POST is 405 — harmless, test-pinned. (b) retain scans restart at seq 1 each pass, so a >600-deep deleted prefix burns 600 gap-GETs/pass with zero deletes — mirrors the pre-existing repo-event contract, bounded maintainer cost. (c) ActivityTab has the usual org-switch load race (slow org-A page merging into org-B) — same pattern as sibling tabs.

MERGE RECOMMENDATION: ready to merge.

REVIEW PR #377 (fix/issue-364, commit c3ee385) — verified in scratch worktree, all green. No browser drive (API+logic change; tests + reasoning per efficiency rules). VERIFIED (file:line on branch): - Substrate reuse: reads/writes the #363 orgevents/ log (internal/notify/orghooks.go:138-147), reuses repo frame bus verbatim (orghooks.go:529-532, OrgActivityFrameKind), same retention floor contract, tray page conventions. No duplication. - Emissions: org_created fresh-CreateOrg-only (internal/identity/orgs.go:236-240; resume/confirm return earlier, pinned by TestOrgEventOrgLifecycle); org_updated post-PutOrg-CAS (orgs.go:365); org_deleted pre-delete with orgevents/ prefix surviving DeleteOrg (orgs.go:920-933; 409-while-owning-repos guard intact); team_updated post-PutTeam-CAS (orgs.go:770); repo_created via nil-safe wal OnCreate hook (internal/wal/registry.go:270-277; composition injects observer in cmd/walhub/collab.go:244,249-272 — law 8 clean: no new wal imports, func-type seam, nil-checked both ends). User births silent (ValidOrg pre-check skips probe; GetOrg miss skips emit — TestOrgBirthObserverEmitsForOrgsOnly). Warm pushes never birth (Create-only choke; registry test pins 412-losers/open/invalid-id silence). - Paging (internal/notify/orgactivity.go:78-117): newest-first, exclusive after cursor, default 50/max 200, []-never-nil, gaps skipped, n+64 probe cap with short-page + more:true degrade — honest, client converges. - SSE: SubscribeRepo on bare org key; repo keys always carry '/' so namespaces disjoint (orgactivity.go:240). Ring replay filters org_activity only + live tail; foreign kinds dropped (TestOrgActivityStreamReplayAndLive). - Gating: owner-gate in handleOrg before dispatch, both lanes; 401 anon / 403 non-owner / 404 bad org / 400 bad n/after (TestOrgActivityHandlerGating). Stream POST->405. - Retention (orgactivity.go:126-165, wired tasks.go:720-727): min-active-org-cursor + 7d floor, 500-delete/600-scan caps, hookless compacts past floor, head survives (TestRetainOrgEvents x5 + LIST-failure-keeps-log). - No LIST on hot path: read = 1 head GET + exact probes; LISTs only in daily maintainer pass. - Budgets: sim green; push-budget test sanctions exactly the +1 cold-birth org.json GET (cmd/walhub/push_budget_test.go:265-287, isOrgBirthProbe). - UI/SDK: owner-only Activity tab (Org.jsx, canManage-gated like Danger), mergeOrgActivity seq-dedupe shared by prepend+append (tested), one SSE conn per mount w/ capped reconnect; SDK activity.list/stream follow the notifications.stream contract (client._controller/_request/_send + readSse, tested). - Docs: 06 §5.5 + §9 floor + Decisions entry appended; #363 out-of-scope clause amended (not silently overridden). Law 12 satisfied. TESTS (scratch worktree /tmp/pr377, since removed): go test -race notify/identity/wal OK; cmd/walhub OrgBirth+push-budget OK; cover notify 95.1% / identity 95.6% / wal 95.4% (all >=95%); sim OK (12.4s); go build ./... OK; gofmt/vet clean; node --test orgs+sdk-notifications 14/14 pass; go.mod/web deps unchanged. NOTES (non-blocking): (a) POST /activity falls through to 404 while stream POST is 405 — harmless, test-pinned. (b) retain scans restart at seq 1 each pass, so a >600-deep deleted prefix burns 600 gap-GETs/pass with zero deletes — mirrors the pre-existing repo-event contract, bounded maintainer cost. (c) ActivityTab has the usual org-switch load race (slow org-A page merging into org-B) — same pattern as sibling tabs. MERGE RECOMMENDATION: ready to merge.
Author
Owner

Fixed by PR #377 (review clean; substrate reuse, emissions, paging/SSE/gating/retention, budgets verified), merged. Closing.

Fixed by PR #377 (review clean; substrate reuse, emissions, paging/SSE/gating/retention, budgets verified), merged. Closing.
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
crueber/walhub#364
No description provided.