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 <noreply@anthropic.com>
This commit is contained in:
ppfeiffer 2026-02-16 17:46:08 +01:00
parent ef4817841f
commit 16465eb8f6
7 changed files with 45 additions and 6 deletions

View file

@ -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()`

View file

@ -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)

View file

@ -1,4 +1,4 @@
version: "0.3.9"
version: "0.3.10"
bot:
name: "MeshDD-Bot"

View file

@ -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")

View file

@ -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}))

View file

@ -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 {

View file

@ -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 = `
<div class="d-flex justify-content-between align-items-center mb-1">
<small class="fw-medium"><i class="bi bi-person-fill me-1 text-body-secondary"></i>${escapeHtml(from)}</small>
<small class="fw-medium"><i class="bi ${icon} me-1 text-body-secondary"></i>${escapeHtml(from)}</small>
<small class="text-body-secondary"><span class="badge badge-pill bg-body-secondary me-1">${escapeHtml(chName)}</span>${time}</small>
</div>
<div class="msg-bubble">${escapeHtml(msg.payload || '')}</div>`;
<div class="${bubbleClass}">${escapeHtml(msg.payload || '')}</div>`;
messagesList.prepend(item);
while (messagesList.children.length > 100) {
messagesList.removeChild(messagesList.lastChild);