feat: community activity log for admins (logins, page views, posts, votes, badges, review, reputation) - #1624
Open
rstrzelecki-inne-projekty wants to merge 2 commits into
Conversation
Adds an admin-only "Activity log" (Admin → Community → Activity log) that records what users do, with filters and a TSV export: - activity_log table (created on start-up), async write queue - the base queue accepts several handlers, so the event queue now feeds both the badge rules and the log - events: question/answer/comment create, update, delete, vote (with direction and cancel), accept, flag, react, comment replies - hooks where no event exists: every login (method, IP, UA), badge awards, review queue (queued / approved / rejected), every reputation change (with the object and activity that caused it), role changes, suspend / unsuspend / delete - page views sent by the UI on route change (deduplicated), kept for ACTIVITY_LOG_PAGEVIEW_DAYS days (default 90, cleaned by cron) - admin API: paged list with date range / user / action / free-text filters, action counts, TSV export (UTF-8 BOM, streamed, 100k cap) - admin UI page with date presets, user suggestions, multi-select action filter, search, export; all filters live in the URL 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0181uZ2px83HmPo3HKSEzWQR Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0181uZ2px83HmPo3HKSEzWQR
rstrzelecki-inne-projekty
pushed a commit
to rstrzelecki-inne-projekty/answer
that referenced
this pull request
Sep 16, 2026
Text columns of the export (titles, excerpts, reasons, user names) are user-influenced; a value starting with =, +, -, @, | or % would be evaluated as a formula by Excel / LibreOffice. Such cells now get a leading apostrophe; the numeric reputation delta is left as is.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed Changes
Adds an admin-only Activity log (Admin → Community → Activity log,
/admin/activity-log): one place to see what the community is doing — logins, page views, posts, votes, badges, review queue, reputation changes, moderation actions — with filters and a TSV export.We run Answer as an internal Q&A for a few hundred accountants; the admins asked "what happened today?" and there was no single answer:
activitycovers reputation-bearing actions only, logins leave justlast_login_date, review decisions live inreview, badges inbadge_award, page views nowhere. This PR records all of it in one table and gives it a screen.What is recorded
eventqueue.Service)question/answer/comment×create/update/delete, votes as*.vote_up/*.vote_down/*.vote_cancel(the vote service now addsvote_directionto the event; cancelled votes are logged directly so badge rules do not re-run),question.accept,*.flag,*.react,comment.reply(a comment answering another comment; the person answered is the target),user.update,user.shareuserRepo.UpdateLastLoginDateuser.login— every login path (password, connectors, user center) goes through it; method is derived from the request path, IP / user agent from the gin contextBadgeAwardService.Awardbadge.award(badge name key, award key)ReviewServicereview.queuedwhen a reviewer plugin sends a post to the queue (reason, submitter),review.approve/review.rejecton the moderator's decisionUserRankRepo.ChangeUserRankreputation.changefor every reputation change, with the delta and the object / activity that caused it (callers pass them viaactivity_log.WithRankContext)UserAdminServiceuser.role,user.suspend,user.unsuspend,user.deletepage.view—POST /answer/api/v1/activity-log/viewfromLayouton route change (logged-in users only, same path deduplicated within 10 s); kept forACTIVITY_LOG_PAGEVIEW_DAYSdays (default 90,0= forever), removed by a daily cronDesign notes
activity_logtable is created on start-up withSync2(no new numbered migration); writes go through a dedicated async queue so requests never wait for the log.internal/base/queuenow keeps a list of handlers instead of one —RegisterHandlerappends, every handler gets every message. The badge handler keeps working unchanged; the log is a second subscriber.ChangeUserRank) do no DB reads — on SQLite a read on another connection would wait for the write lock forever (we hit that). The activity config id is stored and resolved to its key when the log is displayed.0before insert (PostgreSQL rejects""for bigint, SQLite does not).NewActivityLogService) depends only on the repo, the comment repo and the event queue; object/user lookups used to enrich rows are attached by the admin service, because the repos that log (user, rank) sit belowobject_info/user_commonin the graph.API (admin, role 2)
GET /answer/admin/api/activity-log?from=&to=&username=&action=a,b&q=&page=&page_size=— rows enriched with actor / target, object title, question / answer ids;qmatches detail, action, ip, users (username / display name / e-mail) and object ids.GET /answer/admin/api/activity-log/actions?from=&to=— action keys with counts (for the filter).GET /answer/admin/api/activity-log/export?…—text/tab-separated-values, UTF-8 with BOM (opens in Excel), streamed in batches, capped at 100 000 rows.POST /answer/api/v1/activity-log/view {path, title}— the page-view beacon (any logged-in user).UI
Admin → Community → Activity log: date presets (Today — default, Yesterday, 7 days, 30 days, Custom from–to), user (with suggestions), action (multi-select with counts), free-text search, Export TSV; table with time, user, action badge, object link (question / answer / comment / page / profile), details, target user and reputation delta. All filters live in the URL so a view can be shared.
Tests / verification
go test ./internal/service/activity_log/ ./internal/base/queue/(event → entry mapping incl. vote direction, comment replies, page-view dedup).🤖 Generated with Claude Code
https://claude.ai/code/session_0181uZ2px83HmPo3HKSEzWQR