# Architecture ## Overview Stupid Simple Network Inventory is a self-hosted web application for manual network inventory and logical topology visualisation. There is no auto-discovery of topology — only ICMP reachability scanning. All data is entered manually. ## Request Flow ``` Browser │ │ HTTP :8080 ▼ ┌─────────────────────────────────────┐ │ Nginx (frontend container) │ │ serves /usr/share/nginx/html │ │ │ │ location /api/ → proxy_pass │ │ http://backend:8000/api/ │ └──────────────────┬──────────────────┘ │ HTTP :8000 (Docker internal network) ▼ ┌─────────────────────────────────────┐ │ FastAPI (backend container) │ │ uvicorn 0.0.0.0:8000 │ │ │ │ /app/data/ (bind mount) │ │ topology.db ← SQLite │ │ secret_key.txt ← JWT secret │ └─────────────────────────────────────┘ ``` ## Docker Compose Two services defined in `docker-compose.yml`: | Resource | Type | Notes | |----------|------|-------| | `backend` | service | Python 3.11-slim, runs as `DOCKER_UID:DOCKER_GID` (host user), `cap_drop: ALL`, `cap_add: NET_RAW` | | `frontend` | service | Multi-stage (Vite → `nginxinc/nginx-unprivileged`), UID 101, `cap_drop: ALL`, `no-new-privileges`, exposes `:8080` | `./db_data:/app/data` is a bind mount. The backend process runs with the same UID/GID as the host user owning `./db_data/` (set via `DOCKER_UID`/`DOCKER_GID` in `.env`). Pre-create the directory before the first run: `mkdir -p db_data`. Both containers share an internal Docker bridge network (`internal`). The browser never communicates directly with the backend — all API traffic goes through Nginx. ## Build Pipeline **Backend** (`backend/Dockerfile`): 1. `python:3.11-slim` base 2. Install `iputils-ping` (required for ICMP subprocess calls) 3. `pip install -r requirements.txt` 4. Start with `uvicorn main:app --host 0.0.0.0 --port 8000` **Frontend** (`frontend/Dockerfile`): 1. Stage 1: `node:20-alpine` — runs `vite build`, outputs to `/app/dist` 2. Stage 2: `nginx:alpine` — copies `/app/dist` to `/usr/share/nginx/html`, copies `nginx.conf` ## Startup Sequence (backend) `main.py` runs synchronously at import time before the ASGI app is created: 1. `_migrate_vlan_nullable()` — idempotent DDL fix for vlans table 2. `_migrate_device_virt_type()` — adds column if missing 3. `_migrate_device_url()` — adds column if missing 4. `_migrate_users()` — creates users table + seeds admin account 5. `Base.metadata.create_all(bind=engine)` — creates any missing tables 6. FastAPI app instance created, routers registered This ordering ensures migrations never run against an uninitialised schema. ## Authentication Architecture ``` LoginPage.vue │ POST /api/auth/login (form-urlencoded) │ ← { access_token, token_type, username, must_change_password } │ ▼ auth.js (setAuth) stores token + username + mustChangePassword in localStorage ───────────────────────────────────────────────────────────── isAuthenticated (computed ref) ← App.vue reads this mustChangePassword (computed ref) ← App.vue shows AccountModal :forced when true getToken() ← api.js reads this per request App.vue template guard: !isAuthenticated → mustChangePassword → (blocks the app) else → full application api.js (axios interceptor) every request → Authorization: Bearer every 401 response (if token existed) → clearAuth() + reload ``` The JWT payload is `{ sub: username, ver: token_version, exp: <24h> }`, signed with HS256. The secret is loaded from `data/secret_key.txt` (auto-generated on first run, permissions 0600) or `SECRET_KEY` env var. Password change invalidates previous tokens immediately via `token_version` bump. ## Persistence All state is in SQLite at `./db_data/topology.db`. There is no caching layer, no background jobs, no message queue. The only writable files at runtime are `topology.db` and `secret_key.txt`, both in the bind-mounted `./db_data/` directory on the host.