feat(activity): update summary cards to toggles and introduce export menu

This commit is contained in:
Florian Sylvain committed 2026-10-12 05:13:25 +02:00
1 parent 73f4b539fe
commit 03b39e64f2
30 files changed
+2376 -238

No files matched your search

+7
View File
@@ -271,10 +271,17 @@ Feature: The back-office keeps an append-only activity log that owners can read
| range=today | events since 00:00 UTC today |
| range=7d | events of the last 7 days |
| range=30d | events of the last 30 days |
| range=all | every event, whatever its age |
| range=custom&from=2026-09-01&to=2026-09-30 | events from 1 to 30 September 2026 UTC |
| page=2&size=50 | the second page of 50 |
| size=5 | the first page of 5 |
Scenario: range=all covers every event
Given events 2 days old and 90 days old
When the owner sends GET /v1/activity?range=all
Then both events are listed
And the summary and the export with range=all cover both events too
Scenario: Omitting range means 7 days
Given events 2 days old and 10 days old
When the owner sends GET /v1/activity
@@ -0,0 +1,228 @@
Feature: Activity screen, filter cards as toggles and the Export menu
As an owner or admin
I want the Failed actions and Security events cards to toggle their filter, and Export to offer the current view or all activity as CSV
So that I can isolate problems in one click and keep a copy of the log
Background:
Given this slice amends features/activity_screen.feature (header Export CSV button, strip-card effects, Export scenarios, French export label, empty/error export rules) per docs/design/screens/activity.md groups "Activity: filter cards" and "Activity: export" and docs/design/states.md
And the screen, strip, chips, filter bar and list are otherwise as in features/activity_screen.feature
And a page filter is the set of q, actor, category, outcome, security, range, from, to
And the filters live in component state, so card, chip and Outcome segment read the same state: card Failed pressed = outcome "failed" is set = chip "Outcome: Failed" shown = Outcome segment on Failed; card Security pressed = security is true = chip "Type: Security events" shown, Outcome segment unchanged
And "Failed actions" and "Security events" are buttons with aria-pressed (true or false); "Total actions" and "Active people" are not toggles and carry no aria-pressed
And a pressed card uses --rc-sand background with --rc-on-sand text; an unpressed card uses the default look
And the card hint lines are: unpressed "Click to filter the list" (on hover), pressed "Filtering. Click to clear", pressed and hovered "Click to clear this filter" with an x icon
And hover on an unpressed card uses --ui-bg-elevated; keyboard focus-visible draws a 2 px --ui-primary outline with a 2 px offset; Enter and Space toggle
And the Outcome segment can still be changed by hand: choosing Failed presses the Failed card, choosing All or Succeeded unpresses it
And the Export control is shown only in the full view (owner or admin); the regular-user view and the Forbidden panel have none
And the Export control is one outline button, icon download, label "Export", trailing chevron; it is a dropdown menu on desktop and a bottom sheet below md; Enter or ArrowDown opens it, Escape closes it, and focus-visible is a 2 px --ui-primary outline with a 2 px offset
And the menu has two items:
| item | sub-line (enabled) | disabled reason |
| Export current view | CSV · {n} events matching your filters and sort, across all pages (n = list total) | Nothing to export. No events match these filters. |
| Export all activity… | CSV · every event, ignoring filters. Owners and admins only. | Only owners and admins can export all activity. |
And export is CSV only, synchronous, one streamed download: no progress percentage, no job, no "ready later"; JSON (format=json) is proposed in the design, is NOT implemented here, and no format control is offered
And "Export current view" sends GET /v1/activity/export with the active page filters (the list query without page and size) and no format parameter
And "Export all activity…" sends GET /v1/activity/export with no page filter at all and only the dialog's date range: All time sends range=all; Today, 7 days and 30 days send range=today|7d|30d; Custom sends range=custom&from=YYYY-MM-DD&to=YYYY-MM-DD
And the backend accepts range=all on the list, summary and export endpoints (features/activity-log-backend.feature); an omitted range still means 7d
And the download is built by the SPA from the response body (blob and anchor) with the file name activity-YYYY-MM-DD.csv for the current view and activity-all-YYYY-MM-DD.csv for all activity (local date)
And a response whose CSV has only the header row means nothing to export: nothing is downloaded
And 401 and the unverified 403 stay with useApi's global handling; the role-lowered 403 is handled here
And the dialog "Export all activity" is a modal (sm:max-w-md, a bottom sheet below md) with the title "Export all activity", a read-only "Format" row reading "CSV", a "Date range" select (default "All time") with the hint "Default is all time. Filters on the page are ignored.", and the footer Cancel on the left, "Export CSV" on the right
And every new English string has a French entry in messages.ts
Scenario Outline: Cards — the toggle cycle keeps card, chip, segment and request in sync
Given the Default state has loaded with no filter
When the user activates the "<card>" card
Then the card has aria-pressed="true", <chip> is shown with "Clear all", and <segment>
And GET /v1/activity is sent once with <query>, page=1 and size=25
And GET /v1/activity/summary is not requested again
When the user activates the "<card>" card again
Then aria-pressed is "false", the chip and "Clear all" are gone, the Outcome segment is on All
And GET /v1/activity is sent with range=7d, page=1 and size=25 only
Examples:
| card | chip | segment | query |
| Failed actions | "Outcome: Failed" | the Outcome segment is on Failed | outcome=failed |
| Security events | "Type: Security events" | the Outcome segment stays on All | security=true |
Scenario Outline: Cards — every way of removing the filter returns all three controls to default
Given the "<card>" card is pressed with <chip> shown
When the user <remove>
Then the card has aria-pressed="false" and default look, the chip is gone, the Outcome segment is on All
And "Clear all" is gone unless another chip remains
Examples:
| card | chip | remove |
| Failed actions | "Outcome: Failed" | activates the x of the chip |
| Failed actions | "Outcome: Failed" | activates "Clear all" |
| Failed actions | "Outcome: Failed" | chooses All in the Outcome segment |
| Failed actions | "Outcome: Failed" | chooses Succeeded in the Outcome segment |
| Security events | "Type: Security events" | activates the x of the chip |
| Security events | "Type: Security events" | activates "Clear all" |
Scenario: Cards — choosing Failed in the Outcome segment presses the card
Given the Default state has loaded
When the user chooses Failed in the Outcome segment
Then the Failed actions card has aria-pressed="true" and the chip "Outcome: Failed" is shown
Scenario: Cards — both cards can be pressed together
Given the "Failed actions" card is pressed
When the user activates the "Security events" card
Then both cards are pressed, the chips "Outcome: Failed" and "Type: Security events" are shown
And GET /v1/activity is sent with outcome=failed and security=true
Scenario: Cards — the card plus other filters; unpressing removes only its chip
Given Category "Authentication" and 30 days are applied and the "Failed actions" card is pressed
Then the chips read "Category: Authentication", "Outcome: Failed", "Dates: 30 days" and "Clear all"
When the user activates the "Failed actions" card
Then only "Outcome: Failed" is removed, the other two chips remain, and the request keeps category=auth and range=30d
Scenario: Cards — hint lines, hover and keyboard
Given the Default state has loaded
Then the Failed actions card hint reads "Click to filter the list" on hover and the card background is --ui-bg-elevated
When the Failed actions card is focused with the keyboard
Then a 2 px --ui-primary outline with a 2 px offset is drawn
When the user presses Space (or Enter)
Then the card is pressed with the hint "Filtering. Click to clear"
When the pointer is over the pressed card
Then the hint reads "Click to clear this filter" with an x icon
And "Clear all" and the chip x show hover and focus-visible styles per the board
Scenario: Cards — Total actions and Active people are not toggles
Given the "Failed actions" card is pressed
When the user activates "Total actions"
Then every filter is cleared and no card is pressed
When the user activates "Active people"
Then nothing changes and no request is sent
And neither card has aria-pressed
Scenario: Cards — the empty panel's "Clear filters" unpresses cards
Given the "Security events" card is pressed and the list is empty
When the user activates "Clear filters" in the empty panel
Then the card is unpressed and no chip is shown
Scenario: Cards — a mobile Filters sheet shares the same state
Given below md the "Failed actions" card is pressed
Then the Filters button badge counts the Outcome chip and the sheet's Outcome control is on Failed
When the user sets Outcome to All in the sheet
Then the card is unpressed
Scenario: Export menu — open, items and hover
Given the Default state has loaded as owner with total 1284
When the user activates "Export"
Then the menu lists "Export current view" ("CSV · 1,284 events matching your filters and sort, across all pages") and "Export all activity…", both enabled
And hovering "Export all activity…" highlights it
When the user presses Escape
Then the menu closes and focus returns to the Export button
Scenario: Export menu — filtered view states the count
Given the "Failed actions" card is pressed and the list total is 6
When the user opens the Export menu
Then "Export current view" reads "CSV · 6 events matching your filters and sort, across all pages"
Scenario: Export menu — nothing to export in the current view
Given Category "Media" and Outcome "Failed" match nothing (total 0)
When the user opens the Export menu
Then "Export current view" is disabled with "Nothing to export. No events match these filters." and "Export all activity…" is enabled
Scenario: Export menu — no activity at all and error state
Given the list is "No activity yet" (total 0, no chip) or in the Error state
Then the Export button is disabled
Scenario: Export current view — success
Given Category "Authentication" and 30 days are applied
When the user chooses "Export current view"
Then GET /v1/activity/export is sent once with category=auth and range=30d, no page, no size, no format
And when it resolves 200 with a text/csv body that has data rows, the file activity-YYYY-MM-DD.csv is downloaded
And a toast appears: title "Download started", body "activity-YYYY-MM-DD.csv is downloading."
And the menu is closed and no dialog was shown
Scenario Outline: Export current view — failures raise an error toast
When the user chooses "Export current view" and the request <failure>
Then an error toast titled "Couldn't prepare the download" appears with the body "<body>"
And nothing is downloaded and the Export button is enabled
Examples:
| failure | body |
| resolves with a 500 response | Something went wrong on our side. Nothing was changed. Try again in a moment. |
| never reaches the server | Something went wrong on our side. Nothing was changed. Try again in a moment. |
| resolves with a 400 response | That request wasn't valid. Check the fields and try again. |
Scenario: Export current view — the role was lowered while the page was open (403)
When the user chooses "Export current view" and the request resolves 403 with the owner/admin-required message
Then an error toast titled "You can't export all activity" appears and nothing is downloaded
And the Export menu then shows both items disabled with "Only owners and admins can export all activity."
Scenario: Export current view — nothing to export
When the user chooses "Export current view" and the response is 200 with only the CSV header row
Then nothing is downloaded and a toast titled "Nothing to export" appears
Scenario: Export all — the dialog opens configured
When the user chooses "Export all activity…"
Then the dialog "Export all activity" shows Format "CSV" (read-only, no JSON option), Date range "All time", Cancel on the left and "Export CSV" on the right
And no request has been sent
Scenario Outline: Export all — the date range maps to the query and ignores page filters
Given Category "Media", Outcome "Failed" and the "Failed actions" card are applied on the page
When the user picks Date range "<range>" in the dialog and activates "Export CSV"
Then GET /v1/activity/export is sent with <query> and no other parameter
Examples:
| range | query |
| All time | range=all |
| Today | range=today |
| 7 days | range=7d |
| 30 days | range=30d |
| Custom 1 Sep to 30 Sep 2026 | range=custom&from=2026-09-01&to=2026-09-30 |
Scenario: Export all — Custom needs a valid pair
When the user picks "Custom" and sets only one date, or from after to
Then "Export CSV" is disabled and no request is sent
Scenario: Export all — preparing, then download started
When the user activates "Export CSV" in the dialog
Then the dialog shows "Preparing download…" with an indeterminate bar and no percentage, "Keep this window open.", Cancel enabled and a disabled "Preparing…"
When the response resolves 200 with a CSV that has data rows
Then the file activity-all-YYYY-MM-DD.csv is downloaded and the dialog reads "Download started" and "activity-all-YYYY-MM-DD.csv is downloading. Check your browser's downloads if nothing appears." with only "Close"
When the user activates "Close"
Then the dialog closes and a toast appears: "Download started", "activity-all-YYYY-MM-DD.csv is downloading."
Scenario: Export all — Cancel while preparing
Given the export request is in flight
When the user activates "Cancel"
Then the request is aborted, nothing is downloaded, the dialog closes and no toast appears
Scenario Outline: Export all — failure shows Retry
When the request <failure>
Then the dialog reads "Couldn't prepare the download" and "Nothing was downloaded. Try again in a moment." with Cancel and "Retry"
When the user activates "Retry"
Then the same request is sent again and the dialog shows "Preparing download…"
Examples:
| failure |
| resolves with a 500 response |
| never reaches the server |
Scenario: Export all — the chosen range has no events
When the response is 200 with only the CSV header row
Then nothing is downloaded and the dialog reads "Nothing to export" and "There is no activity in this date range. Choose a wider range." with only "Close"
Scenario: Export all — not allowed (403)
When the response is 403
Then the dialog reads "You can't export all activity" and "Only owners and admins can export all activity. Ask an owner if you need a copy." with only "Close"
And nothing is downloaded
Scenario: Export — below md the menu and the dialog are bottom sheets
Given the Default state has loaded below md
When the user activates "Export"
Then a bottom sheet lists "Export current view" and "Export all activity…"
When the user chooses "Export all activity…"
Then the dialog is a bottom sheet with the same fields and footer (secondary left, primary right)
Scenario: Export — owners and admins only
Given the session role is "editor" (or absent)
Then no Export control is rendered and GET /v1/activity/export is never sent
Scenario: Export — French
Given the language is French
Then every string above (menu, dialog, toasts, hints, chips "Type : Événements de sécurité") has a French entry, and the former "Exporter en CSV" button label is replaced by the menu button
@@ -0,0 +1,63 @@
# QA: Activity filter cards as toggles and the Export menu
## Implementation note for coder (informational)
SPA slice with one small backend change: accept `range=all` in the list, summary and export range validation (use case range handling and the handler `validate` tag). An omitted range stays 7d. The range table in features/activity-log-backend.feature is updated accordingly.
This slice amends features/activity_screen.feature and .qa.md: the header button is now "Export" with a menu, the Export success and failure scenarios are replaced, and the French label "Exporter en CSV" becomes "Exporter". Update the matching Vitest cases (strip-card effects, Export success and failure, empty/error export-disabled lines, French labels) in the same change.
`useApi().request` calls response.json(); add a blob read that keeps the global 401 handling and supports an AbortSignal. Downloads use an object URL and an anchor with a `download` file name built from the local date. Toasts use `useToast().add({ color, icon, title, description })`.
## Automated (Vitest + Vue Test Utils, `task test:web`)
Mock `useApi` per case matching on method, path and parsed query. Mount ActivityView in DashboardShell as in the existing QA (owner session; "editor" for the regular case). Run layout cases at desktop and below-md widths. Use fake timers for setTimeout/clearTimeout only. No test freezes or asserts the system date: file names are asserted with `^activity-\d{4}-\d{2}-\d{2}\.csv$` and `^activity-all-\d{4}-\d{2}-\d{2}\.csv$`. Fixtures: existing `activityItems`, `summaryDefault`, and a CSV text with a header and two rows (and a header-only CSV).
1. **Toggle cycle, Examples-driven (Failed, Security)** - click: aria-pressed true, chip text, Outcome segment, request query, no extra summary request; click again: aria-pressed false, chip and Clear all gone, segment All, query without the key.
2. **Every removal path** - chip x, Clear all, Outcome All, Outcome Succeeded (Failed card only): card, chip and segment all return to default (assert the three together after each).
3. **Segment drives the card** - choose Failed in the segment: Failed card pressed and chip present.
4. **Both cards pressed** - queries `outcome=failed&security=true`, two chips.
5. **Card plus other filters** - Category auth, 30d, Failed: unpress removes only the Outcome chip; request keeps category and range.
6. **Hint and style** - hint texts for unpressed, hover, pressed, pressed+hover (with x icon); pressed class uses the sand tokens (assert the token class, not a hex); unpressed hover class; focus-visible class; Enter and Space toggle.
7. **Non-toggle cards** - Total clears all, Active people sends no request, neither has aria-pressed.
8. **Clear filters from the empty panel** unpresses cards; mobile Filters sheet shares state and badge count.
9. **Export menu structure** - owner: button "Export" with the download icon and chevron; menu items and sub-lines with total 1284 and with Failed card total 6; Escape closes and returns focus; ArrowDown and Enter open; below md rendered as a bottom sheet.
10. **Disabled states** - total 0 with a chip: "Export current view" disabled with its reason, "Export all activity…" enabled; "No activity yet" and Error states: Export button disabled; after a 403 on export: both items disabled with the owner/admin reason.
11. **Role gating** - role "editor" or none: no Export control, no `/export` request; Forbidden panel: none.
12. **Export current view success** - request to `/v1/activity/export` with the list filters, no page or size, no format; blob read; one anchor click with download matching the file-name pattern; toast "Download started"; no dialog.
13. **Export current view failure, Examples-driven (500, 400, network)** - error toast "Couldn't prepare the download" with the mapped body; no download; button enabled.
14. **Export current view 403** - toast "You can't export all activity"; both items disabled afterwards.
15. **Export current view, header-only CSV** - no download, toast "Nothing to export".
16. **Export all dialog** - opens configured (Format "CSV" read-only, no JSON, Date range "All time", hint, Cancel left, "Export CSV" right); no request on open.
17. **Export all query, Examples-driven** - with page filters applied: All time -> `range=all` only; Today, 7d, 30d; Custom -> `range=custom&from=2026-09-01&to=2026-09-30`; Custom with one date or from>to keeps "Export CSV" disabled.
18. **Preparing** - request pending: indeterminate bar (no `aria-valuenow`), copy "Preparing download…", Cancel enabled, "Preparing…" disabled.
19. **Started** - resolve with data rows: download triggered once; dialog copy and only "Close"; Close shows the toast with the all-activity file name.
20. **Cancel while preparing** - AbortController aborted, no download, dialog closed, no toast.
21. **Failed + Retry** - 500 and network: copy, Cancel and Retry; Retry resends and re-enters Preparing.
22. **Nothing to export** - header-only CSV: no download, copy, only "Close".
23. **403 dialog** - copy and only "Close"; no download.
24. **Below md** - menu and dialog render as bottom sheets with the same content.
25. **French** - every new key exists in messages.ts (completeness test) and a spot-check of the menu, dialog and toast strings.
26. **No JSON** - no element with text "JSON" and no `format` query parameter in any export request.
Backend (Go, in test/, on the activity handler tests): `range=all` on list, summary and export returns 200 over all events (older than 30 days included); invalid values still 400; omitted range still means 7d; non-owner export still 403.
## Visual (task visual:check -- activity, desktop and mobile, light and dark)
Reference: board groups "Activity: filter cards" and "Activity: export" under `docs/design/source/`; exact values in `docs/design/source/ActivityFrame.dc.html`. Add states to the activity entry in `web/visual/states.ts` with the board's own sample data. Hard gate: every capture within 10 %. Mock only the calls each state needs; an unmocked call fails the run.
- [ ] Filter cards: Default, card hover, card focus-visible, Failed active, active hover, Security active, chip x hover, Clear all hover and focus-visible, card plus other filters. Mark `skip: 'clock-sensitive'` where the rows behind show relative times.
- [ ] Export: menu open, item hover, filtered view, nothing to export (Media / Failed, total 0), no permission, button focus-visible.
- [ ] Export dialog: configure (CSV only), preparing (request hangs), started, failed, nothing to export, not allowed (403). Mock `/export` with hang / 500 / 403 / header-only CSV as needed.
- [ ] Export "CSV or JSON (proposed)": NOT implemented; skip with reason "proposed, JSON not shipped".
- [ ] Export toasts: file name contains the current date; `skip: 'clock-sensitive'`.
- [ ] Mobile bottom sheets for the menu and the dialog at 390x844.
- [ ] Layout from the frame (not components.md): dialog sm:max-w-md; footer secondary left, primary right; Export button 32 px (44 px mobile); pressed card sand fill.
- [ ] Not covered by screenshots: keyboard and focus order (Tab to cards, Space/Enter toggle, focus returns to the Export button after the menu closes), focus trap in the dialog, Escape, hover and motion, the reduced-motion indeterminate bar.
## Manual verification
- **Real CSV download** against `task dev`: Export current view with and without filters, and Export all with each range; open the file: only the filtered rows for the current view, every row for all time; file names and the browser's download prompt.
- **Large export**: with thousands of events, confirm the download starts and the "Preparing download…" dialog behaves.
- **Role lowered while the page is open**: change the user's role in another session, then export; confirm the 403 handling.
- **Time zones**: file names use the local date; check near midnight.
- **French** and **screen readers**: aria-pressed announcements, menu and dialog semantics.
+10 -33
View File
@@ -41,7 +41,7 @@ Feature: Activity screen, details slide-over and My activity
And the header is the title "Activity" (regular user: "My activity") with the subtitle "Who
did what, and when, across your site." (regular user: "Everything you've done on this
site."), and, in the full view except Forbidden, an outline button with icon download and
the label "Export CSV" at the right, aligned to the bottom of the title block
the label "Export" and a trailing chevron at the right, opening the menu described in activity_filter_cards_and_export.feature, aligned to the bottom of the title block
And sentences are composed in the SPA from the event's action code, its outcome, its params
and its target name through the existing i18n (English keys, French in messages.ts), never
stored as text; the target name is user content, is inserted untranslated as a semibold
@@ -226,7 +226,7 @@ Feature: Activity screen, details slide-over and My activity
load activity" is an error alert with icon circle-x, the text and an outline "Try again"
button, and "You can't view this activity" (no trailing period) uses icon lock and the
action "Back to home"
And "Export CSV" is disabled in "No activity yet" and while the list is in the Error state,
And "Export" is disabled in "No activity yet" and while the list is in the Error state,
and enabled otherwise
And ActivityDetailsPanel is a USlideover: 480 px wide from the right on desktop, an 88 %-high
bottom sheet with a 36×4 px handle below md; its header reads "Activity details" with a
@@ -277,7 +277,7 @@ Feature: Activity screen, details slide-over and My activity
failed 6, security 11 and seven-day series, and the session role is "owner"
When the user navigates to /activity
Then the header shows "Activity", "Who did what, and when, across your site." and an enabled
"Export CSV"
"Export"
And the strip reads "1,284" "12% vs previous 7 days" with trending-up; "9" "7 people, 2 API
keys"; "6" "0.5% of all actions"; "11" "11 flagged"
And the filter bar shows "Search activity", "All people", "All categories", All selected in
@@ -340,7 +340,7 @@ Feature: Activity screen, details slide-over and My activity
Scenario: Loading — skeletons after 150 ms while both requests are in flight
Given GET /v1/activity and GET /v1/activity/summary have not resolved
When the user navigates to /activity
Then the header, the filter bar and an enabled "Export CSV" are drawn at once
Then the header, the filter bar and an enabled "Export" are drawn at once
And nothing is drawn in place of the strip and the table for the first 150 ms
And after 150 ms 4 skeleton cards replace the strip and 9 skeleton rows (5 cards below md)
replace the table, the pager is hidden and the table container is aria-busy="true"
@@ -377,7 +377,7 @@ Feature: Activity screen, details slide-over and My activity
"0" "Nothing to flag"
And an empty panel with icon history shows "No activity yet" and "Actions on articles,
media, users and API keys will appear here."
And "Export CSV" is disabled and no table header or pager is shown
And "Export" is disabled and no table header or pager is shown
Scenario: Empty, filters match nothing — the panel offers Clear filters
Given the Default state has loaded
@@ -386,7 +386,7 @@ Feature: Activity screen, details slide-over and My activity
Then the chips read "Search: sitemap", "Category: Media", "Outcome: Failed" and "Clear all"
And an empty panel with icon search-x shows "No activity matches these filters" and "Try a
different search or a wider date range." with the action "Clear filters"
And "Export CSV" stays enabled and the strip is unchanged
And "Export" stays enabled and the strip is unchanged
When the user activates "Clear filters"
Then every filter is reset, GET /v1/activity is sent with range=7d, page=1, size=25 only and
the rows come back
@@ -397,7 +397,7 @@ Feature: Activity screen, details slide-over and My activity
Then the strip and the filter bar are shown
And an error alert shows "Couldn't load activity" and "Check your connection and try again."
with the action "Try again"
And "Export CSV" is disabled and no table or pager is shown
And "Export" is disabled and no table or pager is shown
When the user activates "Try again"
Then the Loading state shows for the list and GET /v1/activity is sent again with the same
query
@@ -426,7 +426,7 @@ Feature: Activity screen, details slide-over and My activity
Scenario: Forbidden (403) — the page itself is refused
Given GET /v1/activity resolves 403 and GET /v1/auth/me resolves 200 with a verified user
When the user navigates to /activity
Then the header shows "Activity" and no "Export CSV"
Then the header shows "Activity" and no "Export"
And no strip, filter bar, chips, table or pager is shown
And an empty panel with icon lock shows "You can't view this activity" and "Your role
doesn't include this page. Ask an owner or admin if you need access." with the action "Back
@@ -529,29 +529,6 @@ Feature: Activity screen, details slide-over and My activity
When the user activates "Show 6 results"
Then the sheet closes and the list stays filtered
Scenario: Export — the file is prepared, then downloads
Given the Default state has loaded with the filters Category "Authentication" and 30 days
When the user activates "Export CSV"
Then GET /v1/activity/export is sent with category=auth and range=30d and without page or size
And while it is in flight the button reads "Preparing file…" and is disabled
When it resolves 200 with a text/csv body
Then the file is downloaded as "activity.csv" and a toast appears: color success, icon
i-lucide-circle-check, title "Export started. Your file will download shortly."
And the button returns to "Export CSV", enabled
Scenario Outline: Export — a failure raises an error toast and re-enables the button
Given the Default state has loaded
When the user activates "Export CSV" and GET /v1/activity/export <failure>
Then a toast appears: color error, icon i-lucide-circle-x, title "Couldn't export activity.
Try again."
And nothing is downloaded and the button returns to "Export CSV", enabled
Examples:
| failure |
| resolves with a 500 response |
| resolves with a 403 response |
| never reaches the server |
Scenario: Details — a row opens the slide-over and focus returns on close
Given the Default state has loaded
When the user activates the second row (click, or Enter with the row focused)
@@ -661,7 +638,7 @@ Feature: Activity screen, details slide-over and My activity
When the user navigates to /activity
Then the title reads "My activity", the subtitle "Everything you've done on this site." and
the breadcrumb "My activity"
And no summary strip, no Person filter and no "Export CSV" are rendered
And no summary strip, no Person filter and no "Export" are rendered
And the table has the columns When, Action, Target, Outcome and no Actor column
And the cards show no actor line below md
And GET /v1/activity is sent with range=7d, page=1, size=25 and no actor parameter, and
@@ -683,7 +660,7 @@ Feature: Activity screen, details slide-over and My activity
Scenario: The French interface composes sentences and chrome in French
Given the language is French and the Default state has loaded as owner
Then the title reads "Activité", the strip labels, filter placeholders, badges, pager and
"Exporter en CSV" are in French, the sentences use the French templates and day
"Exporter" are in French, the sentences use the French templates and day
separators read "Aujourd'hui", "Hier" or "3 oct. 2026"
And the Security sub-line reads "11 signalés"
And every new English string of this screen has a French entry in messages.ts
+9 -15
View File
@@ -52,7 +52,7 @@ zeros) and `summaryNone` (all zeros). The list response is `{ items, total, page
1. **Navigation** — sidebar and drawer list Home, Articles, Media, Users, Activity, Integration,
Settings in order; "Activity" has the `i-lucide-history` icon, links to /activity, has
aria-current="page" on /activity and none elsewhere; breadcrumb and mobile title "Activity".
2. **Default (owner)** — assert header texts, enabled "Export CSV", the four strip cards (labels,
2. **Default (owner)** — assert header texts, enabled "Export", the four strip cards (labels,
"1,284", "9", "6", "11", sub-lines "12% vs previous 7 days" with the trending-up icon, "7
people, 2 API keys", "0.5% of all actions" with icon `circle-x`, "11 flagged" with icon
`shield-alert`), the filter bar (placeholder, "All people", "All categories", Outcome All
@@ -85,7 +85,7 @@ zeros) and `summaryNone` (all zeros). The list response is `{ items, total, page
antérieure" and "Aucun changement"; ranges Today, 30 days and Custom give "vs previous day",
"vs previous 30 days" and "vs previous period".
10. **Empty, no activity yet** — `summaryNone`, items []; assert "No data yet" on two cards, the
calm lines on the other two, the empty panel copy and icon, a disabled "Export CSV", no table
calm lines on the other two, the empty panel copy and icon, a disabled "Export", no table
header and no pager.
11. **Empty, filters match nothing** — type "sitemap", choose Category "Media" and Outcome
"Failed", resolve items []; assert chips, empty panel copy, "Clear filters", enabled export;
@@ -122,14 +122,8 @@ zeros) and `summaryNone` (all zeros). The list response is `{ items, total, page
22. **Mobile list and sheet** — below md assert cards (actor line, sentence, badges, "Target
deleted"), "Filters" with the badge count, the sheet content and footer ("Show 6 results", "Show
1 result"), two date fields for Custom, and that "Show … results" closes it.
23. **Export success** — click "Export CSV" with filters applied; assert one request to
`/v1/activity/export` with the list filters and no `page` or `size`, "Preparing file…" and
disabled while pending, an object URL created and an anchor clicked with
`download="activity.csv"`, the success toast (color success, icon `i-lucide-circle-check`,
title "Export started. Your file will download shortly."), and the button back to "Export CSV".
24. **Export failure, Examples-driven** — 500, 403 and `NetworkError`; assert the error toast
(color error, icon `i-lucide-circle-x`, title "Couldn't export activity. Try again."), no
download, button re-enabled.
23. **Export** — superseded by activity_filter_cards_and_export.qa.md (menu, dialog, toasts).
24. **Export failure** — superseded by activity_filter_cards_and_export.qa.md.
25. **Details open and close** — click, then Enter on a focused row; assert the title, the selected
row class, the footer "Event 2 of 1,284"; close with Escape, the close button and the scrim and
assert focus on the row.
@@ -156,7 +150,7 @@ zeros) and `summaryNone` (all zeros). The list response is `{ items, total, page
request to `/summary` or `/export` and no `actor` parameter; pager "Showing 1–25 of 41"; empty
state copy; details "Event 1 of 41" with previous disabled.
31. **French** — `setLocale('fr')`; assert the chrome strings (Activity "Activité", My activity
"Mon activité", Export CSV "Exporter en CSV", Preparing file… "Préparation du fichier…",
"Mon activité", Export CSV "Exporter", Preparing file… "Préparation du fichier…",
Total actions "Actions au total", Active people "Personnes actives", Failed actions "Actions
en échec", Security events "Événements de sécurité", Succeeded "Réussie", Failed "Échec", Not
signed in "Non connecté", Activity details "Détails de l'activité", Previous event and Next
@@ -199,14 +193,14 @@ the sub-line "11 flagged". Total is 1284 with page size 25 for the owner and 41
- [ ] Loading: both requests with `hang: true`; the setup waits 150 ms or more so the skeleton
strip and rows are drawn before the capture.
- [ ] Empty, no activity yet: list `items: [], total: 0`, summary `summaryNone`; "Export CSV"
- [ ] Empty, no activity yet: list `items: [], total: 0`, summary `summaryNone`; "Export"
disabled.
- [ ] Empty, filters match nothing: the board applies Search "sitemap", Category "Media", Outcome
"Failed" (steps type and select them, or the mock answers with the filtered query), list
`items: [], total: 0`, summary Default.
- [ ] Error with retry: list 500, summary Default; the alert, disabled export, strip drawn.
- [ ] Forbidden (403): list 403, summary 403; session with a non-admin role as the board does
(`role: 'user'`), so no "Export CSV".
(`role: 'user'`), so no "Export".
- [ ] My activity, Empty: `activityEditorSession`, list `items: [], total: 0`.
- [ ] skip: clock-sensitive. Default, large dataset: `skip: 'clock-sensitive'` (day separators and
"2 min ago" depend on the real clock). Prints SKIP, never investigated or date-frozen.
@@ -218,7 +212,7 @@ the sub-line "11 flagged". Total is 1284 with page size 25 for the owner and 41
the rows behind the scrim show relative times and the panel shows the browser's local
time. Their Vitest coverage is cases 3, 5, 25 to 30.
- [ ] Layout, from `ActivityFrame.dc.html`, not `components.md`: header row is the title block
(20 px/28 px title) on the left and the 32 px (44 px mobile) "Export CSV" button on the right,
(20 px/28 px title) on the left and the 32 px (44 px mobile) "Export" button on the right,
aligned to the bottom; the strip is 4 columns with a 12 px gap on desktop and a clipped row
of 156 px cards with an 8 px gap on mobile; the table header is 36 px, day rows 32 px, rows at
least 52 px; the slide-over is 480 px wide on desktop, the bottom sheet 88 % high.
@@ -238,7 +232,7 @@ the sub-line "11 flagged". Total is 1284 with page size 25 for the owner and 41
the right sentences, the shield marker on the failed login, the Succeeded and Failed badges, the
summary numbers and the sparkline bars; apply each filter and each card; page through with size
25, 50 and 100; open the details across a page boundary.
- **Real CSV** — click "Export CSV" with and without filters and open `activity.csv`: columns
- **Real CSV** — click "Export" with and without filters and open `activity.csv`: columns
`when,actor,action,target,outcome,source,ip`, the filtered rows only, no page or size applied,
neutralised cells that start with `=`, `+`, `-` or `@`; confirm the browser download prompt and
the two toasts.