feat: v0.3.6 - Node settings page, map in sidebar layout
Add /settings page showing device, LoRa, channels, position, power and bluetooth/network config read from the Meshtastic node. Rebuild map page with consistent sidebar layout instead of standalone fullscreen view. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
65703b6389
commit
d3c3806ed5
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -1,5 +1,19 @@
|
|||
# Changelog
|
||||
|
||||
## [0.3.6] - 2026-02-15
|
||||
### Added
|
||||
- Node-Einstellungen Seite (`/settings`) zeigt Geraet, LoRa, Channels, Position, Power, Bluetooth/Netzwerk
|
||||
- Neuer API-Endpoint `GET /api/node/config` liest Config vom lokalen Meshtastic-Node
|
||||
- `get_node_config()` Methode im Bot (liest localConfig, myInfo, metadata via Protobuf)
|
||||
- Sidebar-Eintrag "Einstellungen" mit Gear-Icon auf allen Seiten
|
||||
|
||||
### Changed
|
||||
- Karte (`/map`) im Sidebar-Layout statt Vollbild (Top-Navbar, Sidebar, Content-Wrapper)
|
||||
- Karte oeffnet im selben Tab statt `target="_blank"`
|
||||
- Status-Info und Node-Count in Karten-Navbar integriert
|
||||
- Map-Styles (Tooltip, Legende) in zentrale `style.css` verschoben
|
||||
- Sidebar-Navigation auf allen 4 Seiten konsistent (Dashboard, Scheduler, Karte, Einstellungen)
|
||||
|
||||
## [0.3.5] - 2026-02-15
|
||||
### Changed
|
||||
- Dashboard und Scheduler auf AdminLTE-Style umgestellt
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
version: "0.3.5"
|
||||
version: "0.3.6"
|
||||
|
||||
bot:
|
||||
name: "MeshDD-Bot"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import time
|
|||
import json
|
||||
import urllib.request
|
||||
|
||||
from google.protobuf.json_format import MessageToDict
|
||||
from meshtastic.tcp_interface import TCPInterface
|
||||
from pubsub import pub
|
||||
|
||||
|
|
@ -44,6 +45,100 @@ class MeshBot:
|
|||
channels[ch.index] = name
|
||||
return channels
|
||||
|
||||
def get_node_config(self) -> dict:
|
||||
result = {"device": {}, "lora": {}, "channels": [], "position": {}, "power": {}, "bluetooth": {}, "network": {}}
|
||||
if not self.interface or not self.interface.localNode:
|
||||
return result
|
||||
|
||||
node = self.interface.localNode
|
||||
my_info = getattr(self.interface, 'myInfo', None)
|
||||
metadata = getattr(self.interface, 'metadata', None)
|
||||
|
||||
# Device info
|
||||
if my_info:
|
||||
info = MessageToDict(my_info) if hasattr(my_info, 'DESCRIPTOR') else (my_info if isinstance(my_info, dict) else {})
|
||||
result["device"]["node_num"] = info.get("myNodeNum", "")
|
||||
result["device"]["max_channels"] = info.get("maxChannels", "")
|
||||
|
||||
if metadata:
|
||||
meta = MessageToDict(metadata) if hasattr(metadata, 'DESCRIPTOR') else (metadata if isinstance(metadata, dict) else {})
|
||||
result["device"]["firmware_version"] = meta.get("firmwareVersion", "")
|
||||
result["device"]["hw_model"] = meta.get("hwModel", "")
|
||||
|
||||
# Local config sections
|
||||
local_config = getattr(node, 'localConfig', None)
|
||||
if local_config:
|
||||
cfg = MessageToDict(local_config, preserving_proto_field_name=True) if hasattr(local_config, 'DESCRIPTOR') else {}
|
||||
|
||||
lora = cfg.get("lora", {})
|
||||
result["lora"] = {
|
||||
"region": lora.get("region", ""),
|
||||
"modem_preset": lora.get("modem_preset", ""),
|
||||
"hop_limit": lora.get("hop_limit", ""),
|
||||
"tx_power": lora.get("tx_power", ""),
|
||||
"bandwidth": lora.get("bandwidth", ""),
|
||||
"frequency_offset": lora.get("frequency_offset", ""),
|
||||
"tx_enabled": lora.get("tx_enabled", ""),
|
||||
"use_preset": lora.get("use_preset", ""),
|
||||
}
|
||||
|
||||
device = cfg.get("device", {})
|
||||
result["device"]["name"] = device.get("role", "")
|
||||
result["device"]["role"] = device.get("role", "")
|
||||
|
||||
pos = cfg.get("position", {})
|
||||
result["position"] = {
|
||||
"gps_mode": pos.get("gps_mode", pos.get("gps_enabled", "")),
|
||||
"position_broadcast_secs": pos.get("position_broadcast_secs", ""),
|
||||
"fixed_position": pos.get("fixed_position", ""),
|
||||
}
|
||||
|
||||
power = cfg.get("power", {})
|
||||
result["power"] = {
|
||||
"mesh_sds_timeout_secs": power.get("mesh_sds_timeout_secs", ""),
|
||||
"ls_secs": power.get("ls_secs", ""),
|
||||
"min_wake_secs": power.get("min_wake_secs", ""),
|
||||
"is_power_saving": power.get("is_power_saving", ""),
|
||||
}
|
||||
|
||||
bt = cfg.get("bluetooth", {})
|
||||
result["bluetooth"] = {
|
||||
"enabled": bt.get("enabled", ""),
|
||||
"mode": bt.get("mode", ""),
|
||||
}
|
||||
|
||||
network = cfg.get("network", {})
|
||||
result["network"] = {
|
||||
"wifi_enabled": network.get("wifi_enabled", ""),
|
||||
"ntp_server": network.get("ntp_server", ""),
|
||||
}
|
||||
|
||||
# Channels
|
||||
for ch in node.channels:
|
||||
if ch.role:
|
||||
name = ch.settings.name if ch.settings.name else ("Primary" if ch.index == 0 else f"Ch {ch.index}")
|
||||
psk = ch.settings.psk
|
||||
psk_display = "Default" if psk == b'\x01' else ("Custom" if psk and psk != b'\x00' else "None")
|
||||
result["channels"].append({
|
||||
"index": ch.index,
|
||||
"name": name,
|
||||
"role": str(ch.role),
|
||||
"psk": psk_display,
|
||||
})
|
||||
|
||||
# Node long name from interface nodes
|
||||
if self.interface.nodes:
|
||||
my_num = getattr(self.interface, 'myInfo', None)
|
||||
if my_num and hasattr(my_num, 'my_node_num'):
|
||||
for n in self.interface.nodes.values():
|
||||
if n.get("num") == my_num.my_node_num:
|
||||
result["device"]["long_name"] = n.get("user", {}).get("longName", "")
|
||||
result["device"]["short_name"] = n.get("user", {}).get("shortName", "")
|
||||
result["device"]["hw_model"] = n.get("user", {}).get("hwModel", result["device"].get("hw_model", ""))
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def _on_connection(self, interface, topic=pub.AUTO_TOPIC):
|
||||
logger.info("Meshtastic connection established")
|
||||
if hasattr(interface, 'nodes') and interface.nodes:
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ class WebServer:
|
|||
self.app.router.add_put("/api/scheduler/jobs/{name}", self._api_scheduler_update)
|
||||
self.app.router.add_delete("/api/scheduler/jobs/{name}", self._api_scheduler_delete)
|
||||
self.app.router.add_post("/api/send", self._api_send)
|
||||
self.app.router.add_get("/api/node/config", self._api_node_config)
|
||||
self.app.router.add_get("/settings", self._serve_settings)
|
||||
self.app.router.add_get("/scheduler", self._serve_scheduler)
|
||||
self.app.router.add_get("/map", self._serve_map)
|
||||
self.app.router.add_get("/", self._serve_index)
|
||||
|
|
@ -107,6 +109,19 @@ class WebServer:
|
|||
await self.bot.send_message(text, channel)
|
||||
return web.json_response({"ok": True})
|
||||
|
||||
async def _api_node_config(self, request: web.Request) -> web.Response:
|
||||
if not self.bot:
|
||||
return web.json_response({"error": "Bot not available"}, status=503)
|
||||
try:
|
||||
cfg = self.bot.get_node_config()
|
||||
return web.json_response(cfg)
|
||||
except Exception:
|
||||
logger.exception("Error getting node config")
|
||||
return web.json_response({"error": "Failed to get config"}, status=500)
|
||||
|
||||
async def _serve_settings(self, request: web.Request) -> web.Response:
|
||||
return web.FileResponse(os.path.join(STATIC_DIR, "settings.html"))
|
||||
|
||||
async def _serve_index(self, request: web.Request) -> web.Response:
|
||||
return web.FileResponse(os.path.join(STATIC_DIR, "index.html"))
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,41 @@
|
|||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* ── Map wrapper ─────────────────────────────────── */
|
||||
|
||||
.map-wrapper {
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-wrapper #map {
|
||||
width: 100%;
|
||||
height: calc(100vh - 46px);
|
||||
}
|
||||
|
||||
.node-tooltip { font-size: 13px; line-height: 1.6; }
|
||||
.node-tooltip strong { color: #0f3460; }
|
||||
.node-tooltip .label { color: #666; }
|
||||
.node-tooltip-wrap { padding: 0; }
|
||||
|
||||
.legend {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.legend strong { display: block; margin-bottom: 4px; color: #333; }
|
||||
.legend-item { display: flex; align-items: center; gap: 8px; }
|
||||
.legend-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(0,0,0,0.2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Scrollbar ───────────────────────────────────── */
|
||||
|
||||
.table-responsive::-webkit-scrollbar,
|
||||
|
|
|
|||
|
|
@ -38,9 +38,12 @@
|
|||
<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">
|
||||
<a href="/map" class="sidebar-link">
|
||||
<i class="bi bi-map"></i><span>Karte</span>
|
||||
</a>
|
||||
<a href="/settings" class="sidebar-link">
|
||||
<i class="bi bi-gear"></i><span>Einstellungen</span>
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
const map = L.map('map').setView([51.1657, 10.4515], 6);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
const markers = {};
|
||||
const nodeData = {};
|
||||
const statusDot = document.getElementById('statusDot');
|
||||
|
|
@ -13,14 +6,14 @@ const nodeCount = document.getElementById('nodeCount');
|
|||
let ws;
|
||||
|
||||
const hopColors = {
|
||||
0: '#2196F3', // Direkt - Blau
|
||||
1: '#4CAF50', // 1 Hop - Grün
|
||||
2: '#FF9800', // 2 Hops - Orange
|
||||
3: '#F44336', // 3 Hops - Rot
|
||||
4: '#9C27B0', // 4 Hops - Lila
|
||||
5: '#795548', // 5 Hops - Braun
|
||||
0: '#2196F3',
|
||||
1: '#4CAF50',
|
||||
2: '#FF9800',
|
||||
3: '#F44336',
|
||||
4: '#9C27B0',
|
||||
5: '#795548',
|
||||
};
|
||||
const hopColorDefault = '#9E9E9E'; // Unbekannt - Grau
|
||||
const hopColorDefault = '#9E9E9E';
|
||||
|
||||
function getHopColor(hopCount) {
|
||||
if (hopCount == null) return hopColorDefault;
|
||||
|
|
@ -56,7 +49,7 @@ function nodeTooltip(node) {
|
|||
<span class="label">Hops:</span> ${hops}<br>
|
||||
<span class="label">SNR:</span> ${snr}<br>
|
||||
<span class="label">Batterie:</span> ${bat}<br>
|
||||
<span class="label">Höhe:</span> ${alt}<br>
|
||||
<span class="label">Hoehe:</span> ${alt}<br>
|
||||
<span class="label">Zuletzt:</span> ${lastSeen}
|
||||
</div>`;
|
||||
}
|
||||
|
|
@ -147,6 +140,45 @@ legend.onAdd = function () {
|
|||
});
|
||||
return div;
|
||||
};
|
||||
|
||||
// Theme toggle
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
const themeIcon = document.getElementById('themeIcon');
|
||||
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.setAttribute('data-bs-theme', theme);
|
||||
themeIcon.className = theme === 'dark' ? 'bi bi-sun-fill' : 'bi bi-moon-fill';
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
|
||||
applyTheme(localStorage.getItem('theme') || 'dark');
|
||||
|
||||
themeToggle.addEventListener('click', () => {
|
||||
const current = document.documentElement.getAttribute('data-bs-theme');
|
||||
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'));
|
||||
}
|
||||
|
||||
// Init map after layout is ready
|
||||
const map = L.map('map').setView([51.1657, 10.4515], 6);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
legend.addTo(map);
|
||||
|
||||
// Invalidate size after short delay so sidebar layout settles
|
||||
setTimeout(() => { map.invalidateSize(); }, 200);
|
||||
|
||||
connectWebSocket();
|
||||
|
|
|
|||
133
static/js/settings.js
Normal file
133
static/js/settings.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
// Labels for display
|
||||
const deviceLabels = {
|
||||
long_name: "Name",
|
||||
short_name: "Kurzname",
|
||||
node_num: "Node-Nummer",
|
||||
hw_model: "Hardware",
|
||||
firmware_version: "Firmware",
|
||||
role: "Role",
|
||||
max_channels: "Max Channels",
|
||||
};
|
||||
|
||||
const loraLabels = {
|
||||
region: "Region",
|
||||
modem_preset: "Modem-Preset",
|
||||
hop_limit: "Hop-Limit",
|
||||
tx_power: "TX-Power",
|
||||
bandwidth: "Bandwidth",
|
||||
frequency_offset: "Freq-Offset",
|
||||
tx_enabled: "TX aktiviert",
|
||||
use_preset: "Preset nutzen",
|
||||
};
|
||||
|
||||
const positionLabels = {
|
||||
gps_mode: "GPS-Modus",
|
||||
position_broadcast_secs: "Broadcast-Interval (s)",
|
||||
fixed_position: "Feste Position",
|
||||
};
|
||||
|
||||
const powerLabels = {
|
||||
mesh_sds_timeout_secs: "Mesh-SDS-Timeout (s)",
|
||||
ls_secs: "LS-Secs",
|
||||
min_wake_secs: "Min-Wake-Secs",
|
||||
is_power_saving: "Energiesparmodus",
|
||||
};
|
||||
|
||||
function renderKeyValue(tableId, data, labels) {
|
||||
const tbody = document.getElementById(tableId);
|
||||
const rows = [];
|
||||
for (const [key, label] of Object.entries(labels)) {
|
||||
const val = data[key];
|
||||
if (val === "" || val === undefined || val === null) continue;
|
||||
const display = typeof val === "boolean" ? (val ? "Ja" : "Nein") : String(val);
|
||||
rows.push(`<tr><td class="text-body-secondary" style="width:45%">${escapeHtml(label)}</td><td>${escapeHtml(display)}</td></tr>`);
|
||||
}
|
||||
tbody.innerHTML = rows.length > 0 ? rows.join("") : '<tr><td colspan="2" class="text-center text-body-secondary py-2">Keine Daten</td></tr>';
|
||||
}
|
||||
|
||||
function renderChannels(channels) {
|
||||
const tbody = document.getElementById("channelsTable");
|
||||
if (!channels || channels.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="text-center text-body-secondary py-2">Keine Channels</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = channels.map(ch =>
|
||||
`<tr><td>${ch.index}</td><td>${escapeHtml(ch.name)}</td><td>${escapeHtml(ch.role)}</td><td>${escapeHtml(ch.psk)}</td></tr>`
|
||||
).join("");
|
||||
}
|
||||
|
||||
function renderBtNet(bluetooth, network) {
|
||||
const tbody = document.getElementById("btNetTable");
|
||||
const rows = [];
|
||||
const btLabels = { enabled: "BT aktiviert", mode: "BT-Modus" };
|
||||
const netLabels = { wifi_enabled: "WiFi aktiviert", ntp_server: "NTP-Server" };
|
||||
|
||||
for (const [key, label] of Object.entries(btLabels)) {
|
||||
const val = bluetooth[key];
|
||||
if (val === "" || val === undefined || val === null) continue;
|
||||
const display = typeof val === "boolean" ? (val ? "Ja" : "Nein") : String(val);
|
||||
rows.push(`<tr><td class="text-body-secondary" style="width:45%">${escapeHtml(label)}</td><td>${escapeHtml(display)}</td></tr>`);
|
||||
}
|
||||
for (const [key, label] of Object.entries(netLabels)) {
|
||||
const val = network[key];
|
||||
if (val === "" || val === undefined || val === null) continue;
|
||||
const display = typeof val === "boolean" ? (val ? "Ja" : "Nein") : String(val);
|
||||
rows.push(`<tr><td class="text-body-secondary" style="width:45%">${escapeHtml(label)}</td><td>${escapeHtml(display)}</td></tr>`);
|
||||
}
|
||||
tbody.innerHTML = rows.length > 0 ? rows.join("") : '<tr><td colspan="2" class="text-center text-body-secondary py-2">Keine Daten</td></tr>';
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const resp = await fetch("/api/node/config");
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
|
||||
renderKeyValue("deviceTable", data.device || {}, deviceLabels);
|
||||
renderKeyValue("loraTable", data.lora || {}, loraLabels);
|
||||
renderChannels(data.channels || []);
|
||||
renderKeyValue("positionTable", data.position || {}, positionLabels);
|
||||
renderKeyValue("powerTable", data.power || {}, powerLabels);
|
||||
renderBtNet(data.bluetooth || {}, data.network || {});
|
||||
} catch (e) {
|
||||
console.error("Failed to load config:", e);
|
||||
document.getElementById("deviceTable").innerHTML =
|
||||
'<tr><td colspan="2" class="text-center text-danger py-2">Fehler beim Laden</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return "";
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Theme toggle
|
||||
const themeToggle = document.getElementById("themeToggle");
|
||||
const themeIcon = document.getElementById("themeIcon");
|
||||
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.setAttribute("data-bs-theme", theme);
|
||||
themeIcon.className = theme === "dark" ? "bi bi-sun-fill" : "bi bi-moon-fill";
|
||||
localStorage.setItem("theme", theme);
|
||||
}
|
||||
|
||||
applyTheme(localStorage.getItem("theme") || "dark");
|
||||
|
||||
themeToggle.addEventListener("click", () => {
|
||||
const current = document.documentElement.getAttribute("data-bs-theme");
|
||||
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"));
|
||||
}
|
||||
|
||||
loadConfig();
|
||||
121
static/map.html
121
static/map.html
|
|
@ -1,84 +1,59 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<html lang="de" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MeshDD-Bot Karte</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.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="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Segoe UI', system-ui, sans-serif; }
|
||||
#map { width: 100vw; height: 100vh; }
|
||||
.node-tooltip { font-size: 13px; line-height: 1.6; }
|
||||
.node-tooltip strong { color: #0f3460; }
|
||||
.node-tooltip .label { color: #666; }
|
||||
.node-tooltip-wrap { padding: 0; }
|
||||
.node-tooltip-wrap .leaflet-tooltip-content { margin: 0; }
|
||||
.legend {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.legend strong { display: block; margin-bottom: 4px; color: #333; }
|
||||
.legend-item { display: flex; align-items: center; gap: 8px; }
|
||||
.legend-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(0,0,0,0.2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.status-bar {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 1000;
|
||||
background: rgba(26, 26, 46, 0.9);
|
||||
color: #e0e0e0;
|
||||
padding: 8px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.status-bar .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #ff5252;
|
||||
}
|
||||
.status-bar .dot.connected {
|
||||
background: #00e676;
|
||||
box-shadow: 0 0 6px #00e676;
|
||||
}
|
||||
.back-link {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 1000;
|
||||
background: rgba(26, 26, 46, 0.9);
|
||||
color: #00d4ff;
|
||||
padding: 8px 14px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
.back-link:hover { background: rgba(26, 26, 46, 1); }
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<a href="/" class="back-link">Dashboard</a>
|
||||
<div class="status-bar">
|
||||
<span class="dot" id="statusDot"></span>
|
||||
<span id="statusText">Verbinde...</span>
|
||||
<span>|</span>
|
||||
<span id="nodeCount">0 Nodes</span>
|
||||
</div>
|
||||
<!-- 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>
|
||||
<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>
|
||||
<small class="text-body-secondary" id="nodeCount">0 Nodes</small>
|
||||
<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>
|
||||
|
||||
<!-- 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">
|
||||
<i class="bi bi-clock-history"></i><span>Scheduler</span>
|
||||
</a>
|
||||
<a href="/map" class="sidebar-link active">
|
||||
<i class="bi bi-map"></i><span>Karte</span>
|
||||
</a>
|
||||
<a href="/settings" class="sidebar-link">
|
||||
<i class="bi bi-gear"></i><span>Einstellungen</span>
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
|
||||
|
||||
<!-- Content -->
|
||||
<main class="content-wrapper map-wrapper">
|
||||
<div id="map"></div>
|
||||
</main>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="/static/js/map.js"></script>
|
||||
|
|
|
|||
|
|
@ -31,9 +31,12 @@
|
|||
<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">
|
||||
<a href="/map" class="sidebar-link">
|
||||
<i class="bi bi-map"></i><span>Karte</span>
|
||||
</a>
|
||||
<a href="/settings" class="sidebar-link">
|
||||
<i class="bi bi-gear"></i><span>Einstellungen</span>
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
|
|
|
|||
159
static/settings.html
Normal file
159
static/settings.html
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MeshDD-Bot Einstellungen</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.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>
|
||||
<!-- 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>
|
||||
</nav>
|
||||
|
||||
<!-- 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">
|
||||
<i class="bi bi-clock-history"></i><span>Scheduler</span>
|
||||
</a>
|
||||
<a href="/map" class="sidebar-link">
|
||||
<i class="bi bi-map"></i><span>Karte</span>
|
||||
</a>
|
||||
<a href="/settings" class="sidebar-link active">
|
||||
<i class="bi bi-gear"></i><span>Einstellungen</span>
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
|
||||
|
||||
<!-- Content -->
|
||||
<main class="content-wrapper">
|
||||
<h6 class="mb-2"><i class="bi bi-gear me-1 text-info"></i>Node-Einstellungen</h6>
|
||||
|
||||
<div class="row g-2">
|
||||
<!-- Device -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card card-outline card-info">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-cpu me-1"></i>Geraet
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<tbody id="deviceTable">
|
||||
<tr><td colspan="2" class="text-center text-body-secondary py-3">Lade...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LoRa -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card card-outline card-info">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-broadcast me-1"></i>LoRa
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<tbody id="loraTable">
|
||||
<tr><td colspan="2" class="text-center text-body-secondary py-3">Lade...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channels -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card card-outline card-warning">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-list-ul me-1"></i>Channels
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Index</th>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>PSK</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="channelsTable">
|
||||
<tr><td colspan="4" class="text-center text-body-secondary py-3">Lade...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Position -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card card-outline card-success">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-geo-alt me-1"></i>Position
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<tbody id="positionTable">
|
||||
<tr><td colspan="2" class="text-center text-body-secondary py-3">Lade...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Power -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card card-outline card-primary">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-battery-charging me-1"></i>Power
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<tbody id="powerTable">
|
||||
<tr><td colspan="2" class="text-center text-body-secondary py-3">Lade...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bluetooth & Network -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card card-outline">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-bluetooth me-1"></i>Bluetooth & Netzwerk
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<tbody id="btNetTable">
|
||||
<tr><td colspan="2" class="text-center text-body-secondary py-3">Lade...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</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/settings.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue