Files
ledgrab/plans/activity-log/phase-5-frontend-tab.md
T
alexei.dolgolyov 9a0137fa4c feat(activity-log): phase 5 - Activity tab (smart filtering, live updates, export)
- new top-level Activity tab: filter toolbar (category/severity chips, presets, debounced search, actor/entity/date), keyset load-more, expandable detail
- live prepend via server:activity_logged; authed CSV/JSON blob export
- formatTimestamp/formatRelativeTime in core/ui.ts; history+severity SVG icons; Ctrl+7 shortcut
- i18n activity_log.* across en/ru/zh; getting-started tutorial step; activity-log.css (themed)
- review fixes: newest-first ordering, attribute-context XSS hardening (_escapeAttr + event delegation)
2026-06-09 20:42:44 +03:00

7.6 KiB
Raw Blame History

Phase 5: Frontend — Activity tab + smart filtering + live updates

Status: Done Parent plan: PLAN.md Domain: frontend · uses the frontend-design skill

Objective

Build the dedicated top-level Activity tab: a read-only, smart-filterable, keyset-paginated log viewer with an entry detail view, live-append of new events, and export. This is a viewer (Dashboard-style), NOT a CRUD card section.

Tasks

  • core/ui.ts: add formatTimestamp(isoOrMs) (Today/Yesterday/Date · HH:MM, i18n-aware) and formatRelativeTime(isoOrMs) ("2m ago"), with tabular-nums styling guidance for the list. Reuse existing time.* i18n key conventions.
  • features/activity-log.ts:
    • export async function loadActivityLog() — fetch first page from GET /activity-log (via fetchWithAuth), render the toolbar + list into the panel.
    • Smart filter toolbar: category (multi, chips), severity (chips), actor (text input), entity type, date range, free-text search (debounced). Quick presets: Today / Errors / Auth / Devices. Filters drive server-side query params (no client-side filtering of a partial page). Re-query on change; reset cursor.
    • List: one row per entry — severity icon, category badge, relative time (title=absolute), actor, message, entity crosslink (use navigateToCard(...) when the referenced entity is resolvable). Keyset "load more" (or infinite scroll) using next_before_seq.
    • Detail: expandable row / drawer showing full metadata JSON, exact timestamp, ids.
    • Live append: document.addEventListener('server:activity_logged', e => …) — prepend the new entry if it passes the active filters; show a subtle "new" affordance. (Depends on the Phase 2 allowlist entry — already shipped.)
    • Export button: triggers GET /activity-log/export?format=… with current filters via an authed blob download (use fetchWithAuth → blob URL, per frontend.md auth rules).
    • Empty / loading / error states; re-render on languageChanged.
  • Tab wiring:
    • core/tab-registry.ts: add activity_log: { loadFnName: 'loadActivityLog', autoRefresh: false }.
    • templates/index.html: sidebar tab button (data-tab, switchTab('activity_log'), history/clock SVG icon, data-i18n) + <div class="tab-panel" id="tab-activity_log">.
    • app.ts: import + Object.assign(window, { loadActivityLog, … }); global.d.ts decls.
  • Icons: add a history/audit icon to core/icon-paths.ts + core/icons.ts; severity icons (info/warning/error) reuse existing constants where possible.
  • i18n: add activity_log.* keys to static/locales/{en,ru,zh}.json (title, filter labels, category/severity names, column labels, presets, empty/error, export, "N entries").
  • Tutorials: add an Activity-tab step to the getting-started tour in features/tutorials.ts + tour.* keys in all 3 locales.

