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>
6.0 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Development Commands
# 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 8080–8094)
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.AsyncClientinstances:_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}/devicesfiltered bytype == "ap". There is no/apsendpoint in v6.x. - Reboot:
POST /{omadacId}/api/v2/sites/{siteKey}/cmd/devices/{mac}/rebootvia the web session client. ResponseerrorCode: -39009with"Rebooting... Please wait."is the success response — treat it as non-error alongside0. 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,-30109trigger automatic re-login + re-fetch of sites. If all sites fail inget_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 ifuptimeLong < 300seconds (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 viaGET /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 transitionsRebooting… → Rebooted / Failedand stays disabled for the session. - Clients popup: clicking an AP name cell fires
openClientsModal(), which calls/api/ap-clientsand renders a modal table sorted by IP. Uses the sameipToNum()helper as the column sort. - IP range filter ("Filter .150–.155"): fetches
/api/ap-clientsfor every AP in parallel viaPromise.all, retains only APs where a connected client IP has last octet 150–155. Result is cached inipMatchedMacs(aSet) 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 sharedapplyFilters()function.
Deployment
- Docker exposes port
8080;docker-compose.ymlmaps it to host port8098. - Uvicorn runs with
--proxy-headers --forwarded-allow-ips=*to trustX-Forwarded-Protofrom Nginx Proxy Manager. - The
/healthendpoint is used by the Docker healthcheck. /debug/sitesis a diagnostic endpoint that lists all accessible Omada sites — useful for verifying credentials without touching the UI.