feat: v0.2.3 - Command tracking, active nodes 24h, request breakdown

- Track bot command responses in new `commands` DB table
- Stats cards: total nodes, active 24h, total commands answered
- Full-width command breakdown row with badges per command
- Update bot /stats response

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ppfeiffer 2026-02-15 14:26:16 +01:00
parent ed56628ba4
commit a1fe0a297d
7 changed files with 70 additions and 28 deletions

View file

@ -1,5 +1,16 @@
# Changelog # Changelog
## [0.2.3] - 2026-02-15
### Added
- Kommando-Tracking in der Datenbank (neue Tabelle `commands`)
- Stats Card "Aktiv (24h)" zeigt Nodes der letzten 24 Stunden
- Stats Card "Anfragen" zeigt beantwortete Bot-Kommandos
- Kommando-Aufschlüsselung als Badges in voller Breite (z.B. /help 5, /ping 3)
### Changed
- Stats Cards von 4er auf 3er Grid umgestellt plus Breakdown-Zeile
- Bot /stats Kommando zeigt aktualisierte Statistiken
## [0.2.2] - 2026-02-15 ## [0.2.2] - 2026-02-15
### Changed ### Changed
- SNR-Spalte rechtsbündig, Batterie-Spalte linksbündig - SNR-Spalte rechtsbündig, Batterie-Spalte linksbündig

View file

@ -1,4 +1,4 @@
version: "0.2.2" version: "0.2.3"
bot: bot:
name: "MeshDD-Bot" name: "MeshDD-Bot"

View file

@ -196,9 +196,8 @@ class MeshBot:
response = ( response = (
f"📊 Statistiken:\n" f"📊 Statistiken:\n"
f"Nodes: {stats['total_nodes']}\n" f"Nodes: {stats['total_nodes']}\n"
f"Mit Position: {stats['nodes_with_position']}\n" f"Aktiv (24h): {stats['nodes_24h']}\n"
f"Nachrichten: {stats['total_messages']}\n" f"Anfragen: {stats['total_commands']}"
f"Textnachrichten: {stats['text_messages']}"
) )
elif cmd == f"{prefix}uptime": elif cmd == f"{prefix}uptime":
@ -206,6 +205,10 @@ class MeshBot:
if response: if response:
self._send_text(response, channel) self._send_text(response, channel)
await self.db.insert_command(cmd)
if self.ws_manager:
stats = await self.db.get_stats()
await self.ws_manager.broadcast("stats_update", stats)
def _send_text(self, text: str, channel: int): def _send_text(self, text: str, channel: int):
if self.interface: if self.interface:

View file

@ -51,6 +51,12 @@ class Database:
portnum TEXT, portnum TEXT,
payload TEXT payload TEXT
); );
CREATE TABLE IF NOT EXISTS commands (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp REAL,
command TEXT
);
""") """)
await self.db.commit() await self.db.commit()
@ -121,18 +127,28 @@ class Database:
) as cursor: ) as cursor:
return [dict(row) async for row in cursor] return [dict(row) async for row in cursor]
async def insert_command(self, command: str):
now = time.time()
await self.db.execute(
"INSERT INTO commands (timestamp, command) VALUES (?, ?)",
(now, command),
)
await self.db.commit()
async def get_stats(self) -> dict: async def get_stats(self) -> dict:
stats = {} stats = {}
async with self.db.execute("SELECT COUNT(*) FROM nodes") as c: async with self.db.execute("SELECT COUNT(*) FROM nodes") as c:
stats["total_nodes"] = (await c.fetchone())[0] stats["total_nodes"] = (await c.fetchone())[0]
now = time.time()
day_ago = now - 86400
async with self.db.execute( async with self.db.execute(
"SELECT COUNT(*) FROM nodes WHERE lat IS NOT NULL" "SELECT COUNT(*) FROM nodes WHERE last_seen >= ?", (day_ago,)
) as c: ) as c:
stats["nodes_with_position"] = (await c.fetchone())[0] stats["nodes_24h"] = (await c.fetchone())[0]
async with self.db.execute("SELECT COUNT(*) FROM messages") as c: async with self.db.execute("SELECT COUNT(*) FROM commands") as c:
stats["total_messages"] = (await c.fetchone())[0] stats["total_commands"] = (await c.fetchone())[0]
async with self.db.execute( async with self.db.execute(
"SELECT COUNT(*) FROM messages WHERE portnum = 'TEXT_MESSAGE_APP'" "SELECT command, COUNT(*) as cnt FROM commands GROUP BY command ORDER BY cnt DESC"
) as c: ) as cursor:
stats["text_messages"] = (await c.fetchone())[0] stats["command_breakdown"] = {row[0]: row[1] async for row in cursor}
return stats return stats

