// Network Devices band — DB-backed (GET /api/devices). Shows IP+MAC+vendor, // a randomized-MAC badge, and an owner "Discovered" review panel to name/promote // newly-seen devices. Kept SEPARATE from Little Blue's homelab-service band. import { el, mount, clear } from '../dom.js'; import { api } from '../api.js'; let host; const GROUPS = ['Smart Home', 'Entertainment', 'Personal', 'Network', 'Flagged']; function tile(d) { const t = el('div', { class: 'dv-tile' + (d.flagged ? ' flag' : '') + (d.present === false ? ' absent' : '') }); function view() { clear(t); const edit = el('button', { class: 'dv-edit-btn', title: 'Edit device' }, '✎'); edit.onclick = editMode; mount(t, el('span', { class: 'dv-nm' }, d.name || 'Unknown'), el('span', { class: 'dv-ip' }, d.ip || ''), d.mac ? el('span', { class: 'dv-mac' }, d.mac) : null, el('span', { class: 'dv-vendor' }, (d.vendor || '') + (d.randomized ? ' · randomized' : '') + (d.present === false ? ' · absent' : '')), d.mac ? edit : null); } function editMode() { clear(t); const nameI = el('input', { class: 'dv-edit-name', value: d.name || '' }); const grpS = el('select', { class: 'dv-edit-grp' }, ...GROUPS.map(g => el('option', { value: g }, g))); grpS.value = d.grp || 'Flagged'; const save = el('button', { class: 'dv-add' }, 'Save'); save.onclick = async () => { await api.patch('/api/devices/' + d.mac, { name: nameI.value.trim() || null, grp: grpS.value }); load(); }; const del = el('button', { class: 'ghost dv-ignore' }, 'Delete'); del.onclick = async () => { await api.del('/api/devices/' + d.mac); load(); }; const cancel = el('button', { class: 'ghost' }, 'Cancel'); cancel.onclick = view; mount(t, el('span', { class: 'dv-mac' }, d.mac), nameI, grpS, save, del, cancel); } view(); return t; } function discoveredRow(d, onDone) { const nameI = el('input', { class: 'dv-edit-name', placeholder: d.vendor || 'name', value: d.name || '' }); const grpS = el('select', { class: 'dv-edit-grp' }, ...GROUPS.map(g => el('option', { value: g }, g))); const add = el('button', { class: 'dv-add' }, 'Add'); add.onclick = async () => { await api.patch('/api/devices/' + d.mac, { name: nameI.value.trim() || null, grp: grpS.value, status: 'known', flagged: false }); onDone(); }; const ignore = el('button', { class: 'ghost dv-ignore' }, 'Ignore'); ignore.onclick = async () => { await api.patch('/api/devices/' + d.mac, { status: 'ignored' }); onDone(); }; return el('div', { class: 'dv-disc-row' }, el('span', { class: 'dv-ip' }, d.ip || ''), el('span', { class: 'dv-mac' }, d.mac + (d.randomized ? ' · randomized' : '')), el('span', { class: 'dv-vendor' }, d.vendor || ''), nameI, grpS, add, ignore); } async function load() { if (!host) return; let data, discovered = []; try { data = await api.get('/api/devices'); } catch { mount(host, el('div', { class: 'dv-note' }, 'Devices unavailable')); return; } try { discovered = await api.get('/api/devices/discovered'); } catch { /* owner-only; ignore for non-owner */ } const total = data.groups.reduce((n, g) => n + g.devices.length, 0); const sections = data.groups.map(g => el('div', { class: 'dv-section' }, el('div', { class: 'dv-group' }, el('span', { class: 'gname' }, g.name), el('span', { class: 'gcount' }, String(g.devices.length)), el('span', { class: 'line' })), el('div', { class: 'dv-tiles' }, g.devices.map(tile)))); const discPanel = discovered.length ? el('div', { class: 'dv-discovered' }, el('div', { class: 'dv-disc-hd' }, `Discovered · ${discovered.length} awaiting review`), ...discovered.map(d => discoveredRow(d, load))) : null; clear(host); mount(host, el('div', { class: 'dv-hd' }, el('div', { class: 'dv-title' }, 'Network · Devices'), el('span', { class: 'dv-count' }, `${total} known${discovered.length ? ` · ${discovered.length} new` : ''}`)), ...sections, discPanel); } export function renderDevicesBand(root) { host = root; return load(); } export function stopDevicesBand() { host = null; }