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)
This commit is contained in:
@@ -69,3 +69,4 @@ Phase 1 landed (2026-06-09): `activity_log.py` (dataclass + enums + filters + co
|
||||
Phase 2 landed (2026-06-09): `core/activity_log/` package (`context.py`, `recorder.py`, `retention.py`, `__init__.py`); actor ContextVar set in `api/auth.py` (both branches); `ActivityLogRetentionEngine` mirroring AutoBackupEngine; full wiring in `main.py` (repo at module level, recorder+engine in lifespan, `server.shutting_down` first shutdown action, engine stop before db.close); DI getters in `api/dependencies.py`; `activity_logged` added to `_ALLOWED_SERVER_EVENT_TYPES` in `events-ws.ts`; `set_module_recorder` exposes recorder to non-DI sites; 24 new tests — all green. Full suite 2309 passed, 2 skipped, 0 failed. Ruff clean.
|
||||
Phase 4 landed (2026-06-09): schemas (`api/schemas/activity_log.py`), routes (`api/routes/activity_log.py`: list/export/settings/clear), router registration in `api/__init__.py`, `get_seq_for_id` helper on `ActivityLogRepository`. 49 new tests — all green. Full suite 2486 passed, 2 skipped, 0 failed. Ruff clean. Pagination bug found and fixed (limit+1 probe must drop oldest row when has_more, not tail).
|
||||
Phase 3 landed (2026-06-09): instrumented all four categories — entity CRUD via `fire_entity_event` choke-point (`dependencies.py`), auth failures + WS session in `auth.py`, device online/offline in `device_health.py`, device discovered/lost in `discovery_watcher.py`, ADB connect/disconnect in `system_settings.py`, capture start/stop (individual + bulk) in `output_targets_control.py`, scene/playlist/automation activate in their respective route/engine files, backup/restore/delete + restart/shutdown/update/calibration/settings in `backup.py`/`update.py`/`calibration.py`; all 11 entity delete handlers pass `entity_name` to `fire_entity_event`; 22 new tests (security: token never in any field, explicitly asserted) — all green. Full suite 2369 passed, 2 skipped, 0 failed. Ruff clean. Complete (category, action) inventory in phase-3-instrumentation.md Handoff section.
|
||||
Phase 5 landed (2026-06-09): Activity tab frontend — `features/activity-log.ts` (loadActivityLog, filter toolbar with category/severity chips + presets + debounced search + date range + actor/entity-type text fields, keyset-paginated list, expandable detail drawer, live-prepend via `server:activity_logged`, authed CSV/JSON blob export); `core/ui.ts` additions (`formatTimestamp`, `formatRelativeTime`); `core/icon-paths.ts` + `core/icons.ts` additions (scrollText/audit, circleAlert, info, filter, xCircle); `core/tab-registry.ts` registered; `templates/index.html` tab button + panel; `app.ts` + `global.d.ts` wired; `static/css/activity-log.css` (precision-ledger design, category color rail, live-dot pulse, detail drawer, responsive breakpoints); all three locales updated (activity_log.* + time.* relative-time keys + tour.activity_log); `features/tutorials.ts` getting-started tour extended. `tsc --noEmit` clean, `npm run build` passes. Reusable helpers for Phase 6 documented in phase-5-frontend-tab.md Handoff section.
|
||||
|
||||
@@ -83,7 +83,7 @@ is an on-demand CSV/JSON **export** (no separate backup subsystem).
|
||||
| Phase 2: Recorder/Retention | backend | ✅ Done | ✅ Passed | ✅ Passed | ✅ |
|
||||
| Phase 3: Instrumentation | backend | ✅ Done | ✅ Passed | ✅ Passed | ✅ |
|
||||
| Phase 4: REST API | backend | ✅ Done | ✅ Passed | ✅ Passed | ✅ |
|
||||
| Phase 5: Frontend tab | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
| Phase 5: Frontend tab | frontend | ✅ Done | ✅ Passed | ✅ Passed (tsc+build) | ✅ |
|
||||
| Phase 6: Dashboard/Settings | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
|
||||
|
||||
## Outstanding Warnings
|
||||
@@ -99,6 +99,10 @@ is an on-demand CSV/JSON **export** (no separate backup subsystem).
|
||||
| 4 | PUT /settings only AuthRequired → anon could disable auditing/prune trail | 🟠 High (security) | resolved — `require_authenticated` on settings PUT |
|
||||
| 4 | CSV formula-injection missed leading TAB/CR | 🟡 Medium (security) | resolved — added `\t`/`\r` to guard |
|
||||
| 4 | `total` count full-scans on every list request | 🔵 Low (perf) | accepted — bounded by retention; read-only; optional opt-in deferred |
|
||||
| 5 | Inverted list ordering broke pagination + live-append | 🔴 Blocker | resolved — pages reversed to newest-first; re-review PASS |
|
||||
| 5 | Attribute-context XSS (entity_name title + JSON.stringify onclick) | 🟡 Warning (security) | resolved — `_escapeAttr` + data-attr event delegation |
|
||||
| 5 | Filter toolbar value= attrs not quote-escaped (new code) | 🟡 Warning (security) | resolved — `_escapeAttr` on q/actor/entity_type/since/until |
|
||||
| 5 | Manual browser smoke test (tab loads, filters, live, export) | 🔵 Note | open — recommend at final review (server restart needed) |
|
||||
|
||||
## Final Review
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Phase 5: Frontend — Activity tab + smart filtering + live updates
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Status:** ✅ Done
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** frontend · uses the `frontend-design` skill
|
||||
|
||||
@@ -12,14 +12,14 @@ 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)
|
||||
- [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.
|
||||
- [ ] `features/activity-log.ts`:
|
||||
- [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, IconSelect/chips), severity (chips), actor
|
||||
(EntitySelect/text), entity type, date range, free-text search (debounced). Quick presets:
|
||||
- **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),
|
||||
@@ -32,60 +32,106 @@ This is a viewer (Dashboard-style), NOT a CRUD card section.
|
||||
- **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:
|
||||
- [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.
|
||||
- [ ] Icons: add a history/audit icon to `core/icon-paths.ts` + `core/icons.ts`; severity icons
|
||||
- [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.
|
||||
- [ ] i18n: add `activity_log.*` keys to `static/locales/{en,ru,zh}.json` (title, filter labels,
|
||||
- [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").
|
||||
- [ ] Tutorials: add an Activity-tab step to the getting-started tour in
|
||||
- [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` — modify: 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` — modify: register tab
|
||||
- `server/src/ledgrab/templates/index.html` — modify: tab button + panel
|
||||
- `server/src/ledgrab/static/js/app.ts` — modify: import + window globals
|
||||
- `server/src/ledgrab/static/js/global.d.ts` — modify: window decls
|
||||
- `server/src/ledgrab/static/js/core/icon-paths.ts` / `core/icons.ts` — modify: icons
|
||||
- `server/src/ledgrab/static/locales/{en,ru,zh}.json` — modify: i18n keys
|
||||
- `server/src/ledgrab/static/js/features/tutorials.ts` — modify: tour step
|
||||
- `server/src/ledgrab/static/css/*` — modify/new: list + toolbar styling (follow base.css vars)
|
||||
- `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 IconSelect/EntitySelect/chips); SVG icons only (no emoji).
|
||||
- `npx tsc --noEmit` clean; `npm run build` succeeds.
|
||||
- [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
|
||||
|
||||
- **Use the `frontend-design` skill** for the viewer layout, filter toolbar, and detail design
|
||||
— aim for a polished, distinctive result consistent with the app's design language and CSS
|
||||
variables in `static/css/base.css` (`--primary-color`, `--card-bg`, etc.).
|
||||
- Models to mirror: `features/dashboard.ts` (non-card live viewer + load pattern),
|
||||
`core/events-ws.ts` (`server:<type>` dispatch), `core/entity-palette.ts` (EntitySelect),
|
||||
`core/icon-select.ts` (IconSelect). Auth/blob download rules: `contexts/frontend.md`.
|
||||
- Frontend-only phase → run `tsc --noEmit` + `npm run build`; do NOT run pytest/ruff.
|
||||
- **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, selectors, auth fetch, CSS vars)
|
||||
- [ ] No unintended side effects
|
||||
- [ ] Build passes (`tsc --noEmit` + `npm run build`)
|
||||
- [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
|
||||
|
||||
<!-- Filled in by the implementer: the activity-log render/append functions Phase 6's
|
||||
Dashboard widget can reuse, the i18n namespace, and the settings endpoint shape used. -->
|
||||
### 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)
|
||||
|
||||
Reference in New Issue
Block a user