diff --git a/Dockerfile b/Dockerfile index 34ebdea..0394e16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ smartmontools \ dmidecode \ lsb-release \ + nmap \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/app/main.py b/app/main.py index 11c34a3..0d07f11 100644 --- a/app/main.py +++ b/app/main.py @@ -1,8 +1,9 @@ import subprocess, json, time, re, os +from typing import Optional import psutil import cpuinfo -from fastapi import FastAPI +from fastapi import FastAPI, Body from fastapi.staticfiles import StaticFiles app = FastAPI() @@ -400,4 +401,80 @@ def api_raid(): return _parse_mdstat() +# ──────────────────────────────────────────────────────────── NETMAP ── + +NETMAP_DATA = "/app/netmap_devices.json" +NETMAP_RANGE = "192.168.1.10-150" + + +def _netmap_load() -> dict: + try: + with open(NETMAP_DATA) as f: + return json.load(f) + except Exception: + return {} + + +def _netmap_save(data: dict): + with open(NETMAP_DATA, "w") as f: + json.dump(data, f, indent=2) + + +def _netmap_parse(output: str) -> list[dict]: + hosts = [] + current: dict = {} + for line in output.splitlines(): + m_host = re.search(r"Nmap scan report for (.+)", line) + if m_host: + if current.get("ip"): + hosts.append(current) + raw = m_host.group(1) + ip_m = re.search(r"\((\d+\.\d+\.\d+\.\d+)\)", raw) + if ip_m: + current = {"ip": ip_m.group(1), "hostname": raw.split("(")[0].strip(), "mac": "", "vendor": ""} + else: + current = {"ip": raw.strip(), "hostname": "", "mac": "", "vendor": ""} + continue + m_mac = re.search(r"MAC Address: ([0-9A-F:]+)\s*(?:\((.+)\))?", line, re.I) + if m_mac: + current["mac"] = m_mac.group(1) + current["vendor"] = m_mac.group(2) or "" + if current.get("ip"): + hosts.append(current) + return hosts + + +@app.get("/api/netmap/scan") +def api_netmap_scan(): + try: + r = subprocess.run( + ["nmap", "-sn", "-T4", "--host-timeout", "2s", NETMAP_RANGE], + capture_output=True, text=True, timeout=120 + ) + hosts = _netmap_parse(r.stdout) + return {"hosts": hosts, "names": _netmap_load()} + except Exception as e: + return {"hosts": [], "names": {}, "error": str(e)} + + +@app.get("/api/netmap/names") +def api_netmap_names(): + return _netmap_load() + + +@app.post("/api/netmap/name") +def api_netmap_name(payload: dict = Body(...)): + ip = payload.get("ip", "").strip() + name = payload.get("name", "").strip() + if not re.match(r"^\d+\.\d+\.\d+\.\d+$", ip): + return {"ok": False, "error": "IP invalide"} + data = _netmap_load() + if name: + data[ip] = name + elif ip in data: + del data[ip] + _netmap_save(data) + return {"ok": True} + + app.mount("/", StaticFiles(directory="/app/static", html=True), name="static") diff --git a/docker-compose.yml b/docker-compose.yml index 62eb96f..66ea913 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: - /sys:/sys:ro - /dev:/dev - /run/udev:/run/udev:ro + - /home/perco/projects/homelabdash/netmap_devices.json:/app/netmap_devices.json environment: - HOST_ROOT=/rootfs - TZ=Europe/Paris diff --git a/netmap_devices.json b/netmap_devices.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/netmap_devices.json @@ -0,0 +1 @@ +{} diff --git a/static/index.html b/static/index.html index ccaeab3..641646d 100644 --- a/static/index.html +++ b/static/index.html @@ -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) { '
' )); + // NetMap + grid.appendChild(make_card('card-netmap','col-12','📡','Appareils réseau — 192.168.1.10–150', + `
+ + Cliquez sur Scanner pour démarrer +
+
` + )); + // 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 (30–60 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 = 'Aucun appareil détecté.'; return; } + const rows = hosts.map(h => { + const k = h.ip.replace(/\./g, '_'); + const name = _netmapNames[h.ip] || ''; + return ` + ${h.ip} + ${h.mac || '—'} + ${h.vendor || '—'} + ${h.hostname || '—'} + + + + + + `; + }).join(''); + el.innerHTML = ` + + ${rows} +
IPMACFabricantHostnameNom perso
`; +} + +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([