feat(portal-admin): audit log tabs (Table / Charts) + server-side stats consumption #174
Reference in New Issue
Block a user
Delete Branch "feat/portal-admin-audit-tabs"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
PR 2 of the tabs + full-result-charts chantier — closes the loop opened in #173. The audit log page now splits into a Table tab (existing behaviour) and a Charts tab fed by the server-side stats endpoint shipped in PR 1.
What lands
Tabs UX — WAI-ARIA tab pattern
The two tabs sit between the filter card and the content area. Visual treatment is intentionally minimal: thin brand-coloured underline on the active tab, focus rings on
:focus-visible, no surrounding chrome. The panel below inherits the page surface so each tab swap reads as a content-only change.ARIA wiring:
<div role="tablist">with two<button role="tab">children.aria-selectedmirrors the active tab;aria-controlspoints each tab at its panel id; rovingtabindex(active =0, inactive =-1) keeps Tab linear.template/click-events-have-key-eventslint without an artificialtabindex="-1"on the container.<section role="tabpanel">with[hidden]binding,aria-labelledbypointing back at the tab id.Stats consumption — replaces the per-page computeds
AuditEventsServicegrows astats(filters)method that callsGET /api/admin/audit/statsand returnsAdminAuditStats(mirror of the BFF DTO). The audit page replaces the four per-pagecomputed()s —totalOnPage,dailyVolume,outcomeBreakdown,dailyByEventType— with four signals:The chart-tile components on the Charts panel consume
stats()?.dailyVolume,stats()?.outcomeBreakdown,stats()?.eventTypeByDay, andstats()?.totalunchanged — same shape as the old per-page projections, just sourced server-side.Lazy fetch policy
statsstats, re-fetch only if Charts activestats, re-fetch if Charts activeThe Redis cache in PR 1 absorbs repeated identical fetches at ~5 ms; the policy here just makes sure we don't fire a stats call when nobody's looking at it.
Honest panel copy
The Charts panel's note now reads:
Replaces the previous "this only reflects the current page" disclaimer that #172 carried as a temporary truth.
Notes for the reviewer
libs/shared/ui/tabsprimitive? Two tabs, one consumer, ~30 LOC of template + ~25 LOC of SCSS. The two-occurrences-is-duplication rule says wait until a second consumer appears (cf. CMS module or user-list module slated for the next chantier), then extract with the shape both consumers have actually demanded.setTab()async? It triggersfetchStats()on the first switch to Charts, and the spec exercises that flow withawait. Keeping itasynclets the test sequence assertions deterministically withouttick()/fakeAsync.search()clearstatsbefore the fetch even thoughfetchStats()clears it again at the top? So the empty-state never flickers through a "stale 1000-event" total while the new fetch is in flight on a slow link. The double-clear is intentional.audit.scssis 7.5 KB — over the 6 KB component-style warning, under the 8 KB error. The new.tablist/.tabrules account for the bump. If a third consumer of the tab pattern lands, the SCSS extraction comes with it.Test plan
pnpm nx test portal-admin— 62 specs pass (was 58: removed 3 obsolete "per-page chart" cases, added 7 new tabs/stats cases).pnpm nx run portal-admin:lint— clean (the two pre-existinguse-lifecycle-interfacewarnings onaudit.ts/users.tsand the spec non-null assertion are untouched, not regressions).pnpm nx build portal-admin— clean, audit chunk 84.5 KB gzip.pnpm nx run-many -t lint test build -p portal-shell,portal-admin,portal-bff,shared-charts— clean.pnpm nx serve portal-admin+pnpm nx serve portal-bff, sign in withPortal.Admin, navigate to/admin/audit:GET /api/admin/auditonly, no/audit/statscall.eventType=auth.sign_in) while on Charts → tiles re-render with the filtered aggregates; donut centre matches the servertotal./audit/statscalls (pagination doesn't invalidate).Tabskips past the inactive tab to the panel content.What's next
?tab=charts) so deep-linking and Back/Forward survive the swap. Out of scope for this chantier — once the user-list module lands and we have a second tab consumer, persistence becomes a shared concern.libs/shared/ui/tabswhen a second consumer materialises (CMS or user-list module).X-Audit-Stats-Cache: hit|missheader for ops or an OTel attribute on the BFF span. Out of scope here, but worth flagging while the design is fresh.