feat: v0.3.5 - AdminLTE-style layout, fix channel names in messages

Redesign dashboard and scheduler with AdminLTE-inspired layout: fixed sidebar
navigation, top navbar, info-boxes, card-outline styling, table-striped.
Fix channel names missing on initial load by sending channels before messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ppfeiffer 2026-02-15 17:54:12 +01:00
parent 6bfb1595e4
commit 65703b6389
8 changed files with 360 additions and 223 deletions

View file

@ -1,5 +1,26 @@
# Changelog
## [0.3.5] - 2026-02-15
### Changed
- Dashboard und Scheduler auf AdminLTE-Style umgestellt
- Feste Sidebar-Navigation (Dashboard, Scheduler, Karte) mit Active-State
- Fixed Top-Navbar mit Branding, Status-Dot und Theme-Toggle
- Content-Wrapper mit leicht abgesetztem Hintergrund
- Info-Boxes im AdminLTE-Stil (Icon-Spalte + Inhalt) statt Cards
- Card-Outline mit farbiger Oberkante (info/warning) statt Borders
- Table-Striped fuer bessere Lesbarkeit
- Sidebar responsive: auf Mobile als Overlay mit Backdrop
- Einheitliches Layout auf Dashboard und Scheduler
## [0.3.4] - 2026-02-15
### Fixed
- Kanalnamen in Nachrichten fehlten beim Laden (Channels werden jetzt vor Messages gesendet)
### Changed
- Dashboard deutlich kompakter: weniger Padding, kleinere Schriftgroessen
- Stat-Cards, Navbar, Panels und Nachrichten platzsparender
- Hover-Animationen und Pulse-Effekt entfernt (schlichter)
## [0.3.3] - 2026-02-15
### Changed
- Dashboard-Layout modernisiert: Glassmorphism-Navbar (sticky, blur-Effekt)

View file

@ -1,4 +1,4 @@
version: "0.3.3"
version: "0.3.5"
bot:
name: "MeshDD-Bot"

View file

@ -67,13 +67,13 @@ class WebServer:
stats["version"] = config.get("version", "0.0.0")
await ws.send_str(json.dumps({"type": "stats_update", "data": stats}))
messages = await self.db.get_recent_messages(50)
await ws.send_str(json.dumps({"type": "initial_messages", "data": messages}))
if self.bot:
channels = self.bot.get_channels()
await ws.send_str(json.dumps({"type": "channels", "data": channels}))
messages = await self.db.get_recent_messages(50)
await ws.send_str(json.dumps({"type": "initial_messages", "data": messages}))
async for msg in ws:
pass # We only send, not receive
finally:

View file

