Ruff auto-fix: unused Optional imports in sectors router and link
schemas, unused Site import in device service, unused datetime
imports in trend detector, unused text import in site service,
and f-string without placeholders in signal history service.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create signal_history_service with TimescaleDB time_bucket queries for 24h/7d/30d ranges
- Create site_alert_service with full CRUD for rules, events list/resolve, and active count
- Create signal_history router with GET endpoint for time-bucketed signal data
- Create site_alerts router with CRUD endpoints for rules and event management
- Wire both routers into main.py with /api prefix
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create sectors table migration (034) with RLS and devices.sector_id FK
- Add Sector ORM model with site_id and tenant_id foreign keys
- Add SectorCreate/Update/Response/ListResponse Pydantic schemas
- Implement sector_service with CRUD and device assignment functions
- Add sectors router with GET/POST/PUT/DELETE and device sector assignment
- Register sectors router in main.py
- Add sector_id and sector_name to Device model and DeviceResponse
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- LinkResponse/UnknownClientResponse Pydantic schemas with from_attributes
- Link service with get_links, get_device_links, get_site_links, get_unknown_clients
- Unknown clients query uses DISTINCT ON for latest registration per MAC
- 4 REST endpoints: tenant links, device links, site links, unknown clients
- Interface and link discovery subscribers wired into FastAPI lifespan start/stop
- Links router registered at /api prefix
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add site_service with CRUD, health rollup, device assignment functions
- Add sites router with 8 endpoints (CRUD + assign/unassign/bulk-assign)
- RBAC: viewer for reads, operator for writes, tenant_admin for delete
- Wire sites_router into main.py with /api prefix
- Health rollup computes device_count, online_count, online_percent per site
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add device_id to the audit log API response and frontend type, then
use DeviceLink to make device hostnames navigable in AuditLogTable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove unused timedelta import from test_wireless_api.py and
auto-format metrics.py to pass ruff format check.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix _commit_and_sync infinite recursion
- Use admin session for subnet_index allocation (bypass RLS)
- Auto-set VPN endpoint from CORS_ORIGINS hostname
- Remove server address field from VPN setup UI
- Add DELETE endpoint and button for VPN config removal
- Add wg-reload watcher for reliable config hot-reload via wg syncconf
- Add wg_status.json writer for live peer handshake status in UI
- Per-tenant SNAT for poller-to-device routing through VPN
- Restrict VPN→eth0 forwarding to Docker networks only (block exit node abuse)
- Use 10.10.0.0/16 allowed-address in RouterOS commands
- Fix structlog event= conflict (use audit=True)
- Export backup_scheduler proxy for firmware/upgrade imports
sync_wireguard_config opens its own AdminAsyncSessionLocal connection
which cannot see uncommitted data from the caller's transaction. Add
_commit_and_sync helper that commits first, then regenerates wg0.conf.
Also removes the unused db parameter from sync_wireguard_config.
- config_snapshot_created event after successful snapshot INSERT
- config_snapshot_skipped_duplicate event on dedup match
- config_diff_generated event after diff INSERT
- config_backup_manual_trigger event on manual trigger success
- All log_action calls wrapped in try/except for safety
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- GET /config/{snapshot_id} returns decrypted full config with RBAC
- GET /config/{snapshot_id}/diff returns unified diff text with RBAC
- 404 for missing snapshots/diffs, 500 for Transit decrypt failure
- Both endpoints enforce viewer+ role and config:read scope
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- GET /api/tenants/{tid}/devices/{did}/config-history endpoint
- Viewer+ RBAC with config:read scope
- Pagination via limit/offset query params (defaults 50/0)
- Router registered in main.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Bind tunnel listeners to 0.0.0.0 instead of 127.0.0.1 so tunnels
are reachable through reverse proxies and container networks
- Reduce port range to 49000-49004 (5 concurrent tunnels)
- Derive WinBox URI host from request Host header instead of
hardcoding 127.0.0.1, enabling use behind reverse proxies
- Add README security warning about default encryption keys
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Gap 1: Add tenant ID verification after device lookup in SSH relay handleSSH,
closing cross-tenant token reuse vulnerability
- Gap 2: Add X-Forwarded-For fallback (last entry) when X-Real-IP is absent in
SSH relay source IP extraction; import strings package
- Gap 3: Add @limiter.limit("10/minute") to POST /winbox-session and POST
/ssh-session using existing slowapi pattern from app.middleware.rate_limit
- Gap 4: Add TODO comment in open_ssh_session explaining that SSH session count
enforcement is at the poller level; no NATS subject exists yet for API-side
pre-check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements four operator-gated endpoints under /api/tenants/{tenant_id}/devices/{device_id}/:
- POST /winbox-session: opens a WinBox tunnel via NATS request-reply to poller
- POST /ssh-session: mints a single-use Redis token (120s TTL) for WebSocket SSH relay
- DELETE /winbox-session/{tunnel_id}: idempotently closes a WinBox tunnel
- GET /sessions: lists active WinBox tunnels via NATS tunnel.status.list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three bugs fixed:
1. Phase 30 (auth.ts): After SRP login the encrypted_key_set was returned
from the server but the vault key and RSA private key were never unwrapped
with the AUK. keyStore.getVaultKey() was always null, causing Tier 1
config-backup diffs to crash with a TypeError.
Fix: unwrap vault key and private key using crypto.subtle.unwrapKey after
successful SRP verification. Non-fatal: warns to console if decryption
fails so login always succeeds.
2. Token refresh (auth.py): The /refresh endpoint required refresh_token in
the request body, but the frontend never stored or sent it. After the 15-
minute access token TTL, all authenticated API calls would fail silently
because the interceptor sent an empty body and received 422 (not 401),
so the retry loop never fired.
Fix: login/srpVerify now set an httpOnly refresh_token cookie scoped to
/api/auth/refresh. The refresh endpoint now accepts the token from either
cookie (preferred) or body (legacy). Logout clears both cookies.
RefreshRequest.refresh_token is now Optional to allow empty-body calls.
3. Silent token rotation: the /refresh endpoint now also rotates the refresh
token cookie on each use (issues a fresh token), reducing the window for
stolen refresh token replay.
CurrentUser object uses user_id attribute, not id. Caused AttributeError
on PUT /api/settings/smtp.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>