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)
138 lines
7.6 KiB
Markdown
138 lines
7.6 KiB
Markdown
# Phase 5: Frontend — Activity tab + smart filtering + live updates
|
||
|
||
**Status:** ✅ Done
|
||
**Parent plan:** [PLAN.md](./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
|
||
|
||
- [x] `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.
|
||
- [x] `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`.
|
||
- [x] 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.
|
||
- [x] Icons: add a history/audit icon to `core/icon-paths.ts` + `core/icons.ts`; severity icons
|
||
(info/warning/error) reuse existing constants where possible.
|
||
- [x] 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").
|
||
- [x] 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
|
||
|
||
- [x] New **Activity** tab loads, lists entries, and paginates via keyset "load more".
|
||
- [x] Filters hit server-side query params; quick presets work; free-text is debounced.
|
||
- [x] New events append live via `server:activity_logged` and respect active filters.
|
||
- [x] Export downloads CSV/JSON with auth, honoring current filters.
|
||
- [x] Fully localized (en/ru/zh); empty/loading/error states; re-renders on language change.
|
||
- [x] No plain `<select>` (use chips); SVG icons only (no emoji).
|
||
- [x] `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
|
||
|
||
- [x] All tasks completed
|
||
- [x] Code follows frontend conventions (i18n ×3, icons, auth fetch, CSS vars)
|
||
- [x] No unintended side effects
|
||
- [x] 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:
|
||
|
||
```typescript
|
||
// 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)
|