From d5ea4eee4a038961995e1156a328652bd64a7497 Mon Sep 17 00:00:00 2001 From: ppfeiffer Date: Sun, 15 Feb 2026 13:02:34 +0100 Subject: [PATCH] feat: Redesign dashboard with Bootstrap 5.3 dark theme Replace custom CSS layout with Bootstrap 5.3, add Bootstrap Icons, responsive card grid for stats, improved nodes table with hardware column, and styled message list. Online nodes highlighted in green. Co-Authored-By: Claude Opus 4.6 --- static/css/style.css | 214 +++++------------------------------------ static/index.html | 130 ++++++++++++++++--------- static/js/dashboard.js | 51 ++++++---- 3 files changed, 142 insertions(+), 253 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index 1f92510..df64761 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,211 +1,41 @@ -:root { - --bg-primary: #1a1a2e; - --bg-secondary: #16213e; - --bg-card: #0f3460; - --text-primary: #e0e0e0; - --text-secondary: #a0a0b0; - --accent: #00d4ff; - --accent-dim: #0088aa; - --green: #00e676; - --orange: #ff9100; - --red: #ff5252; - --border: #2a2a4a; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; - background: var(--bg-primary); - color: var(--text-primary); - min-height: 100vh; -} - -header { - background: var(--bg-secondary); - border-bottom: 1px solid var(--border); - padding: 1rem 2rem; - display: flex; - justify-content: space-between; - align-items: center; -} - -header h1 { - font-size: 1.5rem; - color: var(--accent); -} - -header .status { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.9rem; -} - .status-dot { + display: inline-block; width: 10px; height: 10px; border-radius: 50%; - background: var(--red); + background: var(--bs-danger); } .status-dot.connected { - background: var(--green); - box-shadow: 0 0 6px var(--green); + background: var(--bs-success); + box-shadow: 0 0 6px var(--bs-success); } -.nav-links { - display: flex; - gap: 1rem; -} - -.nav-links a { - color: var(--accent); - text-decoration: none; - padding: 0.4rem 0.8rem; - border: 1px solid var(--accent-dim); - border-radius: 4px; - font-size: 0.85rem; - transition: background 0.2s; -} - -.nav-links a:hover { - background: var(--accent-dim); - color: #fff; -} - -.container { - max-width: 1400px; - margin: 0 auto; - padding: 1.5rem; -} - -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1rem; - margin-bottom: 1.5rem; -} - -.stat-card { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: 8px; - padding: 1.2rem; - text-align: center; -} - -.stat-card .value { - font-size: 2rem; - font-weight: bold; - color: var(--accent); -} - -.stat-card .label { - color: var(--text-secondary); - font-size: 0.85rem; - margin-top: 0.3rem; -} - -.panels { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1.5rem; -} - -@media (max-width: 900px) { - .panels { - grid-template-columns: 1fr; - } -} - -.panel { - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 8px; - overflow: hidden; -} - -.panel-header { - background: var(--bg-card); - padding: 0.8rem 1rem; - font-weight: 600; - border-bottom: 1px solid var(--border); -} - -.panel-body { - padding: 0; - max-height: 500px; - overflow-y: auto; -} - -table { - width: 100%; - border-collapse: collapse; -} - -table th, -table td { - padding: 0.6rem 0.8rem; - text-align: left; - border-bottom: 1px solid var(--border); - font-size: 0.85rem; -} - -table th { - background: var(--bg-card); - color: var(--text-secondary); - font-weight: 600; - position: sticky; - top: 0; -} - -table tr:hover { - background: rgba(0, 212, 255, 0.05); -} - -.message-list { - list-style: none; -} - -.message-item { - padding: 0.7rem 1rem; - border-bottom: 1px solid var(--border); - font-size: 0.85rem; -} - -.message-item .meta { - color: var(--text-secondary); - font-size: 0.75rem; - margin-bottom: 0.2rem; -} - -.message-item .text { - color: var(--text-primary); +.battery-bar { + display: inline-flex; + align-items: center; + gap: 6px; } .battery-indicator { - display: inline-block; - width: 24px; - height: 12px; - border: 1px solid var(--text-secondary); + width: 28px; + height: 13px; + border: 1.5px solid var(--bs-secondary); border-radius: 2px; position: relative; + display: inline-block; + vertical-align: middle; } .battery-indicator::after { content: ''; position: absolute; - right: -4px; + right: -5px; top: 2px; width: 3px; - height: 6px; - background: var(--text-secondary); - border-radius: 0 1px 1px 0; + height: 7px; + background: var(--bs-secondary); + border-radius: 0 2px 2px 0; } .battery-fill { @@ -213,15 +43,15 @@ table tr:hover { border-radius: 1px; } -::-webkit-scrollbar { +.table-responsive::-webkit-scrollbar { width: 6px; } -::-webkit-scrollbar-track { - background: var(--bg-secondary); +.table-responsive::-webkit-scrollbar-track { + background: transparent; } -::-webkit-scrollbar-thumb { - background: var(--border); +.table-responsive::-webkit-scrollbar-thumb { + background: var(--bs-border-color); border-radius: 3px; } diff --git a/static/index.html b/static/index.html index f02f0b7..354ee10 100644 --- a/static/index.html +++ b/static/index.html @@ -1,71 +1,113 @@ - + MeshDD-Bot Dashboard + + -
-

MeshDD-Bot

-
+ -
-
-
-
0
-
Nodes
+
+ +
+
+
+
+
0
+
Nodes
+
+
-
-
0
-
Mit Position
+
+
+
+
0
+
Mit Position
+
+
-
-
0
-
Nachrichten
+
+
+
+
0
+
Nachrichten
+
+
-
-
0
-
Textnachrichten
+
+
+
+
0
+
Textnachrichten
+
+
-
-
-
Nodes
-
- - - - - - - - - - - -
NameIDSNRBatterieZuletzt gesehen
+ +
+ +
+
+
+ + Nodes + 0 +
+
+ + + + + + + + + + + + +
NameIDHardwareSNRBatterieZuletzt gesehen
+
-
-
Nachrichten
-
-
    + +
    +
    +
    + + Nachrichten +
    +
    +
    +
    + diff --git a/static/js/dashboard.js b/static/js/dashboard.js index b5c1a4c..cd82513 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -2,6 +2,7 @@ const nodesTable = document.getElementById('nodesTable'); const messagesList = document.getElementById('messagesList'); const statusDot = document.getElementById('statusDot'); const statusText = document.getElementById('statusText'); +const nodeCountBadge = document.getElementById('nodeCountBadge'); let nodes = {}; let ws; @@ -48,38 +49,49 @@ function connectWebSocket() { function renderNodes() { const sorted = Object.values(nodes).sort((a, b) => (b.last_seen || 0) - (a.last_seen || 0)); + nodeCountBadge.textContent = sorted.length; nodesTable.innerHTML = sorted.map(node => { const name = node.long_name || node.short_name || node.node_id; - const snr = node.snr != null ? node.snr.toFixed(1) : '-'; + const shortId = node.node_id ? node.node_id.slice(-4) : '?'; + const hw = node.hw_model || '-'; + const snr = node.snr != null ? `${node.snr.toFixed(1)} dB` : '-'; const battery = renderBattery(node.battery); const lastSeen = node.last_seen ? timeAgo(node.last_seen) : '-'; - const shortId = node.node_id ? node.node_id.slice(-4) : '?'; + const onlineClass = isOnline(node.last_seen) ? 'text-success' : ''; return ` - ${escapeHtml(name)} - ${escapeHtml(shortId)} - ${snr} - ${battery} - ${lastSeen} + ${escapeHtml(name)} + ${escapeHtml(shortId)} + ${escapeHtml(hw)} + ${snr} + ${battery} + ${lastSeen} `; }).join(''); } function renderBattery(level) { - if (level == null) return '-'; - let color = '#00e676'; - if (level < 20) color = '#ff5252'; - else if (level < 50) color = '#ff9100'; - return `
    ${level}%`; + if (level == null) return '-'; + let colorClass = 'bg-success'; + if (level < 20) colorClass = 'bg-danger'; + else if (level < 50) colorClass = 'bg-warning'; + return ` + + ${level}% + `; } function addMessage(msg) { - const li = document.createElement('li'); - li.className = 'message-item'; + const item = document.createElement('div'); + item.className = 'list-group-item list-group-item-action py-2 px-3'; const time = msg.timestamp ? new Date(msg.timestamp * 1000).toLocaleTimeString('de-DE') : ''; const from = msg.from_node ? msg.from_node.slice(-4) : '?'; - li.innerHTML = `
    ${time} von ${escapeHtml(from)}
    ${escapeHtml(msg.payload || '')}
    `; - messagesList.prepend(li); - // Keep max 100 messages + item.innerHTML = ` +
    + ${escapeHtml(from)} + ${time} +
    +
    ${escapeHtml(msg.payload || '')}
    `; + messagesList.prepend(item); while (messagesList.children.length > 100) { messagesList.removeChild(messagesList.lastChild); } @@ -92,6 +104,11 @@ function updateStats(stats) { document.getElementById('statTextMessages').textContent = stats.text_messages || 0; } +function isOnline(lastSeen) { + if (!lastSeen) return false; + return (Date.now() / 1000 - lastSeen) < 900; // < 15 min +} + function timeAgo(timestamp) { const seconds = Math.floor(Date.now() / 1000 - timestamp); if (seconds < 60) return `${seconds}s`;