14 KiB
Activity
- Route:
/activity - API: GET /v1/activity · GET /v1/activity/summary · GET /v1/activity/export (see "API contract" below)
- Visual reference:
source/RenewCMS Screens Activity.dc.html, group "Activity, owner and admin" (open it in a browser; each state is drawn at desktop and mobile, with a light/dark toggle). The frame issource/ActivityFrame.dc.html; its copy, component mapping and API tables aresource/activity-spec.js.
The audit log of the back-office: who did what, and when, across articles, media, users and invites, API keys, authentication and system tasks. It is for non-technical owners, so every row reads as a sentence, not a log line. Events are append-only: nothing in the app edits or deletes them. Only the retention period removes them (Settings, "Retention").
Other groups of the same board:
| Group | What it draws | Written up in |
|---|---|---|
| Details slide-over | The slide-over and its bottom-sheet variant, six events | this file, "Details slide-over" |
| My activity | The same screen for a user who is not owner or admin | this file, "My activity" |
| Home card | The Recent activity card on Home | screens/home.md, "Recent activity card" |
| Settings retention | The Retention card on Settings | screens/settings.md, "Activity retention" |
Roles
Owner and admin see every event, the summary strip, the Person filter and Export. Any other role sees only its own events: the title becomes "My activity", the strip, the Person filter and Export are hidden, and the server scopes the list to that user (asking for another actor answers 403).
Today the product has only owner and admin (see screens/users.md, "Roles"), so "My activity" and the scoped answer cannot be reached by a real account yet. They are specified and tested now so a future role needs no rework. The board's sample "Editor" role and the nav it draws for a regular user (Home, Articles, Media, Activity) are illustrative.
Sidebar
"Activity" (icon history) sits between Users and Integration for owner and admin: Home, Articles, Media, Users, Activity, Integration, Settings. The board draws the order that way. It is visible to every verified user. The mobile drawer gets the same item.
The page
Title "Activity" (regular users: "My activity"). Subtitle "Who did what, and when, across your site." (regular users: "Everything you've done on this site."). Page padding 24 px desktop, 16 px mobile; the sidebar and top bar are the existing DashboardShell.
- Header action (owner and admin): outline
UButtonwith icondownload, "Export CSV". While the file is prepared: "Preparing file…" and disabled. Success toast "Export started. Your file will download shortly." Error toast "Couldn't export activity. Try again." Disabled in "Empty, no activity yet". - Summary strip (owner and admin): four
UCard, each abutton. Labels: "Total actions", "Active people", "Failed actions", "Security events". Each shows the number, a sub-line and a seven-bar sparkline (sevendivbars, no chart library, oldest left).- Total: "12% vs previous 7 days" with
trending-up/trending-down(the comparison is against the equal-length period just before the selected range). Click clears the filters. - Active people: "7 people, 2 API keys". Click does nothing (no equivalent filter).
- Failed: "0.5% of all actions". Emphasis with
--rc-error-softand--ui-error, iconcircle-x. Click applies Outcome = Failed. - Security: "5 failed logins". Emphasis with
--rc-warning-softand--ui-warning, iconshield-alert. Click applies the security filter (auth, role changes, API key create and revoke). - Zero: Failed shows "0" with "Nothing failed" and
circle-check; Security shows "0" with "Nothing to flag" andcircle-check; both drop the emphasis fill. No data at all: every card reads "0" with the sub-line "No data yet" (Failed and Security keep their calm lines). - The strip follows the date range filter. Mobile: a 2×2 grid.
- Total: "12% vs previous 7 days" with
- Filter bar:
UInput(iconsearch, clearable, "Search activity"), PersonUSelectMenu(grouped: users, API keys, "System"; "All people"), CategoryUSelectMenu("All categories": Articles, Media, Users and invites, API keys, Authentication, System), Outcome button group (All / Succeeded / Failed;softis the selected one), date presets (Today / 7 days / 30 days / Custom; Custom opens a date-range popover). Mobile: the bar collapses to a search field and a "Filters" button with a count badge; the controls move to a bottom sheet. - Active filter chips:
UBadge(subtle) with a trailing ghostxbutton: "Search: spring", "Person: Marta Alves", "Category: Authentication", "Outcome: Failed", "Dates: 1 – 30 Sep 2026", then alinkbutton "Clear all". The default date range (7 days) is not a chip. - Timeline table: newest first, grouped by day with full-width separator rows ("Today", "Yesterday", "3 Oct 2026"). Columns: When (relative within the day, clock time before; the full time is the cell's
title), Actor (UAvatarwith initials, a key icon for API keys or a cog for System, plus the name; badges "API key" / "System"; a signed-out actor reads "Not signed in"), Action (category icon and the sentence with the target name in semibold; ashieldmarker on security rows), Target (a link when it still exists; struck through with a "Deleted" badge when it does not), Outcome (UBadge"Succeeded" / "Failed"). Row click and Enter open the details. - Pagination: "Showing 1–25 of 1,284",
UPagination, "Rows per page" select (25, 50, 100). Mobile: "Page 1 of 52" with previous and next. - Mobile list: below
mdthe table is a stacked list ofUCardrows (actor, sentence, time, outcome, "Target deleted" when relevant), 44 px touch targets.
Sentences are composed in the SPA from a stable action code and the event params, in the user's language (English and French), never stored as text. The target name and the typed values in a diff stay as the user wrote them.
States
Default, large dataset
Trigger: Both requests resolved with 1,284 events over 52 pages. Newest first, day separators, a security row. Hover on the first row shows its exact time.
Loading
Trigger: Both requests in flight. Skeleton strip and table rows after 150 ms.
Zero-value summary strip
Trigger: A quiet period. Failed and Security show a calm 0.
Filtered, card applied
Trigger: The Failed actions card was clicked. Chips plus "Clear all"; mobile shows the count on Filters. Six results.
Mobile filters sheet
Trigger: Filters open as a bottom sheet on mobile. The desktop frame keeps the inline bar. Sheet: title "Filters", Person (owner and admin), Category, Outcome, Dates (two date fields when Custom), footer "Clear all" and "Show 23 results".
Empty, no activity yet
Trigger: A new site. UEmpty with icon history, title "No activity yet", description "Actions on articles, media, users and API keys will appear here." Export disabled.
Empty, filters match nothing
Trigger: Filters with zero results. Title "No activity matches these filters", description "Try a different search or a wider date range.", action "Clear filters".
Error with retry
Trigger: The list request failed (500 or network). UEmpty in the table area: "Couldn't load activity", "Check your connection and try again.", action "Try again". The strip still shows if its own call succeeded.
Forbidden (403)
Trigger: The API answered 403 for this role or filter. "You can't view this activity.", "Your role doesn't include this page. Ask an owner or admin if you need access.", action "Back to home". The 401 is global.
Details slide-over
USlideover, right side on desktop (about 440 px wide), bottom sheet on mobile. Opens from a row (click or Enter), closes with Escape, the close button or the scrim; focus returns to the row. Title "Activity details".
Body, in UCard sections:
- The full sentence, with the target name in semibold.
- When: the user's local time, then a muted "UTC" line.
- Actor (with the same badges), Source ("Web app", "API key", "System task"), IP address, Browser. For a system event, IP address and Browser read "Not applicable".
- Target: name (struck through with a "Deleted" badge, plus "This item was deleted. Its name is kept for reference." when it no longer exists), and a link when it exists.
- Why it failed (failed events only): the plain-language reason from the API, never a stack trace.
- Changes (edits only): rows of field, Before, After. Values longer than two lines clamp with "Show more" / "Show less". A body edit is a single row "Body edited (+312 characters)" and never shows text.
- Footer: "Previous event" and "Next event" buttons and "Event 2 of 1,284". They move through the current filtered list, across page boundaries.
Edit with a diff
Trigger: A title change and a body edit. Title Before and After, then "Body edited (+312 characters)".
Settings diff, long text
Trigger: Long values clamp to two lines with "Show more".
Failed action, very long target title
Trigger: A failed publish. The reason is shown and the long target title wraps.
Deleted target, no diff
Trigger: A deleted image. Struck-through target, "Deleted" badge, no Changes section.
System-initiated, no human actor
Trigger: A scheduled task. "System" badge, source "System task", IP and Browser "Not applicable".
Security event, failed login
Trigger: The actor was not signed in. "Not signed in", a restrained shield marker, the reason, the IP and browser.
My activity
The same screen for a role that is not owner or admin. No summary strip, no Person filter, no Export. The list is the user's own events (server scoped).
Default
Trigger: The user has events. Title "My activity".
Empty
Trigger: No events yet: "No activity yet", "Your actions will appear here once you start working on the site."
Details
Trigger: The slide-over on one of the user's own events.
API contract
401 and 403 on every route follow states.md, "HTTP status to copy". The board's table in source/activity-spec.js is the source; this section fixes the parts it leaves open.
GET /v1/activity
- Query:
q,actor(user:ID,key:ID,system),category(articles,media,users,api_keys,auth,system),outcome(all,success,failed),security(true),range(today,7d,30d,custom),from,to(ISO dates, withcustom),page(from 1),size(25, 50 or 100; default 25). Sorted bycreated_atdescending, theniddescending. Invalid values answer 400 "The request cannot be processed due to a mismatch in the format of the body." security=trueis what the Security card applies; it is a flag on the event, not a category.- Response:
{ items, total, page, size }. An item:
{
"id": 4821,
"created_at": "2026-10-06T10:42:18Z",
"action": "article.publish",
"category": "articles",
"actor": { "type": "user", "id": 3, "name": "Marta Alves" },
"target": { "type": "article", "id": "8", "name": "Spring release", "exists": true },
"outcome": "success",
"failure_reason": null,
"security": false,
"source": "web",
"ip": "203.0.113.12",
"user_agent": "Chrome 129 on Windows",
"changes": [{ "field": "online", "kind": "text", "before": "false", "after": "true" }]
}
actor.typeisuser,api_key,systemoranonymous(a failed login).actor.nameis a snapshot taken when the event was recorded, so a renamed or deleted user still reads correctly.target.nameis a snapshot too;target.existsis resolved when the list is read.actionis a stable dotted code. The SPA owns the wording, in English and French.paramsholds extra values a sentence needs (for example{ "merged": 3, "articles_updated": 5 }on a media purge); they are never secrets.changes[].kindistextorbody. Abodychange hasdelta(a signed number of characters) and nobeforeorafter. Only allowlisted fields ever appear inchanges, and never a password, hash, API key, token or article body.sourceisweb,api_keyorsystem.ipanduser_agentarenullforsystem.- A caller who is not owner or admin is scoped to its own events;
actorother than itself answers 403.
GET /v1/activity/summary (owner and admin; 403 otherwise)
Same range, from, to. Response: total, previous_total, active_people: { users, api_keys }, failed, security, and series with seven daily counts for total, active, failed and security. The previous period is the equal-length period immediately before the range.
GET /v1/activity/export (owner and admin; 403 otherwise)
The same filters without page or size. Streams text/csv; charset=utf-8 with Content-Disposition: attachment. Columns: when (RFC3339 UTC), actor, action (the code), target, outcome, source, ip. No changes, no user agent, no secrets. Cells that start with =, +, - or @ are prefixed with a quote so a spreadsheet does not evaluate them.
GET /v1/settings and PATCH /v1/settings: see screens/settings.md, "Activity retention".
These routes are session only: with an API key they answer 403 "This action needs a real session." (A key never reads the audit trail.)
What is recorded
Every row of the board is a sample. The real catalogue is the set of actions that exist in the product, in these categories:
| Category | Events |
|---|---|
articles |
create, edit (title, publish state, body size delta), publish, unpublish, delete |
media |
upload, rename, delete, merge, purge (manual; the scheduled run is a system event) |
users |
register, verify account, update profile (display name), invite create, cancel, renew, accept |
api_keys |
create, revoke |
auth |
login succeeded, login failed (security), logout, password reset requested (security), password reset completed (security) |
system |
update checked, update downloaded, restart requested, scheduled media purge, activity retention changed, activity cleanup |
Not drawn as real events: the "Editor" role and role changes, a "Site description" or "Comments" setting, "Ran the nightly backup" (backups are a step of the update job, not a schedule), "Removed 3 expired invites" and an administrator resetting another user's password. They stay in the board as sample data and are only recorded if the matching feature exists.
Read requests are not recorded (no "article viewed"), and neither are public API reads.