# Ops Portal — Architecture ## Topology ``` INTERNET | [Caddy :443] | Core (152.53.192.33) | +---------------+---------------+ | | | /api/* :8090 /data/* :files /static/* | | | [FastAPI app] ops-status.json [HTML/CSS/JS] server.py /var/www/ops/ /opt/ops-portal/ | /data/ static/ | +-------+-------+-------+-------+ | | | | | S3 API UISP Wazuh Bitdef systemd (Wasabi) (FFW) (app1) (Cloud) (Core) ``` ## Data Flow ``` [Collector] [Dashboard] | | |-- python3 ops-data- | | collector.py | | | v | S3 buckets ----+ | UISP API ------+---> ops-status | Wazuh API -----+ .json ------> GET /api/status Bitdefender ---+ | systemd -------+ | cron jobs -----+ | v [Browser renders health grid, widgets, alerts] ``` ## Components ### 1. Collector (`/root/.hermes/scripts/ops-data-collector.py`) - Runs every 5 min via cron - Gathers: S3 backup status (6 buckets), UISP devices (90), Wazuh agents/alerts, Bitdefender endpoints, systemd services, cron jobs, server health, disk/memory/CPU - Timeout: 90s (was 20s — too short for 94K-file S3 bucket) - Output: `/var/www/ops/data/ops-status.json` ### 2. Backend (`/opt/ops-portal/server.py`) - FastAPI on port 8090 - 7 API endpoints (health, status, servers, servers/health, audit-log, ft360/status) - JWT auth from `/root/.hermes/.env` (ADMIN_USERNAME, ADMIN_PASSWORD, JWT_SECRET) - Critical service restart protection (hermes, caddy, ops-portal blocked) - Systemd: `ops-portal.service` ### 3. Frontend (`/opt/ops-portal/static/`) - 11 HTML pages with shared ops.css, app.js, utils.js - Auth: login overlay → localStorage JWT → all API calls Bearer - Auto-refresh: 60s interval + tab visibility API - Mobile: hamburger toggle with .nav-links.open CSS - Cache-busting: all assets versioned with timestamps ### 4. Proxy (Caddy on Core) - `/` and `/*.html` → static file server from `/opt/ops-portal/static/` - `/api/*` → reverse_proxy to 127.0.0.1:8090 - `/data/*` → file server from `/var/www/ops/data/` - Domain: ops.itpropartner.com ## Cross-Service Dependencies | Dependency | Server | Purpose | Fallback | |---|---|---|---| | Wasabi S3 | External | Backup bucket status | Shows "Issues" | | UISP API | unms.forefrontwireless.com | Device/site count | Shows 0 devices | | Wazuh | app1 (152.53.36.131) | Agent count, alerts | Shows "Offline" | | Bitdefender | External API | Endpoint monitoring | Shows "Offline" | | Traccar | app2 (152.53.39.202) | FleetTracker data | Dedicated endpoint | | Core systemd | Local | Service health, disk, memory | N/A (local) | ## Auth Flow ``` Browser Server | | |-- POST /api/auth/login ->| | {username, password} | | |-- Validate against ADMIN_USERNAME/ADMIN_PASSWORD | |-- Generate JWT with JWT_SECRET |<- {access_token} --------| | | |-- GET /api/status ------->| | Authorization: Bearer | | |-- Verify JWT | |-- Read ops-status.json |<- {full dashboard} ------| ``` ## Key Design Decisions 1. **Collector pattern over direct API calls:** Dashboard fetches one JSON blob rather than 6 separate APIs. Single point of failure but fast rendering and offline-capable (shows last-cached data). 2. **Python/FastAPI over Node:** Already have Python toolchain on Core. FastAPI is lightweight, async-native, and the ops portal is read-heavy with minimal write paths. 3. **Static HTML + vanilla JS over React/Vue:** 11-page dashboard with no SPA routing. Auth via localStorage JWT. Zero build step, zero dependencies beyond ops.css. 4. **JWT over session cookies:** Cross-page auth without server-side session state. Token survives page navigations and ops-portal restarts (persistent JWT_SECRET in .env).