mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-10 18:59:56 +00:00
The runtime stylesheet moves to CONFIG_DIR. static/tailwind.min.css is never written again.
━━━ TWO DIFFERENT THINGS WERE SHARING ONE PATH ━━━
static/tailwind.min.css a BUILD ARTEFACT. Committed, image-baked, generated by scanning
the in-tree plugins only. CI's tailwind-fresh check verifies it.
the RUNTIME sheet PER-INSTALL STATE. Additionally scans whatever the user installed
into FEEDBACK_PLUGINS_DIR, so it differs machine to machine.
Writing the second over the first meant that MERELY RUNNING THE DEV SERVER from a git checkout
silently modified a tracked file. `git add -A` then swept a 100KB reshuffle of minified CSS
into the commit and ci/tailwind-fresh went red with a diff that explains nothing — on a PR
whose real change touched no Tailwind classes at all. It also wrote app state into the app
directory, which is read-only in some deploys.
A new route serves the runtime sheet when there is one and falls back to the committed one
otherwise. It is registered BEFORE the /static mount, which would otherwise swallow the path.
━━━ A PERSISTED SHEET MUST NOT OUTLIVE ITS REASON (Codex [P2] x2) ━━━
1. THE USER REMOVES THEIR PLUGINS. Startup only rebuilds when user plugins exist, so nothing
would ever overwrite the stale sheet — and it still carries classes for plugins that are
gone. With no user plugins the COMMITTED sheet is complete by definition. Guarded.
2. THE APP IS UPGRADED, and my first guard for this was WRONG. I compared mtimes. Codex: that
is not a freshness signal across install methods — archives and container images routinely
PRESERVE SOURCE MTIMES, so a just-shipped stylesheet can carry an OLDER timestamp than a
runtime sheet a user built days ago. The mtime check then calls the stale one FRESH and it
masks the new core CSS indefinitely — permanently, if no Tailwind toolchain is present to
trigger a rebuild.
Freshness is decided by CONTENT now. Each runtime build stamps a sidecar with the sha256 of
the committed sheet it was made from. Core ships new CSS -> that file changes -> the hash
changes -> the runtime sheet is correctly judged stale. Timestamps only gesture at the
question that hashing answers.
Falling back to the committed sheet is always safe: at worst it lacks a just-installed plugin's
classes for the seconds until the async rebuild lands.
VERIFIED END TO END. Ran the real dev server with 3 plugins installed: it rebuilt Tailwind over
them (123,291 bytes), wrote the sheet + sidecar to CONFIG_DIR, still served /static/
tailwind.min.css at 200 — and `git diff` on the tracked file came back CLEAN.
8 tests. Bite-tested: reverting to the shared path fails 3, dropping the staleness guards fails
2 more.
pytest 2425, pyflakes 0, Codex 0.
Closes #911
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
36cf77dc44
commit
0a6e0309e5
@@ -18,7 +18,7 @@ configure_logging()
|
||||
|
||||
log = logging.getLogger("feedBack.server")
|
||||
|
||||
from fastapi import FastAPI, File
|
||||
from fastapi import FastAPI, File, Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
|
||||
|
||||
@@ -47,6 +47,7 @@ import appstate
|
||||
import builtin_content
|
||||
import demo_mode
|
||||
import scan
|
||||
import tailwind_rebuild
|
||||
# Extracted route modules. They import `appstate`, never `server` — one-way graph.
|
||||
from routers import audio_effects, artist_aliases, loops, playlists, ws_highway, chart, wanted, library_extras, shop, progression, profile, stats, version, diagnostics
|
||||
from routers import tunings as tunings_router
|
||||
@@ -1639,6 +1640,69 @@ class _RevalidatedStaticFiles(StaticFiles):
|
||||
return response
|
||||
|
||||
|
||||
# ── The Tailwind stylesheet: runtime-augmented if there is one, else the committed build ──
|
||||
#
|
||||
# Two different things used to share one path (#911):
|
||||
#
|
||||
# static/tailwind.min.css a BUILD ARTEFACT. Committed, image-baked, generated from the
|
||||
# in-tree plugins only. CI (`tailwind-fresh`) verifies it.
|
||||
# the RUNTIME sheet PER-INSTALL STATE. Additionally scans whatever the user installed
|
||||
# into FEEDBACK_PLUGINS_DIR, so it differs machine to machine.
|
||||
#
|
||||
# Writing the second over the first meant that merely RUNNING THE DEV SERVER from a git
|
||||
# checkout silently modified a tracked file. `git add -A` then swept a 100KB reshuffle of
|
||||
# minified CSS into the commit and ci/tailwind-fresh went red with a diff that explained
|
||||
# nothing — on a PR whose real change touched no Tailwind classes at all. It also meant writing
|
||||
# app state into the app directory, which is read-only in some deploys.
|
||||
#
|
||||
# The runtime sheet lives in CONFIG_DIR now. This route serves it when it exists and otherwise
|
||||
# falls through to the committed one. It MUST be registered BEFORE the /static mount: routes
|
||||
# are matched in order, and the mount would otherwise swallow the path.
|
||||
def _runtime_css_if_usable() -> Path | None:
|
||||
"""The runtime sheet, but ONLY when it is actually the right answer.
|
||||
|
||||
Codex [P2] on the first cut of #911, and it was right: a persisted sheet can outlive the
|
||||
reason it existed and then MASK newer core CSS indefinitely. Two ways:
|
||||
|
||||
* THE USER REMOVED THEIR PLUGINS. Startup only rebuilds when there are user plugins, so
|
||||
nothing would ever overwrite the old sheet — and it still carries classes for plugins
|
||||
that are gone, while missing nothing. With no user plugins the COMMITTED sheet is by
|
||||
definition complete and authoritative.
|
||||
* THE APP WAS UPGRADED. A new release ships new core classes in static/tailwind.min.css.
|
||||
The runtime sheet on disk predates them. Serving it hides the new CSS until something
|
||||
happens to trigger a rebuild — which, if the toolchain is absent (no node), is never.
|
||||
|
||||
Freshness is decided by CONTENT, not mtime. Codex [P2] again, and again correct: archives
|
||||
and container images routinely PRESERVE SOURCE MTIMES, so a just-shipped stylesheet can
|
||||
carry an older timestamp than a runtime sheet built days ago — and an mtime check would call
|
||||
the stale one fresh. tailwind_rebuild stamps each runtime build with the hash of the
|
||||
committed sheet it was made from; a core upgrade changes that file, hence that hash.
|
||||
|
||||
Falling back to the committed sheet is always safe: at worst it lacks a just-installed
|
||||
plugin's classes for the seconds until the async rebuild lands.
|
||||
"""
|
||||
runtime = tailwind_rebuild.runtime_css_path()
|
||||
if not runtime.is_file():
|
||||
return None
|
||||
if tailwind_rebuild.user_plugin_count() == 0:
|
||||
return None
|
||||
if not tailwind_rebuild.runtime_css_is_current():
|
||||
return None # built against a different core — see runtime_css_is_current()
|
||||
return runtime
|
||||
|
||||
|
||||
@app.get("/static/tailwind.min.css")
|
||||
def tailwind_css(request: Request):
|
||||
target = _runtime_css_if_usable() or (STATIC_DIR / "tailwind.min.css")
|
||||
if not target.is_file():
|
||||
return Response("", status_code=404)
|
||||
# Same cache contract the /static mount applies (_RevalidatedStaticFiles): no-cache, so the
|
||||
# browser always revalidates and picks up a rebuild without a hard refresh.
|
||||
resp = FileResponse(str(target), media_type="text/css")
|
||||
resp.headers["Cache-Control"] = "no-cache"
|
||||
return resp
|
||||
|
||||
|
||||
app.mount("/static", _RevalidatedStaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user