From 16465eb8f6ed5659ce687d6728d5c75149ca9af7 Mon Sep 17 00:00:00 2001 From: ppfeiffer Date: Mon, 16 Feb 2026 17:46:08 +0100 Subject: [PATCH] feat: v0.3.10 - Show sent messages in dashboard with distinct styling Store bot-sent messages in DB and broadcast via WebSocket. Sent messages appear right-aligned with green bubble in the messages panel. Co-Authored-By: Claude Opus 4.6 --- .claude/CLAUDE.md | 2 +- CHANGELOG.md | 6 ++++++ config.yaml | 2 +- meshbot/bot.py | 10 ++++++++++ meshbot/webserver.py | 3 +++ static/css/style.css | 13 +++++++++++++ static/js/dashboard.js | 15 +++++++++++---- 7 files changed, 45 insertions(+), 6 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 7ca1f3f..017ae36 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -32,5 +32,5 @@ - Meshtastic host configured in config.yaml, not env vars - Bot start: `/home/peter/meshdd-bot/venv/bin/python main.py` - Forgejo remote with token in URL -- Current version: 0.3.9 +- Current version: 0.3.10 - Protobuf objects converted via `google.protobuf.json_format.MessageToDict()` diff --git a/CHANGELOG.md b/CHANGELOG.md index e474776..0c8d855 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.3.10] - 2026-02-16 +### Added +- Gesendete Bot-Nachrichten werden im Nachrichtenfenster angezeigt +- Eigene Nachrichten mit gruener Bubble und rechtsbuendiger Ausrichtung +- Bot-Nachrichten werden in DB gespeichert und via WebSocket broadcastet + ## [0.3.9] - 2026-02-16 ### Added - Neuer Befehl `/me` zeigt eigene Node-Infos (Name, HW, Hops, SNR, RSSI, Batterie, Position) diff --git a/config.yaml b/config.yaml index 4402103..82f728b 100644 --- a/config.yaml +++ b/config.yaml @@ -1,4 +1,4 @@ -version: "0.3.9" +version: "0.3.10" bot: name: "MeshDD-Bot" diff --git a/meshbot/bot.py b/meshbot/bot.py index 24832ad..1471790 100644 --- a/meshbot/bot.py +++ b/meshbot/bot.py @@ -332,9 +332,16 @@ class MeshBot: async def _handle_command(self, text: str, channel: int, from_id: str): await self.execute_command(text, channel, from_id) + def get_my_node_id(self) -> str | None: + if self.interface and hasattr(self.interface, 'myInfo') and self.interface.myInfo: + num = self.interface.myInfo.my_node_num + return f"!{num:08x}" + return None + async def _send_text(self, text: str, channel: int, max_len: int = 170): if not self.interface: return + my_id = self.get_my_node_id() or "bot" # Reserve space for "[x/y] " prefix (max 7 bytes) parts = self._split_message(text, max_len - 7) total = len(parts) @@ -344,6 +351,9 @@ class MeshBot: msg = f"[{i+1}/{total}] {part}" if total > 1 else part try: self.interface.sendText(msg, channelIndex=channel) + stored = await self.db.insert_message(my_id, "^all", channel, "TEXT_MESSAGE_APP", msg) + if self.ws_manager: + await self.ws_manager.broadcast("new_message", stored) except Exception: logger.exception("Error sending text") diff --git a/meshbot/webserver.py b/meshbot/webserver.py index 9f7b711..0476d95 100644 --- a/meshbot/webserver.py +++ b/meshbot/webserver.py @@ -72,6 +72,9 @@ class WebServer: if self.bot: channels = self.bot.get_channels() await ws.send_str(json.dumps({"type": "channels", "data": channels})) + my_id = self.bot.get_my_node_id() + if my_id: + await ws.send_str(json.dumps({"type": "my_node_id", "data": my_id})) messages = await self.db.get_recent_messages(50) await ws.send_str(json.dumps({"type": "initial_messages", "data": messages})) diff --git a/static/css/style.css b/static/css/style.css index 5a29b98..54f4d17 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -199,6 +199,19 @@ word-break: break-word; } +.msg-bubble-sent { + background: rgba(var(--bs-success-rgb), 0.10); + border-left: 2px solid var(--bs-success); +} + +.msg-sent { + text-align: right; +} + +.msg-sent .msg-bubble-sent { + text-align: left; +} + /* ── Map wrapper ─────────────────────────────────── */ .map-wrapper { diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 7c2adae..0c78096 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -6,6 +6,7 @@ const nodeCountBadge = document.getElementById('nodeCountBadge'); let nodes = {}; let channels = {}; +let myNodeId = null; let ws; function connectWebSocket() { @@ -45,6 +46,9 @@ function connectWebSocket() { channels = msg.data; populateChannelDropdown(); break; + case 'my_node_id': + myNodeId = msg.data; + break; case 'new_message': addMessage(msg.data); break; @@ -96,18 +100,21 @@ function renderBattery(level) { function addMessage(msg) { const item = document.createElement('div'); - item.className = 'msg-item'; + const isSent = myNodeId && msg.from_node === myNodeId; + item.className = 'msg-item' + (isSent ? ' msg-sent' : ''); const time = msg.timestamp ? new Date(msg.timestamp * 1000).toLocaleTimeString('de-DE') : ''; const fromNode = nodes[msg.from_node]; - const from = (fromNode && fromNode.long_name) ? fromNode.long_name : (msg.from_node || '?'); + const from = isSent ? 'Bot' : ((fromNode && fromNode.long_name) ? fromNode.long_name : (msg.from_node || '?')); const chIdx = msg.channel != null ? msg.channel : '?'; const chName = channels[chIdx] || `Ch ${chIdx}`; + const bubbleClass = isSent ? 'msg-bubble msg-bubble-sent' : 'msg-bubble'; + const icon = isSent ? 'bi-send-fill' : 'bi-person-fill'; item.innerHTML = `
- ${escapeHtml(from)} + ${escapeHtml(from)} ${escapeHtml(chName)}${time}
-
${escapeHtml(msg.payload || '')}
`; +
${escapeHtml(msg.payload || '')}
`; messagesList.prepend(item); while (messagesList.children.length > 100) { messagesList.removeChild(messagesList.lastChild);