From 1e1d0c539d5cedab878a622c7ded7ebeb900c3bd Mon Sep 17 00:00:00 2001 From: root Date: Wed, 3 Jun 2026 00:10:54 +1000 Subject: [PATCH] =?UTF-8?q?feat(ui):=20add=20separate=20Network=C2=B7Devic?= =?UTF-8?q?es=20band=20(IoT/personal)=20below=20Little=20Blue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read-only static band from public/devices.json (ARP scan), grouped Smart Home / Entertainment / Personal / Network / Flagged. Kept distinct from Little Blue's service health band. Live discovery deferred. Co-Authored-By: Claude Opus 4.8 --- public/devices.json | 38 +++++++++++++++++++++++++++++++++++ public/style.css | 22 ++++++++++++++++++++ public/views/devices_band.js | 35 ++++++++++++++++++++++++++++++++ public/views/sacred_valley.js | 7 +++++-- 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 public/devices.json create mode 100644 public/views/devices_band.js diff --git a/public/devices.json b/public/devices.json new file mode 100644 index 0000000..da10e3f --- /dev/null +++ b/public/devices.json @@ -0,0 +1,38 @@ +{ + "note": "Auto-scanned LAN devices (ARP/nmap 2026-06-02). Separate from Little Blue's homelab services. Vendor-guessed; identification + live discovery to come.", + "groups": [ + { "name": "Smart Home", "devices": [ + { "name": "Amazon Echo", "ip": "192.168.1.3", "vendor": "Amazon" }, + { "name": "Amazon Echo", "ip": "192.168.1.4", "vendor": "Amazon" }, + { "name": "Smart device", "ip": "192.168.1.6", "vendor": "Beken" }, + { "name": "Smart device", "ip": "192.168.1.23", "vendor": "Tuya" }, + { "name": "Xiaomi device", "ip": "192.168.1.20", "vendor": "Xiaomi" } + ]}, + { "name": "Entertainment", "devices": [ + { "name": "Google / Nest", "ip": "192.168.1.12", "vendor": "Google" }, + { "name": "Google / Nest", "ip": "192.168.1.14", "vendor": "Google" }, + { "name": "Google / Nest", "ip": "192.168.1.18", "vendor": "Google" }, + { "name": "Google / Nest", "ip": "192.168.1.21", "vendor": "Google" }, + { "name": "Google / Nest", "ip": "192.168.1.22", "vendor": "Google" }, + { "name": "Cambridge Audio", "ip": "192.168.1.29", "vendor": "StreamMagic" }, + { "name": "Apple TV / HomePod", "ip": "192.168.1.43", "vendor": "Apple" }, + { "name": "Samsung TV", "ip": "192.168.1.24", "vendor": "Samsung" } + ]}, + { "name": "Personal", "devices": [ + { "name": "Apple device", "ip": "192.168.1.133", "vendor": "Apple" }, + { "name": "Samsung device", "ip": "192.168.1.61", "vendor": "Samsung" }, + { "name": "TP-Link device", "ip": "192.168.1.10", "vendor": "TP-Link" } + ]}, + { "name": "Network", "devices": [ + { "name": "Gateway / Router", "ip": "192.168.1.1", "vendor": "Netgear" } + ]}, + { "name": "Flagged / Unknown", "devices": [ + { "name": "Rogue OpenWrt", "ip": "192.168.1.13", "vendor": "Netgear · uhttpd", "flag": true }, + { "name": "ASUS device", "ip": "192.168.1.15", "vendor": "ASUSTek", "flag": true }, + { "name": "Unknown", "ip": "192.168.1.34", "vendor": "randomized MAC", "flag": true }, + { "name": "Unknown", "ip": "192.168.1.35", "vendor": "unknown", "flag": true }, + { "name": "Unknown", "ip": "192.168.1.51", "vendor": "randomized MAC", "flag": true }, + { "name": "Unknown", "ip": "192.168.1.171", "vendor": "randomized MAC", "flag": true } + ]} + ] +} diff --git a/public/style.css b/public/style.css index ec332b6..ff3932f 100644 --- a/public/style.css +++ b/public/style.css @@ -302,3 +302,25 @@ body.drawer-open #scrim { opacity: 1; pointer-events: auto; } #main { padding: 14px 12px; } /* Sacred Valley cards already collapse to 1 column at <=900px (see grid rules) */ } + +/* ===== Network · Devices band (separate from Little Blue's health band) ===== */ +#sv-devices { margin-top: 30px; padding-top: 22px; border-top: 1px dashed var(--border); } +.dv-hd { display: flex; align-items: baseline; gap: 12px; } +.dv-title { font-family: var(--font-display); letter-spacing: .14em; text-transform: uppercase; font-size: 13px; color: var(--muted); } +.dv-count { font-family: var(--font-mono); font-size: 10px; color: var(--muted); } +.dv-note { font-family: var(--font-body); font-style: italic; color: var(--muted); font-size: 13px; margin: 4px 0 6px; opacity: .8; } +.dv-section { margin-top: 12px; } +.dv-group { display: flex; align-items: center; gap: 10px; margin: 10px 0 7px; } +.dv-group .gname { font-family: var(--font-display); text-transform: uppercase; letter-spacing: .14em; font-size: 10px; color: var(--muted); } +.dv-group .gcount { font-family: var(--font-mono); font-size: 10px; color: var(--muted); } +.dv-group .line { flex: 1; height: 1px; background: linear-gradient(90deg, var(--border), transparent); } +.dv-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 8px; } +.dv-tile { + border: 1px solid var(--border); border-radius: 7px; padding: 8px 11px; background: #101017; + display: flex; flex-direction: column; gap: 2px; +} +.dv-tile .dv-nm { font-family: var(--font-ui); font-size: 12px; color: var(--text); } +.dv-tile .dv-ip { font-family: var(--font-mono); font-size: 11px; color: var(--muted); } +.dv-tile .dv-vendor { font-family: var(--font-ui); font-size: 10px; color: var(--muted); opacity: .7; } +.dv-tile.flag { border-color: var(--bad); background: #1a1012; } +.dv-tile.flag .dv-nm { color: var(--bad); } diff --git a/public/views/devices_band.js b/public/views/devices_band.js new file mode 100644 index 0000000..15e9213 --- /dev/null +++ b/public/views/devices_band.js @@ -0,0 +1,35 @@ +// 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'; + +let host; +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), + 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')); + } +} +export function renderDevicesBand(el_) { host = el_; load(); } +export function stopDevicesBand() { host = null; } diff --git a/public/views/sacred_valley.js b/public/views/sacred_valley.js index 68d009f..0c8c1df 100644 --- a/public/views/sacred_valley.js +++ b/public/views/sacred_valley.js @@ -1,6 +1,7 @@ import { el, mount } from '../dom.js'; import { api } from '../api.js'; import { renderHealthBand, stopHealthBand } from './health_band.js'; +import { renderDevicesBand, stopDevicesBand } from './devices_band.js'; import { svCard } from '../components/sv_card.js'; import { attachReorder } from '../components/sv_reorder.js'; import { orderCards } from './cards/registry.js'; @@ -18,12 +19,13 @@ let renderGen = 0; // guards against overlapping export async function render(main) { const myGen = ++renderGen; - active.forEach(c => c.stop && c.stop()); active = []; stopHealthBand(); + active.forEach(c => c.stop && c.stop()); active = []; stopHealthBand(); stopDevicesBand(); mount(main, el('h1', { class: 'view-h1' }, 'Sacred Valley'), el('p', { class: 'view-sub' }, 'The homelab, at a glance.'), el('div', { id: 'sv-cards' }), - el('div', { id: 'sv-health' }) + el('div', { id: 'sv-health' }), + el('div', { id: 'sv-devices' }) ); let layout = { card_order: [], hidden: [], sizes: {} }; @@ -50,4 +52,5 @@ export async function render(main) { catch (e) { console.error('save layout', e); } }); renderHealthBand(document.getElementById('sv-health')); + renderDevicesBand(document.getElementById('sv-devices')); }