Skip to content

feat: user-activity admin table with filters + pagination (#1021 fatia 3) - #1024

Merged
milkway merged 1 commit into
mainfrom
feat/1021-user-activity-ui
Jul 20, 2026
Merged

feat: user-activity admin table with filters + pagination (#1021 fatia 3)#1024
milkway merged 1 commit into
mainfrom
feat/1021-user-activity-ui

Conversation

@milkway

@milkway milkway commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

O quê

Fatia 3 (final) de #1021 — a UI: a tabela "Atividades dos usuários" na aba Atividades, com filtros e paginação server-side. Fecha o épico (fundação #1022 + captura #1023 já mergeadas).

O que entra

  • Página /admin/activity (admin-only, RequireAdmin) — "Atividades dos usuários". Colunas Quando · Usuário · Evento · Aplicação · IP; acesso anônimo aparece como "Anônimo"; avatar colorido por hash igual às páginas Users/Audit.
  • Sub-nav de duas pílulas (Atividades dos usuários ⇄ Atividades administrativas) no topo das DUAS páginas (/admin/activity e /admin/audit), reusando o estilo .admin-nav-link.is-active (sem CSS novo). Ambas mantêm nav_section="audit", então o item "Atividades" do topo fica destacado nas duas. O link do topo continua apontando p/ /admin/audit (zero mudança de comportamento existente) — a nova view fica a um clique.
  • Filtros que o store já suporta (exatos, sem máquina de LIKE): evento (todos/login/acesso), usuário e app via dropdowns de valores distintos (distinct_usernames/distinct_specs, no padrão do dropdown de autores da auditoria), e período (24h/7d/30d → since).
  • Paginação server-side (50/página) no padrão perf: /admin/users loads every user unbounded — slow with large user counts; add pagination #999 (list_page/count_filtered), com os filtros preservados nos links do pager (query-string percent-encoded).
  • i18n completa nos 4 locales (pt/en/es/fr), incl. plural Fluent no status do pager.
  • Dep urlencoding = "2" declarada direto (já estava na árvore transitivamente; verificada 2.1.3 no lock) p/ encodar os valores de filtro no query-string do pager.

Testes

  • UI (integration, activity_ui.rs): gate admin-only (sem sessão ≠ 200); renderiza as linhas; filtro por evento estreita (login vs acessos); filtro por usuário estreita; paginação (60 registros → 50 na pág.1, 10 na pág.2), contando linhas reais da tabela pelo marcador data-avatar=" (não conta os dropdowns nem o script).
  • Store: distinct_usernames/distinct_specs (exclui NULL, ordenado) em SQLite + assert no teste postgres-it real.
  • Gate: cargo test (default, incl. o load_default_succeeds do Fluent), clippy limpo, i18n-check, postgres-it contra Postgres 16 real, e o gate icon_subset (troquei ti-login-2, fora do subset, por ti-login, já presente).

Épico #1021 — completo com esta fatia

  1. feat: user_activity store + supervised writer (#1021 fatia 1) #1022 migração + store + writer ✅ 2. feat: capture login + app-access into user_activity (#1021 fatia 2) #1023 captura ✅ 3. esta UI.

Follow-up conhecido (fora do épico, anotado na #1021): captura de clique em app External (o bounce precede o guard de acesso — precisa de reorder do handler).

🤖 Generated with Claude Code

…a 3)

Final slice of #1021 — the UI. A new admin-only page "Atividades dos
usuários" (`/admin/activity`) lists the user_activity rows captured in
fatia 2, alongside the administrative audit log.

- Page: columns When · User · Event · App · IP; anonymous access shows as
  "Anonymous"; hashed-colour avatars like the Users/Audit pages.
- Sub-nav: two pills (user activity ⇄ administrative audit) on both pages,
  reusing the existing .admin-nav-link.is-active style (no new CSS); both
  keep nav_section="audit" so the top nav stays highlighted. The top-nav
  link is unchanged (the new view is one click away).
- Filters the store already supports exactly (no LIKE machinery): event
  select, user + app dropdowns of distinct values (new distinct_usernames /
  distinct_specs), and a period select (24h/7d/30d -> since).
- Server-side pagination (50/page, #999 pattern) with filters preserved on
  the pager links (percent-encoded).
- i18n across pt/en/es/fr incl. a Fluent plural in the pager status.
- Declares urlencoding = "2" directly (already transitive; 2.1.3 in the
  lock) for the pager query-string encoding.

Tests: UI integration (admin-only gate, rendered rows, event/user filters,
60-row pagination counted by the per-row data-avatar marker) and the store
distinct helpers (SQLite + postgres-it). Swapped ti-login-2 (outside the
shipped Tabler subset) for ti-login to keep the icon_subset gate green.

Closes #1021 (External-click capture noted as a follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@milkway

milkway commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Review (self, adversarial pass) — MERGE-OK

  • Access control: /admin/activity is RequireAdmin (test asserts a no-session request never 200s), same gate as the audit log.
  • Injection-safe: every filter value goes through QueryBuilder::push_bind (?/$n); distinct_* are static SQL. The event/period params are normalized to a fixed allow-list before touching the query (a forged value degrades to "all"), so the :class echo and the WHERE are both bounded. Askama auto-escapes the rendered usernames/spec ids.
  • Pagination: count → clamp page into [1, pages] → offset, exactly the perf: /admin/users loads every user unbounded — slow with large user counts; add pagination #999 pattern; pager links carry percent-encoded filters. Verified by the 60-row test (50 + 10) counting real table rows via the per-row data-avatar=" marker (dropdowns/script excluded).
  • i18n: parity green across pt/en/es/fr; load_default_succeeds green (no duplicate ids); Fluent plural in the pager status.
  • No new CSS / minimal surface: sub-nav reuses .admin-nav-link.is-active; both pages keep nav_section="audit" so the top nav stays lit; the top-nav target is unchanged (zero regression to existing behavior). ti-login is in the shipped Tabler subset (icon_subset gate green). urlencoding = "2" declared directly, verified 2.1.3 in the lock.

Store distinct_* helpers tested on SQLite + real Postgres (postgres-it). Gate: cargo test, clippy clean, i18n, migration parity, postgres-it, icon subset — CI 6/6.

Closes #1021 (External-click capture is a noted follow-up). No blockers.

@milkway
milkway marked this pull request as ready for review July 20, 2026 22:12
@milkway
milkway merged commit 3517917 into main Jul 20, 2026
6 checks passed
@milkway
milkway deleted the feat/1021-user-activity-ui branch July 20, 2026 22:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant