# 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 `). WebSocket connections authenticate with a first-message handshake (`{"type":"auth","token":""}`). 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.` 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.`. - **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 ```bash 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](../contexts/frontend.md). ## Server Operations For restart procedures, server modes, and demo mode checklist, see [contexts/server-operations.md](../contexts/server-operations.md).