feat: v0.6.13 - Version in Navbar, Rolling 24h, Karten-Transparenz

- Version in Navbar aller Seiten (app.js holt /api/stats beim Init)
- Statistiken: Anfragen-Zähler rolling 24h statt Mitternacht-Reset
- Karte: Nodes nach Alter transparent (<24h voll, 24-48h 45%, 48-72h 20%, >72h unsichtbar)
- Legende um Alter-Sektion erweitert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ppfeiffer 2026-02-18 17:54:39 +01:00
parent ac98191143
commit 9306cce209
10 changed files with 55 additions and 7 deletions

View file

@ -1,5 +1,17 @@
# Changelog
## [0.6.13] - 2026-02-18
### Added
- **Version in Navbar** aller Seiten sichtbar (via `app.js initPage()` + `/api/stats`).
- **Karte Alter-Transparenz**: Nodes < 24h voll sichtbar (0.9), 2448h halb transparent (0.45),
4872h stark transparent (0.2), älter als 72h werden nicht mehr angezeigt.
Legende um Alter-Sektion erweitert.
### Changed
- **Statistiken Rolling Window**: Anfragen-Zähler und Kanal-Breakdown nutzen jetzt
rollendes 24h-Fenster (jetzt minus 24h) statt Mitternacht-Reset.
## [0.6.12] - 2026-02-18
### Fixed

View file

@ -1,4 +1,4 @@
version: "0.6.12"
version: "0.6.13"
bot:
name: "MeshDD-Bot"

View file

@ -1,7 +1,6 @@
import aiosqlite
import time
import logging
from datetime import datetime
logger = logging.getLogger(__name__)
@ -208,14 +207,13 @@ class Database:
"SELECT COUNT(*) FROM nodes WHERE last_seen >= ?", (day_ago,)
) as c:
stats["nodes_24h"] = (await c.fetchone())[0]
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp()
async with self.db.execute(
"SELECT COUNT(*) FROM commands WHERE timestamp >= ?", (today_start,)
"SELECT COUNT(*) FROM commands WHERE timestamp >= ?", (day_ago,)
) as c:
stats["total_commands"] = (await c.fetchone())[0]
async with self.db.execute(
"SELECT channel, COUNT(*) as cnt FROM commands WHERE timestamp >= ? GROUP BY channel ORDER BY cnt DESC",
(today_start,),
(day_ago,),
) as cursor:
stats["channel_breakdown"] = {row[0]: row[1] async for row in cursor}
return stats

View file

@ -16,6 +16,7 @@
</button>
<span class="fw-bold me-auto">
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
</span>
<div class="d-flex align-items-center gap-2">
<span id="userMenu" class="d-none">

View file

@ -102,4 +102,10 @@ function initPage({ onAuth = null } = {}) {
_setupSidebarToggle();
if (onAuth) onAuth(user);
});
const vl = document.getElementById('versionLabel');
if (vl) {
fetch('/api/stats')
.then(r => r.ok ? r.json() : null)
.then(d => { if (d?.version) vl.textContent = `v${d.version}`; });
}
}

View file

@ -28,7 +28,7 @@ function createIcon(color) {
return L.divIcon({
className: '',
html: `<svg width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="8" fill="${color}" stroke="#fff" stroke-width="2" opacity="0.9"/>
<circle cx="12" cy="12" r="8" fill="${color}" stroke="#fff" stroke-width="2"/>
</svg>`,
iconSize: [24, 24],
iconAnchor: [12, 12],
@ -37,6 +37,16 @@ function createIcon(color) {
});
}
// Returns opacity based on node age: full <24h, half 24-48h, faint 48-72h, null >72h (hide)
function getAgeOpacity(lastSeen) {
if (!lastSeen) return 0.9;
const age = Date.now() / 1000 - lastSeen;
if (age < 86400) return 0.9;
if (age < 172800) return 0.45;
if (age < 259200) return 0.2;
return null;
}
function nodeTooltip(node) {
const name = node.long_name || node.short_name || node.node_id;
const hops = node.hop_count != null ? node.hop_count : '?';
@ -61,15 +71,28 @@ function updateMarker(node) {
if (!node.lat || !node.lon) return;
const id = node.node_id;
const opacity = getAgeOpacity(node.last_seen);
// Node older than 72h: remove from map
if (opacity === null) {
if (markers[id]) {
map.removeLayer(markers[id]);
delete markers[id];
delete nodeData[id];
}
return;
}
nodeData[id] = node;
const icon = createIcon(getHopColor(node.hop_count));
if (markers[id]) {
markers[id].setLatLng([node.lat, node.lon]);
markers[id].setIcon(icon);
markers[id].setOpacity(opacity);
markers[id].setTooltipContent(nodeTooltip(node));
} else {
markers[id] = L.marker([node.lat, node.lon], { icon })
markers[id] = L.marker([node.lat, node.lon], { icon, opacity })
.addTo(map)
.bindTooltip(nodeTooltip(node), { direction: 'right', className: 'node-tooltip-wrap' });
}
@ -134,6 +157,10 @@ legend.onAdd = function () {
const color = hop != null ? (hopColors[hop] || hopColorDefault) : hopColorDefault;
div.innerHTML += `<div class="legend-item"><span class="legend-dot" style="background:${color}"></span>${label}</div>`;
});
div.innerHTML += `<hr style="margin:5px 0;border-color:#aaa"><strong>Alter</strong>
<div class="legend-item"><span class="legend-dot" style="background:rgba(128,128,128,.9)"></span>&lt; 24h</div>
<div class="legend-item"><span class="legend-dot" style="background:rgba(128,128,128,.45)"></span>2448h</div>
<div class="legend-item"><span class="legend-dot" style="background:rgba(128,128,128,.2)"></span>4872h</div>`;
return div;
};

View file

@ -17,6 +17,7 @@
</button>
<span class="fw-bold me-auto">
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
</span>
<div class="d-flex align-items-center gap-2">
<span class="d-flex align-items-center gap-1">

View file

@ -16,6 +16,7 @@
</button>
<span class="fw-bold me-auto">
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
</span>
<div class="d-flex align-items-center gap-2">
<span class="d-flex align-items-center gap-1">

View file

@ -16,6 +16,7 @@
</button>
<span class="fw-bold me-auto">
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
</span>
<div class="d-flex align-items-center gap-2">
<span id="userMenu" class="d-none">

View file

@ -16,6 +16,7 @@
</button>
<span class="fw-bold me-auto">
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
</span>
<div class="d-flex align-items-center gap-2">
<span id="userMenu" class="d-none">