MeshDD-Bot/static/js/app.js
ppfeiffer 511ff20842 feat(ui): Sidebar-Gruppe Konfigurationen, /config-Seite, MeshDD-Dashboard (closes #4)
- app.js: Sidebar um Gruppen-Support erweitert; Konfigurationen-Gruppe
  mit Scheduler, NINA, Einstellungen (/config) als Untereinträge
- style.css: .sidebar-group-label + .sidebar-link-sub
- config.py: save()-Funktion für persistentes Schreiben in config.yaml
- webserver.py: GET/PUT /api/config + GET /config Route (Admin)
- static/config.html + static/js/config.js: neue Konfigurationsseite
  (Bot, Meshtastic, Web, Links editierbar)
- Alle HTML-Dateien: MeshDD-Bot → MeshDD-Dashboard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 22:33:18 +01:00

136 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// MeshDD-Dashboard Shared page module
// Provides: initPage(), escapeHtml(), applyTheme()
// ── Sidebar definition ────────────────────────────────────────
const _SIDEBAR_LINKS = [
{ href: '/', icon: 'bi-speedometer2', label: 'Dashboard', admin: false, 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 },
{ type: 'group', label: 'Konfigurationen',admin: true },
{ href: '/scheduler', icon: 'bi-clock-history', label: 'Scheduler', admin: true, user: false, sub: true },
{ href: '/nina', icon: 'bi-shield-exclamation', label: 'NINA', admin: true, user: false, sub: true },
{ href: '/config', icon: 'bi-sliders', label: 'Einstellungen', admin: true, user: false, sub: true },
{ href: '/admin', icon: 'bi-people', label: 'Benutzer', admin: true, user: false },
];
function _injectSidebar() {
const sidebar = document.getElementById('sidebar');
if (!sidebar) return;
const currentPath = window.location.pathname;
sidebar.innerHTML = '<nav class="sidebar-nav">' +
_SIDEBAR_LINKS.map(link => {
if (link.type === 'group') {
const adm = link.admin ? ' sidebar-admin' : '';
return `<span class="sidebar-group-label${adm}">${link.label}</span>`;
}
const active = currentPath === link.href ? ' active' : '';
const adm = link.admin ? ' sidebar-admin' : '';
const usr = link.user ? ' sidebar-user' : '';
const sub = link.sub ? ' sidebar-link-sub' : '';
return `<a href="${link.href}" class="sidebar-link${sub}${active}${adm}${usr}">` +
`<i class="bi ${link.icon}"></i><span>${link.label}</span></a>`;
}).join('') +
'</nav>';
}
// ── Navbar ────────────────────────────────────────────────────
function _updateNavbar(user) {
const userMenu = document.getElementById('userMenu');
const loginBtn = document.getElementById('loginBtn');
const userName = document.getElementById('userName');
if (user) {
if (userName) userName.textContent = user.name;
if (userMenu) userMenu.classList.remove('d-none');
if (loginBtn) loginBtn.classList.add('d-none');
} else {
if (userMenu) userMenu.classList.add('d-none');
if (loginBtn) loginBtn.classList.remove('d-none');
}
}
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 ─────────────────────────────────────────────────────
function applyTheme(theme) {
document.documentElement.setAttribute('data-bs-theme', theme);
const icon = document.getElementById('themeIcon');
if (icon) icon.className = theme === 'dark' ? 'bi bi-sun-fill' : 'bi bi-moon-fill';
localStorage.setItem('theme', theme);
document.dispatchEvent(new CustomEvent('themechange', { detail: { theme } }));
}
function _setupTheme() {
applyTheme(localStorage.getItem('theme') || 'light');
const btn = document.getElementById('themeToggle');
if (btn) {
btn.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-bs-theme');
applyTheme(current === 'dark' ? 'light' : 'dark');
});
}
}
// ── Sidebar toggle (mobile) ───────────────────────────────────
function _setupSidebarToggle() {
const toggle = document.getElementById('sidebarToggle');
const sidebar = document.getElementById('sidebar');
const backdrop = document.getElementById('sidebarBackdrop');
if (toggle && sidebar) toggle.addEventListener('click', () => sidebar.classList.toggle('open'));
if (backdrop && sidebar) backdrop.addEventListener('click', () => sidebar.classList.remove('open'));
}
// ── Utilities ─────────────────────────────────────────────────
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// ── 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();
fetch('/api/auth/me')
.then(r => r.ok ? r.json() : null)
.then(user => {
_updateNavbar(user);
_updateSidebar(user);
_setupSidebarToggle();
if (onAuth) onAuth(user);
});
const vl = document.getElementById('versionLabel');
fetch('/api/stats')
.then(r => r.ok ? r.json() : null)
.then(d => {
if (d?.version && vl) vl.textContent = `v${d.version}`;
_injectFooter(d?.version);
});
}