Adds optional live status checking per service (not just per node),
requested as a follow-up to issue #196.
Backend:
- New check_service / check_services: HTTP(S) GET for web services, TCP
connect otherwise; UDP and port-less non-web services stay 'unknown'.
- New scheduler job 'service_checks', independent interval (default 300s),
added/removed live via set_service_checks_enabled.
- Settings gain service_check_enabled + service_check_interval (>=30s),
persisted to scan_config.json. New WS message type 'service_status'.
Frontend:
- Live per-service status overlay in canvasStore (not persisted, so it
never round-trips through canvas save), fed by the WS message.
- DetailPanel + canvas node service rows: offline service turns red
(#f85149), otherwise keeps its category colour.
- SettingsModal: toggle + interval input (default 300s / 5 min).
Off by default — no behaviour change until enabled.
ha-relevant: yes
Adds GET /api/v1/stats/summary, gated by HOMEPAGE_API_KEY env var and
X-API-Key header (hmac.compare_digest, matches the liveview pattern).
Payload:
nodes / online / offline / unknown
pending_devices (status='pending')
zigbee_devices (Node.ieee_address IS NOT NULL)
last_scan_at (max ScanRun.finished_at)
Disabled by default — endpoint returns 403 unless HOMEPAGE_API_KEY is set.
README documents activation and ships a ready-to-paste gethomepage
`customapi` widget snippet.
Tests cover: disabled-by-default, missing header, wrong key, empty DB,
and full aggregation across nodes/pending/zigbee/scan-runs.
- 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%.
- Add VERSION file at repo root as single source of truth for app version
- frontend/vite.config.ts reads VERSION file instead of package.json
- backend config.py exposes APP_VERSION read from VERSION (dev) or /app/VERSION (Docker)
- database.py backs up DB to homelab.db.back-{version} before running migrations
(skipped if DB doesn't exist or backup already exists — fully idempotent)
- Dockerfile.backend and Dockerfile.frontend copy VERSION into the image
- Add test_db_backup.py with 4 tests covering create/skip/idempotent/version cases
- 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
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.
- 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'
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.
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
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
- 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
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
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