[ai] improve web stability

This commit is contained in:
Stefan Ostermann 2025-08-18 22:34:23 +02:00
parent b6ac157207
commit 0a08709160
2 changed files with 138 additions and 18 deletions

View File

@ -330,7 +330,9 @@ void handleMoveFile(AsyncWebServerRequest *request)
if (SD.exists(from))
{
sd_lock_acquire();
SD.rename(from, to);
sd_lock_release();
Serial.println("Moved file: " + from + " to " + to);
// Rebuild directory tree to update file list
rootNode.buildDirectoryTree("/");
@ -351,7 +353,9 @@ void handleDeleteFile(AsyncWebServerRequest *request)
if (SD.exists(filename))
{
sd_lock_acquire();
SD.remove(filename.c_str());
sd_lock_release();
Serial.println("Deleted file: " + filename);
// Rebuild directory tree to update file list
rootNode.buildDirectoryTree("/");
@ -800,6 +804,8 @@ void saveMappingToFile(const String filename)
// Function to handle edit requests
void editMapping(AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
if (request->hasParam("rfid", true) && request->hasParam("song", true))
{
String rfid = request->getParam("rfid", true)->value();
@ -1166,6 +1172,7 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
if (SD.exists(htmlPath))
{
AsyncWebServerResponse *response = request->beginResponse(SD, htmlPath, "text/html");
response->addHeader("Cache-Control", "no-store");
request->send(response);
}
else
@ -1186,7 +1193,11 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
String cssPath = getSysDir("style.css");
if (SD.exists(cssPath))
{
request->send(SD, cssPath, "text/css");
{
AsyncWebServerResponse *resp = request->beginResponse(SD, cssPath, "text/css");
resp->addHeader("Cache-Control", "public, max-age=300");
request->send(resp);
}
}
else
{
@ -1205,7 +1216,11 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
String jsPath = getSysDir("script.js");
if (SD.exists(jsPath))
{
request->send(SD, jsPath, "application/javascript");
{
AsyncWebServerResponse *resp = request->beginResponse(SD, jsPath, "application/javascript");
resp->addHeader("Cache-Control", "public, max-age=300");
request->send(resp);
}
}
else
{
@ -1218,48 +1233,59 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
// Dynamic endpoints to avoid template processing heap spikes
server.on("/directory", HTTP_GET, [](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
String html = processor(String("DIRECTORY"));
request->send(200, "text/html; charset=UTF-8", html);
});
server.on("/mapping", HTTP_GET, [](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
String html = processor(String("MAPPING"));
request->send(200, "text/html; charset=UTF-8", html);
});
server.on("/state", HTTP_GET, [](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
String state = getState();
request->send(200, "application/json charset=UTF-8", state.c_str()); });
request->send(200, "application/json; charset=UTF-8", state.c_str()); });
server.on("/start", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->send(200, "text/plain charset=UTF-8", "start");
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain; charset=UTF-8", "start");
start(); });
server.on("/toggleplaypause", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->send(200, "text/plain charset=UTF-8", "toggleplaypause");
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain; charset=UTF-8", "toggleplaypause");
togglePlayPause(); });
server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain", "stop");
stop(); });
server.on("/next", HTTP_GET, [](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain", "next");
next(); });
server.on("/previous", HTTP_GET, [](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain", "previous");
previous(); });
@ -1410,6 +1436,8 @@ void setup()
void id_song_action(AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
int params = request->params();
for (int i = 0; i < params; i++)
{
@ -1425,6 +1453,8 @@ void id_song_action(AsyncWebServerRequest *request)
void progress_action(AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
int params = request->params();
for (int i = 0; i < params; i++)
@ -1441,6 +1471,8 @@ void progress_action(AsyncWebServerRequest *request)
void volume_action(AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
int params = request->params();
for (int i = 0; i < params; i++)

View File

@ -1,6 +1,88 @@
setInterval(getState, 4000);
setInterval(updateProgress, 500); // Update progress every second
/* Global single-flight queue for XMLHttpRequest
- Serializes all XHR to 1 at a time
- Adds timeouts (GET ~3.5s, POST ~6s, /upload 10min)
- Deduplicates idempotent GETs to same URL (drops duplicates)
This reduces concurrent load on the ESP32 web server and SD card. */
(function(){
var origOpen = XMLHttpRequest.prototype.open;
var origSend = XMLHttpRequest.prototype.send;
var queue = [];
var active = null;
var inflightKeys = new Set();
function keyOf(xhr){ return (xhr.__method || 'GET') + ' ' + (xhr.__url || ''); }
function startNext(){
if (active || queue.length === 0) return;
var item = queue.shift();
active = item;
inflightKeys.add(item.key);
var xhr = item.xhr;
var timeoutMs = item.timeoutMs;
var timer = null;
function cleanup() {
active = null;
inflightKeys.delete(item.key);
if (timer) { clearTimeout(timer); timer = null; }
setTimeout(startNext, 0);
}
xhr.addEventListener('loadend', cleanup);
if (timeoutMs > 0 && !xhr.__skipTimeout) {
timer = setTimeout(function(){
try { xhr.abort(); } catch(e){}
}, timeoutMs);
}
item.origSend.call(xhr, item.body);
}
XMLHttpRequest.prototype.open = function(method, url, async){
this.__method = (method || 'GET').toUpperCase();
this.__url = url || '';
return origOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function(body){
var key = keyOf(this);
var isIdempotentGET = (this.__method === 'GET');
var timeoutMs;
if ((this.__url || '').indexOf('/upload') !== -1) {
timeoutMs = 600000; // 10 minutes for uploads
} else if (this.__method === 'GET') {
timeoutMs = 3500;
} else {
timeoutMs = 6000;
}
if (isIdempotentGET && inflightKeys.has(key)) {
// Drop duplicate GET to same resource
return;
}
if (isIdempotentGET) {
for (var i = 0; i < queue.length; i++) {
if (queue[i].key === key) {
// Already queued; keep most recent body if any
queue[i].body = body;
return;
}
}
}
var item = { xhr: this, body: body, key: key, timeoutMs: timeoutMs, origSend: origSend };
queue.push(item);
startNext();
};
})();
/* Dynamic content loaders for playlist and mapping (avoid heavy template processing on server) */
function bindPlaylistClicks() {
var container = document.getElementById('playlistContainer');
@ -107,16 +189,22 @@ function getState() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
var state = JSON.parse(xhr.response);
isPlaying = state['playing'];
if (xhr.status >= 200 && xhr.status < 300) {
try {
var state = JSON.parse(xhr.responseText || xhr.response || '{}');
isPlaying = !!state['playing'];
if (isPlaying) {
songStartTime = Date.now() - state['time'] * 1000;
currentSongLength = state['length'] * 1000;
songStartTime = Date.now() - ((state['time'] || 0) * 1000);
currentSongLength = ((state['length'] || 0) * 1000);
}
lastStateUpdateTime = Date.now();
displayState(state);
} catch (e) {
// Ignore parse errors; will retry on next poll
}
}
}
};
xhr.open("GET","/state", true);
xhr.send();
}