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:
parent
ef4817841f
commit
16465eb8f6
|
|
@ -32,5 +32,5 @@
|
||||||
- Meshtastic host configured in config.yaml, not env vars
|
- Meshtastic host configured in config.yaml, not env vars
|
||||||
- Bot start: `/home/peter/meshdd-bot/venv/bin/python main.py`
|
- Bot start: `/home/peter/meshdd-bot/venv/bin/python main.py`
|
||||||
- Forgejo remote with token in URL
|
- 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()`
|
- Protobuf objects converted via `google.protobuf.json_format.MessageToDict()`
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
# Changelog
|
# 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
|
## [0.3.9] - 2026-02-16
|
||||||
### Added
|
### Added
|
||||||
- Neuer Befehl `/me` zeigt eigene Node-Infos (Name, HW, Hops, SNR, RSSI, Batterie, Position)
|
- Neuer Befehl `/me` zeigt eigene Node-Infos (Name, HW, Hops, SNR, RSSI, Batterie, Position)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
version: "0.3.9"
|
version: "0.3.10"
|
||||||
|
|
||||||
bot:
|
bot:
|
||||||
name: "MeshDD-Bot"
|
name: "MeshDD-Bot"
|
||||||
|
|
|
||||||
|
|
@ -332,9 +332,16 @@ class MeshBot:
|
||||||
async def _handle_command(self, text: str, channel: int, from_id: str):
|
async def _handle_command(self, text: str, channel: int, from_id: str):
|
||||||
await self.execute_command(text, channel, from_id)
|
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):
|
async def _send_text(self, text: str, channel: int, max_len: int = 170):
|
||||||
if not self.interface:
|
if not self.interface:
|
||||||
return
|
return
|
||||||
|
my_id = self.get_my_node_id() or "bot"
|
||||||
# Reserve space for "[x/y] " prefix (max 7 bytes)
|
# Reserve space for "[x/y] " prefix (max 7 bytes)
|
||||||
parts = self._split_message(text, max_len - 7)
|
parts = self._split_message(text, max_len - 7)
|
||||||
total = len(parts)
|
total = len(parts)
|
||||||
|
|
@ -344,6 +351,9 @@ class MeshBot:
|
||||||
msg = f"[{i+1}/{total}] {part}" if total > 1 else part
|
msg = f"[{i+1}/{total}] {part}" if total > 1 else part
|
||||||
try:
|
try:
|
||||||
self.interface.sendText(msg, channelIndex=channel)
|
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:
|
except Exception:
|
||||||
logger.exception("Error sending text")
|
logger.exception("Error sending text")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,9 @@ class WebServer:
|
||||||
if self.bot:
|
if self.bot:
|
||||||
channels = self.bot.get_channels()
|
channels = self.bot.get_channels()
|
||||||
await ws.send_str(json.dumps({"type": "channels", "data": 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)
|
messages = await self.db.get_recent_messages(50)
|
||||||
await ws.send_str(json.dumps({"type": "initial_messages", "data": messages}))
|
await ws.send_str(json.dumps({"type": "initial_messages", "data": messages}))
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,19 @@
|
||||||
word-break: break-word;
|
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 ─────────────────────────────────── */
|
||||||
|
|
||||||
.map-wrapper {
|
.map-wrapper {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ const nodeCountBadge = document.getElementById('nodeCountBadge');
|
||||||
|
|
||||||
let nodes = {};
|
let nodes = {};
|
||||||
let channels = {};
|
let channels = {};
|
||||||
|
let myNodeId = null;
|
||||||
let ws;
|
let ws;
|
||||||
|
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
|
|
@ -45,6 +46,9 @@ function connectWebSocket() {
|
||||||
channels = msg.data;
|
channels = msg.data;
|
||||||
populateChannelDropdown();
|
populateChannelDropdown();
|
||||||
break;
|
break;
|
||||||
|
case 'my_node_id':
|
||||||
|
myNodeId = msg.data;
|
||||||
|
break;
|
||||||
case 'new_message':
|
case 'new_message':
|
||||||
addMessage(msg.data);
|
addMessage(msg.data);
|
||||||
break;
|
break;
|
||||||
|
|
@ -96,18 +100,21 @@ function renderBattery(level) {
|
||||||
|
|
||||||
function addMessage(msg) {
|
function addMessage(msg) {
|
||||||
const item = document.createElement('div');
|
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 time = msg.timestamp ? new Date(msg.timestamp * 1000).toLocaleTimeString('de-DE') : '';
|
||||||
const fromNode = nodes[msg.from_node];
|
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 chIdx = msg.channel != null ? msg.channel : '?';
|
||||||
const chName = channels[chIdx] || `Ch ${chIdx}`;
|
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 = `
|
item.innerHTML = `
|
||||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
<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>
|
<small class="text-body-secondary"><span class="badge badge-pill bg-body-secondary me-1">${escapeHtml(chName)}</span>${time}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="msg-bubble">${escapeHtml(msg.payload || '')}</div>`;
|
<div class="${bubbleClass}">${escapeHtml(msg.payload || '')}</div>`;
|
||||||
messagesList.prepend(item);
|
messagesList.prepend(item);
|
||||||
while (messagesList.children.length > 100) {
|
while (messagesList.children.length > 100) {
|
||||||
messagesList.removeChild(messagesList.lastChild);
|
messagesList.removeChild(messagesList.lastChild);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue