- POST /scan/{run_id}/stop endpoint signals running scan to cancel
- Scanner checks cancellation flag between CIDR ranges and hosts, exits early
- Cancelled scans get status 'cancelled' instead of 'done'
- Stop button (red StopCircle) shown in Scan History panel for running scans
- 6 new backend tests, 5 new frontend tests
- New GET/POST /api/v1/settings for status check interval
- Scan /api/v1/scan/config now handles ranges only
- Frontend: settingsApi client, SettingsPanel uses settingsApi
- ScanConfigModal no longer reads/writes interval
- 4 new backend tests for settings endpoint
Implements issue #5. Off by default; set LIVEVIEW_KEY in .env to enable.
No JWT required — key-based auth via ?key= query param.
Returns 403 when disabled or key is wrong.
Read-only ReactFlow canvas (pan/zoom, no editing).
Standalone mode loads from localStorage without a key.
Includes 8 backend tests and 9 frontend tests.
Token was visible in server logs, browser history, and proxy access logs.
Backend now accepts the connection first, then validates a JSON auth
message {"token": "<jwt>"} sent by the client on open before adding
the socket to the active connections pool.
datetime.UTC was introduced in Python 3.11. Users on 3.10 get an ImportError.
Also lower ruff/mypy target-version from py313 to py310 to match minimum supported version.
All settings (auth credentials, scanner ranges, status_checker interval)
now live in a single .env file via pydantic-settings. config.yml and
config.yml.example are deleted.
- Settings: add auth_username, auth_password_hash, scanner_ranges,
status_checker_interval; add load_overrides()/save_overrides() for
persisting runtime changes to data/scan_config.json
- auth.py: read credentials directly from settings
- scan.py: read ranges/interval from settings; write-back via save_overrides()
- scheduler.py: read interval directly from settings
- main.py: call settings.load_overrides() at startup
- docker-compose.yml: remove config.yml volume mount, add new env vars
- conftest.py: set settings fields directly instead of writing a temp config.yml
C2 - JWT token was stored in localStorage (XSS-accessible):
- Switch Zustand persist storage from localStorage to sessionStorage
- Token is now scoped to the current tab and cleared on browser close
H5 - docker-compose had unsafe SECRET_KEY fallback:
- Replace ${SECRET_KEY:-change_me_in_production} with :? syntax
- Docker Compose now aborts with a clear error if SECRET_KEY is unset
M1 - Login endpoint had timing leak allowing username enumeration:
- Always call verify_password() regardless of username match
- Use hmac.compare_digest() for constant-time username comparison
- Both checks run every time; attacker cannot distinguish wrong
username from wrong password via response timing
C3 - config.yml contains credentials, remove from git tracking:
- Add backend/config.yml to .gitignore
- git rm --cached to untrack it
- Add backend/config.yml.example with instructions
C1 - SECRET_KEY must come from .env, no unsafe default:
- Remove hardcoded "change_me_in_production" default from config.py
- App now fails to start if SECRET_KEY is not set (pydantic required field)
- Generate real random key in backend/.env (gitignored)
- Add backend/.env.example for new contributors
H1 - WebSocket /ws/status was unauthenticated:
- Backend: require ?token= query param, validate via decode_token(),
close with code 1008 (Policy Violation) if missing or invalid
- Frontend: append ?token=<jwt> to WebSocket URL
Backend:
- asyncio.to_thread(_nmap_scan) — nmap no longer blocks the event loop
- Commit each discovered device immediately (previously one bulk commit at end)
- Update ScanRun.devices_found after each device so history panel shows live count
- broadcast_scan_update() pushes {type: scan_device_found} WS event per device
- Refactor broadcast_status to shared _broadcast() helper, adds type: "status" field
Frontend:
- useStatusPolling handles both WS message types (status / scan_device_found)
- canvasStore: scanEventTs + notifyScanDeviceFound() action
- PendingDevicesPanel auto-refreshes when WS scan event is received
Root cause: handleAddNode/handleEdgeConfirm only updated Zustand store,
never creating records in the DB. canvasApi.save() only updated positions
of existing nodes, so nothing survived a page refresh.
Fix: save now sends the full canvas state (all nodes + edges + data) and
the backend does a full sync — upsert incoming, delete anything removed.
This means Save is the single source of truth: no need to call individual
create/update/delete APIs for every drag or edit.
FastAPI's redirect_slashes=True causes GET /api/v1/canvas to 307 redirect
to /api/v1/canvas/ — axios follows the redirect but drops the Authorization
header, resulting in 403. Fixed by declaring routes as empty string instead
of '/' so no redirect is issued. Same fix applied to nodes and edges routes.
Updated all tests to use paths without trailing slashes.
Frontend:
- Split nodeTypes/edgeTypes into separate .ts files (react-refresh)
- Remove setState-in-effect in NodeModal (key prop reset)
- Fix handleSave accessed before declaration (useRef pattern)
- Exclude src/components/ui/** from eslint (shadcn generated)
- Use defineConfig from vitest/config for test type support
Backend:
- ruff --fix: sort imports, datetime.UTC alias
- Raise line-length to 120, ignore E501 in tests
- Break long update_node/update_edge signatures
- pyproject.toml: per-file-ignores for tests