diff --git a/README.md b/README.md index f65eb67..107873d 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,7 @@ The log is **administrator-only**. `/admin/audit-log` is a paginated, filterable | `DEVPLACE_INTERNAL_BASE_URL` | `http://localhost:10500` | Base URL the platform's own services dial for the AI gateway | | `DEVPLACE_XMLRPC_PORT` | `10550` | Loopback port the forking XML-RPC bridge binds; the app and nginx reverse-proxy `/xmlrpc` to it | | `DEVPLACE_XMLRPC_BIND` | `127.0.0.1` | Bind address for the XML-RPC bridge (loopback; the app and nginx are the intended front doors) | -| `DEVPLACE_STATIC_VERSION` | server boot unix timestamp | Cache-busting version stamped into every static asset URL (`/static/v/...`). Set it at launch so multiple workers agree (the `prod` target and Docker image do this); leave unset in dev to refresh on each reload. See [Static asset caching](#static-asset-caching) | +| `DEVPLACE_STATIC_VERSION` | server boot unix timestamp | The boot-id half of the cache-busting version stamped into every static asset URL (`/static/v-/...`, e.g. `/static/v1.0.1-1718040000/...`). Set it at launch so multiple workers agree (the `prod` target and Docker image do this); leave unset in dev to refresh on each reload. See [Static asset caching](#static-asset-caching) | | `DEEPSEEK_API_KEY` / `OPENROUTER_API_KEY` | unset | Upstream provider keys; migrated into the gateway settings on first boot | | `DEVPLACE_PRESENCE_TIMEOUT_SECONDS` | `60` | Online-presence window: a user counts as online for this many seconds after their last activity. `last_seen` is refreshed by a throttled in-place update at most once per half this interval per worker (no per-load inserts, no data growth) | | `DEVPLACE_PRESENCE_ONLINE_LIMIT` | `30` | Maximum avatars shown in the feed's live "Online now" panel (ordered alphabetically by username) | @@ -1202,7 +1202,7 @@ reverse_proxy localhost:10500 { ### Static asset caching -Static assets (CSS, JS, vendored libraries) are served with a **one-year immutable cache** for the best Lighthouse "efficient cache policy" score, while deploys still take effect immediately. Every app-owned static URL carries a boot-time version path segment, `/static/v/...`, where `` is the unix time the server process started (`config.STATIC_VERSION`). A restart changes the segment, so every asset URL changes and returning browsers refetch on their next page load - no cache purge, no hashing build step. +Static assets (CSS, JS, vendored libraries) are served with a **one-year immutable cache** for the best Lighthouse "efficient cache policy" score, while deploys still take effect immediately. Every app-owned static URL carries a version path segment, `/static/v-/...` (`config.STATIC_VERSION`) - `` is `pyproject.toml`'s `version` (auto-bumped on every commit, see "Version bumping" in `CLAUDE.md`) and `` is the unix time the server process started. A restart changes the segment, so every asset URL changes and returning browsers refetch on their next page load - no cache purge, no hashing build step. The version sits in the **path**, not a query string, because the frontend is unbundled ES6 modules wired with relative imports: a path segment is inherited automatically by every transitively imported module and relative CSS `url()`, so the whole graph busts on deploy. Templates emit URLs through the `static_url` Jinja global and runtime JavaScript through the `assetUrl` helper (`static/js/assetVersion.js`, reading ``). User uploads under `/static/uploads/` and the `service-worker.js` route are excluded. Set `DEVPLACE_STATIC_VERSION` at launch so multiple workers share one value (the `prod` target and Docker image do this). Full detail: `/docs/static-caching.html`. diff --git a/devplacepy/config.py b/devplacepy/config.py index e0c36da..db4c0cf 100644 --- a/devplacepy/config.py +++ b/devplacepy/config.py @@ -1,6 +1,7 @@ # retoor import time +import tomllib from pathlib import Path from dotenv import load_dotenv from os import environ @@ -68,7 +69,9 @@ PRESENCE_ONLINE_MARGIN_SECONDS = int( XMLRPC_BIND = environ.get("DEVPLACE_XMLRPC_BIND", "127.0.0.1") XMLRPC_PORT = int(environ.get("DEVPLACE_XMLRPC_PORT", "10550")) -STATIC_VERSION = environ.get("DEVPLACE_STATIC_VERSION") or str(int(time.time())) +APP_VERSION = tomllib.loads((BASE_DIR / "pyproject.toml").read_text())["project"]["version"] +BOOT_ID = environ.get("DEVPLACE_STATIC_VERSION") or str(int(time.time())) +STATIC_VERSION = f"{APP_VERSION}-{BOOT_ID}" TEMPLATE_AUTO_RELOAD = environ.get("DEVPLACE_TEMPLATE_AUTO_RELOAD", "1") != "0" diff --git a/devplacepy/static/js/CLAUDE.md b/devplacepy/static/js/CLAUDE.md index 07a15f0..2cae78a 100644 --- a/devplacepy/static/js/CLAUDE.md +++ b/devplacepy/static/js/CLAUDE.md @@ -6,10 +6,10 @@ This file documents JS module organization, custom web components, and shared fr - Javascript files must be in module (ES6) format and imported as module format. It must be as object oriented as possible and a file per class. There must always be a main application class called `Application`, instantiated as `app`, accessible everywhere. - Never use JavaScript 3rd party frameworks unless specified. - CDN scripts referenced from `base.html` (marked, highlight.js, emoji-picker-element) MUST use `defer` or `type="module"` - otherwise `wait_until="domcontentloaded"` in Playwright tests will time out. -- **Static asset URLs are boot-versioned for cache-busting.** Assets are served `public, immutable, max-age=31536000` (1 year, for the Lighthouse "efficient cache policy" score) while a restart still busts every browser. Never hardcode a bare `/static/...` href/src: templates wrap it in the `static_url(path)` Jinja global (`templating.py`) and runtime JS that builds an absolute static URL uses `assetUrl(path)` (`static/js/assetVersion.js`, reads `` rendered in `base.html`). Both emit a `/static/v/...` path segment (`config.STATIC_VERSION` = `DEVPLACE_STATIC_VERSION` env or `int(time.time())` at process start), so a deploy = a restart = a new timestamp = a new URL for every asset. Both helpers no-op for non-`/static/` paths and for `/static/uploads/`, so they are safe to wrap around dynamic values. +- **Static asset URLs are boot-versioned for cache-busting.** Assets are served `public, immutable, max-age=31536000` (1 year, for the Lighthouse "efficient cache policy" score) while a restart still busts every browser. Never hardcode a bare `/static/...` href/src: templates wrap it in the `static_url(path)` Jinja global (`templating.py`) and runtime JS that builds an absolute static URL uses `assetUrl(path)` (`static/js/assetVersion.js`, reads `` rendered in `base.html`). Both emit a `/static/v/...` path segment where `config.STATIC_VERSION` = `f"{APP_VERSION}-{BOOT_ID}"` - `APP_VERSION` is read straight from `pyproject.toml`'s `version` (see root `CLAUDE.md` "Version bumping" - it moves on every commit via the tracked `pre-commit` hook) and `BOOT_ID` is `DEVPLACE_STATIC_VERSION` env or `int(time.time())` at process start - so a deploy = a restart = a new URL for every asset, and the version segment itself tells you which commit is live. Both helpers no-op for non-`/static/` paths and for `/static/uploads/`, so they are safe to wrap around dynamic values. - **Path segment, never a query string - do NOT switch this to `?v=`.** The frontend is unbundled ES6 with ~130 relative imports (`./Http.js`) and zero absolute ones. A `?v=` only versions the entry `