diff --git a/plans/activity-log/CONTEXT.md b/plans/activity-log/CONTEXT.md
index 1009f54..e4de9a3 100644
--- a/plans/activity-log/CONTEXT.md
+++ b/plans/activity-log/CONTEXT.md
@@ -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.
diff --git a/plans/activity-log/PLAN.md b/plans/activity-log/PLAN.md
index 54fc375..d77d58c 100644
--- a/plans/activity-log/PLAN.md
+++ b/plans/activity-log/PLAN.md
@@ -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
diff --git a/plans/activity-log/phase-5-frontend-tab.md b/plans/activity-log/phase-5-frontend-tab.md
index 0e2a509..2d88c42 100644
--- a/plans/activity-log/phase-5-frontend-tab.md
+++ b/plans/activity-log/phase-5-frontend-tab.md
@@ -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`) + `
`.
- `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 `
@@ -201,6 +202,9 @@
+