- ActivityRecorder: thread-safe record() (inline on loop, call_soon_threadsafe off-loop), best-effort, fires activity_logged event - current_actor ContextVar set in verify_api_key (both branches), default system - ActivityLogRetentionEngine: prune loop (max_days+max_entries), settings persistence, rehydrates recorder.enabled on startup - lifespan wiring: server.shutting_down recorded first on shutdown, retention stop before db.close - events-ws.ts allowlist + parity; DI getters + module accessor; 62 new tests
6.5 KiB
Feature: Activity / Audit Log
Branch: feature/activity-log
Base branch: master (merge target)
Branch point: 17dd2e02bab4d00a93479eb6af1a8c6ddc0c7224 (use for clean review diffs)
Created: 2026-06-09
Status: 🟡 In Progress
Strategy: Incremental
Mode: Automated
Execution: Orchestrator
Remote: origin → https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab.git
Summary
A persistent, queryable audit log of meaningful LedGrab actions, surfaced in the WebUI.
Captures four categories — entity CRUD, authentication, device connect/disconnect, and
capture & system events — as action-metadata-only records (who/what/when + entity
type/name/id + a human-readable message + small structured metadata; no before/after
diffs). Surfaced as a dedicated top-level Activity tab with smart filtering + live
updates, a compact Recent Activity widget on the Dashboard, and a Settings panel
for retention. Durability rides on the existing whole-DB ledgrab.db backup; portability
is an on-demand CSV/JSON export (no separate backup subsystem).
Design pillars (the load-bearing decisions)
- Dedicated
activity_logtable + repository — NOTBaseSqliteStore. That base loads every row into an in-memory cache and uses a genericid/name/datablob — wrong for an append-heavy, unbounded log. We use a purpose-built indexed table with query-on-demand keyset pagination. - Central choke-point instrumentation.
fire_entity_event()(api/dependencies.py:202) is already called by every entity route on create/update/delete and has_depsaccess to resolve names. The recorder hooks there for all entity CRUD. Non-entity events get explicitrecorder.record(...)calls. - Actor via
ContextVar. Set insideverify_api_key(next torequest.state.auth_label), default"system", reset per-request. The recorder reads it without threading actor through every call. - Direct synchronous write on the event-loop thread (no separate buffered-writer
subsystem — simpler, and the request already did a
synchronous=FULLentity write). Cross-thread callers (zeroconf discovery thread) marshal vialoop.call_soon_threadsafe, mirroringutils/log_broadcaster.py. The "server shutting down" event is recorded as the FIRST action in the lifespan shutdown block, before any teardown. - Reuse the existing realtime bus. A new
activity_loggedevent over/api/v1/events/ws(oneevents-ws.tsallowlist entry +test_events_ws_parity.pyupdate). No new socket. - Never log secrets. API-key tokens are never stored — only labels/ids.
- Differentiate from the existing Log Viewer.
utils/log_broadcaster.pyis an ephemeral 500-line debug-log tail. The audit log is persistent, structured, semantic. Cross-link in the Settings panel; never duplicate.
Build & Test Commands
- Build (frontend):
cd server && npm run build - Type-check (TS):
cd server && npx tsc --noEmit - Test:
cd server && py -3.13 -m pytest tests/ --no-cov -q - Lint (Python):
cd server && ruff check src/ tests/ --fix - Events parity test (load-bearing for P2): included in pytest (
tests/test_events_ws_parity.py)
Scope checks to the files actually edited: backend phases run ruff + pytest; frontend-only phases run
tsc --noEmit+npm run build(no pytest/ruff). Phases touching both run both.
Phases
- Phase 1: Storage — model, migration, repository [domain: data] → subplan
- Phase 2: Recorder, actor context, retention, lifecycle [domain: backend] → subplan
- Phase 3: Event instrumentation (4 categories) [domain: backend] → subplan
- Phase 4: REST API — query/filter/export/settings/clear [domain: backend] → subplan
- Phase 5: Frontend — Activity tab + smart filtering + live updates [domain: frontend] → subplan
- Phase 6: Dashboard widget + Settings panel + docs [domain: frontend] → subplan
Parallelizable Phase Groups (Orchestrator mode only)
- Phases 3 and 4 are parallelizable only after the schema is frozen at end of Phase 2.
Both depend on the P2 recorder/schema; P4 registers the router in
api/__init__.py, P3 editsdependencies.py/auth.py(different files, shared schema contract). To keep the Automated run simple and low-risk, they run sequentially (3 → 4) unless time pressure warrants worktree-isolated parallelism. Phases 5 → 6 are sequential (6 reuses P5 formatters and the feature module).
Phase Progress Log
| Phase | Domain | Status | Review | Build | Committed |
|---|---|---|---|---|---|
| Phase 1: Storage | data | ✅ Done | ✅ Passed | ✅ Passed | ✅ |
| Phase 2: Recorder/Retention | backend | ✅ Done | ✅ Passed | ✅ Passed | ✅ |
| Phase 3: Instrumentation | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 4: REST API | backend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 5: Frontend tab | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
| Phase 6: Dashboard/Settings | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ |
Outstanding Warnings
| Phase | Warning | Severity | Status (open / resolved / accepted) |
|---|---|---|---|
Final Review
- Comprehensive code review
- Security review (auth/PII-in-logs/secrets/log-injection — triggered)
- All Outstanding Warnings resolved or consciously accepted
- Full build passes (
npm run build+tsc --noEmit) - Full test suite passes (
pytest) - Merged to
master
Amendment Log
(Filled in if the plan is amended mid-implementation.)
- 2026-06-09: Plan reviewer (pre-implementation) → ⚠️ with 3 Critical Gaps, all resolved
before Phase 1: (G1) descoped non-existent API-key mutation events; (G2) dropped the
buffered-writer subsystem for a direct synchronous-on-loop write with
call_soon_threadsafemarshaling for thread-origin events; (G3) record "server shutting down" first in the shutdown block (no buffer to flush). Concerns folded in: device events viadevice_health_changed+device_discovered/_lost; actor ContextVar inverify_api_key; name-on-delete passed explicitly; settings-audit scoped + self-key excluded; parity allowlist before emit site. Adopted suggestions: rowid keyset tiebreaker; log the disable action. Deferred: setup-scaffold noise suppression.