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`.