Files
Salus/CLAUDE.md
Christoph Gasser 284924e86d Initial release — Salus by Stranto v1.6.1.0
FastAPI/Jinja2 web app for viewing and rebooting TP-Link Omada APs
across all sites. Authentik OIDC auth, SQLite audit log, Docker deploy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 14:36:02 +02:00

3.9 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 omada-ap-manager: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.
  • 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.

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 (/api/reboot, /api/reboot-bulk) 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.

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.