9a0137fa4c
- 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)
7.6 KiB
7.6 KiB
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: addformatTimestamp(isoOrMs)(Today/Yesterday/Date · HH:MM, i18n-aware) andformatRelativeTime(isoOrMs)("2m ago"), withtabular-numsstyling guidance for the list. Reuse existingtime.*i18n key conventions.features/activity-log.ts:export async function loadActivityLog()— fetch first page fromGET /activity-log(viafetchWithAuth), 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) usingnext_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 (usefetchWithAuth→ blob URL, per frontend.md auth rules). - Empty / loading / error states; re-render on
languageChanged.
- Tab wiring:
core/tab-registry.ts: addactivity_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.tsdecls.
- 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 tostatic/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 formattersserver/src/ledgrab/static/js/features/activity-log.ts— NEW: the viewerserver/src/ledgrab/static/js/core/tab-registry.ts— modified: register tabserver/src/ledgrab/templates/index.html— modified: tab button + panelserver/src/ledgrab/static/js/app.ts— modified: import + window globalsserver/src/ledgrab/static/js/global.d.ts— modified: window declsserver/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_CIRCLEserver/src/ledgrab/static/locales/{en,ru,zh}.json— modified: activity_log.* + time.* + tour.activity_logserver/src/ledgrab/static/js/features/tutorials.ts— modified: gettingStartedStepsserver/src/ledgrab/static/css/activity-log.css— NEW: list + toolbar stylingserver/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_loggedand 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 --noEmitclean;npm run buildsucceeds.
Notes
- Used the
frontend-designskill 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=0–3650, max_entries=0–10_000_000)