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

6.0 KiB
Raw Blame History

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 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/currentresult.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.