@ -1,102 +1,189 @@
/* Status indicator */
/* ── AdminLTE-style Layout ─────────────────────────── */
/* Top Navbar */
.top-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 46px;
z-index: 1030;
background: var(--bs-body-bg);
border-bottom: 1px solid var(--bs-border-color);
font-size: .875rem;
}
/* Sidebar */
.sidebar {
position: fixed;
top: 46px;
left: 0;
bottom: 0;
width: 200px;
background: var(--bs-body-bg);
border-right: 1px solid var(--bs-border-color);
z-index: 1020;
overflow-y: auto;
transition: transform 0.2s ease;
}
.sidebar-nav {
display: flex;
flex-direction: column;
padding: 0.5rem 0;
}
.sidebar-link {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.5rem 1rem;
color: var(--bs-body-color);
text-decoration: none;
font-size: .85rem;
border-left: 3px solid transparent;
transition: background 0.15s;
}
.sidebar-link:hover {
background: rgba(var(--bs-emphasis-color-rgb), 0.06);
color: var(--bs-body-color);
}
.sidebar-link.active {
background: rgba(var(--bs-info-rgb), 0.08);
border-left-color: var(--bs-info);
color: var(--bs-info);
font-weight: 600;
}
.sidebar-link i {
font-size: 1rem;
width: 1.25rem;
text-align: center;
}
/* Mobile sidebar */
.sidebar-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
z-index: 1019;
}
@media (max-width: 991.98px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
.sidebar.open ~ .sidebar-backdrop {
display: block;
}
.content-wrapper {
margin-left: 0 !important;
}
}
/* Content wrapper */
.content-wrapper {
margin-top: 46px;
margin-left: 200px;
padding: 0.75rem;
min-height: calc(100vh - 46px);
background: rgba(var(--bs-emphasis-color-rgb), 0.03);
}
/* ── Info Boxes (AdminLTE style) ──────────────────── */
.info-box {
display: flex;
align-items: stretch;
background: var(--bs-body-bg);
border: 1px solid var(--bs-border-color);
border-radius: 0.375rem;
min-height: 60px;
overflow: hidden;
}
.info-box-icon {
display: flex;
align-items: center;
justify-content: center;
width: 56px;
font-size: 1.4rem;
flex-shrink: 0;
}
.info-box-content {
display: flex;
flex-direction: column;
justify-content: center;
padding: 0.4rem 0.75rem;
line-height: 1.2;
}
.info-box-label {
font-size: .75rem;
color: var(--bs-secondary-color);
}
.info-box-number {
font-size: 1.25rem;
font-weight: 700;
}
/* ── Card outline (AdminLTE style) ────────────────── */
.card-outline {
border-top: 3px solid var(--bs-border-color);
}
.card-outline.card-info {
border-top-color: var(--bs-info);
}
.card-outline.card-warning {
border-top-color: var(--bs-warning);
}
.card-outline.card-success {
border-top-color: var(--bs-success);
}
.card-outline.card-primary {
border-top-color: var(--bs-primary);
}
.card-header {
font-weight: 600;
font-size: .875rem;
padding: 0.5rem 0.75rem;
}
/* ── Status dot ──────────────────────────────────── */
.status-dot {
display: inline-block;
width: 10px;
height: 10px;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--bs-danger);
transition: all 0.3s ease;
}
.status-dot.connected {
background: var(--bs-success);
box-shadow: 0 0 8px var(--bs-success);
animation: pulse-dot 2s infinite;
box-shadow: 0 0 5px var(--bs-success);
}
@keyframes pulse-dot {
0%, 100% { box-shadow: 0 0 4px var(--bs-success); }
50% { box-shadow: 0 0 12px var(--bs-success); }
}
/* ── Messages ────────────────────────────────────── */
/* Navbar glass effect */
.navbar-glass {
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
background: rgba(var(--bs-body-bg-rgb), 0.85) !important;
border-bottom: 1px solid rgba(var(--bs-emphasis-color-rgb), 0.08);
}
/* Scrollbar */
.table-responsive::-webkit-scrollbar,
.msg-scroll::-webkit-scrollbar {
width: 5px;
}
.table-responsive::-webkit-scrollbar-track,
.msg-scroll::-webkit-scrollbar-track {
background: transparent;
}
.table-responsive::-webkit-scrollbar-thumb,
.msg-scroll::-webkit-scrollbar-thumb {
background: var(--bs-border-color);
border-radius: 3px;
}
/* Stat cards */
.stat-card {
border: none;
border-radius: 0.75rem;
overflow: hidden;
position: relative;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.stat-card .stat-accent {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
}
.stat-card .stat-icon {
font-size: 1.8rem;
opacity: 0.15;
position: absolute;
right: 12px;
top: 10px;
}
/* Panel cards */
.panel-card {
border: none;
border-radius: 0.75rem;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.panel-card .card-header {
background: transparent;
border-bottom: 1px solid rgba(var(--bs-emphasis-color-rgb), 0.06);
padding: 0.65rem 1rem;
}
/* Messages */
.msg-item {
padding: 0.5rem 0.75rem;
border-bottom: 1px solid rgba(var(--bs-emphasis-color-rgb), 0.04);
transition: background 0.15s ease;
}
.msg-item:hover {
background: rgba(var(--bs-emphasis-color-rgb), 0.03);
padding: 0.35rem 0.65rem;
border-bottom: 1px solid var(--bs-border-color-translucent);
font-size: .8rem;
}
.msg-item:last-child {
@ -104,42 +191,28 @@
}
.msg-bubble {
background: rgba(var(--bs-info-rgb), 0.08);
border-radius: 0.5rem 0.5rem 0.5rem 0.1rem;
padding: 0.35rem 0.6rem;
background: rgba(var(--bs-info-rgb), 0.06);
border-radius: 0.3rem;
padding: 0.2rem 0.45rem;
display: inline-block;
max-width: 100%;
word-break: break-word;
}
/* Node table rows */
.nodes-table tbody tr {
transition: background 0.1s ease;
/* ── Scrollbar ───────────────────────────────────── */
.table-responsive::-webkit-scrollbar,
.card-body::-webkit-scrollbar {
width: 5px;
}
/* Breakdown bar */
.breakdown-bar {
border: none;
border-radius: 0.5rem;
background: rgba(var(--bs-primary-rgb), 0.06);
.table-responsive::-webkit-scrollbar-track,
.card-body::-webkit-scrollbar-track {
background: transparent;
}
/* Send input */
.send-bar {
border-top: 1px solid rgba(var(--bs-emphasis-color-rgb), 0.06);
background: rgba(var(--bs-emphasis-color-rgb), 0.02);
padding: 0.5rem 0.75rem;
border-radius: 0 0 0.75rem 0.75rem;
}
.send-bar .form-control:focus,
.send-bar .form-select:focus {
box-shadow: none;
border-color: var(--bs-info);
}
/* Badge pills */
.badge-pill {
border-radius: 2rem;
font-weight: 500;
.table-responsive::-webkit-scrollbar-thumb,
.card-body::-webkit-scrollbar-thumb {
background: var(--bs-border-color);
border-radius: 3px;
}

View file

@ -9,97 +9,110 @@
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-sm navbar-glass sticky-top">
<div class="container-fluid">
<a class="navbar-brand d-flex align-items-center gap-2" href="/">
<i class="bi bi-broadcast-pin text-info fs-5"></i>
<span class="fw-bold">MeshDD-Bot</span>
<span class="badge badge-pill bg-body-secondary text-body-secondary fw-normal small" id="versionLabel"></span>
</a>
<div class="d-flex align-items-center gap-2">
<a href="/scheduler" class="btn btn-outline-info btn-sm rounded-pill px-3">
<i class="bi bi-clock-history me-1"></i>Scheduler
</a>
<a href="/map" target="_blank" class="btn btn-outline-info btn-sm rounded-pill px-3">
<i class="bi bi-map me-1"></i>Karte
</a>
<button class="btn btn-outline-secondary btn-sm rounded-circle" id="themeToggle" title="Theme wechseln" style="width:32px;height:32px;">
<i class="bi bi-sun-fill" id="themeIcon"></i>
<!-- 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="badge badge-pill bg-body-secondary d-flex align-items-center gap-2 px-3 py-2" id="statusBadge">
<span class="status-dot" id="statusDot"></span>
<span class="small" id="statusText">Verbinde...</span>
<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>
<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>
<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>
<div class="container-fluid py-3">
<!-- Stats Cards -->
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<nav class="sidebar-nav">
<a href="/" class="sidebar-link active">
<i class="bi bi-speedometer2"></i><span>Dashboard</span>
</a>
<a href="/scheduler" class="sidebar-link">
<i class="bi bi-clock-history"></i><span>Scheduler</span>
</a>
<a href="/map" target="_blank" class="sidebar-link">
<i class="bi bi-map"></i><span>Karte</span>
</a>
</nav>
</aside>
<!-- Backdrop for mobile sidebar -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Content -->
<main class="content-wrapper">
<!-- Info Boxes -->
<div class="row g-2 mb-2">
<div class="col-4">
<div class="card stat-card">
<div class="stat-accent" style="background: linear-gradient(90deg, var(--bs-info), transparent);"></div>
<i class="bi bi-router stat-icon text-info"></i>
<div class="card-body py-2 ps-3">
<div class="fs-4 fw-bold text-info" id="statNodes">0</div>
<div class="text-body-secondary small">Nodes gesamt</div>
<div class="col-sm-4">
<div class="info-box">
<span class="info-box-icon bg-info bg-opacity-10 text-info">
<i class="bi bi-router"></i>
</span>
<div class="info-box-content">
<span class="info-box-label">Nodes gesamt</span>
<span class="info-box-number" id="statNodes">0</span>
</div>
</div>
</div>
<div class="col-4">
<div class="card stat-card">
<div class="stat-accent" style="background: linear-gradient(90deg, var(--bs-success), transparent);"></div>
<i class="bi bi-activity stat-icon text-success"></i>
<div class="card-body py-2 ps-3">
<div class="fs-4 fw-bold text-success" id="statNodes24h">0</div>
<div class="text-body-secondary small">Aktiv (24h)</div>
<div class="col-sm-4">
<div class="info-box">
<span class="info-box-icon bg-success bg-opacity-10 text-success">
<i class="bi bi-activity"></i>
</span>
<div class="info-box-content">
<span class="info-box-label">Aktiv (24h)</span>
<span class="info-box-number" id="statNodes24h">0</span>
</div>
</div>
</div>
<div class="col-4">
<div class="card stat-card">
<div class="stat-accent" style="background: linear-gradient(90deg, var(--bs-warning), transparent);"></div>
<i class="bi bi-terminal stat-icon text-warning"></i>
<div class="card-body py-2 ps-3">
<div class="fs-4 fw-bold text-warning" id="statCommands">0</div>
<div class="text-body-secondary small">Anfragen</div>
</div>
</div>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-12">
<div class="card breakdown-bar">
<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 class="col-sm-4">
<div class="info-box">
<span class="info-box-icon bg-warning bg-opacity-10 text-warning">
<i class="bi bi-terminal"></i>
</span>
<div class="info-box-content">
<span class="info-box-label">Anfragen</span>
<span class="info-box-number" id="statCommands">0</span>
</div>
</div>
</div>
</div>
<!-- Panels -->
<div class="row g-3">
<!-- Nodes Table -->
<div class="col-lg-7">
<div class="card panel-card">
<div class="card-header d-flex align-items-center">
<i class="bi bi-router me-2 text-info"></i>
<span class="fw-semibold">Nodes</span>
<span class="badge badge-pill bg-info bg-opacity-25 text-info ms-auto" id="nodeCountBadge">0</span>
<!-- Command Breakdown -->
<div class="card card-outline mb-2">
<div class="card-body py-2 px-3 d-flex align-items-center gap-2 flex-wrap">
<small class="text-body-secondary"><i class="bi bi-bar-chart-fill me-1"></i>Anfragen:</small>
<span id="commandBreakdown" class="d-flex gap-1 flex-wrap"></span>
</div>
<div class="card-body p-0 table-responsive" style="max-height: 500px; overflow-y: auto;">
<table class="table table-hover table-sm mb-0 align-middle nodes-table">
</div>
<!-- Main Cards -->
<div class="row g-2">
<!-- Nodes -->
<div class="col-lg-7">
<div class="card card-outline card-info">
<div class="card-header">
<i class="bi bi-router me-1"></i>Nodes
<span class="badge bg-info float-end" id="nodeCountBadge">0</span>
</div>
<div class="card-body p-0 table-responsive" style="max-height:520px;overflow-y:auto">
<table class="table table-hover table-sm table-striped mb-0 align-middle">
<thead class="table-dark sticky-top">
<tr>
<th>Name</th>
<th>Hardware</th>
<th class="text-end px-1">SNR</th>
<th class="px-1">Batterie</th>
<th class="text-center px-1">Hops</th>
<th class="text-end px-1">Zuletzt</th>
<th class="text-end">SNR</th>
<th>Batterie</th>
<th class="text-center">Hops</th>
<th class="text-end">Zuletzt</th>
</tr>
</thead>
<tbody id="nodesTable"></tbody>
@ -110,19 +123,18 @@
<!-- Messages -->
<div class="col-lg-5">
<div class="card panel-card">
<div class="card-header d-flex align-items-center">
<i class="bi bi-chat-dots me-2 text-warning"></i>
<span class="fw-semibold">Nachrichten</span>
<div class="card card-outline card-warning">
<div class="card-header">
<i class="bi bi-chat-dots me-1"></i>Nachrichten
</div>
<div class="card-body p-0 msg-scroll" style="max-height: 500px; overflow-y: auto;">
<div class="card-body p-0" style="max-height:520px;overflow-y:auto">
<div id="messagesList"></div>
</div>
<div class="send-bar">
<div class="card-footer py-2 px-2">
<div class="input-group input-group-sm">
<select class="form-select form-select-sm rounded-start-pill" id="sendChannel" style="max-width: 120px;"></select>
<select class="form-select form-select-sm" id="sendChannel" style="max-width:110px"></select>
<input type="text" class="form-control form-control-sm" id="sendText" placeholder="Nachricht senden...">
<button class="btn btn-info btn-sm rounded-end-pill px-3" id="btnSend" type="button">
<button class="btn btn-info btn-sm" id="btnSend" type="button">
<i class="bi bi-send-fill"></i>
</button>
</div>
@ -130,7 +142,7 @@
</div>
</div>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/dashboard.js"></script>

View file

@ -209,4 +209,14 @@ themeToggle.addEventListener('click', () => {
applyTheme(current === 'dark' ? 'light' : 'dark');
});
// Sidebar toggle (mobile)
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebar = document.getElementById('sidebar');
const sidebarBackdrop = document.getElementById('sidebarBackdrop');
if (sidebarToggle) {
sidebarToggle.addEventListener('click', () => sidebar.classList.toggle('open'));
sidebarBackdrop.addEventListener('click', () => sidebar.classList.remove('open'));
}
connectWebSocket();

View file

@ -208,5 +208,15 @@ document.getElementById('btnSaveJob').addEventListener('click', async () => {
}
});
// Sidebar toggle (mobile)
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebar = document.getElementById('sidebar');
const sidebarBackdrop = document.getElementById('sidebarBackdrop');
if (sidebarToggle) {
sidebarToggle.addEventListener('click', () => sidebar.classList.toggle('open'));
sidebarBackdrop.addEventListener('click', () => sidebar.classList.remove('open'));
}
loadJobs();
connectWebSocket();

View file

@ -9,37 +9,48 @@
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-sm bg-body-tertiary border-bottom">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<i class="bi bi-broadcast-pin text-info me-2"></i>MeshDD-Bot
</a>
<div class="d-flex align-items-center gap-3">
<a href="/" class="btn btn-outline-info btn-sm">
<i class="bi bi-speedometer2 me-1"></i>Dashboard
</a>
<a href="/map" target="_blank" class="btn btn-outline-info btn-sm">
<i class="bi bi-map me-1"></i>Karte
</a>
<button class="btn btn-outline-secondary btn-sm" id="themeToggle" title="Theme wechseln">
<i class="bi bi-sun-fill" id="themeIcon"></i>
<!-- 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
</span>
<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>
</div>
</nav>
<div class="container-fluid py-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0"><i class="bi bi-clock-history me-2 text-info"></i>Scheduler</h5>
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<nav class="sidebar-nav">
<a href="/" class="sidebar-link">
<i class="bi bi-speedometer2"></i><span>Dashboard</span>
</a>
<a href="/scheduler" class="sidebar-link active">
<i class="bi bi-clock-history"></i><span>Scheduler</span>
</a>
<a href="/map" target="_blank" class="sidebar-link">
<i class="bi bi-map"></i><span>Karte</span>
</a>
</nav>
</aside>
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Content -->
<main class="content-wrapper">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0"><i class="bi bi-clock-history me-1 text-info"></i>Scheduler</h6>
<button class="btn btn-info btn-sm" id="btnAddJob">
<i class="bi bi-plus-lg me-1"></i>Neuer Job
</button>
</div>
<div class="card">
<div class="card card-outline card-info">
<div class="card-body p-0 table-responsive">
<table class="table table-hover table-sm mb-0 align-middle">
<table class="table table-hover table-sm table-striped mb-0 align-middle">
<thead class="table-dark">
<tr>
<th>Name</th>
@ -58,7 +69,7 @@
</table>
</div>
</div>
</div>
</main>
<!-- Job Modal -->
<div class="modal fade" id="jobModal" tabindex="-1">