View file

@ -86,6 +86,7 @@ class WebServer:
async def _api_stats(self, request: web.Request) -> web.Response: async def _api_stats(self, request: web.Request) -> web.Response:
stats = await self.db.get_stats() stats = await self.db.get_stats()
stats["version"] = config.get("version", "0.0.0")
return web.json_response(stats) return web.json_response(stats)
async def _serve_index(self, request: web.Request) -> web.Response: async def _serve_index(self, request: web.Request) -> web.Response:

View file

@ -32,36 +32,38 @@
<div class="container-fluid py-3"> <div class="container-fluid py-3">
<!-- Stats Cards --> <!-- Stats Cards -->
<div class="row g-2 mb-3"> <div class="row g-2 mb-2">
<div class="col-6 col-md-3"> <div class="col-4">
<div class="card text-center border-info border-opacity-25"> <div class="card text-center border-info border-opacity-25">
<div class="card-body py-2"> <div class="card-body py-2">
<div class="fs-4 fw-bold text-info" id="statNodes">0</div> <div class="fs-4 fw-bold text-info" id="statNodes">0</div>
<div class="text-body-secondary small">Nodes</div> <div class="text-body-secondary small">Nodes gesamt</div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-6 col-md-3"> <div class="col-4">
<div class="card text-center border-success border-opacity-25"> <div class="card text-center border-success border-opacity-25">
<div class="card-body py-2"> <div class="card-body py-2">
<div class="fs-4 fw-bold text-success" id="statPositions">0</div> <div class="fs-4 fw-bold text-success" id="statNodes24h">0</div>
<div class="text-body-secondary small">Mit Position</div> <div class="text-body-secondary small">Aktiv (24h)</div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-6 col-md-3"> <div class="col-4">
<div class="card text-center border-warning border-opacity-25"> <div class="card text-center border-warning border-opacity-25">
<div class="card-body py-2"> <div class="card-body py-2">
<div class="fs-4 fw-bold text-warning" id="statMessages">0</div> <div class="fs-4 fw-bold text-warning" id="statCommands">0</div>
<div class="text-body-secondary small">Nachrichten</div> <div class="text-body-secondary small">Anfragen</div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-6 col-md-3"> </div>
<div class="card text-center border-primary border-opacity-25"> <div class="row g-2 mb-3">
<div class="card-body py-2"> <div class="col-12">
<div class="fs-4 fw-bold text-primary" id="statTextMessages">0</div> <div class="card border-primary border-opacity-25">
<div class="text-body-secondary small">Textnachrichten</div> <div class="card-body py-2 d-flex align-items-center gap-3 flex-wrap">
<span class="text-body-secondary small me-1"><i class="bi bi-bar-chart-fill me-1"></i>Anfragen:</span>
<span id="commandBreakdown" class="d-flex gap-2 flex-wrap"></span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -118,9 +118,18 @@ function updateStats(stats) {
document.getElementById('versionLabel').textContent = `v${stats.version}`; document.getElementById('versionLabel').textContent = `v${stats.version}`;
} }
document.getElementById('statNodes').textContent = stats.total_nodes || 0; document.getElementById('statNodes').textContent = stats.total_nodes || 0;
document.getElementById('statPositions').textContent = stats.nodes_with_position || 0; document.getElementById('statNodes24h').textContent = stats.nodes_24h || 0;
document.getElementById('statMessages').textContent = stats.total_messages || 0; document.getElementById('statCommands').textContent = stats.total_commands || 0;
document.getElementById('statTextMessages').textContent = stats.text_messages || 0;
const breakdown = document.getElementById('commandBreakdown');
const cmds = stats.command_breakdown || {};
if (Object.keys(cmds).length > 0) {
breakdown.innerHTML = Object.entries(cmds).map(([cmd, count]) =>
`<span class="badge bg-primary bg-opacity-75">${escapeHtml(cmd)} <span class="badge bg-light text-dark ms-1">${count}</span></span>`
).join('');
} else {
breakdown.innerHTML = '<span class="text-body-secondary small">Noch keine Anfragen</span>';
}
} }
function isOnline(lastSeen) { function isOnline(lastSeen) {