fix(caddy/oci): no-cache SPA routes + ship theme-init.js (PMS-687) #453

Merged
longjacksonle merged 2 commits from fix/PMS-687-spa-cache-and-theme-init into main 2026-07-30 18:33:43 +02:00

PMS-687: production sign-in fails (callback returns empty/corrupted HTML)

The fix lands in mokosh-apps (the static host), as the ticket anticipated.

Root cause

The Caddyfile cached every response immutable (max-age 1y) by default and only excepted /, *.html, /_mokosh_config.js back to no-cache. Client-side routes served index.html via try_files (e.g. /auth/callback) missed that exception and were pinned in the browser for a year. Verified in prod: GET https://msp.a8n.systems/auth/callback returns cache-control: public, max-age=31536000, immutable.

A stale/corrupted callback page then survives both a redeploy (the pinned index.html references the previous content-hashed JS mokosh-apps-dxh<hash>.js, which 404s after a new build) and a server restart (a browser cache is not clearable server-side). That matches every symptom: empty/corrupted callback HTML on reload, nothing in the mokosh-server logs (it is served by Caddy, not the API), and restart not helping.

Secondary bug found: /assets/theme-init.js 404s in prod (served as index.html, text/html) -> Uncaught SyntaxError: Unexpected token '<'. The file exists in the repo but dx build never emits it and the Dockerfile did not copy it.

Changes

  1. Caddyfile - invert the cache policy: default no-cache, no-store, must-revalidate; opt only content-addressed assets under /assets + /wasm back into immutable (their filenames carry a content hash). A file {path} guard keeps a missing asset (served as index.html) from being pinned immutable, and theme-init.js (the one stable-named script under /assets) is excepted back to no-cache.
  2. Dockerfile - copy assets/theme-init.js into the Caddy web root's /assets so it is served as JS.

Verification

caddy validate + a container run of the new Caddyfile against a representative rootfs:

Path Cache-Control
/, /index.html, /auth/callback, /dashboard no-cache
/_mokosh_config.js no-cache
/assets/theme-init.js no-cache
/assets/<missing>.js no-cache
/assets/mokosh-apps-dxh<hash>.js immutable
/wasm/mokosh-apps_bg.wasm immutable

Staleness note (ruled out)

At investigation time all three components were near-current: mokosh-apps 8a669a7 (today), mokosh-server a317a14 (current main, built 07-24), bunyip 0.8.0. The original failure is most consistent with the immutable-cache trap pinning a bad response during the earlier stale-deploy window. A full production sign-in should be re-observed after this deploys (AC #5) - I could not reproduce the empty/corrupted response from a clean external fetch (no browser cache), so an observed interactive run is needed.

🤖 Generated with Claude Code

## PMS-687: production sign-in fails (callback returns empty/corrupted HTML) The fix lands in mokosh-apps (the static host), as the ticket anticipated. ### Root cause The Caddyfile cached every response `immutable` (max-age 1y) by default and only excepted `/`, `*.html`, `/_mokosh_config.js` back to no-cache. Client-side routes served index.html via `try_files` (e.g. `/auth/callback`) missed that exception and were pinned in the browser for a year. Verified in prod: `GET https://msp.a8n.systems/auth/callback` returns `cache-control: public, max-age=31536000, immutable`. A stale/corrupted callback page then survives both a redeploy (the pinned index.html references the previous content-hashed JS `mokosh-apps-dxh<hash>.js`, which 404s after a new build) and a server restart (a browser cache is not clearable server-side). That matches every symptom: empty/corrupted callback HTML on reload, nothing in the mokosh-server logs (it is served by Caddy, not the API), and restart not helping. Secondary bug found: `/assets/theme-init.js` 404s in prod (served as index.html, text/html) -> `Uncaught SyntaxError: Unexpected token '<'`. The file exists in the repo but `dx build` never emits it and the Dockerfile did not copy it. ### Changes 1. **Caddyfile** - invert the cache policy: default `no-cache, no-store, must-revalidate`; opt only content-addressed assets under `/assets` + `/wasm` back into `immutable` (their filenames carry a content hash). A `file {path}` guard keeps a missing asset (served as index.html) from being pinned immutable, and `theme-init.js` (the one stable-named script under `/assets`) is excepted back to no-cache. 2. **Dockerfile** - copy `assets/theme-init.js` into the Caddy web root's `/assets` so it is served as JS. ### Verification `caddy validate` + a container run of the new Caddyfile against a representative rootfs: | Path | Cache-Control | |---|---| | `/`, `/index.html`, `/auth/callback`, `/dashboard` | no-cache | | `/_mokosh_config.js` | no-cache | | `/assets/theme-init.js` | no-cache | | `/assets/<missing>.js` | no-cache | | `/assets/mokosh-apps-dxh<hash>.js` | immutable | | `/wasm/mokosh-apps_bg.wasm` | immutable | ### Staleness note (ruled out) At investigation time all three components were near-current: mokosh-apps `8a669a7` (today), mokosh-server `a317a14` (current main, built 07-24), bunyip `0.8.0`. The original failure is most consistent with the immutable-cache trap pinning a bad response during the earlier stale-deploy window. A full production sign-in should be re-observed after this deploys (AC #5) - I could not reproduce the empty/corrupted response from a clean external fetch (no browser cache), so an observed interactive run is needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The Caddyfile cached every response `immutable` (max-age 1y) by default and only excepted `/`, `*.html`, and `/_mokosh_config.js` back to no-cache. Client-side routes served index.html via `try_files` (e.g. `/auth/callback`) missed that exception and were pinned in the browser for a year. A stale or corrupted callback page then survived both a redeploy (the pinned index.html references the previous content-hashed JS, which 404s after a new build) and a server restart (a browser cache cannot be cleared server-side) - the PMS-687 symptom: sign-in callback returns empty/corrupted HTML on reload, nothing in the server logs, restart does not help.

Invert the policy: default `no-cache, no-store, must-revalidate`, and opt only the content-addressed build assets under `/assets` and `/wasm` back into `immutable` (their filenames carry a content hash, so a new build ships new URLs). A `file {path}` guard keeps a MISSING asset - which `try_files` serves as index.html - from being pinned immutable as HTML, and `theme-init.js` (the one stable-named script under `/assets`) is excepted back to no-cache.

Verified with `caddy validate` + a container run: `/`, `/index.html`, `/auth/callback`, `/dashboard`, `/_mokosh_config.js`, `/assets/theme-init.js`, and any missing `/assets/*` all return no-cache; hashed `/assets/*.js` and `/wasm/*.wasm` return immutable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QvB249F2SV9SBe3cBKRjcS
fix(oci): ship theme-init.js so /assets/theme-init.js serves as JS (PMS-687)
All checks were successful
Check / fmt + clippy + tests (pull_request) Successful in 1m31s
Create release / Create release from merged PR (pull_request) Has been skipped
8dcba3b8d3
`index.html` loads `<script src="/assets/theme-init.js">` (early theme apply to avoid a flash), but it is a raw script tag, not an `asset!()`, so `dx build` never emits it into web/public. In the image the file 404s and Caddy's `try_files` serves index.html for it as text/html, which the browser rejects with "Uncaught SyntaxError: Unexpected token '<'". Copy the repo's `assets/theme-init.js` into the Caddy web root's `/assets`, alongside the existing favicon/manifest copies, so file_server serves it as JS before the SPA fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QvB249F2SV9SBe3cBKRjcS
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-07-30 18:33:36 +02:00
longjacksonle deleted branch fix/PMS-687-spa-cache-and-theme-init 2026-07-30 18:33:43 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-apps!453
No description provided.