feat(devices): DB-backed devices band + discovered review/add/edit UI
This commit is contained in:
@@ -1,36 +1,67 @@
|
||||
// Network Devices band — IoT / personal / unknown LAN devices, kept SEPARATE
|
||||
// from Little Blue's homelab-service health band. Read-only, static source
|
||||
// (public/devices.json), no health probing. Live discovery comes later.
|
||||
import { el, mount } from '../dom.js';
|
||||
// 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) {
|
||||
return el('div', { class: 'dv-tile' + (d.flagged ? ' flag' : '') + (d.present === false ? ' absent' : '') },
|
||||
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' : '')));
|
||||
}
|
||||
|
||||
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;
|
||||
try {
|
||||
const res = await fetch('/devices.json');
|
||||
const data = await res.json();
|
||||
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(d =>
|
||||
el('div', { class: 'dv-tile' + (d.flag ? ' flag' : '') },
|
||||
el('span', { class: 'dv-nm' }, d.name),
|
||||
el('span', { class: 'dv-ip' }, d.ip),
|
||||
d.mac ? el('span', { class: 'dv-mac' }, d.mac) : null,
|
||||
el('span', { class: 'dv-vendor' }, d.vendor || ''))))));
|
||||
mount(host,
|
||||
el('div', { class: 'dv-hd' },
|
||||
el('div', { class: 'dv-title' }, 'Network · Devices'),
|
||||
el('span', { class: 'dv-count' }, `${total} on the LAN`)),
|
||||
el('div', { class: 'dv-note' }, data.note || ''),
|
||||
sections);
|
||||
} catch {
|
||||
mount(host, el('span', { class: 'muted' }, 'Device list unavailable'));
|
||||
}
|
||||
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(el_) { host = el_; load(); }
|
||||
|
||||
export function renderDevicesBand(root) { host = root; return load(); }
|
||||
export function stopDevicesBand() { host = null; }
|
||||
|
||||
Reference in New Issue
Block a user