- 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>
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>
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>
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>
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>
/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>
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>
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>
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>
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>
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>
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>