From b9b94c9777538e1f75c4bf28575c02331e0697c1 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 8 Jun 2026 20:48:21 +1000 Subject: [PATCH] docs(devices): add randomized-MAC retention/prune to discovery spec Co-Authored-By: Claude Opus 4.8 --- .../2026-06-08-lan-device-discovery-design.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/specs/2026-06-08-lan-device-discovery-design.md b/docs/superpowers/specs/2026-06-08-lan-device-discovery-design.md index cc17bfd..02fd24e 100644 --- a/docs/superpowers/specs/2026-06-08-lan-device-discovery-design.md +++ b/docs/superpowers/specs/2026-06-08-lan-device-discovery-design.md @@ -50,6 +50,7 @@ the Void's existing services "discovered → promote" pattern). | Identity | **MAC primary key**; IP a mutable column | Survives DHCP IP changes. | | Review flow | Mirror services `discovered → promote` | New MAC → `status='new'`; owner names/edits → `status='known'`. | | Source of truth | **DB** (`lan_devices`); `devices.json` becomes the one-time migration seed, then removed | Single source of truth. | +| Randomized-MAC bloat | **Auto-prune unreviewed + absent rows** (randomized >24h, others >14d); keep `known`/`ignored` forever | Rotated randomized MACs never accumulate; the table stays bounded. | ## Architecture @@ -97,10 +98,14 @@ Table `lan_devices`: - `listKnown()` (`status='known'`, grouped by `grp`), `listDiscovered()` (`status='new'`), `get(mac)`, `update(mac, {name, grp, status, note, flagged})`, `remove(mac)`. (`ignored` devices show in neither.) +- `prune()` — delete unreviewed + absent rows past their TTL: `status='new' AND + present=false AND ((randomized AND last_seen < now()-'24h') OR (NOT randomized + AND last_seen < now()-'14d'))`. Never touches `known`/`ignored`. ### Cron (`lib/cron/index.js`) -Add hourly (`7 * * * *`): `runScan()` → `upsertScan` → `markAbsent`. Wrapped in -try/catch — a scan failure logs and never crashes the cron. +Add hourly (`7 * * * *`): `runScan()` → `upsertScan` → `markAbsent` → `prune()`. +Wrapped in try/catch — a scan failure logs and never crashes the cron, and +`prune()` only runs after a *successful* scan (so a failed scan can't reap rows). ### API `lib/api/routes/devices.js` (mount `/api/devices`, owner-gated) - `GET /` — known devices grouped for the band. @@ -117,6 +122,10 @@ try/catch — a scan failure logs and never crashes the cron. - **Discovered review** — a section/panel listing `/api/devices/discovered`, each with an **Add / Edit** form (name + group select + notes) that `PATCH`es to promote; plus inline edit for known devices and an Ignore/Delete action. +- **Randomized devices** get a small "randomized MAC" badge (with a tooltip: + naming pins it only until the MAC rotates; disable SSID randomization for + stable tracking). A `known` device that's been `present=false` for ≥30d shows + an "absent Nd" marker for easy manual cleanup (never auto-deleted). - Remove `public/devices.json` (superseded by the DB). ## Infra setup (one-time, on CT 311) @@ -149,8 +158,9 @@ error and the feature degrades to "no new discoveries" (existing data still show ## Out of scope (YAGNI) - Service/port fingerprinting, SNMP/LLDP topology (that's Scanopy's job). - Multi-subnet/VLAN scanning (single `/24`). -- Auto-pruning stale `new` devices (revisit only if the queue gets noisy). - Push notifications on new-device discovery. +- Stable identity for randomized-MAC devices across rotations (not solvable from + L2 alone; the user-side fix is disabling MAC randomization for the SSID). ## References - Scanopy — github.com/scanopy/scanopy ; scanopy.net (self-hosted discovery/topology, AGPL-3.0).