Commit Graph

72 Commits

Author SHA1 Message Date
Pouzor 38c5bcb606 feat: configurable bottom handles, scanner rewrite, UI polish
## New features
- Configurable bottom connection points per node (1–4 handles)
- Fit view on load
- LiveView improvements
- Node modal: inline Type/Icon picker, default icon in trigger
- Remove redundant Save button from ScanConfigModal

## Scanner fixes
- Phase 1: replace nmap ARP sweep with concurrent asyncio ping sweep
  (50 parallel pings, 1s timeout). Zero false positives, works in any
  Docker network mode. Supplements with /proc/net/arp for ICMP-blocked devices.
- Phase 2: explicit -sS (root) / -sT (non-root) scan type; bump
  host-timeout to 60s; gather(return_exceptions=True) so one failing
  host doesn't abort the batch
- Fix 404 on missing device in hide/ignore
- Validate CIDR ranges to prevent nmap injection
- Thread-safe cancel set, pre-fetch canvas/hidden IPs (no N+1 queries)
- Logging: attach StreamHandler to root logger so app.* logs are visible

## Tests
- 21 backend scanner tests (ping sweep, ARP cache, Phase 2 tolerance)
- Full NodeModal coverage (53 tests)
- LiveView, store, edge label tests
2026-04-04 23:15:47 +02:00
Pouzor cf7777e0af feat: improve IoT device detection via two-phase scan and mDNS discovery
- Two-phase nmap: ARP sweep first to find all alive hosts (incl. IoT with
  no open TCP ports), then port scan only alive hosts
- mDNS/Bonjour discovery via zeroconf for Shelly, ESPHome, HomeKit devices
- Add CoAP ports (5683, 5684, 4915) to port scan and IoT type hints
- Expand MAC OUI table with Shelly, Espressif, Sonoff, Tapo, Hue, IKEA, Tuya
- IoT vendor MAC takes precedence over generic HTTP port type hints
- Reorder suggest_node_type priority: iot now beats server
2026-04-02 17:41:51 +02:00
Pouzor 255443b8e1 fix: harden scheduler startup and interval validation
- Wrap shutdown() in try/except to prevent double-scheduler if teardown fails
- Guard reschedule_status_checks with interval_seconds >= 10 validation
2026-04-02 17:39:52 +02:00
Pouzor 7e24878077 fix: pass scalars to _check_single_node to prevent DetachedInstanceError
- Refactor _check_single_node to accept plain scalar args (node_id,
  check_method, check_target, ip) instead of a detached ORM Node object
