feat: v0.6.15 - Scheduler-Variablen, Footer, sauberer WS-Shutdown
- Scheduler Nachrichten: Template-Variablen {time}, {date}, {datetime},
{weekday}, {nodes}, {nodes_24h} serverseitig aufgelöst
- Scheduler UI: klickbare Variablen-Badges beim Nachrichtenfeld-Modus
- Footer auf allen Seiten: © MeshDD / PPfeiffer · vX.Y.Z · MM/YYYY
- Fix: WS-Verbindungen vor runner.cleanup() explizit geschlossen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0fd401a395
commit
997a29842f
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -1,5 +1,21 @@
|
|||
# Changelog
|
||||
|
||||
## [0.6.15] - 2026-02-18
|
||||
|
||||
### Added
|
||||
- **Scheduler Template-Variablen**: Nachrichten-Jobs können Platzhalter nutzen:
|
||||
`{time}`, `{date}`, `{datetime}`, `{weekday}`, `{nodes}`, `{nodes_24h}`.
|
||||
Werden beim Ausführen serverseitig aufgelöst.
|
||||
- **Scheduler UI**: Bei Typ „Nachricht" werden klickbare Variablen-Badges
|
||||
unter dem Eingabefeld angezeigt – Klick fügt Variable an Cursorposition ein.
|
||||
- **Footer**: Auf allen Seiten fixer Footer mit Copyright „MeshDD / PPfeiffer",
|
||||
Versionsnummer und Monat/Jahr (MM/YYYY).
|
||||
|
||||
### Fixed
|
||||
- **Sauberer Shutdown**: WebSocket-Verbindungen werden vor `runner.cleanup()`
|
||||
explizit geschlossen (`ws_manager.close_all()`), so dass der Prozess nicht
|
||||
mehr auf hängende WS-Loops wartet.
|
||||
|
||||
## [0.6.14] - 2026-02-18
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
version: "0.6.14"
|
||||
version: "0.6.15"
|
||||
|
||||
bot:
|
||||
name: "MeshDD-Bot"
|
||||
|
|
|
|||
1
main.py
1
main.py
|
|
@ -64,6 +64,7 @@ async def main():
|
|||
finally:
|
||||
logger.info("Shutting down...")
|
||||
bot.disconnect()
|
||||
await ws_manager.close_all()
|
||||
await runner.cleanup()
|
||||
await db.close()
|
||||
logger.info("Shutdown complete")
|
||||
|
|
|
|||
|
|
@ -79,10 +79,33 @@ class Scheduler:
|
|||
job_type = job.get("type", "command")
|
||||
if command and self.bot:
|
||||
if job_type == "message":
|
||||
command = await self._resolve_vars(command, datetime.now())
|
||||
await self.bot.send_message(command, channel)
|
||||
else:
|
||||
await self.bot.execute_command(command, channel)
|
||||
|
||||
async def _resolve_vars(self, text: str, now: datetime) -> str:
|
||||
if "{" not in text:
|
||||
return text
|
||||
stats: dict = {}
|
||||
if self.bot and self.bot.db:
|
||||
try:
|
||||
stats = await self.bot.db.get_stats()
|
||||
except Exception:
|
||||
logger.exception("Error fetching stats for scheduler vars")
|
||||
weekdays = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
|
||||
replacements = {
|
||||
"{time}": now.strftime("%H:%M"),
|
||||
"{date}": now.strftime("%d.%m.%Y"),
|
||||
"{datetime}": now.strftime("%d.%m.%Y %H:%M"),
|
||||
"{weekday}": weekdays[now.weekday()],
|
||||
"{nodes}": str(stats.get("total_nodes", "?")),
|
||||
"{nodes_24h}": str(stats.get("nodes_24h", "?")),
|
||||
}
|
||||
for key, val in replacements.items():
|
||||
text = text.replace(key, val)
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def _matches_cron(cron_expr: str, dt: datetime) -> bool:
|
||||
parts = cron_expr.strip().split()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,15 @@ class WebSocketManager:
|
|||
self.clients: set[web.WebSocketResponse] = set()
|
||||
self.auth_clients: set[web.WebSocketResponse] = set()
|
||||
|
||||
async def close_all(self):
|
||||
for ws in list(self.clients):
|
||||
try:
|
||||
await ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.clients.clear()
|
||||
self.auth_clients.clear()
|
||||
|
||||
async def broadcast(self, msg_type: str, data: dict | list):
|
||||
message = json.dumps({"type": msg_type, "data": data})
|
||||
closed = set()
|
||||
|
|
|
|||
|
|
@ -186,6 +186,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<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/admin.js"></script>
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@
|
|||
margin-top: 48px;
|
||||
margin-left: 210px;
|
||||
padding: .875rem;
|
||||
padding-bottom: calc(.875rem + 26px);
|
||||
min-height: calc(100vh - 48px);
|
||||
background: var(--tblr-body-bg, var(--bs-body-bg));
|
||||
}
|
||||
|
|
@ -253,7 +254,7 @@
|
|||
|
||||
.map-wrapper #map {
|
||||
width: 100%;
|
||||
height: calc(100vh - 48px);
|
||||
height: calc(100vh - 48px - 26px);
|
||||
}
|
||||
|
||||
.node-tooltip { font-size: 13px; line-height: 1.6; }
|
||||
|
|
@ -291,6 +292,29 @@
|
|||
font-size: .65rem !important;
|
||||
}
|
||||
|
||||
/* ── Page Footer ─────────────────────────────────────────────── */
|
||||
|
||||
.page-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 210px;
|
||||
right: 0;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: .65rem;
|
||||
color: var(--bs-secondary-color);
|
||||
background: var(--tblr-bg-surface, var(--bs-body-bg));
|
||||
border-top: 1px solid var(--tblr-border-color, var(--bs-border-color));
|
||||
z-index: 900;
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.page-footer { left: 0; }
|
||||
}
|
||||
|
||||
/* ── Scrollbars ──────────────────────────────────────────────── */
|
||||
|
||||
.table-responsive::-webkit-scrollbar,
|
||||
|
|
|
|||
|
|
@ -101,9 +101,9 @@
|
|||
<div class="card-body py-2 px-3 d-flex gap-1 flex-wrap" id="channelBreakdown"></div>
|
||||
</div>
|
||||
|
||||
<!-- Charts (Prio 5) -->
|
||||
<!-- Charts -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<div class="card card-outline">
|
||||
<div class="card-header py-1"><i class="bi bi-pie-chart me-1"></i>Kanal-Anfragen</div>
|
||||
<div class="card-body p-2" style="height:160px;position:relative">
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<div class="card card-outline">
|
||||
<div class="card-header py-1"><i class="bi bi-bar-chart me-1"></i>Hop-Verteilung</div>
|
||||
<div class="card-body p-2" style="height:160px;position:relative">
|
||||
|
|
@ -119,7 +119,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<div class="card card-outline">
|
||||
<div class="card-header py-1"><i class="bi bi-cpu me-1"></i>Hardware Top 5</div>
|
||||
<div class="card-body p-2" style="height:160px;position:relative">
|
||||
|
|
@ -127,6 +127,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-xl-3">
|
||||
<div class="card card-outline">
|
||||
<div class="card-header py-1"><i class="bi bi-reception-4 me-1"></i>Pakettypen (24h)</div>
|
||||
<div class="card-body p-2" style="height:160px;position:relative">
|
||||
<canvas id="chartPacketTypes"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Send Message (auth-gated) -->
|
||||
|
|
@ -219,6 +227,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<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="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
|
||||
|
|
|
|||
|
|
@ -91,6 +91,16 @@ function escapeHtml(str) {
|
|||
|
||||
// ── Public init ───────────────────────────────────────────────
|
||||
|
||||
function _injectFooter(version) {
|
||||
const footer = document.getElementById('pageFooter');
|
||||
if (!footer) return;
|
||||
const now = new Date();
|
||||
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const yyyy = now.getFullYear();
|
||||
const ver = version ? ` · v${version}` : '';
|
||||
footer.textContent = `© MeshDD / PPfeiffer${ver} · ${mm}/${yyyy}`;
|
||||
}
|
||||
|
||||
function initPage({ onAuth = null } = {}) {
|
||||
_injectSidebar();
|
||||
_setupTheme();
|
||||
|
|
@ -103,9 +113,10 @@ function initPage({ onAuth = null } = {}) {
|
|||
if (onAuth) onAuth(user);
|
||||
});
|
||||
const vl = document.getElementById('versionLabel');
|
||||
if (vl) {
|
||||
fetch('/api/stats')
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => { if (d?.version) vl.textContent = `v${d.version}`; });
|
||||
}
|
||||
fetch('/api/stats')
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => {
|
||||
if (d?.version && vl) vl.textContent = `v${d.version}`;
|
||||
_injectFooter(d?.version);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,21 @@ themeToggle.addEventListener('click', () => {
|
|||
applyTheme(current === 'dark' ? 'light' : 'dark');
|
||||
});
|
||||
|
||||
// Footer
|
||||
(function () {
|
||||
const footer = document.getElementById('pageFooter');
|
||||
if (!footer) return;
|
||||
const now = new Date();
|
||||
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const yyyy = now.getFullYear();
|
||||
fetch('/api/stats').then(r => r.ok ? r.json() : null).then(d => {
|
||||
const ver = d?.version ? ` · v${d.version}` : '';
|
||||
footer.textContent = `© MeshDD / PPfeiffer${ver} · ${mm}/${yyyy}`;
|
||||
}).catch(() => {
|
||||
footer.textContent = `© MeshDD / PPfeiffer · ${mm}/${yyyy}`;
|
||||
});
|
||||
})();
|
||||
|
||||
// View switching
|
||||
const views = {
|
||||
login: document.getElementById('loginView'),
|
||||
|
|
|
|||
|
|
@ -6,13 +6,46 @@ let editMode = false;
|
|||
|
||||
initPage({ onAuth: (user) => { currentUser = user; } });
|
||||
|
||||
// Type toggle label
|
||||
// Template variables available in message jobs
|
||||
const MSG_VARS = [
|
||||
{ key: '{time}', label: '{time}', desc: 'Uhrzeit (HH:MM)' },
|
||||
{ key: '{date}', label: '{date}', desc: 'Datum (TT.MM.JJJJ)' },
|
||||
{ key: '{datetime}', label: '{datetime}', desc: 'Datum + Uhrzeit' },
|
||||
{ key: '{weekday}', label: '{weekday}', desc: 'Wochentag (Montag…)' },
|
||||
{ key: '{nodes}', label: '{nodes}', desc: 'Anzahl Nodes gesamt' },
|
||||
{ key: '{nodes_24h}', label: '{nodes_24h}', desc: 'Aktive Nodes (24h)' },
|
||||
];
|
||||
|
||||
// Type toggle label + variable hints
|
||||
function updateCommandLabel() {
|
||||
const isMsg = document.getElementById('jobType').value === 'message';
|
||||
document.getElementById('jobCommandLabel').textContent = isMsg ? 'Nachricht' : 'Kommando';
|
||||
document.getElementById('jobCommand').placeholder = isMsg ? 'Hallo Mesh!' : '/weather';
|
||||
document.getElementById('jobCommand').placeholder = isMsg ? 'Hallo Mesh! Heute ist {weekday}, {time} Uhr.' : '/weather';
|
||||
const hints = document.getElementById('varHints');
|
||||
const badges = document.getElementById('varBadges');
|
||||
if (isMsg) {
|
||||
badges.innerHTML = MSG_VARS.map(v =>
|
||||
`<span class="badge bg-secondary me-1 mb-1" role="button" title="${escapeHtml(v.desc)}" data-var="${escapeHtml(v.key)}" style="cursor:pointer;font-size:.7rem">${escapeHtml(v.label)}</span>`
|
||||
).join('');
|
||||
hints.classList.remove('d-none');
|
||||
} else {
|
||||
hints.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('varBadges').addEventListener('click', (e) => {
|
||||
const badge = e.target.closest('[data-var]');
|
||||
if (!badge) return;
|
||||
const input = document.getElementById('jobCommand');
|
||||
const start = input.selectionStart;
|
||||
const end = input.selectionEnd;
|
||||
const val = input.value;
|
||||
input.value = val.slice(0, start) + badge.dataset.var + val.slice(end);
|
||||
const pos = start + badge.dataset.var.length;
|
||||
input.setSelectionRange(pos, pos);
|
||||
input.focus();
|
||||
});
|
||||
|
||||
document.getElementById('jobType').addEventListener('change', updateCommandLabel);
|
||||
|
||||
// WebSocket for live updates
|
||||
|
|
|
|||
|
|
@ -117,6 +117,8 @@
|
|||
</div>
|
||||
</main>
|
||||
|
||||
<footer id="pageFooter" class="page-footer" style="left:0"></footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/js/login.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@
|
|||
<div id="map"></div>
|
||||
</main>
|
||||
|
||||
<footer id="pageFooter" class="page-footer"></footer>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script src="/static/js/map.js"></script>
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@
|
|||
</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/packets.js"></script>
|
||||
|
|
|
|||
|
|
@ -98,6 +98,10 @@
|
|||
<div class="mb-3">
|
||||
<label for="jobCommand" class="form-label" id="jobCommandLabel">Kommando</label>
|
||||
<input type="text" class="form-control" id="jobCommand" placeholder="/weather" required>
|
||||
<div id="varHints" class="d-none mt-1">
|
||||
<small class="text-body-secondary me-1">Variablen:</small>
|
||||
<span id="varBadges"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-8 mb-3">
|
||||
|
|
@ -124,6 +128,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<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/scheduler.js"></script>
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@
|
|||
</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/settings.js"></script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue