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:
parent
ed56628ba4
commit
a1fe0a297d
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -1,5 +1,16 @@
|
|||
# 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
|
||||
### Changed
|
||||
- SNR-Spalte rechtsbündig, Batterie-Spalte linksbündig
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
version: "0.2.2"
|
||||
version: "0.2.3"
|
||||
|
||||
bot:
|
||||
name: "MeshDD-Bot"
|
||||
|
|
|
|||
|
|
@ -196,9 +196,8 @@ class MeshBot:
|
|||
response = (
|
||||
f"📊 Statistiken:\n"
|
||||
f"Nodes: {stats['total_nodes']}\n"
|
||||
f"Mit Position: {stats['nodes_with_position']}\n"
|
||||
f"Nachrichten: {stats['total_messages']}\n"
|
||||
f"Textnachrichten: {stats['text_messages']}"
|
||||
f"Aktiv (24h): {stats['nodes_24h']}\n"
|
||||
f"Anfragen: {stats['total_commands']}"
|
||||
)
|
||||
|
||||
elif cmd == f"{prefix}uptime":
|
||||
|
|
@ -206,6 +205,10 @@ class MeshBot:
|
|||
|
||||
if response:
|
||||
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):
|
||||
if self.interface:
|
||||
|
|
|
|||
|
|
@ -51,6 +51,12 @@ class Database:
|
|||
portnum TEXT,
|
||||
payload TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS commands (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp REAL,
|
||||
command TEXT
|
||||
);
|
||||
""")
|
||||
await self.db.commit()
|
||||
|
||||
|
|
@ -121,18 +127,28 @@ class Database:
|
|||
) as 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:
|
||||
stats = {}
|
||||
async with self.db.execute("SELECT COUNT(*) FROM nodes") as c:
|
||||
stats["total_nodes"] = (await c.fetchone())[0]
|
||||
now = time.time()
|
||||
day_ago = now - 86400
|
||||
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:
|
||||
stats["nodes_with_position"] = (await c.fetchone())[0]
|
||||
async with self.db.execute("SELECT COUNT(*) FROM messages") as c:
|
||||
stats["total_messages"] = (await c.fetchone())[0]
|
||||
stats["nodes_24h"] = (await c.fetchone())[0]
|
||||
async with self.db.execute("SELECT COUNT(*) FROM commands") as c:
|
||||
stats["total_commands"] = (await c.fetchone())[0]
|
||||
async with self.db.execute(
|
||||
"SELECT COUNT(*) FROM messages WHERE portnum = 'TEXT_MESSAGE_APP'"
|
||||
) as c:
|
||||
stats["text_messages"] = (await c.fetchone())[0]
|
||||
"SELECT command, COUNT(*) as cnt FROM commands GROUP BY command ORDER BY cnt DESC"
|
||||
) as cursor:
|
||||
stats["command_breakdown"] = {row[0]: row[1] async for row in cursor}
|
||||
return stats
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ class WebServer:
|
|||
|
||||
async def _api_stats(self, request: web.Request) -> web.Response:
|
||||
stats = await self.db.get_stats()
|
||||
stats["version"] = config.get("version", "0.0.0")
|
||||
return web.json_response(stats)
|
||||
|
||||
async def _serve_index(self, request: web.Request) -> web.Response:
|
||||
|
|
|
|||
|
|
@ -32,36 +32,38 @@
|
|||
|
||||
<div class="container-fluid py-3">
|
||||
<!-- Stats Cards -->
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-4">
|
||||
<div class="card text-center border-info border-opacity-25">
|
||||
<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 class="text-body-secondary small">Nodes gesamt</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-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 class="fs-4 fw-bold text-success" id="statNodes24h">0</div>
|
||||
<div class="text-body-secondary small">Aktiv (24h)</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-body py-2">
|
||||
<div class="fs-4 fw-bold text-warning" id="statMessages">0</div>
|
||||
<div class="text-body-secondary small">Nachrichten</div>
|
||||
<div class="fs-4 fw-bold text-warning" id="statCommands">0</div>
|
||||
<div class="text-body-secondary small">Anfragen</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-2">
|
||||
<div class="fs-4 fw-bold text-primary" id="statTextMessages">0</div>
|
||||
<div class="text-body-secondary small">Textnachrichten</div>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card border-primary border-opacity-25">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -118,9 +118,18 @@ function updateStats(stats) {
|
|||
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;
|
||||
document.getElementById('statTextMessages').textContent = stats.text_messages || 0;
|
||||
document.getElementById('statNodes24h').textContent = stats.nodes_24h || 0;
|
||||
document.getElementById('statCommands').textContent = stats.total_commands || 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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue