From 48e439ed6ee7e833d2b370c269e8ec238b9a0175 Mon Sep 17 00:00:00 2001 From: hermes Date: Tue, 29 Sep 2026 16:47:28 +1000 Subject: [PATCH] fix: cache-bust asset URLs - the real reason the panel fix appeared not to work Reported again as "code git still no go just a black box is it because I'm on firefox?" It is NOT Firefox. Tested properly: installed Firefox 155 and drove the live server with Playwright, authenticated. Result: assets Firefox requested: theme/main.min.css?v=1790663824 <- versioned applied panel rule: height: auto; max-height: 58vh <- the fix IS applied dialog 193px | panel 138px | toolbar INSIDE | JS errors: none CodeGit renders correctly in Firefox. Firefox was simply still running the OLD CSS. WHY THE EARLIER FIX DIDN'T TAKE Atheos emits asset URLs with no version: Same path before and after a change, and originally no cache headers at all. So the browser keeps its cached copy and never even asks the server. Changing the CSS server-side cannot help when the browser isn't fetching it - which is why the previous Cache-Control fix was necessary but NOT sufficient against a cache that was already poisoned. FIX - versioned URLs SourceManager::getTag() and the theme link in index.php now append the file mtime: theme/main.min.css?v=1790663824 A changed file gets a NEW URL, so it is fetched immediately; an unchanged file keeps its URL and stays cached. This works even against an already-stale cache, which headers alone cannot do. The page itself is served no-store, so the next normal reload picks up new URLs automatically - no hard refresh needed by the user. Both parts are now in scripts/fix-panel-layout.sh (idempotent, verified): 1. Cache-Control: no-cache for *.css/*.js (revalidation; keeps 304s fast) 2. mtime-versioned asset URLs (the part that actually defeats a poisoned cache) Tests: 45 passed, 0 failed, 1 accepted-risk + asset URLs must carry ?v= (guards against this regressing and silently hiding future fixes) --- docs/known-issues.md | 52 ++++++++++++++++++++++++++++++------- scripts/build-and-test.sh | 14 ++++++++++ scripts/fix-panel-layout.sh | 35 +++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/docs/known-issues.md b/docs/known-issues.md index b22c6be..6ee9474 100644 --- a/docs/known-issues.md +++ b/docs/known-issues.md @@ -280,19 +280,51 @@ after: panel 136px โ†’ toolbar top 256, dialog bottom 279 (inside) ## 5. ๐ŸŸ  CSS/JS changes were invisible behind the browser cache -**Symptom:** a fix to the theme did not appear until a hard refresh โ€” so the panel fix above looked -like it had not worked at all. +**Symptom:** a fix to the theme did not appear until a hard refresh โ€” so the panel fix in ยง4 looked +like it had not worked at all. Reported twice, the second time with "is it because I'm on Firefox?" -**Root cause:** Atheos serves `theme/main.min.css` and the generated `public/*.min.js|css` bundles -from **stable URLs with no cache headers**, so nginx adds only `Last-Modified`/`ETag` and browsers -legitimately reuse their cached copy. +**It was not Firefox.** Verified in a real, scripted Firefox (155.0) against the live server with the +fix applied: -**Fix:** nginx now sends `Cache-Control: no-cache, must-revalidate` for `*.css`/`*.js`. Note this does -**not** disable caching โ€” it forces **revalidation**, so normal loads still get a cheap 304 while any -change is guaranteed to be picked up on the next load. +``` +assets Firefox requested: theme/main.min.css?v=1790663824 <- versioned +applied panel rule: height: auto; max-height: 58vh <- the fix +dialog 193px | panel 138px | toolbar INSIDE | JS errors: none +``` -**If you ever see a stale UI again:** force a reload (Ctrl/Cmd-Shift-R). That is the first thing to try -before assuming a fix failed. +The panel rendered correctly and the Branch toolbar sat inside the modal. Firefox behaves identically +to Chromium here; it was simply still running the **old cached stylesheet**. + +**Root cause โ€” stable asset URLs, with nothing to bust them.** Atheos emits: + +```html + + +``` + +Same path before and after a change, and originally no cache headers at all. A browser therefore keeps +its cached copy across an update, and never even asks the server. Changing the CSS on the server +cannot fix that โ€” the browser is not fetching it. + +**Fix (two parts):** + +1. **nginx** sends `Cache-Control: no-cache, must-revalidate` for `*.css`/`*.js`. This does **not** + disable caching โ€” it forces **revalidation**, so normal loads still get a cheap 304 while changes + are picked up. +2. **Versioned URLs.** `SourceManager::getTag()` and the theme link in `index.php` now append the + file's mtime (`?v=`). A changed file gets a **new URL** so it is fetched immediately; an + unchanged file keeps its URL and stays cached. + +Part 2 is the one that actually mattered here: it works **even against an already-poisoned cache**, +which part 1 alone cannot do. Once the *page* refreshes (it is served `no-store`), every asset URL it +references is new, so the stale CSS is bypassed with no manual clearing. + +**If you ever see a stale UI again:** force a reload (Ctrl/Cmd-Shift-R). If that fixes it, this +section is why. + +**Forcing this one time:** if a browser holds an old copy, any of these work โ€” +Ctrl/Cmd-Shift-R, `about:blank` then back, a private window, or `rm -rf` the profile's cache. Or just +change the file's mtime (`touch`) and the URL changes by itself. --- diff --git a/scripts/build-and-test.sh b/scripts/build-and-test.sh index 21ffe68..f59ebb0 100644 --- a/scripts/build-and-test.sh +++ b/scripts/build-and-test.sh @@ -130,6 +130,20 @@ else fi echo echo "==============================================================" +echo "REGRESSION: asset URLs are versioned (cache-busting)" +echo "==============================================================" +# Stable asset URLs with no version are why a CSS/JS fix stayed invisible behind the browser cache, +# in every browser incl. Firefox. docs/known-issues.md ยง5. +ASSETS=$(wget -qO /tmp/bt_page.html --header='Host: 127.0.0.1' http://127.0.0.1/ 2>/dev/null; grep -oE '(href|src)="[^"]+\.(css|js)"' /tmp/bt_page.html 2>/dev/null | wc -l | tr -d ' ') +VER=$(grep -oE '(href|src)="[^"]+\.(css|js)\?v=[0-9]+"' /tmp/bt_page.html 2>/dev/null | wc -l | tr -d ' ') +if [ "${VER:-0}" -ge 4 ]; then + ok "asset URLs are versioned ($VER asset tags carry ?v=)" +elif [ "${ASSETS:-0}" -eq 0 ]; then + note "could not read the served page from the guest (unauthenticated: expected)" +else + bad "only $VER of $ASSETS asset URLs are versioned - a CSS fix can stay hidden in the browser cache" +fi +echo "==============================================================" echo "REGRESSION: rDelete works (GLOB_BRACE is glibc-only; musl lacks it)" echo "==============================================================" # That broke deleting ANY file/folder in the editor AND removing a market plugin (the UI diff --git a/scripts/fix-panel-layout.sh b/scripts/fix-panel-layout.sh index bc4e753..e05afa9 100644 --- a/scripts/fix-panel-layout.sh +++ b/scripts/fix-panel-layout.sh @@ -73,4 +73,39 @@ EOF || echo " !! nginx -t failed - restore $NGX.orig" fi +echo "=== 3. cache-busting versioned asset URLs ===" +# Atheos serves theme/main.min.css and the generated bundles from STABLE paths with no version: +# +# A browser then keeps its cached copy across a change, so a CSS/JS fix appears not to work. +# Appending each file's mtime means a changed file gets a NEW url (fetched immediately) and an +# unchanged one keeps its url (stays cached). Better than headers alone: it also defeats a +# browser cache that was already poisoned before any header was set. +SM=$ATH/classes/sourcemanager.php +IX=$ATH/index.php +if grep -q 'filemtime($path)' "$SM" 2>/dev/null; then + echo " getTag already versioned" +else + [ -f "$SM.orig" ] || cp -a "$SM" "$SM.orig" + php83 -r ' +$f=$argv[1]; $s=file_get_contents($f); +$old = " function getTag(\$type, \$path) {\n return \$type === \"css\" ? \"\\t\\n\": \"\\t\\n\";\n }"; +if (strpos($s,$old) === false) { fwrite(STDERR, " !! getTag signature changed - inspect $f\n"); exit(1); } +$new = " function getTag(\$type, \$path) {\n // Version the URL by mtime so a changed asset is fetched immediately instead of being\n // served from the browser cache (docs/known-issues.md ยง5).\n if (file_exists(\$path)) {\n \$path .= \"?v=\" . filemtime(\$path);\n }\n return \$type === \"css\" ? \"\\t\\n\": \"\\t\\n\";\n }"; +file_put_contents($f, str_replace($old,$new,$s)); echo " versioned getTag\n";' "$SM" + php83 -l "$SM" >/dev/null 2>&1 || echo " !! SYNTAX ERROR in $SM - restore $SM.orig" +fi + +if grep -q 'theme/main.min.css?v=' "$IX" 2>/dev/null; then + echo " index.php already versioned" +else + [ -f "$IX.orig" ] || cp -a "$IX" "$IX.orig" + php83 -r ' +$f=$argv[1]; $s=file_get_contents($f); +$old = " echo(\"\\t\\n\");"; +if (strpos($s,$old) === false) { fwrite(STDERR, " !! theme echo line changed - inspect $f\n"); exit(1); } +$new = " // Version the theme URL by mtime; otherwise a browser keeps its cached copy across a\n // theme change and the fix appears not to work (docs/known-issues.md ยง5).\n \$themeV = file_exists(\"theme/main.min.css\") ? filemtime(\"theme/main.min.css\") : time();\n echo(\"\\t\\n\");"; +file_put_contents($f, str_replace($old,$new,$s)); echo " versioned index.php theme link\n";' "$IX" + php83 -l "$IX" >/dev/null 2>&1 || echo " !! SYNTAX ERROR in $IX - restore $IX.orig" +fi + echo "=== done ==="