Fix stale UI after firmware updates (#5120)

Add WEB_BUILD_TIME to html_ui.h and use it for ETag generation

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
Co-authored-by: Aircoookie <21045690+Aircoookie@users.noreply.github.com>
This commit is contained in:
Copilot
2025-11-28 08:08:43 -05:00
committed by Frank
parent a9670435cf
commit 260f26dadb
2 changed files with 58 additions and 34 deletions

View File

@@ -113,6 +113,11 @@ function filter(str, type) {
}
}
// Generate build timestamp as UNIX timestamp (seconds since epoch)
function generateBuildTime() {
return Math.floor(Date.now() / 1000);
}
function writeHtmlGzipped(sourceFile, resultFile, page) {
console.info("Reading " + sourceFile);
new inliner(sourceFile, function (error, html) {
@@ -141,7 +146,13 @@ function writeHtmlGzipped(sourceFile, resultFile, page) {
* Please see https://mm.kno.wled.ge/advanced/custom-features/#changing-web-ui
* to find out how to easily modify the web UI source!
*/
// Automatically generated build time for cache busting (UNIX timestamp)
#ifdef WEB_BUILD_TIME // avoid duplicate defintions
#undef WEB_BUILD_TIME
#endif
#define WEB_BUILD_TIME ${generateBuildTime()}
// Autogenerated from ${sourceFile}, do not edit!!
const uint16_t PAGE_${page}_L = ${result.length};
const uint8_t PAGE_${page}[] PROGMEM = {

View File

@@ -11,18 +11,56 @@
#endif
#include "html_cpal.h"
/*
* Integrated HTTP web server page declarations
*/
bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest* request);
void setStaticContentCacheHeaders(AsyncWebServerResponse *response);
// define flash strings once (saves flash memory)
static const char s_redirecting[] PROGMEM = "Redirecting...";
static const char s_content_enc[] PROGMEM = "Content-Encoding";
static const char s_unlock_ota [] PROGMEM = "Please unlock OTA in security settings!";
static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN code!";
static const char s_cache_control[] PROGMEM = "Cache-Control";
static const char s_no_store[] PROGMEM = "no-store";
static const char s_expires[] PROGMEM = "Expires";
/*
* Integrated HTTP web server page declarations
*/
static void generateEtag(char *etag, uint16_t eTagSuffix) {
sprintf_P(etag, PSTR("%u-%02x-%04x"), WEB_BUILD_TIME, cacheInvalidate, eTagSuffix);
}
static void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int code=200, uint16_t eTagSuffix = 0) {
// Only send ETag for 200 (OK) responses
if (code != 200) return;
// https://medium.com/@codebyamir/a-web-developers-guide-to-browser-caching-cc41f3b73e7c
#ifndef WLED_DEBUG
// this header name is misleading, "no-cache" will not disable cache,
// it just revalidates on every load using the "If-None-Match" header with the last ETag value
response->addHeader(FPSTR(s_cache_control), F("no-cache"));
#else
response->addHeader(FPSTR(s_cache_control), F("no-store,max-age=0")); // prevent caching if debug build
#endif
char etag[32] = {'\0'};
generateEtag(etag, eTagSuffix);
response->addHeader(F("ETag"), etag);
}
static bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest *request, int code=200, uint16_t eTagSuffix = 0) {
// Only send 304 (Not Modified) if response code is 200 (OK)
if (code != 200) return false;
AsyncWebHeader *header = request->getHeader(F("If-None-Match"));
char etag[32] = {'\0'};
generateEtag(etag, eTagSuffix);
if (header && header->value() == etag) {
AsyncWebServerResponse *response = request->beginResponse(304);
setStaticContentCacheHeaders(response, code, eTagSuffix);
request->send(response);
return true;
}
return false;
}
//Is this an IP?
bool isIp(String str) {
@@ -451,7 +489,7 @@ void initServer()
AsyncWebServerResponse *response = request->beginResponse_P(404, "text/html", PAGE_404, PAGE_404_length);
#endif
response->addHeader(FPSTR(s_content_enc),"gzip");
setStaticContentCacheHeaders(response);
setStaticContentCacheHeaders(response, 404);
request->send(response);
//request->send_P(404, "text/html", PAGE_404);
});
@@ -467,31 +505,6 @@ void serveIndexOrWelcome(AsyncWebServerRequest *request)
}
}
bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest* request)
{
AsyncWebHeader* header = request->getHeader("If-None-Match");
if (header && header->value() == String(VERSION)) {
request->send(304);
return true;
}
return false;
}
void setStaticContentCacheHeaders(AsyncWebServerResponse *response)
{
char tmp[12];
// https://medium.com/@codebyamir/a-web-developers-guide-to-browser-caching-cc41f3b73e7c
#ifndef WLED_DEBUG
//this header name is misleading, "no-cache" will not disable cache,
//it just revalidates on every load using the "If-None-Match" header with the last ETag value
response->addHeader(F("Cache-Control"),"no-cache");
#else
response->addHeader(F("Cache-Control"),"no-store,max-age=0"); // prevent caching if debug build
#endif
sprintf_P(tmp, PSTR("%8d-%02x"), VERSION, cacheInvalidate);
response->addHeader(F("ETag"), tmp);
}
void serveIndex(AsyncWebServerRequest* request)
{
if (handleFileRead(request, "/index.htm")) return;