MeshDD-Bot/static/js/settings.js
ppfeiffer d3c3806ed5 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>
2026-02-15 18:06:32 +01:00

134 lines
5.1 KiB
JavaScript

// 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();