Commit Graph

12 Commits

Author SHA1 Message Date
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 960c926dd2 fix: 'New' badge flagged the entire existing device population
CI / web (push) Successful in 19s
CI / api (push) Successful in 26s
Deployed the new-device feature, checked the live API response
instead of assuming the earlier unit test covered it, and found 65/71
devices marked isNew: true. The notification path was correctly
bootstrap-safe (verified separately), but the UI's isNew check just
tested "first_ever_seen within 24h" with no bootstrap awareness --
and bootstrap timestamps are, correctly, "now", so the whole existing
population qualified on day one.

Added seen_macs.is_bootstrap, set on the seeding call and excluded
from isNew. Migration backfills is_bootstrap=1 for any seen_macs rows
that already existed before this column did (the already-deployed
CT122 instance) -- verified locally against both a fresh DB and a
simulated pre-migration table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-13 00:02:04 -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 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 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 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 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