Initial commit — Targo Device Monitor
Standalone dashboard reading from Oktopus MongoDB. Displays: serial, MAC, WAN IP, firmware, uptime, WiFi signal/clients, CPU/RAM usage with visual bars. Auto-refresh 30s. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
7d17b5eb53
149
index.html
Normal file
149
index.html
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Targo — Device Monitor</title>
|
||||
<style>
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:'Inter',system-ui,sans-serif; background:#f4f6f9; color:#1e1e2a; }
|
||||
.header { background:#fff; border-bottom:1px solid #e0e3e8; padding:12px 24px; display:flex; align-items:center; gap:16px; }
|
||||
.header h1 { font-size:1.1rem; font-weight:700; color:#0ea550; }
|
||||
.header .count { background:#0ea550; color:#fff; font-size:0.7rem; font-weight:700; padding:2px 8px; border-radius:10px; }
|
||||
.header .status-ok { color:#0ea550; font-size:0.75rem; }
|
||||
.header .status-warn { color:#e4a944; font-size:0.75rem; }
|
||||
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(340px,1fr)); gap:16px; padding:20px 24px; }
|
||||
.card { background:#fff; border-radius:12px; border:1px solid #e8eaef; overflow:hidden; transition:box-shadow 0.15s; }
|
||||
.card:hover { box-shadow:0 4px 20px rgba(0,0,0,0.08); }
|
||||
.card-hdr { display:flex; align-items:center; gap:10px; padding:14px 16px; border-bottom:1px solid #f0f1f4; }
|
||||
.card-hdr .dot { width:10px; height:10px; border-radius:50%; flex-shrink:0; }
|
||||
.dot-online { background:#0ea550; box-shadow:0 0 6px rgba(14,165,80,0.4); }
|
||||
.dot-offline { background:#e6364b; box-shadow:0 0 6px rgba(230,54,75,0.3); }
|
||||
.card-hdr .alias { font-weight:700; font-size:0.85rem; flex:1; }
|
||||
.card-hdr .model { font-size:0.7rem; color:#888; }
|
||||
.card-body { padding:12px 16px; display:grid; grid-template-columns:1fr 1fr; gap:6px 16px; }
|
||||
.field { display:flex; flex-direction:column; }
|
||||
.field-label { font-size:0.6rem; font-weight:700; text-transform:uppercase; letter-spacing:0.04em; color:#999; }
|
||||
.field-value { font-size:0.8rem; font-weight:600; font-variant-numeric:tabular-nums; }
|
||||
.field-value.mono { font-family:'Roboto Mono',monospace; font-size:0.75rem; }
|
||||
.wifi-section { grid-column:1/-1; display:flex; gap:12px; margin-top:4px; padding-top:8px; border-top:1px solid #f0f1f4; }
|
||||
.wifi-band { flex:1; background:#f8f9fb; border-radius:8px; padding:8px 10px; }
|
||||
.wifi-band-title { font-size:0.6rem; font-weight:700; text-transform:uppercase; color:#0ea550; margin-bottom:4px; }
|
||||
.wifi-row { display:flex; justify-content:space-between; font-size:0.72rem; }
|
||||
.wifi-row .wlabel { color:#888; }
|
||||
.wifi-row .wval { font-weight:600; }
|
||||
.signal-bar { display:flex; gap:2px; align-items:flex-end; height:14px; }
|
||||
.signal-bar span { width:4px; border-radius:1px; background:#ddd; }
|
||||
.signal-bar span.active { background:#0ea550; }
|
||||
.signal-bar span:nth-child(1) { height:4px; }
|
||||
.signal-bar span:nth-child(2) { height:7px; }
|
||||
.signal-bar span:nth-child(3) { height:10px; }
|
||||
.signal-bar span:nth-child(4) { height:14px; }
|
||||
.card-footer { padding:8px 16px; background:#fafbfc; border-top:1px solid #f0f1f4; display:flex; gap:12px; align-items:center; }
|
||||
.usage-bar { flex:1; height:4px; background:#eee; border-radius:2px; overflow:hidden; }
|
||||
.usage-fill { height:100%; border-radius:2px; transition:width 0.3s; }
|
||||
.usage-label { font-size:0.6rem; font-weight:600; color:#888; min-width:50px; }
|
||||
.uptime { font-size:0.7rem; color:#0ea550; font-weight:600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>📡 Targo Device Monitor</h1>
|
||||
<span class="count" id="device-count">0</span>
|
||||
<span class="status-ok" id="online-count"></span>
|
||||
<span class="status-warn" id="offline-count"></span>
|
||||
<div style="flex:1"></div>
|
||||
<span style="font-size:0.7rem;color:#999" id="last-refresh"></span>
|
||||
</div>
|
||||
<div class="grid" id="grid"></div>
|
||||
|
||||
<script>
|
||||
function signalBars(dbm) {
|
||||
const s = Math.abs(dbm);
|
||||
const level = s < 45 ? 4 : s < 55 ? 3 : s < 65 ? 2 : s < 75 ? 1 : 0;
|
||||
return `<div class="signal-bar">${[1,2,3,4].map(i => `<span class="${i<=level?'active':''}"></span>`).join('')}</div>`;
|
||||
}
|
||||
|
||||
function fmtUptime(sec) {
|
||||
if (!sec) return 'Offline';
|
||||
const d = Math.floor(sec/86400), h = Math.floor((sec%86400)/3600);
|
||||
return d > 0 ? `${d}j ${h}h` : `${h}h`;
|
||||
}
|
||||
|
||||
function fmtDate(d) {
|
||||
if (!d) return '—';
|
||||
const dt = new Date(d);
|
||||
const diff = Date.now() - dt.getTime();
|
||||
if (diff < 60000) return 'À l\'instant';
|
||||
if (diff < 3600000) return `Il y a ${Math.floor(diff/60000)}min`;
|
||||
if (diff < 86400000) return `Il y a ${Math.floor(diff/3600000)}h`;
|
||||
return dt.toLocaleDateString('fr-CA');
|
||||
}
|
||||
|
||||
function usageColor(pct) {
|
||||
if (pct < 50) return '#0ea550';
|
||||
if (pct < 80) return '#e4a944';
|
||||
return '#e6364b';
|
||||
}
|
||||
|
||||
function renderDevice(d) {
|
||||
const online = d.status === 1;
|
||||
const wifi24 = d.wifi_24 || {};
|
||||
const wifi5 = d.wifi_5 || null;
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<div class="dot ${online ? 'dot-online' : 'dot-offline'}"></div>
|
||||
<span class="alias">${d.alias || d.sn}</span>
|
||||
<span class="model">${d.vendor} ${d.model}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="field"><span class="field-label">Serial</span><span class="field-value mono">${d.sn}</span></div>
|
||||
<div class="field"><span class="field-label">MAC</span><span class="field-value mono">${d.mac || '—'}</span></div>
|
||||
<div class="field"><span class="field-label">WAN IP</span><span class="field-value mono">${d.wan_ip || '—'}</span></div>
|
||||
<div class="field"><span class="field-label">Firmware</span><span class="field-value">${d.firmware || d.version}</span></div>
|
||||
<div class="field"><span class="field-label">Uptime</span><span class="field-value uptime">${fmtUptime(d.uptime)}</span></div>
|
||||
<div class="field"><span class="field-label">Dernière vue</span><span class="field-value">${fmtDate(d.last_seen)}</span></div>
|
||||
<div class="wifi-section">
|
||||
<div class="wifi-band">
|
||||
<div class="wifi-band-title">2.4 GHz</div>
|
||||
<div class="wifi-row"><span class="wlabel">SSID</span><span class="wval">${wifi24.ssid || '—'}</span></div>
|
||||
<div class="wifi-row"><span class="wlabel">Signal</span><span class="wval">${wifi24.signal ? wifi24.signal + 'dBm ' + signalBars(wifi24.signal) : '—'}</span></div>
|
||||
<div class="wifi-row"><span class="wlabel">Ch</span><span class="wval">${wifi24.channel || '—'}</span></div>
|
||||
<div class="wifi-row"><span class="wlabel">Clients</span><span class="wval">${wifi24.clients ?? '—'}</span></div>
|
||||
</div>
|
||||
${wifi5 ? `<div class="wifi-band">
|
||||
<div class="wifi-band-title">5 GHz</div>
|
||||
<div class="wifi-row"><span class="wlabel">SSID</span><span class="wval">${wifi5.ssid}</span></div>
|
||||
<div class="wifi-row"><span class="wlabel">Signal</span><span class="wval">${wifi5.signal}dBm ${signalBars(wifi5.signal)}</span></div>
|
||||
<div class="wifi-row"><span class="wlabel">Ch</span><span class="wval">${wifi5.channel}</span></div>
|
||||
<div class="wifi-row"><span class="wlabel">Clients</span><span class="wval">${wifi5.clients}</span></div>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<span class="usage-label">CPU ${d.cpu_usage || 0}%</span>
|
||||
<div class="usage-bar"><div class="usage-fill" style="width:${d.cpu_usage||0}%;background:${usageColor(d.cpu_usage||0)}"></div></div>
|
||||
<span class="usage-label">RAM ${d.memory_usage || 0}%</span>
|
||||
<div class="usage-bar"><div class="usage-fill" style="width:${d.memory_usage||0}%;background:${usageColor(d.memory_usage||0)}"></div></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function loadDevices() {
|
||||
try {
|
||||
const res = await fetch('/api/monitor/devices');
|
||||
const devices = await res.json();
|
||||
document.getElementById('grid').innerHTML = devices.map(renderDevice).join('');
|
||||
document.getElementById('device-count').textContent = devices.length;
|
||||
const online = devices.filter(d => d.status === 1).length;
|
||||
document.getElementById('online-count').textContent = `● ${online} en ligne`;
|
||||
document.getElementById('offline-count').textContent = `● ${devices.length - online} hors ligne`;
|
||||
document.getElementById('last-refresh').textContent = new Date().toLocaleTimeString('fr-CA');
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
loadDevices();
|
||||
setInterval(loadDevices, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
17
package.json
Normal file
17
package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "device-monitor",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"mongodb": "^7.1.1"
|
||||
}
|
||||
}
|
||||
45
server.js
Normal file
45
server.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const { MongoClient } = require('mongodb');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = 3002;
|
||||
const MONGO_URI = 'mongodb://localhost:27017';
|
||||
|
||||
let db;
|
||||
|
||||
async function connectDB() {
|
||||
const client = new MongoClient(MONGO_URI);
|
||||
await client.connect();
|
||||
db = client.db('oktopus');
|
||||
console.log('Connected to MongoDB');
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
if (req.url === '/api/monitor/devices') {
|
||||
try {
|
||||
const devices = await db.collection('devices').find({}).toArray();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(devices));
|
||||
} catch (e) {
|
||||
res.writeHead(500); res.end(JSON.stringify({ error: e.message }));
|
||||
}
|
||||
} else {
|
||||
// Serve static files
|
||||
let filePath = req.url === '/' ? '/index.html' : req.url;
|
||||
filePath = path.join(__dirname, filePath);
|
||||
try {
|
||||
const content = fs.readFileSync(filePath);
|
||||
const ext = path.extname(filePath);
|
||||
const types = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript' };
|
||||
res.writeHead(200, { 'Content-Type': types[ext] || 'text/plain' });
|
||||
res.end(content);
|
||||
} catch {
|
||||
res.writeHead(404); res.end('Not found');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connectDB().then(() => {
|
||||
server.listen(PORT, () => console.log(`Device Monitor: http://localhost:${PORT}`));
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user