Commit Graph

14 Commits

Author SHA1 Message Date
jhodgkin 5329136bad Show memory and disk capacity (used/total) on dashboard tiles
CI / web (pull_request) Successful in 24s
CI / api (pull_request) Successful in 31s
Previously the Mem/Disk bars only showed a percentage. Plumbed the
underlying byte totals through the whole stack -- collectors, DB
(with a migration for the already-deployed CT122 instance), API, and
the web tile -- so each card also shows e.g. "19.6/32.0 GB".

Verified end-to-end with a throwaway local instance (seeded snapshot,
logged in, screenshotted the rendered tile).

Note: the SSH-collected hosts (omv, ripper) assume the remote
monitor-readonly.sh script emits raw bytes for MEMLINE/DISK_, matching
Proxmox's convention -- unverified since that script only lives on
those two hosts, not in this repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 23:36:09 +00:00
jhodgkin 74c7bf6ef7 Sign out also ends the Authentik SSO session (RP-Initiated Logout)
CI / web (push) Successful in 22s
CI / api (push) Successful in 29s
Previously logout only destroyed our own session -- someone who
signed in via Authentik stayed logged into Authentik itself, so
"Sign in with Authentik" again would silently re-authenticate with
no prompt.

Session now tracks authMethod ("local" | "oidc") and, for OIDC
sessions, the raw id_token (needed as id_token_hint at logout time).
New GET /api/auth/oidc/logout redirects through Authentik's
end_session_endpoint (openid-client's buildEndSessionUrl, not
hand-rolled) before landing back on /. Must be a full-page navigation
-- Authentik needs a real browser request to clear its own session
cookie, a fetch() wouldn't do that. Local sessions still use the
existing POST /api/auth/logout unchanged.

Confirmed Authentik has no dedicated post_logout_redirect_uri
allowlist field by checking the provider's DB schema directly before
implementing, rather than assuming.

Closes #19.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 08:54:21 -06:00
jhodgkin 199d0da675 Alert on genuinely new (never-seen-before) devices
CI / web (push) Successful in 16s
CI / api (push) Successful in 23s
New seen_macs table: permanent, insert-only, MAC-keyed record of the
first time each device was ever seen -- deliberately decoupled from
devices.first_seen (IP-keyed, would false-positive on every DHCP
lease change). Bootstrap-safe: first call seeds the baseline from
whatever's currently on the network without alerting on all 71+
existing devices at once. Verified locally: bootstrap call reports
nothing new, repeat calls with the same MACs report nothing new, one
genuinely new MAC gets reported exactly once.

Pushes a Home Assistant persistent_notification when a new MAC
appears (gated behind HOME_ASSISTANT_TOKEN + homeAssistant.url in
hosts.yaml -- missing config just means no push, detection still
runs). Also surfaced directly in the dashboard as a blue "new" badge
for anything first seen in the last 24h, independent of HA config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:58:26 -06:00
jhodgkin d0d6ae95f1 Add optional Fingerbank device fingerprinting to deep-check
CI / web (push) Successful in 16s
CI / api (push) Successful in 21s
Folded into the existing on-demand Deep check button: queries
Fingerbank's interrogate API with the device's MAC plus the SSDP
SERVER header when deep-check-device.sh finds one, showing the
confidence band alongside the result. Runs directly from the API
container (no host-level access needed, just an outbound HTTPS call),
unlike the SSDP/mDNS steps.

