Files
Salus/CLAUDE.md
Christoph Gasser e37ee99054 Update CLAUDE.md with features added since initial version
Covers: connected clients popup, get_ap_clients/get_all_clients,
new API endpoints, session recovery behaviour, reboot button locking,
IP range filter, and sortable columns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:34:21 +02:00

97 lines
6.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
```bash
# Local setup
python -m venv .venv
source .venv/Scripts/activate # Windows Git Bash
pip install -r requirements.txt
cp .env.example .env # fill in values
# Run locally (pick any free port; earlier dev sessions may occupy 80808094)
uvicorn app.main:app --reload --port 8080
# Skip Authentik login entirely during local dev
AUTH_DISABLED=true uvicorn app.main:app --reload --port 8080
# Docker
docker build -t qwe-salus:latest .
docker compose up
```
There are no tests and no linter configured.
## Architecture
### Request flow
```
Browser → FastAPI (main.py)
├── auth.py Authentik OIDC + session management
├── omada.py Omada Controller API client (singleton)
└── database.py SQLAlchemy / SQLite audit log
```
Templates extend `base.html` (nav, footer, toast system). TailwindCSS is loaded from CDN — no build step.
### Critical: module-level `os.getenv()` ordering
`app/main.py` calls `load_dotenv()` **before** any `from app.*` imports. `omada.py` and `auth.py` read their config via `os.getenv()` at module load time, so `.env` must be loaded first. Moving `load_dotenv()` below those imports silently breaks all env var config.
### OmadaClient (`app/omada.py`)
A module-level singleton (`omada_client`). Key behaviour:
- **Two separate `httpx.AsyncClient` instances**: `_get_client()` carries the web session cookie (`TPOMADA_SESSIONID`); `_get_openapi_client()` is cookie-free. Mixing them causes 401s on the OpenAPI token endpoint.
- **Auth flow**: `_fetch_omadac_id()``_login()` (username/password → CSRF token + session cookie) → `_fetch_sites()`. `_ensure_ready()` gates all public methods.
- **Sites are fetched from** `GET /api/v2/users/current``result.privilege.sites[]`, not `/api/v2/sites` (which requires root admin and returns empty for regular admins).
- **Devices list**: `GET /{omadacId}/api/v2/sites/{siteKey}/devices` filtered by `type == "ap"`. There is no `/aps` endpoint in v6.x.
- **Reboot**: `POST /{omadacId}/api/v2/sites/{siteKey}/cmd/devices/{mac}/reboot` via the web session client. Response `errorCode: -39009` with `"Rebooting... Please wait."` is the success response — treat it as non-error alongside `0`. OpenAPI client credentials (`OMADA_CLIENT_ID`/`OMADA_CLIENT_SECRET`) are no longer used for reboot.
- **Session expiry** is handled in `_request_with_retry`: error codes `-1006`, `-1003`, `-30109` trigger automatic re-login + re-fetch of sites. If all sites fail in `get_aps()`, the token and sites cache are cleared so the next request triggers a fresh login rather than staying broken indefinitely.
- **Uptime safety check**: `reboot_ap()` re-fetches live uptime before issuing the command; it raises if `uptimeLong < 300` seconds (5 minutes). The UI also disables the reboot button client-side for the same condition.
- **Connected clients**: `get_ap_clients(ap_mac, site_key)` fetches clients for a specific AP via `GET /sites/{key}/clients?filters.apMac={mac}`, with automatic fallback to fetching all site clients and filtering client-side if the controller doesn't support that query param. `get_all_clients()` fetches all clients across all sites grouped by AP MAC.
### API endpoints (`app/main.py`)
Beyond the page routes, the JSON API endpoints are:
| Method | Path | Purpose |
|--------|------|---------|
| `POST` | `/api/reboot` | Reboot a single AP; CSRF-validated |
| `POST` | `/api/reboot-bulk` | Reboot multiple APs; CSRF-validated |
| `GET` | `/api/ap-clients` | `?mac=&site_key=` — clients connected to one AP |
| `GET` | `/api/all-clients` | All clients across all sites, keyed by AP MAC |
| `GET` | `/health` | Docker healthcheck |
| `GET` | `/debug/sites` | Lists accessible Omada sites (diagnostic) |
### Authentication (`app/auth.py`)
Authentik OIDC Authorization Code Flow. The JWT `id_token` is decoded **without signature verification** (Authentik is a trusted internal IdP). User dict `{username, email, name, sub}` is stored in the Starlette signed-cookie session.
`AUTH_DISABLED=true` injects a hardcoded `DEV_USER` on `/auth/login` — no Authentik required.
All POST API endpoints validate a CSRF token stored in the session and echoed in the JSON request body.
### Database (`app/database.py`)
Single table `reboot_log` written on every reboot attempt (success and error). `DB_PATH` defaults to `/data/audit.db` (Docker volume mount). Override to `./audit.db` for local dev.
### Frontend (`app/templates/index.html`)
Key client-side behaviours to be aware of when editing:
- **Reboot locking**: `lockApRow(mac)` disables the row button and checkbox immediately on confirm — button text transitions `Rebooting… → Rebooted / Failed` and stays disabled for the session.
- **Clients popup**: clicking an AP name cell fires `openClientsModal()`, which calls `/api/ap-clients` and renders a modal table sorted by IP. Uses the same `ipToNum()` helper as the column sort.
- **IP range filter** ("Filter .150.155"): fetches `/api/ap-clients` for every AP in parallel via `Promise.all`, retains only APs where a connected client IP has last octet 150155. Result is cached in `ipMatchedMacs` (a `Set`) for the session.
- **Column sort**: Name, Site, IP Address headers are clickable. `ipToNum()` is used for numeric IP comparison. Default sort on load is by Name (triggered by programmatic `.click()` on the Name header). Both the text filter and the IP range filter compose with the sort via a shared `applyFilters()` function.
### Deployment
- Docker exposes port `8080`; `docker-compose.yml` maps it to host port `8098`.
- Uvicorn runs with `--proxy-headers --forwarded-allow-ips=*` to trust `X-Forwarded-Proto` from Nginx Proxy Manager.
- The `/health` endpoint is used by the Docker healthcheck.
- `/debug/sites` is a diagnostic endpoint that lists all accessible Omada sites — useful for verifying credentials without touching the UI.