Merge branch 'audit/telephony-webserver'

This commit is contained in:
Clément SAILLANT
2026-02-21 21:28:01 +01:00
4 changed files with 273 additions and 1 deletions
+38
View File
@@ -32,6 +32,29 @@
<button id="refreshConfigBtn">Rafraîchir configuration</button>
</div>
<pre id="configJson"></pre>
<div class="grid2">
<div>
<h3>Audio config (JSON)</h3>
<textarea id="audioConfigInput" rows="8" placeholder='{"volume":80}'></textarea>
<div class="inline-actions">
<button id="applyAudioConfigBtn">Appliquer audio</button>
</div>
</div>
<div>
<h3>MQTT config (JSON)</h3>
<textarea id="mqttConfigInput" rows="8" placeholder='{"enabled":false}'></textarea>
<div class="inline-actions">
<button id="applyMqttConfigBtn">Appliquer MQTT</button>
</div>
</div>
<div>
<h3>Pins config (JSON)</h3>
<textarea id="pinsConfigInput" rows="8" placeholder='{"slic":{"line":23}}'></textarea>
<div class="inline-actions">
<button id="applyPinsConfigBtn">Appliquer pins</button>
</div>
</div>
</div>
</section>
<section id="networkSection">
@@ -86,6 +109,21 @@
<pre id="espnowJson"></pre>
<pre id="espnowPeersJson"></pre>
</div>
<div>
<h3>Bluetooth</h3>
<form id="btHfpConnectForm">
<input type="text" id="btHfpAddr" placeholder="MAC téléphone HFP (AA:BB:CC:DD:EE:FF)">
<button type="submit">HFP Connect</button>
</form>
<div class="inline-actions">
<button id="btHfpDisconnectBtn">HFP Disconnect</button>
<button id="btBleStartBtn">BLE Start</button>
<button id="btBleStopBtn">BLE Stop</button>
<button id="btRefreshBtn">Rafraîchir BT</button>
</div>
<pre id="btJson"></pre>
</div>
</div>
</section>
+134 -1
View File
@@ -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");
});
+1
View File
@@ -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 |
@@ -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`.