diff --git a/data/webui/index.html b/data/webui/index.html index 27030f8..bef7f97 100644 --- a/data/webui/index.html +++ b/data/webui/index.html @@ -32,6 +32,29 @@

+      
+
+

Audio config (JSON)

+ +
+ +
+
+
+

MQTT config (JSON)

+ +
+ +
+
+
+

Pins config (JSON)

+ +
+ +
+
+
@@ -86,6 +109,21 @@

           

         
+
+        
+

Bluetooth

+
+ + +
+
+ + + + +
+

+        
diff --git a/data/webui/script.js b/data/webui/script.js index f664b66..5fc4943 100644 --- a/data/webui/script.js +++ b/data/webui/script.js @@ -51,6 +51,14 @@ function jsonHeaders() { return { "Content-Type": "application/json" }; } +function parseJsonInput(id) { + const raw = document.getElementById(id).value.trim(); + if (!raw) { + return {}; + } + return JSON.parse(raw); +} + function parsePayloadValue(rawPayload) { const trimmed = rawPayload.trim(); if (trimmed.startsWith("{") || trimmed.startsWith("[")) { @@ -90,6 +98,18 @@ async function refreshConfig() { requestJson("/api/config/mqtt"), ]); setJson("configJson", { pins, audio, mqtt }); + const audioInput = document.getElementById("audioConfigInput"); + const mqttInput = document.getElementById("mqttConfigInput"); + const pinsInput = document.getElementById("pinsConfigInput"); + if (audioInput) { + audioInput.value = JSON.stringify(audio, null, 2); + } + if (mqttInput) { + mqttInput.value = JSON.stringify(mqtt.config || mqtt, null, 2); + } + if (pinsInput) { + pinsInput.value = JSON.stringify(pins, null, 2); + } } catch (error) { setJson("configJson", { error: error.message }); } @@ -116,6 +136,15 @@ async function refreshNetwork() { } } +async function refreshBluetooth() { + try { + const bt = await requestJson("/api/bluetooth"); + setJson("btJson", bt); + } catch (error) { + setJson("btJson", { error: error.message }); + } +} + async function sendControl(action) { const result = await requestJson("/api/control", { method: "POST", @@ -137,6 +166,52 @@ function bindEvents() { }); document.getElementById("refreshConfigBtn").addEventListener("click", refreshConfig); document.getElementById("espnowRefreshBtn").addEventListener("click", refreshNetwork); + document.getElementById("btRefreshBtn").addEventListener("click", refreshBluetooth); + + document.getElementById("applyAudioConfigBtn").addEventListener("click", async () => { + try { + const payload = parseJsonInput("audioConfigInput"); + const result = await requestJson("/api/config/audio", { + method: "POST", + headers: jsonHeaders(), + body: JSON.stringify(payload), + }); + setJson("actionResult", result); + await Promise.all([refreshConfig(), refreshStatus()]); + } catch (error) { + setJson("actionResult", { error: error.message }); + } + }); + + document.getElementById("applyMqttConfigBtn").addEventListener("click", async () => { + try { + const payload = parseJsonInput("mqttConfigInput"); + const result = await requestJson("/api/config/mqtt", { + method: "POST", + headers: jsonHeaders(), + body: JSON.stringify(payload), + }); + setJson("actionResult", result); + await Promise.all([refreshConfig(), refreshNetwork(), refreshStatus()]); + } catch (error) { + setJson("actionResult", { error: error.message }); + } + }); + + document.getElementById("applyPinsConfigBtn").addEventListener("click", async () => { + try { + const payload = parseJsonInput("pinsConfigInput"); + const result = await requestJson("/api/config/pins", { + method: "POST", + headers: jsonHeaders(), + body: JSON.stringify(payload), + }); + setJson("actionResult", result); + await Promise.all([refreshConfig(), refreshStatus()]); + } catch (error) { + setJson("actionResult", { error: error.message }); + } + }); document.getElementById("wifiConnectForm").addEventListener("submit", async (event) => { event.preventDefault(); @@ -319,6 +394,64 @@ function bindEvents() { } }); + document.getElementById("btHfpConnectForm").addEventListener("submit", async (event) => { + event.preventDefault(); + const addr = document.getElementById("btHfpAddr").value.trim(); + try { + const result = await requestJson("/api/bluetooth/hfp/connect", { + method: "POST", + headers: jsonHeaders(), + body: JSON.stringify({ addr }), + }); + setJson("actionResult", result); + await Promise.all([refreshBluetooth(), refreshStatus()]); + } catch (error) { + setJson("actionResult", { error: error.message }); + } + }); + + document.getElementById("btHfpDisconnectBtn").addEventListener("click", async () => { + try { + const result = await requestJson("/api/bluetooth/hfp/disconnect", { + method: "POST", + headers: jsonHeaders(), + body: "{}", + }); + setJson("actionResult", result); + await Promise.all([refreshBluetooth(), refreshStatus()]); + } catch (error) { + setJson("actionResult", { error: error.message }); + } + }); + + document.getElementById("btBleStartBtn").addEventListener("click", async () => { + try { + const result = await requestJson("/api/bluetooth/ble/start", { + method: "POST", + headers: jsonHeaders(), + body: "{}", + }); + setJson("actionResult", result); + await Promise.all([refreshBluetooth(), refreshStatus()]); + } catch (error) { + setJson("actionResult", { error: error.message }); + } + }); + + document.getElementById("btBleStopBtn").addEventListener("click", async () => { + try { + const result = await requestJson("/api/bluetooth/ble/stop", { + method: "POST", + headers: jsonHeaders(), + body: "{}", + }); + setJson("actionResult", result); + await Promise.all([refreshBluetooth(), refreshStatus()]); + } catch (error) { + setJson("actionResult", { error: error.message }); + } + }); + document.querySelectorAll("#controlSection button[data-action]").forEach((button) => { button.addEventListener("click", async () => { try { @@ -342,6 +475,6 @@ function bindEvents() { document.addEventListener("DOMContentLoaded", async () => { bindEvents(); - await Promise.all([refreshStatus(), refreshConfig(), refreshNetwork()]); + await Promise.all([refreshStatus(), refreshConfig(), refreshNetwork(), refreshBluetooth()]); showSection("dashboard"); }); diff --git a/docs/README.md b/docs/README.md index 7c6333e..6985c7a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -36,6 +36,7 @@ Chaque fiche détaille : rôle, API, notifications, points de validation, synerg | Stack téléphone SFP | plan_stack_telephone_sfp.md | | Tests livraison | plan_tests_livraison.md | | WebUI | plan_webui.md | +| Spec WebUI route parity | spec_webui_route_parity_and_coverage_v1.md | ## Procédures QA & hardware | Procédure | Fichier | diff --git a/docs/spec_webui_route_parity_and_coverage_v1.md b/docs/spec_webui_route_parity_and_coverage_v1.md new file mode 100644 index 0000000..5ad97c5 --- /dev/null +++ b/docs/spec_webui_route_parity_and_coverage_v1.md @@ -0,0 +1,100 @@ +# Spécification Orchestrateur RTC — WebUI Route Parity & Coverage (v1) + +Date: 2026-02-20 +Repo: `electron-rare/RTC_BL_PHONE` +Issues liées: `#10`, `#11` + +## 1. Contexte + +Le backend HTTP firmware expose un ensemble de routes `/api/...` plus large que celles utilisées par la WebUI. +Des régressions passées ont montré que la dérive frontend/backend doit être contrôlée par un gate dédié. + +Références d'exécution: +- PR #7 (alignement routes WebUI/backend) +- PR #9 (hardening + validation hardware) +- PR #15 (correctif parser parity) + +## 2. Objectif + +Mettre en place une spécification unique qui couvre: +1. la parité stricte des routes appelées par la WebUI, +2. l'extension de la WebUI vers les routes backend encore non exposées. + +## 3. Périmètre + +In-scope: +- route parity CI entre: + - frontend: `data/webui/script.js` + - backend: `src/web/WebServerManager.cpp` +- couverture UI des endpoints backend existants non encore pilotés. + +Out-of-scope: +- redesign visuel complet de la WebUI, +- modification du contrat ESP-NOW inter-repos. + +## 4. Exigences fonctionnelles + +### EF-01 — Gate route parity +- Le script `scripts/check_web_route_parity.py` DOIT être exécuté en CI. +- Le gate DOIT échouer si une route appelée en WebUI n'existe pas côté backend. + +### EF-02 — Artefact de preuve parity +- Chaque run de gate DOIT produire un artefact lisible contenant: + - routes frontend détectées, + - routes backend détectées, + - delta (missing frontend->backend, backend non utilisés). + +### EF-03 — Couverture WebUI des routes backend +- La WebUI DOIT couvrir les routes de pilotage/lecture non encore exposées côté opérateur: + - `/api/bluetooth` + - `/api/bluetooth/hfp/connect` + - `/api/bluetooth/hfp/disconnect` + - `/api/bluetooth/ble/start` + - `/api/bluetooth/ble/stop` + - `/api/config/audio` + - `/api/config/mqtt` + - `/api/config/pins` + - `/api/network/mqtt` + - `/api/network/espnow` + - `/api/network/espnow/peer` + +### EF-04 — Non-régression des routes existantes +- Les flux déjà supportés DOIVENT rester fonctionnels: + - status global, + - wifi connect/disconnect/reconnect, + - mqtt connect/disconnect/publish, + - espnow on/off/send/peer, + - control actions. + +## 5. Exigences non fonctionnelles + +- Exécution gate parity < 3 secondes en CI standard. +- Zéro dépendance réseau externe pour le gate parity. +- Messages d'échec actionnables (route + méthode). + +## 6. Plan de livraison + +Phase A (gate): +1. stabiliser la sortie du checker parity, +2. l'intégrer à `ci.yml`, +3. publier un artefact parity. + +Phase B (coverage UI): +1. ajouter les blocs UI manquants, +2. connecter les endpoints listés en EF-03, +3. enrichir le rapport fonctionnel. + +## 7. Critères d'acceptation (DoD) + +- [ ] `scripts/check_web_route_parity.py` exécuté en CI. +- [ ] Échec CI confirmé quand une route frontend orpheline est injectée (test négatif). +- [ ] Les routes EF-03 sont accessibles depuis la WebUI. +- [ ] `docs/rapport_tests_fonctionnels.md` inclut la vérification de ces routes. +- [ ] Issue `#11` mise à jour avec run CI de preuve. +- [ ] Issue `#10` mise à jour avec captures/JSON de couverture. + +## 8. Coordination inter-repos + +- Pattern source: `Kill_LIFE` PR #2 (route parity gate pattern). +- Companion Zacus: issue `le-mystere-professeur-zacus#94`. +- Contrat ESP-NOW inchangé: `cmd/raw/command/action` + `event/message/payload`.