sx-gitea Phase 6: activity — feed timelines, dashboard, durable notifications (TDD, 520/520)

lib/gitea/activity.sx: every forge action lands as a feed activity in an
append-only persist log stream. Instrumentation is done IN the runtime —
repo-create!/issue-create!/issue-comment!/pr-create!/pr-review!/pr-merge!
are redefined around their originals, so SX callers and web handlers emit
activity with zero call-site edits (failed mutations emit nothing).

Timelines are lib/feed (APL) queries: global/repo/user, newest-first,
visibility follows repo access (private-repo activity invisible to
non-readers). Follows (user: or repo: targets) drive a dashboard of
followed actors/repos minus one's own actions.

Notifications ride lib/events durable delivery: activities after a
cursor expand to (id recipient body) messages (comment -> author+
participants, review/merge -> PR author, open-issue -> assignees, never
the actor), ev/deliver-messages runs the at-least-once digest flow, and
delivered messages file into per-user kv inboxes; the cursor advance
makes reruns no-ops.

Web: /activity + /:owner/:name/activity pages, user-activity/dashboard/
follow/notifications/notify-run JSON API. gitea/all-routes now hoists
every /api/* route ahead of the wildcard /:owner/:name patterns so later
packs can add API endpoints without being shadowed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 14:25:10 +00:00
parent 24821e3f77
commit b4fbfa5603
6 changed files with 1033 additions and 13 deletions

View File

@@ -11,8 +11,10 @@
; needs the owner (or org admin), delete and collaborator management need
; "admin". 401 = no credentials, 403 = authenticated but not allowed.
;
; Later modules (wire, issues, ...) extend the app by appending a routes
; pack to gitea/route-packs at load time; gitea/app serves them all.
; Later modules (wire, issues, pr, activity, ...) extend the app by
; appending a routes pack to gitea/route-packs at load time; gitea/app
; serves them all, with every /api/* route hoisted ahead of the wildcard
; /:owner/:name patterns so a pack can never be shadowed.
;
; Requires: lib/gitea/{repo,access}.sx, lib/dream/{types,router,middleware,
; error,html,json,auth,api}.sx
@@ -458,8 +460,6 @@
(else (dream-not-found)))))))
; ── routes ───────────────────────────────────────────────────────────
; /api/* is listed first so an owner segment can never shadow it (owner
; names matching router words are rejected by gitea/valid-name? anyway).
(define
gitea/routes
@@ -500,16 +500,21 @@
"/:owner/:name/raw/:ref/**"
(fn (req) (gitea/w-raw forge req))))))
; extension point: wire/issues/... append their packs at load time
; extension point: wire/issues/pr/activity/... append their packs at load
(define gitea/route-packs (list gitea/routes))
; every /api/* route (from any pack) dispatches before the wildcard
; /:owner/:name patterns, so later packs can add API endpoints freely
(define
gitea/all-routes
(fn
(forge)
(reduce
(fn (acc pack) (concat acc (pack forge)))
(list)
gitea/route-packs)))
(let
((rs (reduce (fn (acc pack) (concat acc (pack forge))) (list) gitea/route-packs)))
(concat
(filter (fn (r) (starts-with? (dream-route-path r) "/api/")) rs)
(filter
(fn (r) (not (starts-with? (dream-route-path r) "/api/")))
rs)))))
(define gitea/app (fn (forge) (dream-make-app (gitea/all-routes forge))))