Commit Graph

25 Commits

Author SHA1 Message Date
jhodgkin 7a685b9bd8 fix: SQLite has no ADD COLUMN IF NOT EXISTS, crashed on startup
CI / web (push) Successful in 18s
CI / api (push) Successful in 23s
Conflated CREATE TABLE/INDEX's IF NOT EXISTS support with ALTER
TABLE ADD COLUMN, which SQLite has never supported -- syntax error,
not a version issue (confirmed on 3.49.2). Check pragma table_info
for the column first instead. Verified against both a fresh DB and
one simulating the existing pre-migration production schema, and
confirmed idempotent on a second open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:05:54 -06:00
jhodgkin d614be4747 fix: mac-oui-lookup ESM/CJS interop crash on startup
CI / web (push) Successful in 17s
CI / api (push) Successful in 23s
Named import of a CommonJS module's export crashed the whole process
at boot (SyntaxError, not caught by tsc since it's a runtime module
resolution behavior, not a type error). Verified by actually running
the built output with node this time instead of trusting tsc alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:04:52 -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 669c51d348 docs: record Vaultwarden credential backup completion
CI / web (push) Successful in 18s
CI / api (push) Successful in 24s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:46:27 -06:00
jhodgkin cae8b7fc7e docs: update e2e/README.md coverage list
CI / api (push) Successful in 27s
CI / web (push) Successful in 13s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:38:25 -06:00
jhodgkin 48acfcd715 Broaden e2e coverage: invalid credentials, session persistence, API auth
CI / web (push) Successful in 17s
CI / api (push) Successful in 32s
Added: wrong-password rejection (local + OIDC), session survives a
page reload, and API-level checks that /api/hosts, /api/devices,
/api/auth/me all reject unauthenticated requests regardless of what
the UI does.

No new app bugs found this round -- one test assertion was itself
wrong (expected no session cookie on failed login; @fastify/session
issues an anonymous cookie on any response by design, that's normal).
Fixed to assert the property that actually matters: the cookie grants
no access. 9/9 tests green across 4 consecutive full-suite runs with
parallel workers, no flakiness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:38:10 -06:00
jhodgkin 13f69ea1aa docs: e2e suite writeup, update CLAUDE.md/README with OIDC verification status
CI / web (push) Successful in 17s
CI / api (push) Successful in 23s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:30:21 -06:00
jhodgkin 294be26500 fix: OIDC callback sent wrong scheme in token exchange redirect_uri
CI / web (push) Successful in 17s
CI / api (push) Successful in 24s
Fastify only sees plain HTTP -- TLS terminates at NPM/Cloudflare
before reaching this process. Building the callback's currentUrl from
req.headers.host with a hardcoded "http://" sent
redirect_uri=http://monitor.jerodrigged.com/... during the token
exchange, which Authentik rejects (logged as generic "invalid_client"
to the client, but its own event log said plainly: "Invalid redirect
URI used by provider"). Fixed by reusing the known-correct
redirectUri's origin and only taking the query string from the actual
request, instead of trying to infer scheme from headers.

Also fixes the Playwright OIDC test's selectors (Authentik's password
field has no <label> association -- placeholder text, not getByLabel).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:27:39 -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 a49ce0ab5b chore: mark discover-devices.sh executable in git
CI / web (push) Successful in 16s
CI / api (push) Successful in 23s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:09:14 -06:00
jhodgkin 9750aa9000 docs: record backlog completion in CLAUDE.md
CI / web (push) Successful in 14s
CI / api (push) Successful in 20s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:08:54 -06:00
jhodgkin 5143840d90 fix: bind-mount config/ as a directory, not a single file
CI / web (push) Successful in 17s
CI / api (push) Successful in 23s
Single-file bind mounts pin the container to that file's inode at
mount time. sed -i and most editors write-then-rename (atomic write),
which swaps in a new inode at the same path -- the container kept
reading the orphaned original and never saw edits, silently breaking
the hot-reload from the previous commit. Caught by actually testing
the reload live instead of trusting the code. Directory mounts
resolve paths dynamically and don't have this problem.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 22:06:52 -06:00
jhodgkin 1e1fb98afb Hot-reload sshHosts/knownDevices from hosts.yaml, no restart needed
CI / web (push) Successful in 16s
CI / api (push) Successful in 22s
Checks the file's mtime on each poll cycle (already running every
30s) rather than adding a separate file-watcher or admin UI. Proxmox
hosts already needed no config (auto-discovered every poll); this
covers the two lists that did. A parse failure logs and keeps the
previous config running instead of crashing the poller.

Closes #14.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 21:36:37 -06:00
jhodgkin 27c83d52b2 fix: ProxmoxCollector read wrong shape for node mem/disk
CI / web (push) Successful in 18s
CI / api (push) Successful in 24s
/nodes/{node}/status nests memory/rootfs objects; the LXC listing
endpoint uses flat mem/maxmem/disk/maxdisk. Code assumed the LXC
shape for both, so the Proxmox host's own memPct/diskPct were NaN ->
serialized as null the whole time. Found while checking sparkline
history data looked wrong for the host card specifically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 21:34:10 -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 ce232c5a53 Finish public exposure: monitor.jerodrigged.com is live
CI / web (push) Successful in 17s
CI / api (push) Successful in 22s
Cloudflare tunnel route -> NPM -> dashboard, Let's Encrypt cert via
NPM's API, both public and LAN OIDC redirect URIs registered in
Authentik. Hit and fixed a Flexible-SSL redirect loop (ssl_forced
must stay false since Cloudflare terminates TLS at the edge and talks
plain HTTP to the origin) -- documented clearly so it doesn't get
"fixed" by accident later.

Closes #13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 21:27:54 -06:00
jhodgkin 04131006ed docs: record Zabbix decision, CT122 conflict, OIDC/exposure status
CI / web (push) Successful in 18s
CI / api (push) Successful in 24s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 21:17:06 -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 9891751d37 docs: update CLAUDE.md for device discovery
CI / web (push) Successful in 17s
CI / api (push) Successful in 22s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:50:50 -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 d75bd58792 docs: update CLAUDE.md for SSH collector + Vaultwarden entry
CI / web (push) Successful in 19s
CI / api (push) Successful in 24s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:42:05 -06:00
jhodgkin 7df08cd16d Add SshHostCollector for .180 (omv) and .171 (ripper)
CI / web (push) Successful in 20s
CI / api (push) Successful in 27s
Extends monitoring to the two bare-metal boxes Proxmox can't see.
Uses a dedicated ed25519 key with a forced authorized_keys command
(see docs/ssh-collector-key-setup.md) so a leaked key can only ever
run the fixed read-only stats script, never arbitrary commands.

CPU is approximated from 1-min load average / core count (a true
utilization % would need two /proc/stat samples).

Closes #8.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:40:10 -06:00
jhodgkin ac768ed0db fix: decouple session cookie 'secure' flag from NODE_ENV
CI / web (push) Successful in 16s
CI / api (push) Successful in 17s
Cookie was silently never set because NODE_ENV=production forced
secure=true while the app is served over plain HTTP on the LAN (TLS
terminates at a reverse proxy later, not here). Add explicit
COOKIE_SECURE env var, default false.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 20:21:52 -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