feat: recent activity card on Home and Retention card on Settings
This commit is contained in:
1 parent
35db442cd3
commit
43469ef08d
33 files changed
+2748
-204
No files matched your search
@@ -0,0 +1,74 @@
|
||||
# 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 gains `activity_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):
|
||||
|
||||
1. 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.
|
||||
2. Regular user (editor, absent role): "Your recent activity"; meta lines are time only; no actor parameter.
|
||||
3. Marker table over outcome x security (no badge and no shield, Failed badge, shield only, badge only).
|
||||
4. 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.
|
||||
5. 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.
|
||||
6. Rows are inert: clicking one changes no route and opens nothing.
|
||||
7. 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.
|
||||
8. 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.
|
||||
9. Empty: both copies by role; "View all" still present.
|
||||
10. Independence in both directions (articles 500 + activity 200; articles [] + activity 500).
|
||||
11. "View all" navigates to /activity (test router).
|
||||
12. 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).
|
||||
13. 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):
|
||||
|
||||
14. Hidden for admin, editor and absent role; GET /v1/settings called once.
|
||||
15. 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.
|
||||
16. 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".
|
||||
17. Skeleton after 150 ms with aria-busy; load error alert and "Try again" re-runs the shared GET.
|
||||
18. 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".
|
||||
19. 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.
|
||||
20. 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.
|
||||
21. Offline (mock useOnline false): select, Apply and Cancel disabled; nothing sent; page offline banner text unchanged.
|
||||
22. 401 handled globally: no local alert.
|
||||
23. French key parity: every new string has a messages.ts entry.
|
||||
24. Keyboard: the select opens with Enter or Space, arrows and Enter choose, Escape closes; Apply then Cancel reachable in order after the select.
|
||||
25. 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 dev` with 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.
|
||||
Reference in new issue
Block a user