Commit Graph

32 Commits

Author SHA1 Message Date
Pouzor aa0a97c245 fix: reduce status flapping, add IPv6 ping, color manual web services
Addresses three reports from issue #196:

- Ping now sends 2 probes with a ~2s timeout (was 1 probe / 1s) so a
  single dropped packet or a slow IoT/ESPHome device no longer flaps a
  node offline (#196.1, #196.2).
- IPv6-only devices (e.g. Alexa) are now pinged over IPv6: ping6 on
  macOS, -6 flag on Linux/Windows, detected via inet_pton (#196.3).
- Manually-added services carry no category and so always rendered grey
  even when they were reachable HTTP/HTTPS. A resolvable web URL now
  falls back to the web colour (#196.9).

ha-relevant: yes
2026-06-09 21:20:05 +02:00
Pouzor aa17edf1d0 chore(security): drop passlib for direct bcrypt + reject CLI-flag check targets
- Replace passlib.CryptContext with bcrypt 4.2.1 directly (passlib is
  unmaintained and only emitted warnings when paired with bcrypt 4+).
  hash_password / verify_password now call bcrypt.checkpw / .hashpw, and
  verify_password is hardened against empty inputs without raising.
  scripts/hash_password.py + tests/conftest.py updated to match.
- status_checker.check_node() now rejects targets starting with '-' before
  any subprocess invocation, defending ping/tcp paths against arg-injection
  if an admin sets a check_target like '-O'.

Tests added:
- test_auth: expired JWT, malformed JWT, wrong-secret JWT, empty password
  vs empty server hash, verify_password edge cases.
- test_scan: _background_scan failure path marks ScanRun failed, leaves
  non-running terminal status alone, success path invokes run_scan.
- test_status_checker: ping/tcp invocations are bypassed when target or
  ip starts with '-'.

Backend coverage 89% → 90%.
2026-05-14 12:11:40 +02:00
Pouzor 5f87c64dcf fix(zigbee): default IEEE/Vendor/Model/LQI props to hidden, preserve user visibility
- New zigbee props (approve + first-time re-import) ship with visible=false
  so the canvas card stays clean. User opts in from the right panel.
- On re-import of an already-approved node, merge instead of overwrite:
  keys already present keep their visible flag (and any user-edited value
  is replaced with the freshly imported one), brand-new keys are appended
  hidden. Non-zigbee custom properties are preserved untouched.
2026-05-14 01:10:45 +02:00
Pouzor 3a303a1376 feat(zigbee): populate IEEE/Vendor/Model/LQI properties on approve and re-import
- Approval (single + bulk) of zigbee pending devices now writes IEEE, Vendor,
  Model, LQI into Node.properties so they show in the right panel.
- Zigbee re-import refreshes properties on existing canvas Nodes and skips
  creating a pending row when the device was already approved — keeps
  approved devices out of pending/hidden modals.
- Coordinator Node also receives the same properties on first creation and
  on re-import.
- Remove the Parent Container selector from the add/edit node modal.
2026-05-14 00:17:13 +02:00
Pouzor ec8f1c87f1 fix(status): use ms timeout for ping on macOS
ping(8) -W flag is milliseconds on macOS, seconds on Linux. Code used
-W 1 for both, giving macOS a 1ms timeout that fails any host with
RTT >1ms (typical wifi/mesh is 2-10ms).

Use -W 1000 on darwin to match the intended 1s timeout. Add regression
test asserting the platform-specific value.
2026-05-10 19:45:36 +02:00
Pouzor 5e567d4628 feat(zigbee): add slow-fetch hint in modal + bump timeout to 300s
- Modal: small italic note under Test/Fetch buttons warning users that
  large meshes can take several minutes
- Service: _NETWORKMAP_TIMEOUT 180s -> 300s (5 min) for very large meshes
2026-05-07 00:27:48 +02:00
Pouzor 05db9a59f5 fix(zigbee): strict tree edges + auto-select imported nodes
Edges
- Z2M `links` is bidirectional and includes router mesh paths, which
  caused duplicate edges and edges entering the coordinator from the
  bottom. Walk `links` only to derive parent_id + LQI; build final
  edges strictly from the parent->child hierarchy (one edge per
  non-coordinator node). Result: parent bottom -> child top, every time.
- Tests: bidirectional pair collapses to one edge, router-mesh siblings
  dropped, coordinator never receives an edge.

Auto-select
- After import, deselect existing canvas nodes and mark only the
  freshly-imported ones as selected, so the user can drag the whole
  subtree as a group.
2026-05-06 23:53:31 +02:00
Pouzor a46e505505 fix(zigbee): parse real Z2M networkmap shape (data.value.nodes/links)
The previous parser read `data.routes` which is just an echo of the
`routes` request flag (a boolean). On real brokers this caused
`TypeError: 'bool' object is not iterable` and 500s during /import.

- Rewrite parse_networkmap to read data.value.nodes + data.value.links
  with fallback to data.{nodes,links} for legacy variants
- Defensive: drop links to unknown nodes, propagate lqi from link to
  target node, extract model/vendor from definition block
- Bump networkmap timeout 10s -> 180s (large meshes are slow)
- Tests: rewrite fixture builders + sample payload to real Z2M shape;
  add cases for legacy shape, routes:false echo (regression), malformed
  list, link to unknown node, lqi propagation, definition extraction
- Update docs to mention 60s+ wait window

53 backend tests pass, mypy + ruff clean.
2026-05-06 22:38:15 +02:00
Pouzor 9970780e7a fix(zigbee): sanitize MQTT error messages to prevent credential leakage
aiomqtt/paho exception strings can include the broker URI with embedded
credentials (mqtt://user:pass@host) or auth detail. The 502 response
from /import and the message field on /test-connection echoed these
verbatim via str(exc).

- Add _sanitize_mqtt_error() that maps known patterns (auth, refused,
  DNS, TLS, timeout) to coarse, credential-free categories
- Original exception still logged at WARNING level for operator debug
- Drop hostname:port from TimeoutError messages
- /test-connection unexpected-error path no longer interpolates exc

Tests: 6 new (auth/refused/DNS/TLS/unknown sanitization, end-to-end
fetch_networkmap leak check).
2026-05-06 14:55:51 +00:00
Pouzor d49610163f feat(zigbee): add MQTT TLS support with optional cert verify skip
- Schema: mqtt_tls + mqtt_tls_insecure flags on import + test-connection
  requests; model_validator enforces insecure requires tls
- Service: _build_tls_context() using ssl.create_default_context();
  logger.warning when verification disabled; tls_context plumbed into
  aiomqtt.Client for both fetch_networkmap and test_mqtt_connection
- Route: passes tls flags through to service
- Frontend: TLS checkbox auto-toggles port 1883<->8883 unless user
  edited; insecure checkbox disabled until TLS on, red-tinted; password
  field marked autocomplete=new-password
- Tests: 7 new backend tests (TLS context build, client kwargs assertion,
  router happy path, insecure-without-tls 422)
2026-05-06 16:30:23 +02:00
pranjal-joshi 67b0620be2 fix: resolve all mypy errors in zigbee_service
- Add dict[str, Any] type parameters throughout (mypy type-arg)
- Fix str | None returns with explicit 'or None' coercion (no-any-return)
- Remove unused type: ignore[import] comment (unused-ignore)
- Decode message.payload bytes before json.loads (arg-type)
- Use X | Y syntax in isinstance check (ruff UP038)
- All 33 tests still passing, ruff + mypy clean

Co-authored-by: CyberKeys <noreply@openclaw.ai>
2026-05-04 14:25:15 +00:00
pranjal-joshi cc9c010002 fix: adhere to CONTRIBUTING.md — ruff clean + all tests passing
- Move aiomqtt to module-level import (enables proper patch() in tests)
- Remove unused variable (description) in zigbee_service — ruff F841
- Split long line (132 chars) to fit 120 char limit — ruff E501
- Fix import sort order in test files — ruff I001
- Remove unused imports (asyncio, AsyncMock, MagicMock) — ruff F401
- Rename test_mqtt_connection import alias to _test_mqtt_connection
  to avoid pytest fixture name collision (ERROR at setup)
- All 33 backend tests now pass (21 service + 12 router)
- TypeScript typecheck: 0 errors

Co-authored-by: CyberKeys <noreply@openclaw.ai>
2026-05-04 14:12:38 +00:00
pranjal-joshi 103e24e5fa feat: add Zigbee2MQTT network map importer
- Backend: async MQTT service (aiomqtt) to fetch Z2M networkmap via bridge API
- Backend: FastAPI router at /api/v1/zigbee with /import and /test-connection
- Backend: Pydantic v2 schemas for request/response validation
- Backend: coordinator → router → end-device parent_id hierarchy builder
- Frontend: ZigbeeImportModal with MQTT config form, Test Connection, Fetch Devices
- Frontend: device list grouped by type (coordinator/router/enddevice) with checkboxes
- Frontend: ZigbeeCoordinatorNode, ZigbeeRouterNode, ZigbeeEndDeviceNode canvas nodes
- Frontend: Zigbee Import button in sidebar alongside Scan Network
- Frontend: handleZigbeeAddToCanvas wires selected devices + edges onto canvas
- Tests: full unit test suite for parser, hierarchy builder, MQTT mocks
- Tests: API endpoint tests for /zigbee/import and /zigbee/test-connection
- Tests: Vitest component tests for ZigbeeImportModal
- Docs: docs/zigbee-import.md with full usage, MQTT config, troubleshooting guide
- Docs: README.md Zigbee2MQTT Import section

Co-authored-by: CyberKeys <noreply@openclaw.ai>
2026-05-04 13:58:58 +00:00
Pouzor ef96cafcc8 feat: IPv6 support and multi-IP per node (closes #60)
- maskIp handles IPv6 addresses (masks second and last group)
- maskIp handles comma-separated IP strings (masks each address)
- Add splitIps() helper to parse comma-separated IP field
- Add primaryIp() helper used by status checker (first IP wins)
- BaseNode renders each IP on its own line when comma-separated
- NodeModal placeholder shows comma-separated example
- Backend status_checker uses only first IP for connectivity checks
- Expand maskIp test suite: IPv6, comma-separated, splitIps, primaryIp
2026-04-19 22:13:40 +02:00
Pouzor 07da498d18 fix: use correct ping args on Windows (-n/-w instead of -c/-W) 2026-04-05 14:08:36 +02:00
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 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 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 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 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 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 627f67336f feat: add 'none' check method — node always appears online 2026-03-08 00:41:17 +01:00
Pouzor 2441d72b41 fix: banner_regex sigs require actual banner match — prevents wrong type inference
Banner-specific signatures (e.g. AdGuard on port 3000) were incorrectly
matching when nmap returned no banner, causing wrong suggested_node_type
(e.g. 'router' for a media server). Now a signature with banner_regex is
only matched when a banner is present AND matches the regex.

Also make _PORT_TYPE_HINTS always contribute to suggest_node_type found set
(not just as elif fallback) so well-known ports like 8006 still resolve to
proxmox even when a generic sig also matched.
2026-03-07 23:31:33 +01:00
Pouzor 6e6041d871 fix: re-scan updates services on existing pending devices instead of skipping 2026-03-07 23:09:54 +01:00
Pouzor 1a72f9fa20 feat: improve service detection — expanded signatures, port hints, better nmap args
- Expand service_signatures.json from 35 → ~120 entries covering *arr apps,
  smart-home (HA, MQTT, ESPHome), cameras (RTSP, Dahua, Tapo, Reolink),
  network gear (MikroTik, UniFi, Pi-hole), auth (Authelia, Authentik, Vault),
  monitoring (Grafana, InfluxDB, Loki, Uptime Kuma), containers (Portainer),
  and many more home lab services
- Expand nmap port range to cover home lab ports (8989, 7878, 8123, 554, 1883, etc.)
  and increase --host-timeout from 30s to 120s for reliable -sV detection
- Add _PORT_TYPE_HINTS fallback in suggest_node_type for ports without signatures
  (cameras→iot, MQTT→iot, Proxmox→proxmox, MikroTik→router, etc.)
- Show actual port/protocol (e.g. TCP/9999) instead of "unknown_service" so
  all open ports are visible even when not matched
- Update tests to reflect new unknown-port label format
2026-03-07 16:33:03 +01:00
Pouzor bec699ba93 feat: non-blocking scan with progressive device discovery
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
2026-03-07 16:06:44 +01:00
Pouzor 974e782057 fix: resolve all 64 mypy errors across backend
- Add dict[str, Any] / list[Any] type params throughout (fingerprint, models, schemas, scanner, status_checker)
- Add return type annotations to all route functions (nodes, edges, canvas, scan, auth, status, main)
- Fix no-any-return in security.py: cast pwd/jwt results to bool/str explicitly
- Fix canvas.py: use model_validate() for NodeResponse/EdgeResponse, rename db_node/db_edge upsert vars
- Fix scheduler.py: rename 'result' → 'check_result' to avoid type collision
- Fix get_db() return type: AsyncGenerator[AsyncSession, None]
- Add types-PyYAML for yaml import stubs
- Fix scanner.py: remove unnecessary type: ignore comment (nmap has stubs)
- Fix scan.py: wrap scalars().all() with list() for Sequence→list compatibility
2026-03-07 15:48:41 +01:00
Pouzor fb85468cef fix: propagate nmap errors to UI + sudo scan script
- scanner.py: re-raise nmap exceptions so they reach ScanRun.error
  (previously silently returned [] hiding the root cause)
- Sidebar: after triggering scan, auto-switch to History tab
- ScanHistoryPanel: auto-refresh every 3s while any run is 'running';
  show error toast when a run transitions running→error; show spinner
  on running runs; error message fully visible (no truncation)
- scripts/run_scan.py: standalone scan script to run with sudo for
  nmap OS detection / SYN scans on macOS
2026-03-07 01:28:21 +01:00
Pouzor 0bd714a68b feat: Phase 3 & 4 — monitoring, discovery, polish, deployment
Phase 3 — Discovery & Monitoring:
- Network scanner: nmap wrapper + mock fallback, fingerprint service (35 signatures)
- Status checker: ping/http/https/tcp/ssh/prometheus/health per-node checks
- APScheduler: status checks every 60s, WebSocket broadcast
- WebSocket /ws/status: live node status updates to frontend
- Sidebar panels: Pending Devices, Hidden Devices, Scan History
- Auth token persisted to localStorage (survive page refresh)
- 24 new backend tests (scan flow + status_checker)

Phase 4 — Polish & Deployment:
- Auto-layout: Dagre hierarchical TB via Toolbar button
- Export PNG: html-to-image download via Toolbar button
- Scan config modal: CIDR ranges + check interval, GET/POST /api/v1/scan/config
- Dockerfile.backend (Python 3.13 slim + nmap), Dockerfile.frontend (nginx)
- docker-compose.yml with data volume and NET_RAW cap for ping
- scripts/lxc-install.sh: Proxmox VE systemd bootstrap
- README.md: quick-start, config reference, stack overview
2026-03-07 00:45:50 +01:00