Files to Modify/Create

  • server/src/ledgrab/static/js/core/ui.ts — modified: timestamp/relative-time formatters
  • server/src/ledgrab/static/js/features/activity-log.ts — NEW: the viewer
  • server/src/ledgrab/static/js/core/tab-registry.ts — modified: register tab
  • server/src/ledgrab/templates/index.html — modified: tab button + panel
  • server/src/ledgrab/static/js/app.ts — modified: import + window globals
  • server/src/ledgrab/static/js/global.d.ts — modified: window decls
  • server/src/ledgrab/static/js/core/icon-paths.ts — modified: icons (scrollText, circleAlert, info, filter, xCircle)
  • server/src/ledgrab/static/js/core/icons.ts — modified: ICON_ACTIVITY_LOG, ICON_SEVERITY_*, ICON_FILTER, ICON_X_CIRCLE
  • server/src/ledgrab/static/locales/{en,ru,zh}.json — modified: activity_log.* + time.* + tour.activity_log
  • server/src/ledgrab/static/js/features/tutorials.ts — modified: gettingStartedSteps
  • server/src/ledgrab/static/css/activity-log.css — NEW: list + toolbar styling
  • server/src/ledgrab/static/css/all.css — modified: import activity-log.css

Acceptance Criteria

  • New Activity tab loads, lists entries, and paginates via keyset "load more".
  • Filters hit server-side query params; quick presets work; free-text is debounced.
  • New events append live via server:activity_logged and respect active filters.
  • Export downloads CSV/JSON with auth, honoring current filters.
  • Fully localized (en/ru/zh); empty/loading/error states; re-renders on language change.
  • No plain <select> (use chips); SVG icons only (no emoji).
  • npx tsc --noEmit clean; npm run build succeeds.

Notes

  • Used the frontend-design skill for the viewer layout, filter toolbar, and detail design.
  • Models mirrored: features/dashboard.ts (live viewer pattern), core/events-ws.ts (WS dispatch), core/api.ts (fetchWithAuth), core/navigation.ts (navigateToCard).
  • Frontend-only phase — ran tsc --noEmit + npm run build; did NOT run pytest/ruff.

Review Checklist

  • All tasks completed
  • Code follows frontend conventions (i18n ×3, icons, auth fetch, CSS vars)
  • No unintended side effects
  • Build passes (tsc --noEmit + npm run build)
  • Manual smoke: tab loads, filters query server, live append, export

Handoff to Next Phase

Reusable helpers for Phase 6 (Dashboard widget + Settings panel)

From features/activity-log.ts:

Export Purpose How Phase 6 uses it
loadActivityLog() Loads the full tab (already registered as the tab loader) Phase 6 doesn't call this, but it shares state
activityLogToggleDetail(id) Expand/collapse an entry row Dashboard widget can reuse for the Recent Activity list
activityLogExport(format) Authed blob download with current filters Could surface a direct link in the Settings panel
_renderEntryRow(entry, isNew) Renders a single entry as an HTML string Dashboard widget should import this to render its 5-10 most-recent entries
_fetchPage(beforeSeq, append) Keyset-paged fetch from /activity-log Dashboard widget calls with limit=5&categories=… for its mini-list

Note: _renderEntryRow and _fetchPage are module-private (underscore prefix). Phase 6 should either (a) re-export them with public names, or (b) duplicate the minimal render logic for the compact widget format.

Recommended approach for Phase 6: Export two new public helpers:

// Add to activity-log.ts
export async function fetchRecentEntries(limit = 5): Promise<ActivityEntry[]>
export function renderCompactEntry(entry: ActivityEntry): string

i18n namespace

All keys are under activity_log.*. Time-relative formatters use time.* keys.

CSS classes and tokens introduced

Class Purpose
.al-panel Tab root wrapper
.al-toolbar Filter toolbar container
.al-chip / .al-chip.active Category/severity toggle chips
.al-preset-btn Quick-preset buttons
.al-entry / .al-entry-row Log entry row
.al-detail / .al-detail-grid Expandable entry detail
.al-cat-* Per-category badge colors (auth/device/entity/capture/system)
.al-sev-info/warning/error Severity icon color classes
.al-live-dot Pulsing green live-update dot
.al-meta-pre Scrollable metadata JSON block
.tabular-nums font-variant-numeric: tabular-nums utility

Settings endpoint shape used

Phase 6 Settings panel will call:

  • GET /activity-log/settings{ enabled: bool, max_days: int, max_entries: int }
  • PUT /activity-log/settings → same shape (bounds: enabled=bool, max_days=03650, max_entries=010_000_000)