Activity feed
The dashboard ships an Activity page at /activity (sidebar entry next to Home, shown when no project is selected). It is a cross-project timeline of audit-log rows, scoped to the projects whose audit trail the user may read, polling every five seconds.
What appears in the feed
Section titled “What appears in the feed”Every row written to the audit log shows up: command issuance, task transitions, subscription edits, channel CRUD, user/role changes, schedule edits, MFA enrolments, login attempts, and so on. The feed is the same data the per-project audit page exposes, behind the same role bar; the difference is breadth (every project you may audit, in one view) and freshness (poll every 5s instead of every 30s). A project's rows appear for a caller exactly when that caller could open the project's audit page: the auditor or admin role on the project, decided by the same policy table (RBAC). A viewer or operator membership contributes nothing to the feed, so the feed is not a way around the audit tier.
| Caller | Sees |
|---|---|
| Instance admin | Every audit row, including brain-wide rows that have no project_id (first-boot setup, system-wide config events) |
User with auditor or admin on a project |
That project's rows, plus their own rows that carry no project_id (MFA, login, password events); other users' brain-wide rows are excluded |
User with only viewer or operator on a project |
None of that project's rows; only their own user-scoped rows |
A user with no audit-tier membership anywhere sees only their own user-scoped rows. The project_slug filter follows the same rule: a project the caller may not audit returns an empty page, not its rows.
Filters
Section titled “Filters”The page exposes two filters in a sticky filter bar:
- Project -- "All projects" (default) or any single project from the set the user may audit. Filtering to a project they cannot see, or hold only
vieweroroperatoron, is a no-op (returns empty rather than 403, so an enumeration probe gives nothing away). The dashboard offers only the projects the caller may audit, and a caller with noauditororadminmembership sees a note that project activity needs the auditor or admin role while their own account activity is listed. - Action prefix -- free-form
LIKEagainst the action column. Common prefixes:task.,user.,agent.,auth.,subscription..
Filters apply server-side and the polling cycle picks them up immediately.
Wire endpoint
Section titled “Wire endpoint”The page is backed by GET /api/v1/activity:
GET /api/v1/activity?limit=50&project_slug=alpha&action_prefix=task. HTTP/1.1Cookie: <session cookie>The session cookie is resolved first; an Authorization: Bearer <api key> header is accepted instead.
Returns:
{ "items": [ { "id": "0e6c1f3a-12ab-4cde-9876-543210fedcba", "project_id": "...", "project_slug": "alpha", "user_id": "...", "action": "task.failed", "target_type": "task", "target_id": "task-id", "result": "success", "outcome": "allow", "metadata": {"task_name": "send_invoices"}, "source_ip": null, "occurred_at": "2026-05-12T12:00:00.000000+00:00" } ], "next_before_cursor": "2026-05-12T11:30:00.123456+00:00|0e6c1f3a-12ab-4cde-9876-543210fedcba", "newest_cursor": "2026-05-12T12:05:00.000000+00:00|aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}Cursor format is <iso_occurred_at>|<row_id> because the underlying audit_log.id column is uuid4 (random, not time-ordered); ordering by id alone would shuffle rows on every page. The dashboard's infinite-query refetches the head of the timeline on each 5-second poll and walks next_before_cursor for the Load older button.
source_ip returns null for non-admins regardless of project membership. The per-project audit page returns the field, and only project auditors and admins can open that page, so they are the only readers who see it.
Query parameters:
| Parameter | Notes |
|---|---|
limit |
1..200, default 50 |
since_cursor |
Return only rows newer than this <iso>|<uuid> cursor. Use for live polling. |
before_cursor |
Return only rows older than this <iso>|<uuid> cursor. Use for backward pagination. |
project_slug |
Constrain to one project (must be one the caller may audit; unknown slugs and projects outside that set return empty rather than 403 so enumeration probes give nothing away). |
action_prefix |
LIKE-escaped literal prefix against the action column. % and _ are NOT wildcards. |
Rate limit
Section titled “Rate limit”The endpoint enforces a sliding window of 60 requests / minute per user.id per worker process. Exceeding the limit returns 429 Too Many Requests with a Retry-After: 60 header and a body of {"detail": "activity feed rate limit exceeded (60/min per worker)"}. With N uvicorn workers per brain (z4j serve --workers N, default min(4, cpu_count)) and M brain replicas, the cluster-wide effective ceiling is NM60/min per user; the per-worker enforcement is the correct shape for a polling dashboard but is NOT a cluster-wide throttle.
Why polling and not WebSocket
Section titled “Why polling and not WebSocket”The audit log is the canonical source of truth. A WebSocket push from the brain would add a fan-out boundary (the dashboard hub already exists, but tying the activity feed to it would couple the two surfaces) and a stale-on-disconnect failure mode. Polling at 5 seconds is one extra GET per visible tab; the endpoint is two cheap queries (audit_log ordered by (occurred_at, id) DESC with the scope filter, plus a slug lookup for the page's projects), so the cost is negligible. There is no WebSocket-pushed mode.
Threat model
Section titled “Threat model”The activity feed is a READ-ONLY view of the audit log. The endpoint never mutates state, and the data passing through is the same data the per-project audit page already exposes, to the same readers. What it adds is the cross-project aggregation: a user who audits two projects sees both projects' audit rows in one view. This is by design. It is a wider read than the per-project audit page in breadth, never in audience: a role the audit page refuses is refused the same rows here.
The endpoint relies on is_admin plus the caller's memberships filtered by the policy table's answer for the audit read action, the same answer the per-project audit page gets. The endpoint has its own unit test suite covering scope enforcement, filter handling, pagination, and the unauthenticated-rejection path, and the separation-of-duties suite holds the viewer and operator exclusions and the auditor and admin admissions against the real application.
Disabling
Section titled “Disabling”The feed cannot be disabled per-tenant; it is always available to logged-in users, who see only what their roles admit (a user without an audit-tier membership sees their own user-scoped rows and nothing else). If you need to hide the page itself, gate the /activity route in your reverse proxy or remove the sidebar entry in a downstream fork of the dashboard build.