feat(mqtt,web): FR-015 + FR-016 sofortiger MQTT-Publish + WebSocket Live-Update (v1.6.0)
FR-015: - publishHeartbeat() nach Display-Toggle, Session-Reset und Reboot-CMD - publishHeartbeat() von private nach public (mqtt_client.h) - Heartbeat-Intervall 60s -> 10s (config.h) - HA Discovery Switch: state_on/state_off ergaenzt (mqtt_client.cpp) FR-016: - Neuer WebSocket-Endpunkt /status-ws (AsyncWebSocket) - sendStatusWs() am Ende von publishHeartbeat() -> alle Ausloeser abgedeckt - Statusseite: DOM-Updates via WebSocket, kein location.reload() mehr - Config-Seite: Reboot-Button live deaktiviert wenn Laser aktiv - Alle Action-Buttons auf fetch() umgestellt (Reboot, WLAN-Reset, Laufzeit-Reset) - Display-Button: blau+gelb wenn an, grau+grau wenn aus
This commit is contained in:
parent
e6ad5363ea
commit
fc5e1694fd
|
|
@ -19,31 +19,28 @@ Status: `[ ]` = offen · `[x]` = erledigt
|
|||
|
||||
## Offen
|
||||
|
||||
### Version 1.6.0
|
||||
|
||||
- [ ] **FR-015** Feature: Sofortiger MQTT-Status-Publish nach jeder Statusänderung
|
||||
- **Motivation**: HA zeigt aktuell veraltete Werte bis zum nächsten Heartbeat (60s). Änderungen durch Webinterface, MQTT-CMD oder lokale Buttons sollen sofort in HA sichtbar sein.
|
||||
- **Auslöser für sofortigen Publish**:
|
||||
- Display-Toggle (Web `/display-toggle` + MQTT CMD `{"display":...}`)
|
||||
- Session-Reset (Web `/reset` + MQTT CMD `{"reset_session_ram":true}`)
|
||||
- Reboot-CMD (MQTT CMD `{"reboot":true}`) — vor dem Neustart
|
||||
- **Mechanismus**: Nach jeder der o.g. Aktionen wird `publishHeartbeat()` sofort aufgerufen (zusätzlich zum 60s-Timer)
|
||||
- **Betroffene Dateien**: `src/web_server.cpp`, `src/mqtt_client.cpp`
|
||||
- Commit: `TODO`
|
||||
- Version: 1.6.0
|
||||
|
||||
- [ ] **FR-016** Feature: Webinterface Live-Aktualisierung ohne Neuladen (SSE oder Polling)
|
||||
- **Motivation**: Nach einer Aktion im Webinterface (z.B. Display-Toggle, Session-Reset) bleibt die Statusseite veraltet bis der Nutzer manuell neu lädt. Die Seite soll sich automatisch aktualisieren.
|
||||
- **Mechanismus**: Server-Sent Events (SSE) — ESP32 sendet bei Statusänderungen ein Event an alle offenen Browser-Verbindungen; die Seite aktualisiert daraufhin den Status-Bereich via JavaScript ohne vollständigen Reload. Alternativ: kurzes Polling-Intervall (z.B. alle 5s) als einfachere Fallback-Lösung.
|
||||
- **Betroffene Dateien**: `src/web_server.cpp` (SSE-Endpoint + Event-Publisher), HTML/JS in `buildStatusPage()`
|
||||
- Commit: `TODO`
|
||||
- Version: 1.6.0
|
||||
|
||||
---
|
||||
|
||||
## Erledigt
|
||||
|
||||
|
||||
### Version 1.6.0
|
||||
|
||||
- [x] **FR-015** Feature: Sofortiger MQTT-Status-Publish nach jeder Statusänderung ✅
|
||||
- **Motivation**: HA zeigte veraltete Werte bis zum nächsten Heartbeat. Änderungen durch Webinterface, MQTT-CMD oder lokale Buttons sind nun sofort in HA sichtbar.
|
||||
- **Umsetzung**: `publishHeartbeat()` von `private` nach `public` verschoben; wird nach Display-Toggle, Session-Reset und Reboot-CMD sofort aufgerufen (zusätzlich zum 10s-Timer). Heartbeat-Intervall von 60s auf 10s reduziert. HA MQTT Discovery Switch: `state_on`/`state_off` ergänzt damit Display-Zustand korrekt angezeigt wird.
|
||||
- **Betroffene Dateien**: `include/mqtt_client.h`, `src/web_server.cpp`, `src/mqtt_client.cpp`, `include/config.h`
|
||||
- Commit: `TODO`
|
||||
- Version: 1.6.0
|
||||
|
||||
- [x] **FR-016** Feature: Webinterface Live-Aktualisierung ohne Neuladen ✅
|
||||
- **Motivation**: Nach einer Aktion im Webinterface blieb die Statusseite veraltet bis zum manuellen Reload.
|
||||
- **Umsetzung**: Neuer WebSocket-Endpunkt `/status-ws` (`AsyncWebSocket`). `sendStatusWs()` wird am Ende von `publishHeartbeat()` aufgerufen – deckt damit alle Auslöser ab (10s-Timer, Web-Aktionen, Laser-Statuswechsel). Statusseite und Config-Seite nutzen `/status-ws` für Live-DOM-Updates ohne Reload. Reboot-Button auf Config-Seite wird automatisch deaktiviert wenn Laser aktiv. Alle Action-Buttons (Reboot, WLAN-Reset, Maschinenlaufzeit-Reset) auf `fetch()` umgestellt.
|
||||
- **Betroffene Dateien**: `include/web_server.h`, `src/web_server.cpp`, `src/mqtt_client.cpp`
|
||||
- Commit: `TODO`
|
||||
- Version: 1.6.0
|
||||
|
||||
|
||||
### Version 1.5.2
|
||||
|
||||
- [x] **FR-014** Bug: Display Störung bei Relais Umschaltung ✅
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -255,7 +255,7 @@ Der ESP32 verbindet sich mit einem MQTT-Broker (konfigurierbar über Webinterfac
|
|||
| Richtung | Topic | Format | Beschreibung |
|
||||
|------------|---------------------------------|------------------------------|-----------------------------------------|
|
||||
| Publish | `lasercutter/session` | JSON | Wird beim **Ende eines Laser-Bursts** oder nach **Session-Reset** gesendet |
|
||||
| Publish | `lasercutter/status` | JSON (retained) | Heartbeat alle 60 Sekunden |
|
||||
| Publish | `lasercutter/status` | JSON (retained) | Heartbeat alle **10 Sekunden**; sofortiger Publish nach jeder Statusänderung |
|
||||
| Publish | `lasercutter/availability` | `online` / `offline` | Verbindungsstatus (retained, LWT) |
|
||||
| Subscribe | `lasercutter/cmd` | JSON | Steuerkommandos: Reset, Display, Reboot |
|
||||
| Publish | `homeassistant/…/config` | JSON (retained) | HA MQTT Discovery – nach jedem Connect |
|
||||
|
|
@ -367,14 +367,15 @@ Das Webinterface ist über die IP-Adresse des ESP32 im Browser erreichbar.
|
|||
|
||||
| Seite | URL | Auth | Funktion |
|
||||
|--------------------------|-------------------|------|-----------------------------------------------------------------------|
|
||||
| Laser Cutter Status | `/` | – | Laserzeit, Laserstatus, Display-Toggle, Session-Reset |
|
||||
| Laser Cutter Status | `/` | – | Laserzeit, Laserstatus, Display-Toggle, Session-Reset; Live-Updates via WebSocket |
|
||||
| Laser Cutter Setup | `/config` | ✔ | MQTT, Web-Auth, WLAN, Maschinenlaufzeit-Reset, Gratiszeit, Polarität, Reboot |
|
||||
| Session-Reset | `/reset` | – | Setzt die **Session-Summe** (RAM) auf 0 – NVS bleibt erhalten |
|
||||
| Maschinenlaufzeit-Reset | `/reset-total` | ✔ | Setzt Maschinenlaufzeit im NVS auf 0 |
|
||||
| Display-Toggle | `/display-toggle` | – | Display ein-/ausschalten (POST, antwortet 204) |
|
||||
| Reboot | `/reboot` | ✔ | ESP32 sofort neu starten |
|
||||
| OTA Update | `/update` | – | Firmware-Update über Browser (ElegantOTA) |
|
||||
| Log Console | `/log` | ✔ | Live-Log über WebSocket |
|
||||
| Log Console | `/log` | ✔ | Live-Log über WebSocket (`/log-ws`) |
|
||||
| Status WebSocket | `/status-ws` | – | Live-Status-Push für Statusseite und Config-Seite |
|
||||
|
||||
> `/` ist **öffentlich** (kein Passwortschutz) – der Display-Toggle und Session-Reset sind bewusst ohne Auth erreichbar. Alle sicherheitsrelevanten Seiten (Setup, Reboot, Maschinenlaufzeit-Reset) erfordern HTTP Basic Auth, sofern ein Web-Passwort gesetzt ist.
|
||||
|
||||
|
|
@ -416,10 +417,11 @@ Firmware-Updates können kabellos über die Weboberfläche unter `/update` einge
|
|||
|
||||
## Remote-Debugging (Web Console)
|
||||
|
||||
Geplant (FR-002): Neue Route `/log` im bestehenden Webinterface zeigt alle Log-Ausgaben live im Browser über WebSocket.
|
||||
Die Route `/log` zeigt alle Log-Ausgaben live im Browser.
|
||||
|
||||
- Kein extra Library nötig – nutzt den bestehenden `ESPAsyncWebServer`
|
||||
- Aufruf: `http://ipadresse/log`
|
||||
- Live-Streaming über WebSocket (`/log-ws`) – kein Polling
|
||||
- Auto-Scroll, manuelles Leeren, farbkodierte Log-Level (Info/Debug/Error)
|
||||
- Aufruf: `http://<ip>/log` (Auth erforderlich wenn Web-Passwort gesetzt)
|
||||
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
#define DEFAULT_MQTT_PASSWORD ""
|
||||
#define MQTT_CLIENT_ID "lasercutter-display"
|
||||
#define MQTT_RECONNECT_MS 10000 // Reconnect-Intervall in ms
|
||||
#define MQTT_HEARTBEAT_MS 60000 // Heartbeat-Intervall in ms
|
||||
#define MQTT_HEARTBEAT_MS 10000 // Heartbeat-Intervall in ms
|
||||
|
||||
// MQTT Topics
|
||||
#define MQTT_TOPIC_SESSION "lasercutter/session" // Publish beim Session-Ende
|
||||
|
|
|
|||
|
|
@ -64,6 +64,10 @@ public:
|
|||
// Home Assistant MQTT Discovery (retained Config-Nachrichten für alle Entities)
|
||||
void publishDiscovery();
|
||||
|
||||
// Status-Heartbeat sofort senden (normalerweise alle 10s automatisch)
|
||||
// Kann von außen aufgerufen werden um HA sofort zu aktualisieren (FR-015)
|
||||
void publishHeartbeat();
|
||||
|
||||
// Debug-Ausgabe auf Serial
|
||||
void printToSerial();
|
||||
|
||||
|
|
@ -105,9 +109,6 @@ private:
|
|||
char _broker[64];
|
||||
uint16_t _port;
|
||||
|
||||
// Heartbeat-Publish (lasercutter/status)
|
||||
void publishHeartbeat();
|
||||
|
||||
// Eigentlicher Session-Publish (aus MQTT-Task auf Core 0)
|
||||
// Gibt true zurück wenn Publish erfolgreich, false bei Fehler (Queue-Eintrag bleibt dann erhalten)
|
||||
bool _doPublishSession(int lastSessionSec, int gratisSec, time_t startTime, uint32_t sessionId);;
|
||||
|
|
|
|||
|
|
@ -39,9 +39,13 @@ public:
|
|||
// ArduinoOTA verarbeiten – in jedem loop()-Durchlauf aufrufen
|
||||
void loop();
|
||||
|
||||
// WebSocket: Status-JSON an alle /status-ws Clients pushen
|
||||
void sendStatusWs();
|
||||
|
||||
private:
|
||||
AsyncWebServer* _server; // PIMPL: Pointer, full type only in web_server.cpp
|
||||
AsyncWebSocket* _ws; // WebSocket-Endpunkt /log-ws
|
||||
AsyncWebServer* _server; // PIMPL: Pointer, full type only in web_server.cpp
|
||||
AsyncWebSocket* _ws; // WebSocket-Endpunkt /log-ws
|
||||
AsyncWebSocket* _statusWs; // WebSocket-Endpunkt /status-ws
|
||||
|
||||
void registerRoutes();
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "mqtt_client.h"
|
||||
#include "laser_tracker.h"
|
||||
#include "display_manager.h"
|
||||
#include "web_server.h"
|
||||
#include "config.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
|
|
@ -207,6 +208,7 @@ void MqttClient::publishHeartbeat() {
|
|||
|
||||
bool ok = _client->publish(MQTT_TOPIC_STATUS, buf, /*retained=*/true);
|
||||
LOG_I("MQTT", "Heartbeat: %s -> %s", buf, ok ? "OK" : "FEHLER");
|
||||
webServer.sendStatusWs(); // FR-016: Web-Status synchron aktualisieren
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
|
@ -305,6 +307,8 @@ void MqttClient::publishDiscovery() {
|
|||
"\"unique_id\":\"lc_display\","
|
||||
"\"state_topic\":\"" MQTT_TOPIC_STATUS "\","
|
||||
"\"value_template\":\"{{ 'ON' if value_json.display_on else 'OFF' }}\","
|
||||
"\"state_on\":\"ON\","
|
||||
"\"state_off\":\"OFF\","
|
||||
"\"command_topic\":\"" MQTT_TOPIC_CMD "\","
|
||||
"\"payload_on\":\"{\\\"display\\\":true}\","
|
||||
"\"payload_off\":\"{\\\"display\\\":false}\","
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ static String htmlReplace(String html, const String& key, const String& value) {
|
|||
WebServerManager::WebServerManager()
|
||||
: _server(new AsyncWebServer(80))
|
||||
, _ws(new AsyncWebSocket("/log-ws"))
|
||||
, _statusWs(new AsyncWebSocket("/status-ws"))
|
||||
{}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
@ -88,6 +89,28 @@ void WebServerManager::begin() {
|
|||
_server->addHandler(_ws);
|
||||
_wsPtr = _ws; // ab jetzt ist webLogForward() aktiv
|
||||
|
||||
// WebSocket /status-ws – sofortige Status-Updates bei Statusänderungen
|
||||
_statusWs->onEvent([this](AsyncWebSocket* server, AsyncWebSocketClient* client,
|
||||
AwsEventType type, void* arg, uint8_t* data, size_t len) {
|
||||
(void)server; (void)arg; (void)data; (void)len;
|
||||
if (type == WS_EVT_CONNECT) {
|
||||
// Initialen Status direkt an neuen Client senden
|
||||
int runSec = laserTracker.getRunningSessionSeconds();
|
||||
char buf[160];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"allSessions\":%d,\"currentSec\":%d,\"laserActive\":%s,\"displayOn\":%s}",
|
||||
laserTracker.getAllSessionsSumMinutes(),
|
||||
runSec,
|
||||
laserTracker.isActive() ? "true" : "false",
|
||||
display.isEnabled() ? "true" : "false");
|
||||
client->text(buf);
|
||||
LOG_I("WEB", "Status-WS Client #%u verbunden", client->id());
|
||||
} else if (type == WS_EVT_DISCONNECT) {
|
||||
LOG_I("WEB", "Status-WS Client #%u getrennt", client->id());
|
||||
}
|
||||
});
|
||||
_server->addHandler(_statusWs);
|
||||
|
||||
registerRoutes();
|
||||
// ElegantOTA-Auth: nur wenn Passwort gesetzt
|
||||
const Settings& s = settings.get();
|
||||
|
|
@ -118,7 +141,8 @@ void WebServerManager::begin() {
|
|||
// -----------------------------------------------------------------------------
|
||||
void WebServerManager::loop() {
|
||||
ArduinoOTA.handle();
|
||||
_ws->cleanupClients(); // abgemeldete Clients freigeben (verhindert Memory-Leak)
|
||||
_ws->cleanupClients(); // abgemeldete Clients freigeben (verhindert Memory-Leak)
|
||||
_statusWs->cleanupClients();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
@ -221,6 +245,7 @@ void WebServerManager::registerRoutes() {
|
|||
// --- POST /reset – Session zurücksetzen (öffentlich, kein Auth) ----------
|
||||
_server->on("/reset", HTTP_POST, [this](AsyncWebServerRequest* request) {
|
||||
laserTracker.resetSessionSum();
|
||||
mqttClient.publishHeartbeat(); // FR-015+016: HA-Update + WS-Push
|
||||
LOG_I("WEB", "Session zurueckgesetzt");
|
||||
request->send(204); // No Content – fetch() auf Client-Seite, kein Page-Redirect
|
||||
});
|
||||
|
|
@ -231,18 +256,13 @@ void WebServerManager::registerRoutes() {
|
|||
laserTracker.resetTotal();
|
||||
mqttClient.resetSessionCounter();
|
||||
LOG_I("WEB", "Maschinenlaufzeit zurueckgesetzt (NVS)");
|
||||
request->send(200, "text/html; charset=utf-8",
|
||||
"<!DOCTYPE html><html><head><meta charset='utf-8'>"
|
||||
"<meta http-equiv='refresh' content='2;url=/config'></head>"
|
||||
"<body style='font-family:sans-serif;max-width:480px;margin:2rem auto;padding:0 1rem'>"
|
||||
"<h2>✓ Maschinenlaufzeit zurückgesetzt</h2>"
|
||||
"<p>Weiterleitung …</p>"
|
||||
"</body></html>");
|
||||
request->send(204);
|
||||
});
|
||||
|
||||
// --- POST /display-toggle – Display ein-/ausschalten (öffentlich) ----------
|
||||
_server->on("/display-toggle", HTTP_POST, [this](AsyncWebServerRequest* request) {
|
||||
display.setEnabled(!display.isEnabled());
|
||||
mqttClient.publishHeartbeat(); // FR-015+016: HA-Update + WS-Push
|
||||
LOG_I("WEB", "Display %s", display.isEnabled() ? "an" : "aus");
|
||||
request->send(204); // No Content – Browser bleibt auf aktueller Seite
|
||||
});
|
||||
|
|
@ -297,6 +317,23 @@ void WebServerManager::registerRoutes() {
|
|||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// sendStatusWs() – Status-JSON via WebSocket an alle /status-ws Clients pushen
|
||||
// -----------------------------------------------------------------------------
|
||||
void WebServerManager::sendStatusWs() {
|
||||
if (!_statusWs || _statusWs->count() == 0) return;
|
||||
int runSec = laserTracker.getRunningSessionSeconds();
|
||||
char data[160];
|
||||
snprintf(data, sizeof(data),
|
||||
"{\"allSessions\":%d,\"currentSec\":%d,\"laserActive\":%s,\"displayOn\":%s}",
|
||||
laserTracker.getAllSessionsSumMinutes(),
|
||||
runSec,
|
||||
laserTracker.isActive() ? "true" : "false",
|
||||
display.isEnabled() ? "true" : "false");
|
||||
_statusWs->textAll(data);
|
||||
LOG_D("WEB", "Status-WS push: %s", data);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// buildStatusPage()
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
@ -336,21 +373,36 @@ String WebServerManager::buildStatusPage() {
|
|||
"<body>"
|
||||
"<h1>Laser Cutter Status</h1>"
|
||||
"<table>"
|
||||
"<tr><td>Laserzeit Summe</td><td>%ALLSESSIONS% Minuten</td></tr>"
|
||||
"<tr><td>Laserzeit Aktuell</td><td>%CURRENTSESSION%</td></tr>"
|
||||
"<tr><td>Laserstatus</td><td>%LASER%</td></tr>"
|
||||
"<tr><td>Laserzeit Summe</td><td id='tdAll'>%ALLSESSIONS% Minuten</td></tr>"
|
||||
"<tr><td>Laserzeit Aktuell</td><td id='tdCurr'>%CURRENTSESSION%</td></tr>"
|
||||
"<tr><td>Laserstatus</td><td id='tdLaser'>%LASER%</td></tr>"
|
||||
"<tr><td>Gratiszeit</td><td>%GRATIS% Sekunden</td></tr>"
|
||||
"<tr><td>Firmware</td><td>v" FIRMWARE_VERSION " (" __DATE__ ")</td></tr>"
|
||||
"</table>"
|
||||
"<button class='btn btn-blue' type='button' "
|
||||
"onclick=\"if(confirm('Summe Laserzeit wirklich zur\\u00fccksetzen?'))"
|
||||
"{fetch('/reset',{method:'POST'}).then(()=>location.reload())}\">"
|
||||
"{fetch('/reset',{method:'POST'})}\">"
|
||||
"Summe Laserzeit zurücksetzen</button>"
|
||||
"<button class='btn %DISP_BTN_CLASS%' type='button' "
|
||||
"onclick=\"fetch('/display-toggle',{method:'POST'}).then(()=>location.reload())\""
|
||||
"<button id='dispBtn' class='btn %DISP_BTN_CLASS%' type='button' "
|
||||
"onclick=\"fetch('/display-toggle',{method:'POST'})\""
|
||||
">%DISP_BTN_LABEL%</button>"
|
||||
"<a class='btn btn-gray' href='/config'>🔒 Laser Cutter Setup & Status</a>"
|
||||
"<script>setTimeout(()=>location.reload(),10000)</script>"
|
||||
"<script>"
|
||||
"function applyStatus(d){"
|
||||
"document.getElementById('tdAll').textContent=d.allSessions+' Minuten';"
|
||||
"var m=Math.floor(d.currentSec/60),s=d.currentSec%60;"
|
||||
"document.getElementById('tdCurr').textContent=m+':'+(s<10?'0':'')+s;"
|
||||
"document.getElementById('tdLaser').innerHTML=d.laserActive"
|
||||
"?'<span style=\"color:#2f9e44;font-weight:bold\">an</span>'"
|
||||
":'<span style=\"color:#868e96\">aus</span>';"
|
||||
"var b=document.getElementById('dispBtn');"
|
||||
"if(d.displayOn){b.className='btn btn-blue';b.innerHTML='💡 Display ausschalten';}"
|
||||
"else{b.className='btn btn-gray';b.innerHTML='<span style=\"filter:grayscale(1)\">💡</span> Display einschalten';}"
|
||||
"}"
|
||||
"var ws=new WebSocket('ws://'+location.host+'/status-ws');"
|
||||
"ws.onmessage=function(e){applyStatus(JSON.parse(e.data));};"
|
||||
"ws.onclose=function(){setTimeout(function(){location.reload();},3000);};"
|
||||
"</script>"
|
||||
"</body></html>"
|
||||
);
|
||||
|
||||
|
|
@ -359,11 +411,11 @@ String WebServerManager::buildStatusPage() {
|
|||
html.replace("%LASER%", laserStatus);
|
||||
html.replace("%GRATIS%", String(s.gratisSeconds));
|
||||
if (display.isEnabled()) {
|
||||
html.replace("%DISP_BTN_CLASS%", "btn-gray");
|
||||
html.replace("%DISP_BTN_CLASS%", "btn-blue");
|
||||
html.replace("%DISP_BTN_LABEL%", "💡 Display ausschalten");
|
||||
} else {
|
||||
html.replace("%DISP_BTN_CLASS%", "btn-blue");
|
||||
html.replace("%DISP_BTN_LABEL%", "💡 Display einschalten");
|
||||
html.replace("%DISP_BTN_CLASS%", "btn-gray");
|
||||
html.replace("%DISP_BTN_LABEL%", "<span style='filter:grayscale(1)'>💡</span> Display einschalten");
|
||||
}
|
||||
|
||||
return html;
|
||||
|
|
@ -474,10 +526,10 @@ String WebServerManager::buildConfigPage() {
|
|||
"<p style='font-size:1.1rem;font-weight:600;margin:.2rem 0'>%TOTAL%</p>"
|
||||
"<div class='info'>⚠️ Maschinenlaufzeit zurücksetzen löscht den Gesamtspeicher im NVS "
|
||||
"unwiderruflich – diese Aktion kann nicht rückgängig gemacht werden.</div>"
|
||||
"<form action='/reset-total' method='post' "
|
||||
"onsubmit=\"return confirm('Maschinenlaufzeit wirklich unwiderruflich aus dem NVS l\u00f6schen?')\">"
|
||||
"<button class='btn btn-red' type='submit'>Maschinenlaufzeit zurücksetzen</button>"
|
||||
"</form>"
|
||||
"<button class='btn btn-red' type='button' "
|
||||
"onclick=\"if(confirm('Maschinenlaufzeit wirklich unwiderruflich aus dem NVS l\u00f6schen?'))"
|
||||
"{fetch('/reset-total',{method:'POST'}).then(function(){location.reload();})}\">"
|
||||
"Maschinenlaufzeit zurücksetzen</button>"
|
||||
|
||||
"<form action='/config' method='post'>"
|
||||
"<label>Gratiszeit: <span id='gv'>%GRATIS%</span> Sekunden</label>"
|
||||
|
|
@ -518,19 +570,21 @@ String WebServerManager::buildConfigPage() {
|
|||
"<h2 style='color:#c53030'>WLAN</h2>"
|
||||
"<p style='font-size:.9rem;color:#555'>Löscht gespeicherte WLAN-Daten und startet neu.<br>"
|
||||
"Danach mit <strong>LaserCutter-Setup</strong> verbinden.</p>"
|
||||
"<form action='/wifi-reset' method='post' "
|
||||
"onsubmit=\"return confirm('WLAN-Daten wirklich l\u00f6schen und neu starten?')\">"
|
||||
"<button class='btn btn-red' type='submit'>WLAN neu konfigurieren</button>"
|
||||
"</form>"
|
||||
"<button class='btn btn-red' type='button' "
|
||||
"onclick=\"if(confirm('WLAN-Daten wirklich l\\u00f6schen und neu starten?'))"
|
||||
"{fetch('/wifi-reset',{method:'POST'}).then(function(){"
|
||||
"document.body.innerHTML='<p style=\\'font-family:sans-serif;padding:2rem\\'>WLAN-Daten gel\\u00f6scht. Bitte mit <strong>LaserCutter-Setup</strong> verbinden.</p>';"
|
||||
"})}\">WLAN neu konfigurieren</button>"
|
||||
|
||||
"<h2>Gerät</h2>"
|
||||
"<p style='font-size:.9rem;color:#555'>Der ESP32 wird sofort neu gestartet. "
|
||||
"Eine laufende Laser-Session wird unterbrochen und zurückgesetzt.</p>"
|
||||
"<form action='/reboot' method='post' "
|
||||
"onsubmit=\"return confirm('ESP32 wirklich jetzt neu starten?')\">"
|
||||
"<button class='btn btn-gray' type='submit' %REBOOT_DISABLED%>"
|
||||
"🔄 ESP32 neu starten</button>"
|
||||
"</form>"
|
||||
"<button id='rebootBtn' class='btn btn-gray' type='button' %REBOOT_DISABLED% "
|
||||
"onclick=\"if(confirm('ESP32 wirklich jetzt neu starten?'))"
|
||||
"{fetch('/reboot',{method:'POST'}).then(function(){"
|
||||
"document.body.innerHTML='<p style=\\'font-family:sans-serif;padding:2rem\\'>🔄 ESP32 startet neu… <a href=\\'http://'+location.host+'\\'>Zur\\u00fcck</a></p>';"
|
||||
"setTimeout(function(){location.href='/';},5000);"
|
||||
"})}\">🔄 ESP32 neu starten</button>"
|
||||
|
||||
"<h2>Tools</h2>"
|
||||
"<a class='btn btn-gray' href='/update'>OTA Update</a>"
|
||||
|
|
@ -539,6 +593,15 @@ String WebServerManager::buildConfigPage() {
|
|||
"<footer style='margin-top:2rem;text-align:center;font-size:.8rem;color:#aaa'>"
|
||||
"v" FIRMWARE_VERSION " (" __DATE__ ")"
|
||||
"</footer>"
|
||||
"<script>"
|
||||
"var ws=new WebSocket('ws://'+location.host+'/status-ws');"
|
||||
"ws.onmessage=function(e){"
|
||||
"var d=JSON.parse(e.data);"
|
||||
"var rb=document.getElementById('rebootBtn');"
|
||||
"if(rb){rb.disabled=d.laserActive;"
|
||||
"rb.title=d.laserActive?'Laser aktiv \u2013 bitte erst beenden':'';}"
|
||||
"};"
|
||||
"</script>"
|
||||
"</body></html>"
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user