From 8ef445eac3393e9a15ab1fa702cf92734b875129 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cl=C3=A9ment=20SAILLANT?=
<108685187+electron-rare@users.noreply.github.com>
Date: Fri, 20 Feb 2026 23:26:32 +0100
Subject: [PATCH] fix web ui to match exposed firmware api
---
data/webui/index.html | 106 +++++++----
data/webui/script.js | 408 +++++++++++++++++++++++++-----------------
data/webui/style.css | 43 +++--
3 files changed, 337 insertions(+), 220 deletions(-)
diff --git a/data/webui/index.html b/data/webui/index.html
index b2e0681..2e17a04 100644
--- a/data/webui/index.html
+++ b/data/webui/index.html
@@ -3,63 +3,99 @@
- RTC_BL_PHONE WebUI
+ RTC_BL_PHONE Web UI
- RTC_BL_PHONE Dashboard
- Chargement...
+ RTC_BL_PHONE Web UI
+ Chargement...
-
diff --git a/data/webui/script.js b/data/webui/script.js
index 6a04322..7584b9a 100644
--- a/data/webui/script.js
+++ b/data/webui/script.js
@@ -1,155 +1,130 @@
-let contactsData = [];
+const SECTION_MAP = {
+ dashboard: "dashboardSection",
+ config: "configSection",
+ network: "networkSection",
+ control: "controlSection",
+};
function showSection(section) {
- const map = {
- contacts: "contactsSection",
- config: "configSection",
- logs: "logsSection",
- control: "controlSection",
- };
- Object.values(map).forEach((id) => {
+ Object.values(SECTION_MAP).forEach((id) => {
const el = document.getElementById(id);
if (el) {
el.classList.remove("active");
}
});
- const sectionEl = document.getElementById(map[section]);
+ const sectionEl = document.getElementById(SECTION_MAP[section]);
if (sectionEl) {
sectionEl.classList.add("active");
}
}
-async function safeFetchJson(url, options = {}) {
- const response = await fetch(url, options);
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
+function setJson(id, value) {
+ const el = document.getElementById(id);
+ if (!el) {
+ return;
}
- return response.json();
+ if (typeof value === "string") {
+ el.textContent = value;
+ return;
+ }
+ el.textContent = JSON.stringify(value, null, 2);
+}
+
+async function requestJson(url, options = {}) {
+ const response = await fetch(url, options);
+ const text = await response.text();
+ let parsed = {};
+ if (text) {
+ try {
+ parsed = JSON.parse(text);
+ } catch (_) {
+ parsed = { raw: text };
+ }
+ }
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status} ${text || ""}`.trim());
+ }
+ return parsed;
+}
+
+function jsonHeaders() {
+ return { "Content-Type": "application/json" };
+}
+
+function parsePayloadValue(rawPayload) {
+ const trimmed = rawPayload.trim();
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
+ try {
+ return JSON.parse(trimmed);
+ } catch (_) {
+ return rawPayload;
+ }
+ }
+ return rawPayload;
}
async function refreshStatus() {
- const status = document.getElementById("status");
+ const line = document.getElementById("statusLine");
try {
- const data = await safeFetchJson("/api/status");
- status.textContent =
- `state=${data.state} board=${data.board_profile || "n/a"} ` +
- `telephony=${data.telephony || "n/a"} hook=${data.hook || "n/a"} ` +
- `full_duplex=${data.full_duplex} underrun=${data.audio_underrun_count || 0} ` +
- `drop=${data.audio_drop_frames || 0}`;
+ const status = await requestJson("/api/status");
+ const telephonyState = status.telephony?.state || "n/a";
+ const hook = status.telephony?.hook || "n/a";
+ const wifiState = status.wifi?.state || "n/a";
+ const mqttConnected = status.mqtt?.connected ? "on" : "off";
+ const peers = status.espnow?.peer_count ?? 0;
+ line.textContent =
+ `board=${status.board_profile || "n/a"} telephony=${telephonyState} hook=${hook} ` +
+ `wifi=${wifiState} mqtt=${mqttConnected} espnow_peers=${peers}`;
+ setJson("statusJson", status);
} catch (error) {
- status.textContent = `Erreur statut: ${error.message}`;
+ line.textContent = `Erreur statut: ${error.message}`;
+ setJson("statusJson", { error: error.message });
}
}
-async function loadContacts() {
+async function refreshConfig() {
try {
- contactsData = await safeFetchJson("/api/contacts");
- renderContacts();
+ const [pins, audio, mqtt] = await Promise.all([
+ requestJson("/api/config/pins"),
+ requestJson("/api/config/audio"),
+ requestJson("/api/config/mqtt"),
+ ]);
+ setJson("configJson", { pins, audio, mqtt });
} catch (error) {
- document.getElementById("contactFeedback").textContent = `Erreur contacts: ${error.message}`;
+ setJson("configJson", { error: error.message });
}
}
-function renderContacts() {
- const list = document.getElementById("contactsList");
- const searchInput = document.getElementById("searchContact");
- const search = (searchInput?.value || "").toLowerCase();
- list.innerHTML = "";
-
- contactsData
- .filter((c) => c.nom.toLowerCase().includes(search) || c.numero.includes(search))
- .forEach((c, idx) => {
- const card = document.createElement("div");
- card.className = "contact-card";
- card.innerHTML = `${c.nom}
${c.numero}
${c.type}`;
-
- const actions = document.createElement("div");
- actions.className = "contact-actions";
- actions.innerHTML =
- `` +
- `` +
- ``;
- card.appendChild(actions);
- list.appendChild(card);
- });
-}
-
-function editContact(idx) {
- const c = contactsData[idx];
- if (!c) {
- return;
- }
- const form = document.getElementById("contactForm");
- form.nom.value = c.nom;
- form.numero.value = c.numero;
- form.type.value = c.type;
- form.dataset.editIdx = String(idx);
-}
-
-async function deleteContact(idx) {
- const response = await fetch("/api/contacts", {
- method: "DELETE",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ idx }),
- });
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
- await loadContacts();
- document.getElementById("contactFeedback").textContent = "Contact supprimé";
-}
-
-async function callContact(numero) {
- await sendControl("call", { numero });
- document.getElementById("contactFeedback").textContent = `Appel lancé vers ${numero}`;
-}
-
-async function loadConfig() {
+async function refreshNetwork() {
try {
- const data = await safeFetchJson("/api/config");
- document.getElementById("config").textContent = JSON.stringify(data, null, 2);
+ const [wifi, mqtt, espnow, peers] = await Promise.all([
+ requestJson("/api/network/wifi"),
+ requestJson("/api/network/mqtt"),
+ requestJson("/api/network/espnow"),
+ requestJson("/api/network/espnow/peer"),
+ ]);
+ setJson("wifiJson", wifi);
+ setJson("mqttJson", mqtt);
+ setJson("espnowJson", espnow);
+ setJson("espnowPeersJson", peers);
} catch (error) {
- document.getElementById("config").textContent = `Erreur config: ${error.message}`;
+ const err = { error: error.message };
+ setJson("wifiJson", err);
+ setJson("mqttJson", err);
+ setJson("espnowJson", err);
+ setJson("espnowPeersJson", err);
}
}
-async function saveConfig(event) {
- event.preventDefault();
- const form = event.target;
- const payload = {
- param1: form.param1.value || "valeur1",
- param2: form.param2.value || "valeur2",
- };
- const response = await fetch("/api/config", {
+async function sendControl(action) {
+ const result = await requestJson("/api/control", {
method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(payload),
+ headers: jsonHeaders(),
+ body: JSON.stringify({ action }),
});
- if (!response.ok) {
- document.getElementById("config").textContent = `Erreur config: HTTP ${response.status}`;
- return;
- }
- await loadConfig();
-}
-
-async function refreshLogs() {
- const response = await fetch("/api/logs");
- const logs = response.ok ? await response.text() : `Erreur logs: HTTP ${response.status}`;
- document.getElementById("logs").textContent = logs;
-}
-
-async function sendControl(action, extraPayload = {}) {
- const response = await fetch("/api/control", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ action, ...extraPayload }),
- });
- const body = await response.text();
- document.getElementById("controlResult").textContent = body;
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
- return true;
+ setJson("actionResult", result);
+ await Promise.all([refreshStatus(), refreshNetwork()]);
+ return result;
}
function bindEvents() {
@@ -157,58 +132,147 @@ function bindEvents() {
button.addEventListener("click", () => showSection(button.dataset.section));
});
- document.getElementById("refreshStatusBtn").addEventListener("click", refreshStatus);
- document.getElementById("refreshLogsBtn").addEventListener("click", refreshLogs);
- document.getElementById("searchContact").addEventListener("input", renderContacts);
- document.getElementById("configForm").addEventListener("submit", saveConfig);
+ document.getElementById("refreshAllBtn").addEventListener("click", async () => {
+ await Promise.all([refreshStatus(), refreshConfig(), refreshNetwork()]);
+ });
+ document.getElementById("refreshConfigBtn").addEventListener("click", refreshConfig);
+ document.getElementById("espnowRefreshBtn").addEventListener("click", refreshNetwork);
- document.getElementById("contactForm").addEventListener("submit", async (event) => {
+ document.getElementById("wifiConnectForm").addEventListener("submit", async (event) => {
event.preventDefault();
- const form = event.target;
- const editIdxRaw = form.dataset.editIdx;
- const hasEditIdx = typeof editIdxRaw !== "undefined";
- const payload = {
- nom: form.nom.value,
- numero: form.numero.value,
- type: form.type.value,
- };
- const method = hasEditIdx ? "PUT" : "POST";
- const body = hasEditIdx ? { ...payload, idx: Number(editIdxRaw) } : payload;
-
- const response = await fetch("/api/contacts", {
- method,
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(body),
- });
- if (!response.ok) {
- document.getElementById("contactFeedback").textContent = `Erreur contact: HTTP ${response.status}`;
- return;
+ const ssid = document.getElementById("wifiSsid").value.trim();
+ const pass = document.getElementById("wifiPass").value;
+ try {
+ const result = await requestJson("/api/network/wifi/connect", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: JSON.stringify({ ssid, pass }),
+ });
+ setJson("actionResult", result);
+ await Promise.all([refreshStatus(), refreshNetwork()]);
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
}
- delete form.dataset.editIdx;
- form.reset();
- document.getElementById("contactFeedback").textContent = hasEditIdx
- ? "Contact modifié"
- : "Contact ajouté";
- await loadContacts();
});
- document.getElementById("contactsList").addEventListener("click", async (event) => {
- const target = event.target;
- if (!(target instanceof HTMLElement)) {
- return;
- }
+ document.getElementById("wifiDisconnectBtn").addEventListener("click", async () => {
try {
- if (target.dataset.call) {
- await callContact(target.dataset.call);
- }
- if (target.dataset.edit) {
- editContact(Number(target.dataset.edit));
- }
- if (target.dataset.delete) {
- await deleteContact(Number(target.dataset.delete));
- }
+ const result = await requestJson("/api/network/wifi/disconnect", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: "{}",
+ });
+ setJson("actionResult", result);
+ await Promise.all([refreshStatus(), refreshNetwork()]);
} catch (error) {
- document.getElementById("contactFeedback").textContent = error.message;
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("wifiScanBtn").addEventListener("click", async () => {
+ try {
+ const result = await requestJson("/api/network/wifi/scan", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: "{}",
+ });
+ setJson("actionResult", result);
+ await refreshNetwork();
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("mqttConnectBtn").addEventListener("click", async () => {
+ try {
+ const result = await requestJson("/api/network/mqtt/connect", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: "{}",
+ });
+ setJson("actionResult", result);
+ await Promise.all([refreshStatus(), refreshNetwork()]);
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("mqttDisconnectBtn").addEventListener("click", async () => {
+ try {
+ const result = await requestJson("/api/network/mqtt/disconnect", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: "{}",
+ });
+ setJson("actionResult", result);
+ await Promise.all([refreshStatus(), refreshNetwork()]);
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("espnowOnBtn").addEventListener("click", async () => {
+ try {
+ await sendControl("ESPNOW_ON");
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("espnowOffBtn").addEventListener("click", async () => {
+ try {
+ await sendControl("ESPNOW_OFF");
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("espnowPeerForm").addEventListener("submit", async (event) => {
+ event.preventDefault();
+ const mac = document.getElementById("espnowMac").value.trim();
+ try {
+ const result = await requestJson("/api/network/espnow/peer", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: JSON.stringify({ mac }),
+ });
+ setJson("actionResult", result);
+ await refreshNetwork();
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("espnowDelBtn").addEventListener("click", async () => {
+ const mac = document.getElementById("espnowMac").value.trim();
+ try {
+ const result = await requestJson("/api/network/espnow/peer", {
+ method: "DELETE",
+ headers: jsonHeaders(),
+ body: JSON.stringify({ mac }),
+ });
+ setJson("actionResult", result);
+ await refreshNetwork();
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
+
+ document.getElementById("espnowSendForm").addEventListener("submit", async (event) => {
+ event.preventDefault();
+ const target = document.getElementById("espnowTarget").value.trim();
+ const payloadRaw = document.getElementById("espnowPayload").value;
+ const payload = parsePayloadValue(payloadRaw);
+ try {
+ const result = await requestJson("/api/network/espnow/send", {
+ method: "POST",
+ headers: jsonHeaders(),
+ body: JSON.stringify({ mac: target, payload }),
+ });
+ setJson("actionResult", result);
+ await refreshNetwork();
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
}
});
@@ -217,14 +281,24 @@ function bindEvents() {
try {
await sendControl(button.dataset.action);
} catch (error) {
- document.getElementById("controlResult").textContent = error.message;
+ setJson("actionResult", { error: error.message });
}
});
});
+
+ document.getElementById("rawCommandForm").addEventListener("submit", async (event) => {
+ event.preventDefault();
+ const action = document.getElementById("rawCommandInput").value.trim();
+ try {
+ await sendControl(action);
+ } catch (error) {
+ setJson("actionResult", { error: error.message });
+ }
+ });
}
document.addEventListener("DOMContentLoaded", async () => {
bindEvents();
- await Promise.all([refreshStatus(), loadContacts(), loadConfig(), refreshLogs()]);
- showSection("contacts");
+ await Promise.all([refreshStatus(), refreshConfig(), refreshNetwork()]);
+ showSection("dashboard");
});
diff --git a/data/webui/style.css b/data/webui/style.css
index 7cf69aa..8b71e44 100644
--- a/data/webui/style.css
+++ b/data/webui/style.css
@@ -6,6 +6,7 @@
--accent-strong: #084a81;
--ok: #19753d;
--border: #d8dee4;
+ --warn: #9a5f00;
}
* {
@@ -61,31 +62,28 @@ section.active {
display: block;
}
-#contactsList {
+.grid2 {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
- gap: 12px;
- margin-top: 10px;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 16px;
}
-.contact-card {
- border: 1px solid var(--border);
- border-radius: 8px;
- padding: 10px;
- background: #fcfdff;
-}
-
-.contact-actions {
- margin-top: 8px;
+.inline-actions {
+ margin: 8px 0;
display: flex;
- gap: 6px;
+ flex-wrap: wrap;
+ gap: 8px;
}
input,
-select {
+select,
+textarea {
padding: 8px;
border: 1px solid var(--border);
border-radius: 6px;
+ font: inherit;
+ color: inherit;
+ background: #fff;
}
form {
@@ -101,9 +99,18 @@ pre {
border-radius: 6px;
padding: 12px;
overflow-x: auto;
+ white-space: pre-wrap;
+ word-break: break-word;
}
-#contactFeedback {
- margin-top: 8px;
- color: var(--ok);
+@media (max-width: 760px) {
+ body {
+ padding: 12px;
+ }
+
+ button,
+ input,
+ textarea {
+ width: 100%;
+ }
}
--
2.54.0