9.7 KiB
QA: Recent activity card on Home and Retention card on Settings
Implementation note for coder (informational, not a QA assertion)
- Backend: add NextCleanupAt (nullable time) to the audit settings entity with a domain method computing the next 03:00 (audit.CleanupHour) strictly after a given time, in that time's location; nil when RetentionDays is audit.ForeverRetentionDays. GetActivityRetentionUseCase and UpdateActivityRetentionUseCase get an injected
now func() time.Time(wired in api/dependecyInjection.go) and populate it. settingsResponse in api/handlers/settings gainsactivity_next_cleanup_at(*time.Time, RFC 3339, null when nil). No logic in the handler, none in the SPA. - Home: HomeView mounts a RecentActivityCard calling GET /v1/activity?size=5 once, reusing ActivityActor, ActivitySentence and lib/activityTime.
- Settings: widen WorkspaceSettings with the four activity_* fields, add a PATCH helper to useSettings, render RetentionCard after MediaCleanupCard for session.role === 'owner' only. No second GET /v1/settings.
- features/settings_page.feature and its qa.md say the body is exactly {"version"}; this feature supersedes that statement, so update those tests to the new five-key body.
Automated, Go (httptest, in-memory fakes, task test)
Build settings.NewHandler(GetSettingsUseCase, GetActivityRetentionUseCase, UpdateActivityRetentionUseCase) over a fake audit.SettingsRepository (records saved retention and last cleanup) and a fake audit.Recorder, with a fixed fake clock. Mount on a fresh chi.NewRouter mirroring NewSettingsRouter: GET "/" behind jwtauth.Verifier + Authenticator (test jwtauth.New("HS256", ...)); PATCH "/" additionally behind the owner check using a fake user repository holding an owner, an admin and an editor. Call validator.InitValidator() first. No live server, no SQLite file. Fake type names must be unique across test files.
- G1. GET table (clock in a +02:00 zone): retention 90 at 10:00 gives next = next day 03:00+02:00; at 02:59:59 gives same day 03:00; at exactly 03:00:00 gives next day 03:00; 30 at 2026-12-31T23:30 gives 2027-01-01T03:00; 365 at 10:00 gives next day; retention 0 gives JSON null (assert the key is present with value null, not absent).
- G2. GET body has exactly the five keys (decode into map[string]any, assert the key set); last cleanup 2026-10-06T01:00:00Z and 142 preserved.
- G3. GET is 200 for owner, admin and editor tokens; 401 without a cookie.
- G4. PATCH owner 90->30: 200, activity_retention_days 30, next cleanup non-null at the next 03:00; the fake repository holds 30; one audit event with params from 90, to 30.
- G5. PATCH owner 90->0: 200, next null. PATCH 0->365: next non-null.
- G6. PATCH owner 90->90: 200, next non-null, no repository save, no audit event.
- G7. Ownership: PATCH as admin and as editor: 403 with "Only the owner can do this."; repository unchanged; no audit event. PATCH without a cookie: 401.
- G8. Invalid: {"activity_retention_days": 7}, "30" (string), {}, malformed JSON: each 400 with the apperr.BodyErrorMessage text (plain text via http.Error), repository unchanged.
- G9. Use-case level (no HTTP): GetActivityRetention returns NextCleanupAt per G1 rows; UpdateActivityRetention(7) returns an error satisfying errors.Is(err, audit.ErrInvalidRetention) and saves nothing; a repository Get error propagates and the handler answers 500 "Internal server error".
- G10. Domain: NextCleanupAt table including a DST-change day in a named zone (for example Europe/Paris, 2026-10-25), asserting it is still 03:00 local on the right date.
Automated, SPA (Vitest + Vue Test Utils, task test:web)
Suggested files: web/src/views/tests/HomeActivityCard.spec.ts and SettingsRetention.spec.ts. Mock useApi per case; no real backend. Freeze the clock with vi.setSystemTime only where a time label is asserted (these specs are not visual).
Home (mount HomeView with a resolved session; GET /v1/article/ mocked with the 6-article fixture):
- Owner and admin: 5 events, title "Recent activity", "View all" links to /activity; row 1 text with semibold target, "Marta Alves · 2 min ago", avatar "MA"; the activity mock was called once with exactly size=5 and no other parameter; /summary and /export never called.
- Regular user (editor, absent role): "Your recent activity"; meta lines are time only; no actor parameter.
- Marker table over outcome x security (no badge and no shield, Failed badge, shield only, badge only).
- Time label table (2 min ago, 3 h ago, "Yesterday, 14:20", "3 Oct 2026, 14:20") with fixed time; the title attribute holds the full local time.
- Clamp class on a 150-character title; unknown action "article.archive" renders the code; auth.login_failed sentence with Failed badge and user-round-x avatar.
- Rows are inert: clicking one changes no route and opens nothing.
- Skeleton only after 150 ms (fake timers): none before, 5 skeleton rows and aria-busy after, replaced on resolve; a fast response never shows one.
- Error table (500, 403, network): role="alert", title "Couldn't load recent activity", no description, "Try again"; stat tiles, article list and "Connect your website" still rendered; retry sends a second GET and recovers.
- Empty: both copies by role; "View all" still present.
- Independence in both directions (articles 500 + activity 200; articles [] + activity 500).
- "View all" navigates to /activity (test router).
- Layout classes: grid with the lg two-column template and the 420 px column, and no max-width class on the Home wrapper or the grid (the grid spans the full content width); DOM order puts the card first below lg (real pixels are manual).
- Existing home specs (HomeView, HomeUpdateBanner, HomeUpdateReady) keep passing with the activity mock added to the shared mount helper.
Settings (mount SettingsView, GET /v1/settings mocked with the five-key body):
- Hidden for admin, editor and absent role; GET /v1/settings called once.
- Owner Default: header, "90 days", helper, lock alert, "Last cleanup 6 Oct 2026, 03:00 · 142 events removed", "Next cleanup 7 Oct 2026, 03:00" (from activity_next_cleanup_at; the SPA computes nothing); no PATCH; "1 event removed" singular.
- last_cleanup_at null: "Never". Forever with null/null: "Never" and "Not scheduled". Forever with a past cleanup: the real last line and "Not scheduled".
- Skeleton after 150 ms with aria-busy; load error alert and "Try again" re-runs the shared GET.
- Longer periods (90->365, 30->90, 90->Forever): one PATCH with {"activity_retention_days": N}, "Saving…" then "Saved", gone after 2 s (fake timers), no warning; the returned next cleanup replaces the line; a Forever response with next null shows "Not scheduled".
- Shortening: no PATCH, warning copy with the date taken from the loaded activity_next_cleanup_at; Apply sends once; Cancel reverts the select with no PATCH; table Forever->365, 365->90, 90->90 (no-op), 30->Forever (immediate); a longer choice while the warning is open drops it and leaves no stale warning after 30 days then 365 days again.
- Save error x (500, 400, 403, network): alert copy, select reverts, lines unchanged; Try again re-sends the same value without re-confirming; success shows "Saved"; error after Apply also clears the warning; a 403 triggers a session refresh and the card disappears when the role is no longer owner.
- Offline (mock useOnline false): select, Apply and Cancel disabled; nothing sent; page offline banner text unchanged.
- 401 handled globally: no local alert.
- French key parity: every new string has a messages.ts entry.
- Keyboard: the select opens with Enter or Space, arrows and Enter choose, Escape closes; Apply then Cancel reachable in order after the select.
- Existing SettingsView, SettingsMediaCleanup and Settings update specs keep passing with the widened mock body.
Visual (task visual:check)
- Neither card can be pixel-compared (dashed placeholders); no new visual state is added. The card sub-states are not
###headings in home.md or settings.md, so the tool should not iterate them; confirm on the first run. - Existing Home states and owner-session Settings states must still pass at 10 % with the card present. Every Home setup in web/visual/states.ts needs GET /v1/activity?size=5 mocked (an unmocked /v1 call fails the gate), and every owner Settings setup needs the widened GET /v1/settings body. Layout changes may exceed the gate: measure first, then ask the user for a per-state
exception(needs approvedBy). Do not add any exception beforehand. - Time-dependent states stay
skip: 'clock-sensitive'; no Vitest spec freezes the date for a visual claim.
Manual verification
- Real GET /v1/settings and PATCH /v1/settings against
task devwith a live SQLite file: five keys, next cleanup at the next 03:00 server time, null after switching to Forever, a timestamp after leaving Forever; as an admin PATCH is 403, as the owner a bad value is 400. - Browser in a time zone different from the server's: the "Next cleanup" and "Last cleanup" lines both show the user's local time of the same instants; the warning date matches the "Next cleanup" line.
- Real daily cleanup (or a forced run) updates "Last cleanup" and moves "Next cleanup" a day forward.
- Real GET /v1/activity?size=5 as owner, admin and editor (server-side own-event scoping), including a site with only events older than 7 days (reads "No activity yet").
- Home at 1440 and 390 px, light and dark: 420 px column in the far-right of the full content width, 16 px gap, row clamping, "View all" 44 px touch target; the article column fills the remaining width and still reads well.
- Keyboard and focus: Home tab order "New article", article rows, "View all"; Settings select, Apply, Cancel in order and focus back on the select after Cancel.
- Hover, focus rings, the 2 s "Saved" fade and reduced motion.