feat: Dashboard-Charts-Fix, Nachrichten-Seite, Legende-Hintergrund (fixes #10)
- Fix: Dashboard-Charts (Kanal-Anfragen + Pakettypen) erscheinen nun initial korrekt: lastStats gecacht, updateChannelChart nach channels-Event aufgerufen; packet_type_breakdown in get_stats() ergänzt (SQL über packets-Tabelle, 24h) - Fix: Kartenlegende hat jetzt explizite Hintergrundfarben per [data-bs-theme]- Selektor (light=#fff, dark=#1e2128) – keine transparente Legende mehr - Feat: Neue Nachrichten-Seite /messages (User-only) mit Kanal-Farbcodierung und Richtungs-Kennzeichnung (empfangen=links/kanalfarbe, gesendet=rechts/grün), Channel-Filter-Tabs, Absender-Node-ID, Löschen-Button - Feat: Dashboard Nodes-Tabelle: neue Spalten RSSI und GPS-Positions-Indikator - Feat: app.js sidebar-user Klasse für eingeloggte Benutzer (non-admin) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
79f80563c8
commit
d6631c1554
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -1,5 +1,31 @@
|
|||
# Changelog
|
||||
|
||||
## [0.8.10] - 2026-02-19
|
||||
|
||||
### Added
|
||||
- **Neue Nachrichten-Seite** (`/messages`, User-only): Komplett eigenständige Seite für
|
||||
den Nachrichtenverlauf mit Kanal-Farbcodierung und Richtungs-Kennzeichnung:
|
||||
- Empfangene Nachrichten: links, Absenderbild (Initialen-Avatar) mit Kanalfarbe,
|
||||
Bubble mit farbigem linkem Rand
|
||||
- Gesendete Nachrichten (Bot): rechts, grüne Bubble mit rechtem Rand
|
||||
- Kanalfilter-Buttons oben (farblich je Kanal), Löschen-Button
|
||||
- Node-ID des Absenders als zweite Zeile unterhalb des Namens
|
||||
- Sidebar-Eintrag „Nachrichten" nur für eingeloggte Benutzer sichtbar (`sidebar-user`)
|
||||
- **Dashboard Nodes-Tabelle erweitert**: Neue Spalten RSSI (dBm) und GPS-Positions-Indikator
|
||||
(grünes Stecknadel-Icon wenn Position bekannt, graues sonst)
|
||||
- **`packet_type_breakdown` in Stats-API**: `GET /api/stats` und `stats_update`-WebSocket
|
||||
liefern jetzt Pakettypen-Verteilung der letzten 24h aus der `packets`-Tabelle
|
||||
|
||||
### Fixed
|
||||
- **Dashboard Charts initial leer** (fixes #10): `updateChannelChart` wurde aufgerufen
|
||||
bevor der `channels`-WS-Event ankam → Chart blieb leer. Fix: `lastStats` speichern,
|
||||
Chart neu zeichnen wenn Channels eintreffen.
|
||||
- **Dashboard Pakettypen-Chart immer leer** (fixes #10): `packet_type_breakdown` fehlte
|
||||
komplett in `get_stats()` – jetzt per SQL-Abfrage über `packets`-Tabelle befüllt.
|
||||
- **Kartenlegende transparent** (fixes #10): CSS-Variablen in Leaflet-Control-Container
|
||||
wurden nicht zuverlässig aufgelöst. Fix: explizite Hintergrundfarben für Light- und
|
||||
Dark-Mode per `[data-bs-theme]`-Selektor statt CSS-Variablen.
|
||||
|
||||
## [0.8.9] - 2026-02-19
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
version: "0.8.9"
|
||||
version: "0.8.10"
|
||||
|
||||
bot:
|
||||
name: "MeshDD-Bot"
|
||||
|
|
|
|||
|
|
@ -216,6 +216,11 @@ class Database:
|
|||
(day_ago,),
|
||||
) as cursor:
|
||||
stats["channel_breakdown"] = {row[0]: row[1] async for row in cursor}
|
||||
async with self.db.execute(
|
||||
"SELECT portnum, COUNT(*) as cnt FROM packets WHERE timestamp >= ? GROUP BY portnum ORDER BY cnt DESC",
|
||||
(day_ago,),
|
||||
) as cursor:
|
||||
stats["packet_type_breakdown"] = {(row[0] or "?"): row[1] async for row in cursor}
|
||||
return stats
|
||||
|
||||
# ── User methods ──────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ class WebServer:
|
|||
self.app.router.add_get("/nina", self._serve_nina)
|
||||
self.app.router.add_get("/map", self._serve_map)
|
||||
self.app.router.add_get("/packets", self._serve_packets)
|
||||
self.app.router.add_get("/messages", self._serve_messages)
|
||||
self.app.router.add_get("/", self._serve_index)
|
||||
self.app.router.add_static("/static", STATIC_DIR)
|
||||
|
||||
|
|
@ -202,6 +203,9 @@ class WebServer:
|
|||
async def _serve_packets(self, request: web.Request) -> web.Response:
|
||||
return web.FileResponse(os.path.join(STATIC_DIR, "packets.html"))
|
||||
|
||||
async def _serve_messages(self, request: web.Request) -> web.Response:
|
||||
return web.FileResponse(os.path.join(STATIC_DIR, "messages.html"))
|
||||
|
||||
async def _serve_scheduler(self, request: web.Request) -> web.Response:
|
||||
return web.FileResponse(os.path.join(STATIC_DIR, "scheduler.html"))
|
||||
|
||||
|
|
|
|||
|
|
@ -263,15 +263,26 @@
|
|||
.node-tooltip-wrap { padding: 0; }
|
||||
|
||||
.legend {
|
||||
background: var(--tblr-bg-surface, var(--bs-body-bg, #fff));
|
||||
border: 1px solid var(--tblr-border-color, var(--bs-border-color, rgba(0,0,0,.12)));
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,.12);
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,.15);
|
||||
padding: 7px 10px;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.4;
|
||||
min-width: 100px;
|
||||
color: var(--bs-body-color);
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .legend {
|
||||
background: #1e2128;
|
||||
border-color: rgba(255,255,255,.1);
|
||||
color: #c8d3e1;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,.45);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .legend-section {
|
||||
color: #6c7a8d;
|
||||
}
|
||||
.legend-section {
|
||||
font-size: 9.5px;
|
||||
|
|
@ -291,6 +302,85 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Full Messages Page ──────────────────────────────────────── */
|
||||
|
||||
.msg-full-item {
|
||||
padding: .5rem .875rem;
|
||||
border-bottom: 1px solid var(--bs-border-color-translucent);
|
||||
}
|
||||
.msg-full-item:last-child { border-bottom: none; }
|
||||
|
||||
.msg-full-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: .5rem;
|
||||
}
|
||||
.msg-full-row-sent {
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.msg-full-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.msg-full-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.msg-full-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .3rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 3px;
|
||||
font-size: .78rem;
|
||||
}
|
||||
|
||||
.msg-full-name {
|
||||
font-size: .78rem;
|
||||
}
|
||||
|
||||
.msg-ch-badge {
|
||||
display: inline-block;
|
||||
padding: 0 .4rem;
|
||||
border-radius: 3px;
|
||||
border: 1px solid;
|
||||
font-size: .68rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.msg-full-bubble {
|
||||
display: inline-block;
|
||||
background: rgba(var(--bs-info-rgb), .07);
|
||||
border: 1px solid rgba(var(--bs-info-rgb), .15);
|
||||
border-left: 3px solid currentColor;
|
||||
border-radius: 0 .375rem .375rem 0;
|
||||
padding: .3rem .55rem;
|
||||
font-size: .83rem;
|
||||
max-width: 100%;
|
||||
word-break: break-word;
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.msg-full-bubble-sent {
|
||||
background: rgba(var(--bs-success-rgb), .09);
|
||||
border: 1px solid rgba(var(--bs-success-rgb), .18);
|
||||
border-right: 3px solid var(--bs-success);
|
||||
border-radius: .375rem 0 0 .375rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* ── Packet-Log Badges ───────────────────────────────────────── */
|
||||
|
||||
.pkt-type-badge {
|
||||
|
|
|
|||
|
|
@ -171,8 +171,10 @@
|
|||
<th data-sort="name" style="cursor:pointer;user-select:none;white-space:nowrap">Name <i class="bi bi-arrow-down-up text-body-secondary" style="font-size:.65rem"></i></th>
|
||||
<th data-sort="hw_model" style="cursor:pointer;user-select:none;white-space:nowrap">Hardware <i class="bi bi-arrow-down-up text-body-secondary" style="font-size:.65rem"></i></th>
|
||||
<th class="text-end" data-sort="snr" style="cursor:pointer;user-select:none;white-space:nowrap">SNR <i class="bi bi-arrow-down-up text-body-secondary" style="font-size:.65rem"></i></th>
|
||||
<th class="text-end" data-sort="rssi" style="cursor:pointer;user-select:none;white-space:nowrap">RSSI <i class="bi bi-arrow-down-up text-body-secondary" style="font-size:.65rem"></i></th>
|
||||
<th data-sort="battery" style="cursor:pointer;user-select:none;white-space:nowrap">Batterie <i class="bi bi-arrow-down-up text-body-secondary" style="font-size:.65rem"></i></th>
|
||||
<th class="text-center" data-sort="hop_count" style="cursor:pointer;user-select:none;white-space:nowrap">Hops <i class="bi bi-arrow-down-up text-body-secondary" style="font-size:.65rem"></i></th>
|
||||
<th class="text-center" title="GPS-Position verfügbar"><i class="bi bi-geo-alt" style="font-size:.75rem"></i></th>
|
||||
<th class="text-end" data-sort="last_seen" style="cursor:pointer;user-select:none;white-space:nowrap">Zuletzt <i class="bi bi-sort-down text-info" style="font-size:.65rem"></i></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@
|
|||
// ── Sidebar definition ────────────────────────────────────────
|
||||
|
||||
const _SIDEBAR_LINKS = [
|
||||
{ href: '/', icon: 'bi-speedometer2', label: 'Dashboard', admin: false },
|
||||
{ href: '/scheduler', icon: 'bi-clock-history', label: 'Scheduler', admin: true },
|
||||
{ href: '/nina', icon: 'bi-shield-exclamation', label: 'NINA', admin: true },
|
||||
{ href: '/map', icon: 'bi-map', label: 'Karte', admin: false },
|
||||
{ href: '/packets', icon: 'bi-reception-4', label: 'Pakete', admin: false },
|
||||
{ href: '/settings', icon: 'bi-gear', label: 'Einstellungen',admin: true },
|
||||
{ href: '/admin', icon: 'bi-people', label: 'Benutzer', admin: true },
|
||||
{ href: '/', icon: 'bi-speedometer2', label: 'Dashboard', admin: false, user: false },
|
||||
{ href: '/scheduler', icon: 'bi-clock-history', label: 'Scheduler', admin: true, user: false },
|
||||
{ href: '/nina', icon: 'bi-shield-exclamation', label: 'NINA', admin: true, user: false },
|
||||
{ href: '/map', icon: 'bi-map', label: 'Karte', admin: false, user: false },
|
||||
{ href: '/packets', icon: 'bi-reception-4', label: 'Pakete', admin: false, user: false },
|
||||
{ href: '/messages', icon: 'bi-chat-dots', label: 'Nachrichten', admin: false, user: true },
|
||||
{ href: '/settings', icon: 'bi-gear', label: 'Einstellungen',admin: true, user: false },
|
||||
{ href: '/admin', icon: 'bi-people', label: 'Benutzer', admin: true, user: false },
|
||||
];
|
||||
|
||||
function _injectSidebar() {
|
||||
|
|
@ -21,7 +22,8 @@ function _injectSidebar() {
|
|||
_SIDEBAR_LINKS.map(link => {
|
||||
const active = currentPath === link.href ? ' active' : '';
|
||||
const adm = link.admin ? ' sidebar-admin' : '';
|
||||
return `<a href="${link.href}" class="sidebar-link${active}${adm}">` +
|
||||
const usr = link.user ? ' sidebar-user' : '';
|
||||
return `<a href="${link.href}" class="sidebar-link${active}${adm}${usr}">` +
|
||||
`<i class="bi ${link.icon}"></i><span>${link.label}</span></a>`;
|
||||
}).join('') +
|
||||
'</nav>';
|
||||
|
|
@ -45,9 +47,13 @@ function _updateNavbar(user) {
|
|||
|
||||
function _updateSidebar(user) {
|
||||
const isAdmin = user && user.role === 'admin';
|
||||
const isUser = !!user;
|
||||
document.querySelectorAll('.sidebar-admin').forEach(el => {
|
||||
el.style.display = isAdmin ? '' : 'none';
|
||||
});
|
||||
document.querySelectorAll('.sidebar-user').forEach(el => {
|
||||
el.style.display = isUser ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// ── Theme ─────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ let chartChannel = null;
|
|||
let chartHops = null;
|
||||
let chartHardware = null;
|
||||
let chartPacketTypes = null;
|
||||
let lastStats = null;
|
||||
|
||||
initPage({ onAuth: (user) => {
|
||||
currentUser = user;
|
||||
|
|
@ -72,6 +73,7 @@ function connectWebSocket() {
|
|||
channels = msg.data;
|
||||
populateChannelDropdown();
|
||||
renderMsgFilterBar();
|
||||
if (lastStats) updateChannelChart(lastStats);
|
||||
break;
|
||||
case 'my_node_id':
|
||||
myNodeId = msg.data;
|
||||
|
|
@ -126,16 +128,23 @@ function renderNodes() {
|
|||
}
|
||||
const hw = node.hw_model || '-';
|
||||
const snr = node.snr != null ? `${node.snr.toFixed(1)} dB` : '-';
|
||||
const rssi = node.rssi != null ? `${node.rssi}` : '-';
|
||||
const battery = renderBattery(node.battery);
|
||||
const hops = node.hop_count != null ? node.hop_count : '-';
|
||||
const lastSeen = node.last_seen ? timeAgo(node.last_seen) : '-';
|
||||
const onlineClass = isOnline(node.last_seen) ? 'text-success' : '';
|
||||
const hasPos = node.lat != null && node.lon != null;
|
||||
const posIcon = hasPos
|
||||
? '<i class="bi bi-geo-alt-fill text-success" style="font-size:.75rem"></i>'
|
||||
: '<i class="bi bi-geo-alt text-body-secondary" style="font-size:.75rem;opacity:.35"></i>';
|
||||
return `<tr data-node-id="${escapeHtml(node.node_id)}" style="cursor:pointer">
|
||||
<td class="${onlineClass}">${escapeHtml(name)}</td>
|
||||
<td class="text-body-secondary">${escapeHtml(hw)}</td>
|
||||
<td class="text-end px-1">${snr}</td>
|
||||
<td class="text-end px-1 text-body-secondary">${rssi}</td>
|
||||
<td class="px-1">${battery}</td>
|
||||
<td class="text-center px-1">${hops}</td>
|
||||
<td class="text-center px-1">${posIcon}</td>
|
||||
<td class="text-end px-1 text-body-secondary">${lastSeen}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
|
@ -193,6 +202,7 @@ function updateBotStatus(status) {
|
|||
}
|
||||
|
||||
function updateStats(stats) {
|
||||
lastStats = stats;
|
||||
if (stats.version) {
|
||||
document.getElementById('versionLabel').textContent = `v${stats.version}`;
|
||||
}
|
||||
|
|
|
|||
179
static/js/messages.js
Normal file
179
static/js/messages.js
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
// ── Channel color palette ──────────────────────────────────────
|
||||
const CH_COLORS = [
|
||||
{ bg: 'rgba(13,202,240,.12)', border: '#0dcaf0', text: '#0dcaf0' }, // 0 cyan
|
||||
{ bg: 'rgba(25,135,84,.12)', border: '#198754', text: '#198754' }, // 1 green
|
||||
{ bg: 'rgba(255,193,7,.12)', border: '#ffc107', text: '#d9a406' }, // 2 yellow
|
||||
{ bg: 'rgba(220,53,69,.12)', border: '#dc3545', text: '#dc3545' }, // 3 red
|
||||
{ bg: 'rgba(102,16,242,.12)', border: '#6610f2', text: '#6610f2' }, // 4 indigo
|
||||
{ bg: 'rgba(13,110,253,.12)', border: '#0d6efd', text: '#0d6efd' }, // 5 blue
|
||||
{ bg: 'rgba(32,201,151,.12)', border: '#20c997', text: '#20c997' }, // 6 teal
|
||||
{ bg: 'rgba(253,126,20,.12)', border: '#fd7e14', text: '#fd7e14' }, // 7 orange
|
||||
];
|
||||
|
||||
function chColor(chIdx) {
|
||||
return CH_COLORS[chIdx % CH_COLORS.length] || CH_COLORS[0];
|
||||
}
|
||||
|
||||
// ── State ──────────────────────────────────────────────────────
|
||||
let nodes = {};
|
||||
let channels = {};
|
||||
let myNodeId = null;
|
||||
let ws;
|
||||
let msgChannelFilter = 'all';
|
||||
let messages = [];
|
||||
|
||||
initPage({ onAuth: (user) => {
|
||||
if (!user) {
|
||||
document.getElementById('loginNotice').classList.remove('d-none');
|
||||
} else {
|
||||
document.getElementById('messagesView').classList.remove('d-none');
|
||||
connectWebSocket();
|
||||
}
|
||||
}});
|
||||
|
||||
// ── WebSocket ─────────────────────────────────────────────────
|
||||
function connectWebSocket() {
|
||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
ws = new WebSocket(`${proto}//${location.host}/ws`);
|
||||
|
||||
ws.onopen = () => {
|
||||
document.getElementById('statusDot').classList.add('connected');
|
||||
document.getElementById('statusText').textContent = 'Verbunden';
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
document.getElementById('statusDot').classList.remove('connected');
|
||||
document.getElementById('statusText').textContent = 'Getrennt - Reconnect...';
|
||||
setTimeout(connectWebSocket, 3000);
|
||||
};
|
||||
|
||||
ws.onerror = () => { ws.close(); };
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
switch (msg.type) {
|
||||
case 'initial':
|
||||
msg.data.forEach(n => { nodes[n.node_id] = n; });
|
||||
break;
|
||||
case 'node_update':
|
||||
nodes[msg.data.node_id] = msg.data;
|
||||
break;
|
||||
case 'channels':
|
||||
channels = msg.data;
|
||||
renderFilterBar();
|
||||
break;
|
||||
case 'my_node_id':
|
||||
myNodeId = msg.data;
|
||||
break;
|
||||
case 'initial_messages':
|
||||
messages = [];
|
||||
document.getElementById('messagesList').innerHTML = '';
|
||||
msg.data.reverse().forEach(m => addMessage(m));
|
||||
break;
|
||||
case 'new_message':
|
||||
addMessage(msg.data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ── Message rendering ─────────────────────────────────────────
|
||||
function addMessage(msg) {
|
||||
messages.push(msg);
|
||||
if (messages.length > 300) messages.shift();
|
||||
|
||||
const isSent = myNodeId && msg.from_node === myNodeId;
|
||||
const chIdx = msg.channel != null ? msg.channel : 0;
|
||||
const chName = channels[chIdx] != null ? channels[chIdx] : `Ch ${chIdx}`;
|
||||
const time = msg.timestamp ? new Date(msg.timestamp * 1000).toLocaleString('de-DE', {
|
||||
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'
|
||||
}) : '';
|
||||
const fromNode = nodes[msg.from_node];
|
||||
const fromName = isSent ? 'Bot' : ((fromNode && fromNode.long_name) ? fromNode.long_name : (msg.from_node || '?'));
|
||||
const fromId = isSent ? '' : msg.from_node || '';
|
||||
const col = chColor(chIdx);
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'msg-full-item' + (isSent ? ' msg-full-sent' : '');
|
||||
item.dataset.channel = String(chIdx);
|
||||
|
||||
if (msgChannelFilter !== 'all' && String(chIdx) !== msgChannelFilter) {
|
||||
item.classList.add('d-none');
|
||||
}
|
||||
|
||||
if (isSent) {
|
||||
// Sent: right-aligned, green
|
||||
item.innerHTML = `
|
||||
<div class="msg-full-row msg-full-row-sent">
|
||||
<div class="msg-full-meta text-end">
|
||||
<small class="text-body-secondary">${time}</small>
|
||||
<span class="msg-ch-badge" style="background:${col.bg};border-color:${col.border};color:${col.text}">${escapeHtml(chName)}</span>
|
||||
<span class="fw-medium msg-full-name"><i class="bi bi-send-fill me-1" style="font-size:.7rem"></i>${escapeHtml(fromName)}</span>
|
||||
</div>
|
||||
<div class="msg-full-bubble msg-full-bubble-sent">${escapeHtml(msg.payload || '')}</div>
|
||||
</div>`;
|
||||
} else {
|
||||
// Received: left-aligned, channel colored
|
||||
item.innerHTML = `
|
||||
<div class="msg-full-row">
|
||||
<div class="msg-full-icon" style="background:${col.bg};border-color:${col.border}">
|
||||
<i class="bi bi-person-fill" style="color:${col.text};font-size:.85rem"></i>
|
||||
</div>
|
||||
<div class="msg-full-body">
|
||||
<div class="msg-full-meta">
|
||||
<span class="fw-medium msg-full-name">${escapeHtml(fromName)}</span>
|
||||
${fromId ? `<span class="text-body-secondary ms-1" style="font-size:.7rem">${escapeHtml(fromId)}</span>` : ''}
|
||||
<span class="msg-ch-badge" style="background:${col.bg};border-color:${col.border};color:${col.text}">${escapeHtml(chName)}</span>
|
||||
<small class="text-body-secondary ms-auto">${time}</small>
|
||||
</div>
|
||||
<div class="msg-full-bubble" style="border-left-color:${col.border}">${escapeHtml(msg.payload || '')}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const list = document.getElementById('messagesList');
|
||||
list.prepend(item);
|
||||
updateCount();
|
||||
}
|
||||
|
||||
function updateCount() {
|
||||
const visible = document.getElementById('messagesList').querySelectorAll('.msg-full-item:not(.d-none)').length;
|
||||
document.getElementById('msgCount').textContent = visible;
|
||||
}
|
||||
|
||||
// ── Channel filter bar ────────────────────────────────────────
|
||||
function renderFilterBar() {
|
||||
const bar = document.getElementById('msgFilterBar');
|
||||
const sorted = Object.entries(channels).sort((a, b) => parseInt(a[0]) - parseInt(b[0]));
|
||||
const mkBtn = (ch, label, col) => {
|
||||
const active = ch === msgChannelFilter;
|
||||
const colStyle = col ? `background:${active ? col.border : 'transparent'};border-color:${col.border};color:${active ? '#fff' : col.text}` : '';
|
||||
const cls = col ? '' : (active ? 'btn-secondary' : 'btn-outline-secondary');
|
||||
return `<button class="btn btn-sm ${cls} py-0 px-2" data-ch="${escapeHtml(ch)}" style="font-size:.7rem;${colStyle}">${escapeHtml(label)}</button>`;
|
||||
};
|
||||
bar.innerHTML =
|
||||
mkBtn('all', 'Alle', null) +
|
||||
sorted.map(([idx, name]) => mkBtn(String(idx), name, chColor(parseInt(idx)))).join('');
|
||||
bar.onclick = (e) => {
|
||||
const btn = e.target.closest('button[data-ch]');
|
||||
if (!btn) return;
|
||||
msgChannelFilter = btn.dataset.ch;
|
||||
renderFilterBar();
|
||||
applyFilter();
|
||||
};
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
document.getElementById('messagesList').querySelectorAll('.msg-full-item').forEach(item => {
|
||||
const vis = msgChannelFilter === 'all' || item.dataset.channel === msgChannelFilter;
|
||||
item.classList.toggle('d-none', !vis);
|
||||
});
|
||||
updateCount();
|
||||
}
|
||||
|
||||
// Clear button
|
||||
document.getElementById('msgClearBtn').addEventListener('click', () => {
|
||||
messages = [];
|
||||
document.getElementById('messagesList').innerHTML = '';
|
||||
updateCount();
|
||||
});
|
||||
83
static/messages.html
Normal file
83
static/messages.html
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de" data-bs-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MeshDD-Bot Nachrichten</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.4.0/dist/css/tabler.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body class="antialiased">
|
||||
<!-- Top Navbar -->
|
||||
<nav class="top-navbar d-flex align-items-center px-3">
|
||||
<button class="btn btn-link text-body p-0 me-2 d-lg-none" id="sidebarToggle">
|
||||
<i class="bi bi-list fs-5"></i>
|
||||
</button>
|
||||
<span class="fw-bold me-auto">
|
||||
<i class="bi bi-broadcast-pin text-info me-1"></i>MeshDD-Bot
|
||||
<small class="text-body-secondary fw-normal" id="versionLabel"></small>
|
||||
</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="d-flex align-items-center gap-1">
|
||||
<span class="status-dot" id="statusDot"></span>
|
||||
<small class="text-body-secondary" id="statusText">Verbinde...</small>
|
||||
</span>
|
||||
<span id="userMenu" class="d-none">
|
||||
<small class="text-body-secondary me-1"><i class="bi bi-person-fill me-1"></i><span id="userName"></span></small>
|
||||
<a href="/auth/logout" class="btn btn-sm btn-outline-secondary py-0 px-1" title="Abmelden">
|
||||
<i class="bi bi-box-arrow-right" style="font-size:.75rem"></i>
|
||||
</a>
|
||||
</span>
|
||||
<a href="/login" id="loginBtn" class="btn btn-sm btn-outline-info py-0 px-1 d-none">
|
||||
<i class="bi bi-person" style="font-size:.75rem"></i> Login
|
||||
</a>
|
||||
<button class="btn btn-sm btn-outline-secondary py-0 px-1" id="themeToggle" title="Theme wechseln">
|
||||
<i class="bi bi-sun-fill" id="themeIcon" style="font-size:.75rem"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
|
||||
|
||||
<main class="content-wrapper">
|
||||
<!-- Login required notice -->
|
||||
<div id="loginNotice" class="d-none">
|
||||
<div class="card card-outline text-center py-5">
|
||||
<div class="card-body">
|
||||
<i class="bi bi-lock fs-1 text-body-secondary d-block mb-3"></i>
|
||||
<p class="mb-3 text-body-secondary">Du musst angemeldet sein, um Nachrichten zu sehen.</p>
|
||||
<a href="/login" class="btn btn-info btn-sm">
|
||||
<i class="bi bi-person me-1"></i>Anmelden
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages view (auth-gated) -->
|
||||
<div id="messagesView" class="d-none">
|
||||
<div class="card card-outline card-warning">
|
||||
<div class="card-header py-2 d-flex align-items-center gap-2 flex-wrap">
|
||||
<i class="bi bi-chat-dots me-1"></i>
|
||||
<span class="fw-semibold">Nachrichten</span>
|
||||
<span class="badge bg-secondary ms-1" id="msgCount">0</span>
|
||||
<!-- Channel filter tabs -->
|
||||
<div class="ms-auto d-flex gap-1 flex-wrap" id="msgFilterBar"></div>
|
||||
<button class="btn btn-sm btn-outline-danger py-0 px-2" id="msgClearBtn" title="Nachrichten löschen">
|
||||
<i class="bi bi-trash" style="font-size:.75rem"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0" style="max-height:calc(100vh - 130px);overflow-y:auto" id="messagesList">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer id="pageFooter" class="page-footer"></footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script src="/static/js/messages.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue