e584235676
- 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)
92 lines
6.9 KiB
Markdown
92 lines
6.9 KiB
Markdown
# 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
|
|
|
|
```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).
|