- Extract scalars inside the session in _run_status_checks before it closes
- Capture datetime.now() once per check to keep DB and WebSocket timestamps consistent
2026-04-02 17:32:42 +02:00
Pouzor ff1bc7340d fix: resolve mypy errors in scheduler (dict type params + str | None arg) 2026-04-02 17:24:26 +02:00
Pouzor e4bfab7e58 perf: run status checks concurrently to prevent scheduler overrun
Replace sequential node checks with asyncio.gather so all nodes are
checked in parallel. Add coalesce=True + max_instances=1 to suppress
APScheduler "maximum instances reached" log spam.
2026-04-02 11:41:08 +02:00
Pouzor 057891f7d5 feat: add stop scan button in UI with backend cancellation support
- 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
2026-04-01 14:18:44 +02:00
Pouzor 1f884fd1db fix: guard scheduler against double-start and unguarded reschedule 2026-03-31 00:03:01 +02:00
Pouzor e9152df17a fix: remove stale reschedule call from scan config after settings endpoint split 2026-03-31 00:01:31 +02:00
Pouzor e7fc091701 fix: reschedule APScheduler job immediately when status check interval is updated
Interval was read once at startup — changing it via API had no effect
until server restart. Now calls reschedule_status_checks() after saving.
2026-03-30 23:59:47 +02:00
Pouzor 68c7672cea feat: split scan config and app settings into separate endpoints
- 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
2026-03-30 23:06:51 +02:00
Pouzor ce4af14ee6 feat: deduplicate pending devices and skip canvas/hidden nodes on scan
- At scan start, purge any pending entries whose IPs already exist in canvas
- Skip canvas nodes (approved) during scan — don't re-add to pending
- Skip hidden devices during scan — respect user's hide decision
- Add 4 tests covering all new behaviors
2026-03-30 22:01:47 +02:00
Pouzor 9c92d39629 fix: return 401 (not 500) when bcrypt hash is malformed (#21)
- verify_password catches ValueError from passlib so a mangled hash
  ($ signs stripped by shell/Docker) returns False instead of crashing
- Settings.check_password_hash logs a clear startup error with fix
  instructions when AUTH_PASSWORD_HASH doesn't start with '$2'
2026-03-29 16:03:40 +02:00
Pouzor 32b60a201b fix: persist edge animation mode (None/Snake/Flow) end-to-end
- canvasStore.onConnect: include animated in edge data object (was silently dropped)
- Backend schemas: normalize animated bool/int to string ('none'/'snake'/'flow') via field_validator
- ORM model: change animated column from Boolean to String
- DB migration: convert existing 0/1 boolean rows to 'none'/'snake' strings
2026-03-29 14:41:18 +02:00
Pouzor 4ccdbed711 feat: add label position (inside/outside) and text size to Zone modal
- Label position toggle: inside (default) or outside the border
- Outside mode renders the label above/below the zone based on text_position
- Text size selector: 10/12/14/16/18/20px (default 12)
- Both fields persisted in custom_colors (no backend schema change needed)
- 8 new frontend tests, 1 new backend test
2026-03-29 03:04:29 +02:00
Pouzor 61b30a95fe chore: bump version to 1.4.0 2026-03-28 17:59:25 +01:00
Pouzor 2ce942ae61 Update readme 2026-03-28 16:59:33 +01:00
Pouzor 5897be70c2 fix: timing-safe key comparison and network-error state in liveview
Use hmac.compare_digest() to prevent timing-based key enumeration.
Distinguish network failures from invalid-key errors in the frontend.
2026-03-28 15:30:09 +01:00
Pouzor 210304394e feat: read-only live view at /view?key=<LIVEVIEW_KEY>
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.
2026-03-28 15:27:54 +01:00
Pouzor ae29d2c8f5 fix(tests): correct integration test API shapes and test isolation 2026-03-28 12:36:28 +01:00
Pouzor cc68fcf1c1 test: add deployment test tiers (shellcheck, hadolint, Docker smoke, integration)
Tier 1 — quality.yml: ShellCheck on lxc-install.sh, hadolint on both Dockerfiles
Tier 2 — docker-ci.yml: build images, smoke-test backend health + frontend 200
Tier 3 — test_integration.py: full stack pytest (auth, canvas save/reload, dimensions)
Also adds Docker healthcheck to backend service in docker-compose.yml
2026-03-28 12:33:35 +01:00
Pouzor 2a9cbc5932 feat: resizable nodes with width/height persistence
Add NodeResizer to BaseNode so users can drag corners to resize any node.
Persist width/height through the full stack: DB model, schemas, canvas
save/load route, and migration for existing databases.

Add tests covering save, update, clear, and load of node dimensions.
2026-03-28 11:57:09 +01:00
Pouzor 09b5317a0c fix: thread-safe signature loading, clear missing-file error, restrict CORS methods 2026-03-27 18:01:43 +01:00
Pouzor 0f643477f6 fix: move service_signatures.json inside app package and bump to 1.3.3
The file was in /app/data/ which gets overwritten by the Docker volume
mount (backend_data:/app/data), making it invisible at runtime and
causing scan failures on fresh installs. Moved to /app/app/data/ so
it stays baked into the image.
2026-03-27 17:54:39 +01:00
Pouzor 861d2822b9 fix: add iputils-ping to Docker image and bump version to 1.3.2
ping was missing from python:3.13-slim — ping check method always
returned offline on fresh Docker installs.
2026-03-27 14:59:58 +01:00
Pouzor ec0519d2b7 chore: bump version to 1.3.1 2026-03-27 11:26:26 +01:00
Pouzor 06ec18a137 feat: add hardware specs to nodes (CPU, RAM, Disk) with canvas display
- Add cpu_count, cpu_model, ram_gb, disk_gb, show_hardware fields to NodeData
- NodeModal: collapsible Hardware section with toggle to show specs on canvas
- BaseNode: 2-line hardware section with Cpu/MemoryStick/HardDrive icons, only renders set fields
- DetailPanel: Hardware section with GB/TB formatting
- Backend: schema, model, DB migration, canvas save/load support
- Tests: frontend (NodeModal, DetailPanel) + backend (canvas persist/load)
2026-03-21 00:01:15 +01:00
Pouzor e14a9e87aa fix: stop exposing JWT in WebSocket URL query param
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.
2026-03-18 00:49:03 +01:00
Pouzor 300567c88d feat(mcp): expose parent_id in update_node tool
- Add parent_id to NodeUpdate schema in backend so PATCH /nodes/{id}
  accepts it (was silently ignored before)
- Expose parent_id in update_node MCP tool schema so the MCP SDK
  forwards it to the backend instead of stripping it
- Add regression tests in both backend and MCP layers
2026-03-13 21:29:27 +01:00
Pouzor e3f8c27a04 test: add backend tests for MCP service key auth in deps.py 2026-03-13 16:42:48 +01:00
Pouzor 7935b671d3 feat: add MCP server with HTTP/SSE transport for AI integration
Exposes homelab topology to MCP-compatible AI clients (Claude Code, etc.)
over LAN via HTTP/SSE on port 8001.

- New mcp/ service: FastAPI + mcp SDK, SSE transport
- Auth: X-API-Key for AI clients, X-MCP-Service-Key for backend (Docker-internal)
- Resources: canvas, nodes, edges, scan/pending, scan/runs
- Tools: create/update/delete nodes+edges, trigger scan, approve/hide devices
- Backend deps.py: accepts JWT or MCP service key (no plain-text password)
- 40 tests (auth, resources, tools) across asyncio + trio
- docker-compose.yml: mcp service on port 8001
- README: MCP setup section with Claude Code/Desktop config examples
2026-03-13 16:41:34 +01:00
Pouzor 41cfccbd37 feat: add edge flow animation (dot traveling source→target)
- Add animated toggle per edge in EdgeModal (cyan switch, "Flow Animation")
- SVG-native <animate> element for reliable cross-browser dot animation
- Persist animated field: backend model, schemas (EdgeBase/EdgeUpdate/EdgeSave), DB migration
- Include animated in App.tsx edgesToSave serialization so it survives save/reload
- Add animated: bool to EdgeData TypeScript type
2026-03-12 10:23:18 +01:00
Pouzor c0f1d1ff1a chore: bump version to 1.0.0 2026-03-10 00:30:05 +01:00
Pouzor 5246770a3a fix: ensure data directory exists before SQLite engine init
On fresh installs the data/ dir may not exist, causing SQLite to create a
stub file without write permissions. mkdir(parents=True, exist_ok=True) runs
at import time before any DB operation.

Also add data/.gitignore to prevent homelab.db from ever being committed.
2026-03-09 22:38:26 +01:00
Pouzor 20d9473f38 feat: detect virtual machines from MAC OUI and show badge on pending devices
Backend:
- Add MAC OUI lookup table (QEMU 52:54:00, Proxmox bc:24:11, VMware, VBox, Hyper-V)
- suggest_node_type() now accepts mac param and uses OUI as a low-priority hint

Frontend:
- Detect VM type from MAC prefix on pending device cards
- Show colored badge (QEMU / PVE / VMware / VBox / Hyper-V) in orange with tooltip
2026-03-09 22:22:38 +01:00
Pouzor 6ae7f7768f fix: replace datetime.UTC with timezone.utc for Python 3.10 compatibility
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.
2026-03-09 22:01:32 +01:00
Pouzor 84860b0d99 fix: consolidate .env.example to root, add missing auth defaults
- Add AUTH_USERNAME, AUTH_PASSWORD_HASH (admin/admin) to root .env.example
- Remove duplicate backend/.env.example — root file is the single source of truth
- Fix fresh-install login failure caused by missing password hash
2026-03-09 11:42:00 +01:00
Pouzor dafcd12e40 fix: default admin/admin hash in .env.example, remove required constraint on AUTH_PASSWORD_HASH 2026-03-09 11:32:32 +01:00
Pouzor a56204a5f2 refactor: replace config.yml with .env for all configuration
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
2026-03-09 11:26:49 +01:00
Pouzor 45189e2748 fix: generate test config.yml in conftest for CI
Login route reads config.yml for credentials but it is gitignored.
Session-scoped autouse fixture now writes a minimal config.yml to a
temp path and patches settings.config_path so test_scan.py auth
fixtures work in CI without a real config file.
2026-03-09 09:30:53 +01:00
Pouzor 8da39327ab test: add tests for status WebSocket and scheduler
- test_status.py: 8 tests covering WS auth rejection/acceptance, broadcast_status (dead connection removal, no response_time, no connections), broadcast_scan_update
- test_scheduler.py: 9 tests covering _load_interval variants, _run_status_checks DB update/last_seen/error handling, start/stop lifecycle
- scheduler.py: reinitialize AsyncIOScheduler on each start() to avoid stale event loop across test restarts
2026-03-09 01:32:31 +01:00
Pouzor 20fe4ed9e4 fix: set SECRET_KEY in test conftest before app imports for CI 2026-03-09 00:17:59 +01:00
Pouzor 41e29ac199 fix: suppress mypy false positive on pydantic-settings required field 2026-03-09 00:15:42 +01:00
Pouzor 6ace796c8b security: fix C2, H5 and M1
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
2026-03-09 00:13:01 +01:00
Pouzor dbfc8a2a32 security: fix C3, C1 and H1
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
2026-03-09 00:05:10 +01:00
Pouzor 0cd263537f test: cover custom_icon persistence and cctv icon presence 2026-03-08 12:01:46 +01:00
Pouzor 4511763bac fix: persist custom_icon on canvas save + add CCTV icon
- NodeSave schema was missing custom_icon field — icon reset to default on reload
- handleSave was not including custom_icon in the payload
- CameraNode now uses Cctv icon (clearer than Camera)
- Added 'cctv' entry to ICON_REGISTRY for the icon picker
2026-03-08 12:00:11 +01:00
Pouzor 88634aeb1d feat: add Camera node type + flag RTSP/camera ports during scan
- New 'camera' node type with Camera icon (frontend + types)
- Scanner fingerprint: camera ports (554, 8554, 37777, 34567, 2020) now suggest 'camera' instead of 'iot'
- service_signatures.json: all camera/NVR entries updated to suggested_node_type=camera
- 'camera' added to priority list in suggest_node_type (above iot)
2026-03-08 11:55:26 +01:00
Pouzor e306cd7b49 test: add missing tests for cluster edges, handles, colors, and none check method
- backend: cluster edge creation with source/target handles, handle persistence through PATCH
- frontend: cluster color in edgeColors, onConnect preserves sourceHandle/targetHandle
2026-03-08 11:41:09 +01:00
Pouzor f43a94a403 fix: persist sourceHandle/targetHandle so cluster edges survive reload 2026-03-08 11:32:38 +01:00