Compare commits

...

2 Commits

Author SHA1 Message Date
Alexander Heldt 2b4731185c Keep PWA builds coherent by bypassing the HTTP cache
The service worker populated a new build's cache with plain addAll(),
which the browser may satisfy from its HTTP cache — and static assets
were served without Cache-Control, so Safari's heuristic caching could
hold app.js for days. Together that could install a mixed build: a
fresh index.html whose buttons reference listeners a stale app.js never
registers.

Install now fetches assets with cache: "reload", and the server marks
all static assets no-cache (revalidation is a cheap 304; fast/offline
loads are the SW cache's job anyway).
2026-07-12 17:13:13 +00:00
Alexander Heldt ced415c3a5 Make Save the exercise dialog's default button
All three menu buttons were implicit submit buttons, so pressing Enter
(or the iOS keyboard's "Go") triggered implicit form submission against
the first one — the hidden Delete button — which closed the dialog and
silently discarded the typed exercise. Delete and Cancel are now
type="button", leaving Save as the form's default button.
2026-07-12 17:12:59 +00:00
3 changed files with 17 additions and 7 deletions
+5 -4
View File
@@ -806,12 +806,13 @@ func main() {
serveSW(w, *staticDir, swVer) serveSW(w, *staticDir, swVer)
return return
} }
// PWA: manifest.json must revalidate so updates propagate.
if r.URL.Path == "/manifest.json" {
w.Header().Set("Cache-Control", "no-cache")
}
// SPA fallback: unknown paths -> index.html (so deep links work). // SPA fallback: unknown paths -> index.html (so deep links work).
if !strings.HasPrefix(r.URL.Path, "/api/") { if !strings.HasPrefix(r.URL.Path, "/api/") {
// All static assets revalidate on every request (cheap 304s via
// Last-Modified). Offline/fast loads are the service worker
// cache's job; leaving these to the browser's heuristic HTTP
// caching let a stale app.js pair with a fresh index.html.
w.Header().Set("Cache-Control", "no-cache")
candidate := filepath.Join(*staticDir, filepath.FromSlash(r.URL.Path)) candidate := filepath.Join(*staticDir, filepath.FromSlash(r.URL.Path))
if r.URL.Path != "/" { if r.URL.Path != "/" {
if info, err := os.Stat(candidate); err != nil || info.IsDir() { if info, err := os.Stat(candidate); err != nil || info.IsDir() {
+5 -2
View File
@@ -277,8 +277,11 @@
<textarea id="exercise-note" rows="5" placeholder="Reminder for how to train it, e.g. lure with a treat, mark the moment the butt touches the ground, reward"></textarea> <textarea id="exercise-note" rows="5" placeholder="Reminder for how to train it, e.g. lure with a treat, mark the moment the butt touches the ground, reward"></textarea>
</label> </label>
<menu> <menu>
<button value="delete" id="exercise-delete" class="danger" hidden>Delete</button> <!-- type="button" keeps Save the form's default button, so Enter /
<button value="cancel" class="ghost">Cancel</button> the iOS keyboard's "Go" saves instead of silently hitting the
(hidden) Delete button via implicit form submission. -->
<button type="button" value="delete" id="exercise-delete" class="danger" hidden>Delete</button>
<button type="button" value="cancel" class="ghost">Cancel</button>
<button value="save" id="exercise-save">Save</button> <button value="save" id="exercise-save">Save</button>
</menu> </menu>
</form> </form>
+7 -1
View File
@@ -16,8 +16,14 @@ const ASSETS = [
]; ];
self.addEventListener("install", (event) => { self.addEventListener("install", (event) => {
// cache: "reload" bypasses the browser's HTTP cache, so a new build always
// caches assets fetched fresh from the server. Without it, addAll could mix
// a fresh index.html with a heuristically-cached stale app.js and install a
// build whose markup references listeners the old script never registers.
event.waitUntil( event.waitUntil(
caches.open(CACHE).then((cache) => cache.addAll(ASSETS)) caches.open(CACHE).then((cache) =>
cache.addAll(ASSETS.map((u) => new Request(u, { cache: "reload" })))
)
); );
// No skipWaiting() here: a new worker stays in "waiting" while an old one is // No skipWaiting() here: a new worker stays in "waiting" while an old one is
// controlling a tab, so the page can prompt before swapping assets out from // controlling a tab, so the page can prompt before swapping assets out from