'Scan Now' triggers POST /api/devices/scan from the band header. '+ Add by MAC' renamed '+ Manual Add' with an optional IP field (addBody/addManual accept ip) and a MAC input that auto-inserts colons as you type. Frontend test 4/4; DB-backed api/repo tests written (run with the suite — skipped locally to avoid colliding with a concurrent test run on void_test). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
90 lines
3.5 KiB
JavaScript
90 lines
3.5 KiB
JavaScript
import { pool } from '../pool.js';
|
|
|
|
const COLS = 'mac, ip, vendor, name, grp, note, status, randomized, flagged, first_seen, last_seen, present';
|
|
|
|
export async function listKnown() {
|
|
const { rows } = await pool.query(
|
|
`SELECT ${COLS} FROM lan_devices WHERE status='known' ORDER BY grp, name NULLS LAST, ip`);
|
|
return rows;
|
|
}
|
|
|
|
export async function listDiscovered() {
|
|
const { rows } = await pool.query(
|
|
`SELECT ${COLS} FROM lan_devices WHERE status='new' ORDER BY last_seen DESC`);
|
|
return rows;
|
|
}
|
|
|
|
export async function get(mac) {
|
|
const { rows: [r] } = await pool.query(`SELECT ${COLS} FROM lan_devices WHERE mac=$1`, [mac]);
|
|
return r || null;
|
|
}
|
|
|
|
// Manually add a device by MAC (e.g. an offline device whose MAC you know). Lands
|
|
// as status='known', present=false. Idempotent — re-adding updates name/grp/vendor.
|
|
export async function addManual({ mac, ip = null, name = null, grp = 'Flagged', vendor = null, randomized = false }) {
|
|
const { rows: [r] } = await pool.query(
|
|
`INSERT INTO lan_devices (mac, ip, name, grp, vendor, randomized, status, present, first_seen, last_seen)
|
|
VALUES ($1,$2,$3,$4,$5,$6,'known',false,now(),now())
|
|
ON CONFLICT (mac) DO UPDATE SET
|
|
ip = COALESCE(NULLIF(EXCLUDED.ip,''), lan_devices.ip),
|
|
name = EXCLUDED.name, grp = EXCLUDED.grp,
|
|
vendor = COALESCE(NULLIF(EXCLUDED.vendor,''), lan_devices.vendor),
|
|
status = 'known'
|
|
RETURNING ${COLS}`,
|
|
[mac, ip, name, grp, vendor, !!randomized]);
|
|
return r;
|
|
}
|
|
|
|
// Insert unseen MACs as status='new'; for existing, refresh ip/vendor/last_seen/present
|
|
// WITHOUT touching owner-curated name/grp/status/flagged.
|
|
export async function upsertScan(rows) {
|
|
for (const r of rows) {
|
|
await pool.query(
|
|
`INSERT INTO lan_devices (mac, ip, vendor, randomized, status, present, first_seen, last_seen)
|
|
VALUES ($1,$2,$3,$4,'new',true,now(),now())
|
|
ON CONFLICT (mac) DO UPDATE SET
|
|
ip = EXCLUDED.ip,
|
|
vendor = COALESCE(NULLIF(EXCLUDED.vendor,''), lan_devices.vendor),
|
|
last_seen = now(), present = true`,
|
|
[r.mac, r.ip ?? null, r.vendor ?? null, !!r.randomized]);
|
|
}
|
|
return rows.length;
|
|
}
|
|
|
|
// Mark devices not in the latest scan as absent. Empty input is a no-op so a
|
|
// failed/empty scan can never blanket-mark everything offline.
|
|
export async function markAbsent(seenMacs) {
|
|
if (!seenMacs || !seenMacs.length) return 0;
|
|
const { rowCount } = await pool.query(
|
|
`UPDATE lan_devices SET present=false WHERE present=true AND NOT (mac = ANY($1::text[]))`,
|
|
[seenMacs]);
|
|
return rowCount;
|
|
}
|
|
|
|
// Reap unreviewed + absent rows past their TTL. Never touches known/ignored.
|
|
export async function prune() {
|
|
const { rowCount } = await pool.query(
|
|
`DELETE FROM lan_devices WHERE status='new' AND present=false AND (
|
|
(randomized AND last_seen < now() - interval '24 hours') OR
|
|
(NOT randomized AND last_seen < now() - interval '14 days'))`);
|
|
return rowCount;
|
|
}
|
|
|
|
const PATCHABLE = ['name', 'grp', 'note', 'status', 'flagged'];
|
|
export async function update(mac, patch) {
|
|
const sets = [], vals = [];
|
|
for (const k of PATCHABLE) {
|
|
if (patch[k] !== undefined) { vals.push(patch[k]); sets.push(`${k}=$${vals.length}`); }
|
|
}
|
|
if (!sets.length) return get(mac);
|
|
vals.push(mac);
|
|
const { rows: [r] } = await pool.query(
|
|
`UPDATE lan_devices SET ${sets.join(', ')} WHERE mac=$${vals.length} RETURNING ${COLS}`, vals);
|
|
return r || null;
|
|
}
|
|
|
|
export async function remove(mac) {
|
|
const { rowCount } = await pool.query(`DELETE FROM lan_devices WHERE mac=$1`, [mac]);
|
|
return rowCount > 0;
|
|
}
|