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:
    <link rel="stylesheet" href="theme/main.min.css">
    <script src="public/modules.min.js"></script>
  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)
This commit is contained in:
2026-09-29 16:47:28 +10:00
parent 012711614c
commit 48e439ed6e
3 changed files with 91 additions and 10 deletions
+42 -10
View File
@@ -280,19 +280,51 @@ after: panel 136px → toolbar top 256, dialog bottom 279 (inside)
## 5. 🟠 CSS/JS changes were invisible behind the browser cache ## 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 **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. 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 **It was not Firefox.** Verified in a real, scripted Firefox (155.0) against the live server with the
from **stable URLs with no cache headers**, so nginx adds only `Last-Modified`/`ETag` and browsers fix applied:
legitimately reuse their cached copy.
**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 assets Firefox requested: theme/main.min.css?v=1790663824 <- versioned
change is guaranteed to be picked up on the next load. 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 The panel rendered correctly and the Branch toolbar sat inside the modal. Firefox behaves identically
before assuming a fix failed. 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
<link rel="stylesheet" href="theme/main.min.css"> <!-- no version -->
<script src="public/modules.min.js"></script> <!-- no version -->
```
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=<mtime>`). 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.
--- ---
+14
View File
@@ -130,6 +130,20 @@ else
fi fi
echo echo
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 "REGRESSION: rDelete works (GLOB_BRACE is glibc-only; musl lacks it)"
echo "==============================================================" echo "=============================================================="
# That broke deleting ANY file/folder in the editor AND removing a market plugin (the UI # That broke deleting ANY file/folder in the editor AND removing a market plugin (the UI
+35
View File
@@ -73,4 +73,39 @@ EOF
|| echo " !! nginx -t failed - restore $NGX.orig" || echo " !! nginx -t failed - restore $NGX.orig"
fi fi
echo "=== 3. cache-busting versioned asset URLs ==="
# Atheos serves theme/main.min.css and the generated bundles from STABLE paths with no version:
# <link rel="stylesheet" href="theme/main.min.css">
# 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<link rel=\\\"stylesheet\\\" href=\\\"\$path\\\">\\n\": \"\\t<script type=\\\"text/javascript\\\" src=\\\"\$path\\\"></script>\\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<link rel=\\\"stylesheet\\\" href=\\\"\$path\\\">\\n\": \"\\t<script type=\\\"text/javascript\\\" src=\\\"\$path\\\"></script>\\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<link rel=\\\"stylesheet\\\" href=\\\"theme/main.min.css\\\">\\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<link rel=\\\"stylesheet\\\" href=\\\"theme/main.min.css?v=\$themeV\\\">\\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 ===" echo "=== done ==="