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, name = null, grp = 'Flagged', vendor = null, randomized = false }) { const { rows: [r] } = await pool.query( `INSERT INTO lan_devices (mac, name, grp, vendor, randomized, status, present, first_seen, last_seen) VALUES ($1,$2,$3,$4,$5,'known',false,now(),now()) ON CONFLICT (mac) DO UPDATE SET name = EXCLUDED.name, grp = EXCLUDED.grp, vendor = COALESCE(NULLIF(EXCLUDED.vendor,''), lan_devices.vendor), status = 'known' RETURNING ${COLS}`, [mac, 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; }