Confirmed via direct testing: without DHCP fingerprint data (which we
structurally don't have, not being the DHCP server), MAC-only queries
often can't get past manufacturer-level confidence -- same info the
free OUI lookup already provides. Documented honestly in
docs/device-discovery.md rather than overselling it. Still worth
having as opt-in enrichment for devices that do expose richer signals.

Gated behind optional FINGERBANK_API_KEY -- missing key, API errors,
or no match all degrade gracefully without affecting the rest of
deep-check's local findings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:35:35 -06:00
jhodgkin c772ca95c9 Add known/unknown device ratio next to the device count
CI / web (push) Successful in 16s
CI / api (push) Successful in 22s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:22:04 -06:00
jhodgkin 542a3d8ce0 Wire up on-demand deep-check: API route + dashboard button
CI / web (push) Successful in 18s
CI / api (push) Successful in 24s
POST /api/devices/:ip/deep-check runs deep-check-device.sh on the
CT122 host via SSH (reaches its own LAN IP), returns mDNS/SSDP/port
scan results. "Deep check" button on unknown device rows in the
dashboard shows results inline below the row.

Verified end-to-end via SSH before wiring into the API: correctly
identified Home Assistant via SSDP (friendlyName/manufacturer/model),
and confirmed both a shell-injection attempt and an out-of-subnet IP
get rejected cleanly by the forced command's input validation.

Closes #15 (all four pieces: OUI, mDNS, manual labels, deep check).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:15:24 -06:00
jhodgkin b84fd373c2 Add device labeling e2e test; add data-ip for reliable row targeting
CI / web (push) Successful in 17s
CI / api (push) Successful in 23s
First test run hit a bug in the test itself: Playwright's hasText
filter does substring matching, so IP 192.168.1.1 matched
192.168.1.10, 192.168.1.100, 192.168.1.171, etc -- flaky/wrong row
selection. Added a data-ip attribute to each row for exact targeting
instead of relying on text content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:08:06 -06:00
jhodgkin 1caad69448 Identify unknown devices: OUI vendor lookup, mDNS, manual labels
CI / web (push) Successful in 20s
CI / api (push) Successful in 29s
- OUI: mac-oui-lookup package resolves vendor from the MAC prefix
  (computed on read, no storage needed). Already correctly identifies
  the LXC host prefix as "Proxmox Server Solutions GmbH" and several
  "unknown" devices as "Amazon Technologies Inc." -- likely the Echo
  Dots / Ring gear.
- mDNS: discover-devices.sh now runs avahi-resolve per discovered IP
  (parallel, bounded 2s timeout per host so one non-mDNS device can't
  stall the run), stored in a new devices.mdns_hostname column.
- Manual labels: new device_labels table keyed by MAC (survives DHCP
  IP changes), PUT/DELETE /api/devices/:mac/label, inline-editable
  Name cell in the dashboard. Deliberately separate from vendor/mDNS
  info -- those are shown as an italic *hint* for unlabeled devices,
  not treated as "known" until the admin actually confirms one.
- Fixed the Name column's sort comparator to match what's rendered
  (name, else vendor/mDNS hint) instead of just the raw name field --
  caught while reasoning through what the existing sort test would
  actually need to assert once hints appear in the column.

Part of #15 (OUI/mDNS/manual labels done; on-demand deep-check next).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:03:06 -06:00
jhodgkin cdbdd01541 Make the network device table sortable by column
CI / web (push) Successful in 17s
CI / api (push) Successful in 24s
Click a header to sort by it (ascending), click again to reverse.
Status defaults to known-first (its display string sorts that way
naturally, no special-casing needed). IP sorts numerically by octet,
not lexically. Ties fall back to IP order so the table doesn't
reshuffle mid-poll for devices sharing a sort value (e.g. many
unnamed unknowns).

Added a Playwright test verifying IP asc/desc and Name asc against
real rendered data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:48:51 -06:00
jhodgkin 9b5051d3ac Add Playwright e2e tests; fix two real bugs they caught
CI / web (push) Successful in 17s
CI / api (push) Failing after 3h0m1s
fix: OIDC_ISSUER_URL used Authentik's LAN IP (192.168.1.208:9443).
Authentik's discovery doc echoes back whichever host you query it
through, so that LAN IP got baked into authorization_endpoint -- the
URL the *browser* gets redirected to. Anyone off the LAN got sent to
an address they couldn't reach. Authentik was already publicly
exposed at auth.jerodrigged.com (pre-existing NPM proxy host); switched
to that, which also has a real cert so OIDC_ALLOW_INSECURE_TLS could
go back to false. Reported as "signed in via Authentik, redirected to
the local IP, failed."

fix: frontend's request() helper always sent Content-Type:
application/json, even for logout's bodyless POST. Fastify's default
JSON parser rejects an empty body under that content-type (400) --
sign-out silently failed to log the user out. curl-based testing
missed this because curl doesn't set that header without -d. Caught
immediately by the new Playwright local-login test.

e2e/: Playwright suite for local auth and OIDC login. OIDC test uses
a dedicated Authentik test user (blueprint-provisioned, never a real
personal login) so the whole flow can run unattended and repeatedly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:22:34 -06:00
jhodgkin f3ee4c2424 Add historical sparklines to host cards
CI / web (push) Successful in 17s
CI / api (push) Successful in 24s
24h of snapshots were already being retained but never read. Adds a
windowed query (last ~40 samples/host, one query total via
ROW_NUMBER() OVER PARTITION BY, not N+1) embedded in the existing
/api/hosts response, rendered as small hand-rolled SVG sparklines
(cpu/mem/disk overlaid) -- no charting library needed at this scale.

Closes #10.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 21:32:36 -06:00
jhodgkin a431b87f1f Add Authentik OIDC login as an additional sign-in option
CI / web (push) Successful in 19s
CI / api (push) Successful in 28s
Local auth stays the primary/always-available login (don't want to
lock out the saved admin password) — OIDC is additive, shown as a
second button when OIDC_ENABLED=true. Uses openid-client v6 with PKCE.

Authentik-side provider was set up via an authentik blueprint (its own
declarative automation, see docs/oidc-setup.md) rather than touching
any existing admin credentials.

Closes #12.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:59:25 -06:00
jhodgkin 04282232cc Add LAN device discovery (ping sweep + ARP, known/unknown labeling)
CI / web (push) Successful in 17s
CI / api (push) Successful in 23s
Runs as a host-level systemd timer on CT122 (scripts/discover-devices.sh)
rather than inside the api container, since real ARP entries live in the
host's network namespace, not Docker's bridge network. See
docs/device-discovery.md for the full writeup, including why literal
passive-only ARP reading was dropped (near-empty result in practice).

API reads the resulting JSON file each poll cycle, cross-references
config/hosts.yaml's knownDevices list by IP, and serves /api/devices.
Dashboard gets a new "Network Devices" table.

Closes #9.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:49:22 -06:00
jhodgkin a075488f4b Scaffold homelab-monitor: Fastify API + React dashboard + CI
CI / web (push) Failing after 1m11s
CI / api (push) Successful in 1m19s
Vertical slice for Phase 1 (v1-dashboard milestone): Proxmox collector,
SQLite storage, local auth, and a dashboard UI showing host/container
status cards. Config-driven collector registry so future data sources
(SSH-based hosts, Zabbix, network discovery) plug in without rewiring.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:18:44 -06:00