feat: Smaller stat cards, battery icons, version in navbar, map fit

- Stats cards compacter with smaller font and padding
- Battery status with Bootstrap Icons and color coding
- Version displayed in navbar from config
- Map fits all nodes on open with invalidateSize

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ppfeiffer 2026-02-15 14:17:15 +01:00
parent 9e880a1f36
commit 0dbb1e0184
5 changed files with 23 additions and 50 deletions

View file

@ -5,6 +5,7 @@ import os
from aiohttp import web
from meshbot import config
from meshbot.database import Database
logger = logging.getLogger(__name__)
@ -56,6 +57,7 @@ class WebServer:
await ws.send_str(json.dumps({"type": "initial", "data": nodes}))
stats = await self.db.get_stats()
stats["version"] = config.get("version", "0.0.0")
await ws.send_str(json.dumps({"type": "stats_update", "data": stats}))
messages = await self.db.get_recent_messages(50)

View file

@ -11,38 +11,6 @@
box-shadow: 0 0 6px var(--bs-success);
}
.battery-bar {
display: inline-flex;
align-items: center;
gap: 6px;
}
.battery-indicator {
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: -5px;
top: 2px;
width: 3px;
height: 7px;
background: var(--bs-secondary);
border-radius: 0 2px 2px 0;
}
.battery-fill {
height: 100%;
border-radius: 1px;
}
.table-responsive::-webkit-scrollbar {
width: 6px;
}

View file

@ -13,7 +13,7 @@
<nav class="navbar navbar-expand-sm bg-body-tertiary border-bottom">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<i class="bi bi-broadcast-pin text-info me-2"></i>MeshDD-Bot
<i class="bi bi-broadcast-pin text-info me-2"></i>MeshDD-Bot <small class="text-body-secondary" id="versionLabel"></small>
</a>
<div class="d-flex align-items-center gap-3">
<a href="/map" target="_blank" class="btn btn-outline-info btn-sm">
@ -32,35 +32,35 @@
<div class="container-fluid py-3">
<!-- Stats Cards -->
<div class="row g-3 mb-3">
<div class="row g-2 mb-3">
<div class="col-6 col-md-3">
<div class="card text-center border-info border-opacity-25">
<div class="card-body py-3">
<div class="fs-2 fw-bold text-info" id="statNodes">0</div>
<div class="card-body py-2">
<div class="fs-4 fw-bold text-info" id="statNodes">0</div>
<div class="text-body-secondary small">Nodes</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center border-success border-opacity-25">
<div class="card-body py-3">
<div class="fs-2 fw-bold text-success" id="statPositions">0</div>
<div class="card-body py-2">
<div class="fs-4 fw-bold text-success" id="statPositions">0</div>
<div class="text-body-secondary small">Mit Position</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center border-warning border-opacity-25">
<div class="card-body py-3">
<div class="fs-2 fw-bold text-warning" id="statMessages">0</div>
<div class="card-body py-2">
<div class="fs-4 fw-bold text-warning" id="statMessages">0</div>
<div class="text-body-secondary small">Nachrichten</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center border-primary border-opacity-25">
<div class="card-body py-3">
<div class="fs-2 fw-bold text-primary" id="statTextMessages">0</div>
<div class="card-body py-2">
<div class="fs-4 fw-bold text-primary" id="statTextMessages">0</div>
<div class="text-body-secondary small">Textnachrichten</div>
</div>
</div>

View file

@ -85,13 +85,12 @@ function renderNodes() {
function renderBattery(level) {
if (level == null) return '<span class="text-body-secondary">-</span>';
let colorClass = 'bg-success';
if (level < 20) colorClass = 'bg-danger';
else if (level < 50) colorClass = 'bg-warning';
return `<span class="battery-bar">
<span class="battery-indicator"><span class="battery-fill ${colorClass}" style="width:${Math.min(level, 100)}%"></span></span>
<small>${level}%</small>
</span>`;
let iconClass, colorClass;
if (level > 75) { iconClass = 'bi-battery-full'; colorClass = 'text-success'; }
else if (level > 50) { iconClass = 'bi-battery-half'; colorClass = 'text-success'; }
else if (level > 20) { iconClass = 'bi-battery-half'; colorClass = 'text-warning'; }
else { iconClass = 'bi-battery'; colorClass = 'text-danger'; }
return `<span class="${colorClass}"><i class="bi ${iconClass} me-1"></i>${level}%</span>`;
}
function addMessage(msg) {
@ -115,6 +114,9 @@ function addMessage(msg) {
}
function updateStats(stats) {
if (stats.version) {
document.getElementById('versionLabel').textContent = `v${stats.version}`;
}
document.getElementById('statNodes').textContent = stats.total_nodes || 0;
document.getElementById('statPositions').textContent = stats.nodes_with_position || 0;
document.getElementById('statMessages').textContent = stats.total_messages || 0;

View file

@ -82,7 +82,8 @@ function updateMarker(node) {
function fitBounds() {
const coords = Object.values(markers).map(m => m.getLatLng());
if (coords.length > 0) {
map.fitBounds(L.latLngBounds(coords).pad(0.1));
map.invalidateSize();
map.fitBounds(L.latLngBounds(coords).pad(0.1), { maxZoom: 14 });
}
}