Files
ledgrab/server/CLAUDE.md
T
alexei.dolgolyov e584235676 chore(activity-log): post-merge cleanup + graduate context to CLAUDE.md
- remove plans/activity-log/ (feature merged; learnings in CLAUDE.md + git history)
- server/CLAUDE.md: Activity/Audit Log architecture + extension points (recorder, fire_entity_event hook, sanitize_display, events allowlist, retention, API auth posture)
2026-06-10 18:42:15 +03:00

6.9 KiB

Claude Instructions for LedGrab Server

Project Structure

  • src/ledgrab/main.py — FastAPI application entry point
  • src/ledgrab/api/routes/ — REST API endpoints (one file per entity)
  • src/ledgrab/api/schemas/ — Pydantic request/response models (one file per entity)
  • src/ledgrab/core/ — Core business logic (capture, devices, audio, processing, automations)
  • src/ledgrab/storage/ — Data models (dataclasses) and SQLite-backed persistence stores (BaseSqliteStore)
  • src/ledgrab/utils/ — Utility functions (logging, monitor detection, SSRF validation, sound playback)
  • src/ledgrab/static/ — Frontend files (TypeScript, CSS, locales)
  • src/ledgrab/templates/ — Jinja2 HTML templates
  • config/ — Configuration files (YAML)
  • data/ — Runtime data: SQLite database (ledgrab.db) + assets. Relocate the root with LEDGRAB_DATA_DIR.

Entity & Storage Pattern

Each entity follows: dataclass model (storage/) + SQLite store (storage/*_store.py, subclassing BaseSqliteStore) + Pydantic schemas (api/schemas/) + routes (api/routes/).

Stores keep an in-memory write-through cache over a per-entity SQLite table (the legacy BaseJsonStore still exists for reference but new stores use BaseSqliteStore). Schema/data shape changes go through storage/data_migrations.py — migrations are idempotent and tracked in a dedicated data_migrations audit table, so they run safely on every startup. When renaming or restructuring stored fields, add a migration there (see the Data Migration Policy in the root CLAUDE.md).

Authentication

API key authentication via Bearer token in the Authorization header (Authorization: Bearer <key>). WebSocket connections authenticate with a first-message handshake ({"type":"auth","token":"<key>"}). See src/ledgrab/api/auth.py for the canonical logic.

  • Config: config/default_config.yaml under auth.api_keys; env var LEDGRAB_AUTH__API_KEYS
  • When api_keys is empty (default): loopback requests (127.0.0.1 / ::1 / localhost) are allowed anonymously, but LAN / remote requests are rejected with 401. Auth is not fully open.
  • When api_keys is set: a valid Bearer token is required from every client (loopback included).
  • require_authenticated() rejects even loopback-anonymous callers on sensitive endpoints (e.g. backup download, secret reveal).

Activity / Audit Log

Persistent, queryable audit log of meaningful actions (auth, device, entity CRUD, capture, system), surfaced in the WebUI (Activity tab + Dashboard widget + Settings retention panel).

  • Storage is NOT a BaseSqliteStore. storage/activity_log_repository.py is a purpose-built repository over a dedicated indexed activity_log table (migration 002_add_activity_log) — query-on-demand with keyset pagination (seq cursor), never load-all-into-memory. Don't route it through the entity-store pattern.
  • Recording. core/activity_log/recorder.py (ActivityRecorder) is best-effort (never raises into the audited action) and thread-safe (inline on the event loop; loop.call_soon_threadsafe from non-loop threads, e.g. zeroconf discovery). It persists the entry and fires an activity_logged realtime event. Actor comes from the current_actor ContextVar (set in verify_api_key), default "system".
  • Entity CRUD is auto-audited via the fire_entity_event() choke point in api/dependencies.py — every create/update/delete already calls it. Delete handlers must pass entity_name (the entity is gone by record time). Non-entity events use explicit recorder.record(...) (get it via get_activity_recorder() DI or get_module_recorder() for engine/thread sites).
  • Never log secrets. API-key tokens are never recorded. Wrap any untrusted/attacker-controllable string (mDNS names, headers, user-authored names) with sanitize_display() (core/activity_log/sanitize.py) before it enters a message/metadata field. Per-IP throttle bounds auth-failure audit writes.
  • Adding a new audited event: pick a dotted action (e.g. "thing.created"), call the recorder; for it to render localized in the UI, add activity_log.msg.<action> to all three static/locales/*.json (the frontend localizeMessage() maps action→template; falls back to the server message). Entity-type labels live under activity_log.entity_type.<type>.
  • Adding a new realtime event type (pm.fire_event({"type": ...})): add it to _ALLOWED_SERVER_EVENT_TYPES in static/js/core/events-ws.ts AND keep tests/test_events_ws_parity.py green.
  • Retention + API. core/activity_log/retention.py prunes by max_days + max_entries (settings persisted via db.set_setting("activity_log")); the recorder's enabled flag is rehydrated from those settings on startup. REST in api/routes/activity_log.py: GET /activity-log (list, AuthRequired), GET /export (CSV/JSON stream — require_authenticated; chunked keyset so it never holds the DB lock across the stream; CSV formula-injection guarded), GET|PUT /settings (PUT is require_authenticated), DELETE (clear — require_authenticated, self-audited). The table is covered by the existing whole-DB backup (no STORE_MAP change needed).

Common Tasks

Adding a new API endpoint

  1. Create route file in api/routes/ (define an APIRouter(prefix="/api/v1/..."))
  2. Define request/response schemas in api/schemas/
  3. Register the router in api/__init__.py (it aggregates every route module into the single router that main.py mounts)
  4. Restart the server
  5. Test via /docs (Swagger UI)

Adding a new field to existing API

  1. Update Pydantic schema in api/schemas/
  2. Update corresponding dataclass in storage/
  3. Update backend logic to populate the field
  4. Restart the server
  5. Update frontend to display the new field
  6. Rebuild bundle: cd server && npm run build

Adding translations

  1. Add keys to static/locales/en.json, static/locales/ru.json, and static/locales/zh.json
  2. Add data-i18n attributes to HTML elements in templates/
  3. Use t('key') in TypeScript modules (static/js/)
  4. No server restart needed (frontend only), but rebuild bundle if JS changed

Modifying display/monitor detection

  1. Update functions in utils/monitor_names.py or core/screen_capture.py
  2. Restart the server
  3. Test with GET /api/v1/config/displays

Testing

cd server && pytest                    # Run all tests
cd server && pytest --cov              # With coverage report
cd server && pytest tests/test_api.py  # Single test file

Tests are in server/tests/. Config in pyproject.toml under [tool.pytest].

Frontend Conventions

For all frontend conventions (CSS variables, UI patterns, modals, localization, icons, bundling), see contexts/frontend.md.

Server Operations

For restart procedures, server modes, and demo mode checklist, see contexts/server-operations.md.