Merge branch 'audit/telephony-webserver'
This commit is contained in:
@@ -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
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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`.
|
||||
Reference in New Issue
Block a user