diff --git a/docs/design/README.md b/docs/design/README.md index 6ee4f23..18d172f 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -33,6 +33,7 @@ The visual spec of record for the back-office SPA and the transactional emails, | [Media picker](screens/media-picker.md) | `/article/:id/edit` (dialog) | 13 | `source/RenewCMS Screens Back-office.dc.html` | | [Integration](screens/integration.md) | `/integration` | 8 | `source/RenewCMS Screens Back-office.dc.html` | | [API keys](screens/integration-api-keys.md) | `/integration/api-keys` | 12 | `source/RenewCMS Screens API keys.dc.html` | +| [Activity](screens/activity.md) | `/activity` | 9 (plus 6 details, 3 My activity) | `source/RenewCMS Screens Activity.dc.html` | | [Users](screens/users.md) | `/users` | 17 | `source/RenewCMS Screens Back-office.dc.html` | | [Settings](screens/settings.md) | `/settings` | 25 | `source/RenewCMS Screens Back-office.dc.html`, `source/RenewCMS Screens Media cleanup.dc.html` | | [Profile](screens/profile.md) | `/profile` | 13 | `source/RenewCMS Screens Back-office.dc.html` | @@ -41,6 +42,7 @@ The visual spec of record for the back-office SPA and the transactional emails, ## Other boards +- `source/ActivityFrame.dc.html`: the frame that draws every Activity state, the Home card and the Settings retention card (the Activity board imports it). Its copy, component and API tables are in `source/activity-spec.js`. It holds the exact values. Home and Settings are drawn around dashed placeholders. - `source/MediaFrame.dc.html`: the frame that draws the Media and Media picker states (the Back-office board imports it). Like `AppFrame.dc.html`, it holds the exact values. - `source/SettingsFrame.dc.html`: the Media cleanup card and its purge dialog (the Media cleanup board imports it, with `MediaFrame.dc.html` for the Duplicates toolbar button). They hold the exact values. - `source/IntegrationPage.dc.html` and `source/ApiKeysPage.dc.html`: the page components for the Integration and API keys screens (imported by `AppFrame.dc.html`). They hold the exact values. diff --git a/docs/design/components.md b/docs/design/components.md index 2ee5c29..da4c9eb 100644 --- a/docs/design/components.md +++ b/docs/design/components.md @@ -829,3 +829,23 @@ Settings card. `USwitch` (square, `rounded-none`), `UInputNumber` with "days" su ### Media toolbar button Secondary `UButton` icon `merge`, label "Purge duplicates", after the sort menu on desktop; full width in the stacked toolbar on mobile (44 px). + +## Added with Activity +Source: `source/activity-spec.js` (component inventory) and `source/ActivityFrame.dc.html`. All built on existing Nuxt UI v4 components and tokens; no new token. + +| Component | Nuxt UI base | Props | Emits | Slots | Used on | +|---|---|---|---|---|---| +| ActivityPage | custom page | none (reads the session role) | none | none | Activity | +| ActivitySummaryStrip | UCard (as button) | summary, loading, activeFilter | apply-filter | none | Activity (owner, admin) | +| ActivityFilterBar | UInput, USelectMenu, UButton, UBadge | filters, people, canFilterPeople | update:filters, clear | none | Activity | +| ActivityTable | UTable (day separator rows), UAvatar, UBadge, UPagination, USelectMenu | items, total, page, size, loading | open, update:page, update:size | empty | Activity (md and up) | +| ActivityCardList | UCard | same as ActivityTable | open | empty | Activity (below md) | +| ActivityDetailsPanel | USlideover (right on desktop, bottom on mobile), UCard, UButton | item, index, total, open | previous, next, close | none | Activity | +| ActivityDiff | custom list | changes | none | none | ActivityDetailsPanel | +| ActivityFeedCard | UCard, UAvatar, UButton | items, loading, error, own | retry | none | Home | +| RetentionCard | UCard, USelectMenu, UAlert, UButton | settings, canEdit | saved | none | Settings (owner) | + +- Sparkline: seven `div` bars inside the strip card, no chart library. The strip cards are real `button`s with the card as their label. +- Day separators are full-width rows of the table. The "When" cell carries the exact time as its `title`. +- The sentence is composed from the `action` code and params with the i18n messages (English and French); the target name is user content and is not translated. +- Failed and security rows use `--rc-error-soft` / `--ui-error` and `--rc-warning-soft` / `--ui-warning` for emphasis, always paired with an icon or a word, never colour alone. diff --git a/docs/design/routes.md b/docs/design/routes.md index e5b4cc0..5461802 100644 --- a/docs/design/routes.md +++ b/docs/design/routes.md @@ -11,15 +11,16 @@ A global `router.beforeEach` guard reads `meta.auth`. On the first navigation it | `/register/validate` | AuthShell | public | VerifyPage | POST /v1/auth/verify (+ POST /login inline on 401) | | `/register/reset/request` | AuthShell | public | ResetRequestPage | POST /v1/auth/password-reset | | `/register/reset/validate` | AuthShell | public | ResetConfirmPage | POST /v1/auth/password-reset/confirm | -| `/home` | DashboardShell | verified | HomePage | GET /v1/article/ · GET /v1/system/update (owner, for the banner) | +| `/home` | DashboardShell | verified | HomePage | GET /v1/article/ · GET /v1/activity?size=5 · GET /v1/system/update (owner, for the banner) | | `/article` | DashboardShell | verified | ArticlesPage | GET /v1/article/ · PATCH /{id}/publish\|unpublish · DELETE /{id} | | `/article/new` | DashboardShell | verified | ArticlesPage + NewArticleModal | POST /v1/article/ | | `/article/:id/edit` | DashboardShell | verified | EditorPage | GET /{id} · PATCH /{id} · PATCH /{id}/publish\|unpublish · GET /v1/media/ · POST /v1/media/ · DELETE /{id} | | `/media` | DashboardShell | verified | MediaPage | GET /v1/media/purge-preview · POST /v1/media/purge · GET /v1/media/ · POST /v1/media/ · PATCH /v1/media/{id} · DELETE /v1/media/{id} · POST /v1/media/merge · GET /v1/media/{id}/usage | +| `/activity` | DashboardShell | verified | ActivityPage + ActivityDetailsPanel | GET /v1/activity · GET /v1/activity/summary (owner, admin) · GET /v1/activity/export (owner, admin) | | `/integration` | DashboardShell | verified | IntegrationPage | none | | `/integration/api-keys` | DashboardShell | verified | ApiKeysPage + CreateApiKeyModal | GET /v1/api-key · POST /v1/api-key · DELETE /v1/api-key/{id} | | `/users` | DashboardShell | verified | UsersPage + InviteUserModal | GET /v1/user/ · POST /v1/user/invite · DELETE /v1/user/invite/:id · POST /v1/user/invite/:id/renew | -| `/settings` | DashboardShell | verified | SettingsPage | GET /v1/settings · GET /v1/system/update · POST /v1/system/update/check · POST /v1/system/update/download · POST /v1/system/update/restart · GET /v1/media/purge-settings · PATCH /v1/media/purge-settings · GET /v1/media/purge-preview · POST /v1/media/purge | +| `/settings` | DashboardShell | verified | SettingsPage | GET /v1/settings · PATCH /v1/settings (owner, retention) · GET /v1/system/update · POST /v1/system/update/check · POST /v1/system/update/download · POST /v1/system/update/restart · GET /v1/media/purge-settings · PATCH /v1/media/purge-settings · GET /v1/media/purge-preview · POST /v1/media/purge | | `/profile` | DashboardShell | verified | ProfilePage | GET /v1/auth/me · PATCH /v1/user/me · POST /v1/auth/password-reset | | `/invite/accept` | AuthShell | public | AcceptInvitePage | GET /v1/auth/invite?c=* · POST /v1/auth/invite/accept* | | `/:pathMatch(.*)*` | either | any | NotFoundPage | none | diff --git a/docs/design/screens/activity.md b/docs/design/screens/activity.md new file mode 100644 index 0000000..e1e496c --- /dev/null +++ b/docs/design/screens/activity.md @@ -0,0 +1,183 @@ +# 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 is `source/ActivityFrame.dc.html`; its copy, component mapping and API tables are `source/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. + +1. **Header action** (owner and admin): outline `UButton` with icon `download`, "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". +2. **Summary strip** (owner and admin): four `UCard`, each a `button`. Labels: "Total actions", "Active people", "Failed actions", "Security events". Each shows the number, a sub-line and a seven-bar sparkline (seven `div` bars, 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-soft` and `--ui-error`, icon `circle-x`. Click applies Outcome = Failed. + - Security: "5 failed logins". Emphasis with `--rc-warning-soft` and `--ui-warning`, icon `shield-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" and `circle-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. +3. **Filter bar**: `UInput` (icon `search`, clearable, "Search activity"), Person `USelectMenu` (grouped: users, API keys, "System"; "All people"), Category `USelectMenu` ("All categories": Articles, Media, Users and invites, API keys, Authentication, System), Outcome button group (All / Succeeded / Failed; `soft` is 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. +4. **Active filter chips**: `UBadge` (subtle) with a trailing ghost `x` button: "Search: spring", "Person: Marta Alves", "Category: Authentication", "Outcome: Failed", "Dates: 1 – 30 Sep 2026", then a `link` button "Clear all". The default date range (7 days) is not a chip. +5. **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 (`UAvatar` with 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; a `shield` marker 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. +6. **Pagination**: "Showing 1–25 of 1,284", `UPagination`, "Rows per page" select (25, 50, 100). Mobile: "Page 1 of 52" with previous and next. +7. **Mobile list**: below `md` the table is a stacked list of `UCard` rows (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, with `custom`), `page` (from 1), `size` (25, 50 or 100; default 25). Sorted by `created_at` descending, then `id` descending. Invalid values answer 400 "The request cannot be processed due to a mismatch in the format of the body." +- `security=true` is what the Security card applies; it is a flag on the event, not a category. +- Response: `{ items, total, page, size }`. An item: + +```json +{ + "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.type` is `user`, `api_key`, `system` or `anonymous` (a failed login). `actor.name` is a snapshot taken when the event was recorded, so a renamed or deleted user still reads correctly. `target.name` is a snapshot too; `target.exists` is resolved when the list is read. +- `action` is a stable dotted code. The SPA owns the wording, in English and French. `params` holds extra values a sentence needs (for example `{ "merged": 3, "articles_updated": 5 }` on a media purge); they are never secrets. +- `changes[].kind` is `text` or `body`. A `body` change has `delta` (a signed number of characters) and no `before` or `after`. Only allowlisted fields ever appear in `changes`, and never a password, hash, API key, token or article body. +- `source` is `web`, `api_key` or `system`. `ip` and `user_agent` are `null` for `system`. +- A caller who is not owner or admin is scoped to its own events; `actor` other 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. diff --git a/docs/design/screens/home.md b/docs/design/screens/home.md index 5fd574d..76724b4 100644 --- a/docs/design/screens/home.md +++ b/docs/design/screens/home.md @@ -54,3 +54,13 @@ The greeting reads "Good morning, {name}", where the name is the display name wh **Trigger:** The download is checked. The same banner changes its copy, and a dismiss comes back on the next visit because the update is waiting. - Banner: success tone, `circle-check` icon, title "Version 2.3.0 is ready to install", text "Restart from Settings to finish. The site is unavailable for about 10 seconds.", button "Go to Settings" + +## Recent activity card + +**API:** GET /v1/activity?size=5. Visual reference: `source/RenewCMS Screens Activity.dc.html`, group "Home card". The board draws the rest of Home as a dashed "Existing Home cards" placeholder, so this card is not pixel-checked against it. + +A card with the last five events. Header "Recent activity" (regular users: "Your recent activity") and a "View all" link with `arrow-right` to `/activity`. Each row: a 24 px avatar, the sentence clamped to two lines with the target in semibold, then "Marta Alves · 2 min ago" (own events: only the time). A failed event gets a "Failed" badge; a successful security event a small `shield`. + +- Placement as drawn: a 420 px column to the right of the existing cards on desktop; stacked on mobile, drawn above the existing cards. Confirm against the real Home layout before coding. +- States: Owner or admin, Regular user, Loading (five skeleton rows after 150 ms), Error ("Couldn't load recent activity" with "Try again"), Empty ("No activity yet", "Actions on your site will appear here." / own: "Your actions will appear here."). +- The card never blocks the page: an error or empty card leaves the stats and the article list as they are. diff --git a/docs/design/screens/settings.md b/docs/design/screens/settings.md index 9771e8e..042eb4a 100644 --- a/docs/design/screens/settings.md +++ b/docs/design/screens/settings.md @@ -177,3 +177,18 @@ Duplicates tab: secondary "Purge duplicates" button (merge) next to the sort men ### Components (Nuxt UI v4) `UCard`, `USwitch`, `UInputNumber` (min 1, max 365, suffix), `UFormField`, `UAlert`, `UButton`, `UModal`, `USkeleton`, `useToast`. + +## Activity retention + +**API:** GET /v1/settings · PATCH /v1/settings (owner only). Visual reference: `source/RenewCMS Screens Activity.dc.html`, group "Settings retention". The board draws the other cards as a dashed "Existing Settings cards" placeholder and gives the page a different subtitle ("Site-wide options. Only owners can change them."), which is not a decision to change the existing one. + +A third card below Media cleanup, owner only (hidden for admins), same card styling, 640 px at most as drawn. + +- Header: "Retention" / "How long activity is kept." "Saved" (check, success) at the right after a save, fading after 2 s. +- "Keep activity for" `USelectMenu`: 30 days, 90 days, 365 days, Forever. Default 90 days. Helper: "Older events are removed automatically." For Forever: "Nothing is removed automatically. The log grows over time." +- Info alert (`lock`): "Activity logs are append-only", "Events can't be edited or deleted from the app. Only the retention period removes them." +- Divider, then read-only lines: "Last cleanup: 6 Oct 2026, 03:00 · 142 events removed" and "Next cleanup: 7 Oct 2026, 03:00". Forever: "Never" and "Not scheduled". +- Saves on change. Choosing a shorter period than the current one first shows a warning alert, "Events older than 30 days will be removed", "This happens at the next cleanup on 7 Oct 2026, 03:00. It can't be undone.", buttons "Apply 30 days" and "Cancel"; nothing is sent until Apply. Lengthening or choosing Forever saves at once. +- Save error alert: "Couldn't save the retention period", "Your previous setting is still in place.", "Try again". The control reverts. +- States: Default (90 days, saved), Shortening the period, Forever, Save error. Loading and Offline follow the Media cleanup card. +- `GET /v1/settings` adds `activity_retention_days` (30, 90, 365 or 0 for forever), `activity_last_cleanup_at` (null before the first cleanup) and `activity_last_cleanup_removed`. `PATCH /v1/settings` takes `{ "activity_retention_days": 30 }`; any other value answers 400 "The request cannot be processed due to a mismatch in the format of the body."; a non-owner gets 403 "Only the owner can do this.". The cleanup runs once a day at 03:00 server time and never touches the last 24 hours. diff --git a/docs/design/source/ActivityFrame.dc.html b/docs/design/source/ActivityFrame.dc.html new file mode 100644 index 0000000..106cd0e --- /dev/null +++ b/docs/design/source/ActivityFrame.dc.html @@ -0,0 +1,252 @@ + + +
+ + + + + +New /activity screen, a Recent activity card for Home, and a Retention card for Settings. 1440 px at 50%, 390 px at 75%. Owner and admin see everything; regular users get "My activity". The theme switch shows every state in dark mode. Copy, component mapping and API calls are at the bottom.
+