feat: intégration NetMap dans homelabdash

Nouveau card "Appareils réseau" : scan nmap 192.168.1.10–150,
affichage IP/MAC/fabricant/hostname, nommage persistant par device.
Routes: /api/netmap/scan, /api/netmap/names, /api/netmap/name (POST).
Stockage dans netmap_devices.json (volume Docker).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
perco
2026-06-28 11:57:32 +02:00
co-authored by Claude Sonnet 4.6
parent c8faffb079
commit f421636ab8
5 changed files with 174 additions and 1 deletions
+93
View File
@@ -103,6 +103,18 @@
.s-size { font-size: 18px; font-weight: 800; color: var(--accent); }
.s-info { font-size: 11px; color: var(--muted); }
/* NETMAP */
.netmap-toolbar { display: flex; gap: 10px; align-items: center; margin-bottom: 8px; }
.netmap-btn { background: var(--accent); color: #0d1117; border: none; border-radius: 6px; padding: 6px 16px; font-size: 12px; font-weight: 700; cursor: pointer; transition: opacity .15s; }
.netmap-btn:hover { opacity: .8; }
.netmap-btn:disabled { opacity: .4; cursor: not-allowed; }
.netmap-status { font-size: 12px; color: var(--muted); }
.netmap-name-input { background: #21262d; border: 1px solid var(--border); color: var(--text); border-radius: 5px; padding: 3px 8px; font-size: 12px; width: 130px; transition: border-color .15s; }
.netmap-name-input:focus { outline: none; border-color: var(--accent); }
.netmap-save-btn { background: none; border: 1px solid var(--border); color: var(--accent); border-radius: 5px; padding: 3px 10px; font-size: 11px; cursor: pointer; transition: background .15s; }
.netmap-save-btn:hover { background: #21262d; }
.netmap-flash { font-size: 11px; color: var(--green); width: 14px; display: inline-block; }
/* RAID */
.raid-grid { display: grid; gap: 10px; }
.raid-array { background: #21262d; border-radius: 8px; padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
@@ -368,6 +380,15 @@ function build_ui(os, hw, raid) {
'<div id="temp-table"></div>'
));
// NetMap
grid.appendChild(make_card('card-netmap','col-12','📡','Appareils réseau — 192.168.1.10150',
`<div class="netmap-toolbar">
<button class="netmap-btn" id="netmap-scan-btn" onclick="netmapScan()">🔍 Scanner</button>
<span class="netmap-status" id="netmap-status">Cliquez sur Scanner pour démarrer</span>
</div>
<div id="netmap-table"></div>`
));
// Restaurer l'ordre mémorisé
apply_order();
@@ -482,6 +503,78 @@ function update_live(live) {
setTimeout(() => { dot.style.background = 'var(--green)'; }, 250);
}
// ──────────────────────────────── netmap ──
let _netmapNames = {};
async function netmapScan() {
const btn = document.getElementById('netmap-scan-btn');
const status = document.getElementById('netmap-status');
if (!btn) return;
btn.disabled = true;
status.style.color = 'var(--yellow)';
status.textContent = '⏳ Scan en cours (3060 s)…';
try {
const data = await fetch('/api/netmap/scan').then(r => r.json());
if (data.error) {
status.style.color = 'var(--red)';
status.textContent = '❌ ' + data.error;
} else {
status.style.color = 'var(--green)';
status.textContent = `${data.hosts.length} appareil(s) détecté(s)`;
_netmapNames = data.names || {};
netmapRender(data.hosts);
}
} catch(e) {
status.style.color = 'var(--red)';
status.textContent = '❌ Erreur : ' + e;
}
btn.disabled = false;
}
function netmapRender(hosts) {
const el = document.getElementById('netmap-table');
if (!el) return;
if (!hosts.length) { el.innerHTML = '<span style="color:var(--muted)">Aucun appareil détecté.</span>'; return; }
const rows = hosts.map(h => {
const k = h.ip.replace(/\./g, '_');
const name = _netmapNames[h.ip] || '';
return `<tr>
<td><code style="color:var(--accent)">${h.ip}</code></td>
<td><code style="color:var(--muted);font-size:12px">${h.mac || '—'}</code></td>
<td style="color:var(--muted);font-size:12px">${h.vendor || '—'}</td>
<td style="color:var(--muted);font-size:12px">${h.hostname || '—'}</td>
<td>
<input class="netmap-name-input" id="nm_${k}" value="${h.ip in _netmapNames ? _netmapNames[h.ip].replace(/"/g,'&quot;') : ''}" placeholder="Nom perso…">
<button class="netmap-save-btn" onclick="netmapSaveName('${h.ip}')">Sauver</button>
<span class="netmap-flash" id="nmf_${k}"></span>
</td>
</tr>`;
}).join('');
el.innerHTML = `<table>
<thead><tr><th>IP</th><th>MAC</th><th>Fabricant</th><th>Hostname</th><th>Nom perso</th></tr></thead>
<tbody>${rows}</tbody>
</table>`;
}
async function netmapSaveName(ip) {
const k = ip.replace(/\./g, '_');
const input = document.getElementById('nm_' + k);
const flash = document.getElementById('nmf_' + k);
if (!input) return;
const name = input.value.trim();
try {
const r = await fetch('/api/netmap/name', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ip, name})
});
const d = await r.json();
if (d.ok) { _netmapNames[ip] = name; flash.textContent = '✓'; }
else flash.textContent = '✗';
} catch { flash.textContent = '✗'; }
setTimeout(() => { if (flash) flash.textContent = ''; }, 2000);
}
// ──────────────────────────────── init ──
async function init() {
const [os, hw, raid] = await Promise.all([