mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-08-11 00:30:12 -04:00
fix(deploy): kiosk picks up new builds without operator intervention
Reproduced live during the #1133 rollout: the SpoolBuddy display kept serving the pre-fix picker for hours after every cache-clear, chromium-restart, and pkill attempt because a chain of stale state across HTTP cache + Service Worker + persistent profile prevented fresh code from reaching the running tab. Three independent changes — any one of them sufficient on a clean profile, but all three needed to escape an already-corrupted one: (1) backend/app/main.py — index.html now served with Cache-Control: no-cache, must-revalidate on both / and the SPA catch-all. Vite emits content-hashed JS/CSS bundle filenames so the assets themselves are safe to cache forever, but the HTML wrapping them is the only file that knows which hash is current. Without explicit cache directives Chromium falls back to heuristic caching (typically 10% of time since Last-Modified) and on long-running kiosks happily serves stale HTML across browser restarts. That stale HTML references an old bundle hash which is also still in disk cache, so the kiosk runs pre-deploy JS forever without ever knowing why. (2) frontend/public/sw.js — CACHE_NAME bumped from bambuddy-v25 to bambuddy-v26 so any client that fetches the new sw.js drops its old CacheStorage. The SW does network-first for HTML/JS/CSS but intercepts and falls back to cache, and cache-control on HTTP responses doesn't reach into the SW's own cache layer. (3) spoolbuddy/install/install.sh — generated kiosk launcher now uses --user-data-dir=/tmp/spoolbuddy-kiosk-userdata with a pre-launch rm -rf, so every kiosk restart starts from a clean slate (no HTTP cache, no SW registration, no IndexedDB). Trade-off is a slightly slower first paint and zero offline support; neither matters for a single-purpose kiosk facing a backend on the same LAN, and the guarantee that next-deploy-just-works is worth far more. 4 new tests in test_static_html_cache_headers.py: index.html on / and SPA catch-all paths emit Cache-Control: no-cache, must-revalidate; API routes are unaffected (no leak of HTML cache directive onto endpoints we want React Query to cache aggressively). For existing kiosks already trapped by an old persistent profile, operator runs once: rm -rf ~/.config/chromium && systemctl restart getty@tty1.service. The new launcher then picks up automatically.
This commit is contained in:
parent
11fe68578e
commit
e9200449ae
8 changed files with 251 additions and 114 deletions
|
|
@ -22,6 +22,7 @@ All notable changes to Bambuddy will be documented in this file.
|
|||
**Test coverage** — 46 unit tests for `services/makerworld.py` (header shape, API base, `get_design`/`get_design_instances`/`get_profile`, `get_profile_download` 200/401/403/404/no-token, `download_3mf` SSRF rejection of 4 hostile hosts, S3 path delegation, CDN path with minimal headers, size-cap, `_download_s3_urllib` happy/redirect/size/network paths, `fetch_thumbnail` with `follow_redirects=False`); 19 route tests (`/resolve`, `/import` with folder autocreation + explicit folder + dedupe + filename basename + profile_id response, `/recent-imports` with empty-list / ordering / pydantic shape / limit clamping, `_canonical_url` unit); 12 frontend tests (button labels, slicer-name interpolation, URL-change detection, inline post-import actions, Recent imports rendering, DOMPurify `<script>` strip).
|
||||
|
||||
### Changed
|
||||
- **AMS slot "Assign to inventory spool" picker now lists every spool, including RFID-tagged Bambu Lab ones** ([#1133](https://github.com/maziggy/bambuddy/issues/1133)) — The picker that opens from `<FilamentHoverCard>` / SpoolBuddy's slot-action sheet had two stacked filters that together blocked a real workflow: (1) `AssignSpoolModal` only listed spools whose `tag_uid` AND `tray_uuid` were both null, hiding any Bambu Lab spool that had been auto-created from RFID or scanned via SpoolBuddy NFC; (2) `FilamentHoverCard` rendered its inventory section (assign + unassign affordances) only when the slot's vendor was *not* `Bambu Lab`, so even if you fixed the picker the button to open it wasn't visible on a BL slot. The use case both filters blocked: a user who has a Bambu Lab spool sitting in their inventory but doesn't want to scan it via SpoolBuddy NFC each time and just wants to pick it from the list. Both gates are gone now: the modal lists every spool that isn't already taken by a different (printer / ams_id / tray_id) tuple, and the hover-card inventory section renders for every vendor including Bambu Lab. The AMS-vs-external-slot distinction in the modal also collapsed — external slots (amsId 254/255) used to be the only path that allowed picking a tagged spool, and that special-case is now redundant. Empty slots (`<EmptySlotHoverCard>` in Bambuddy, `slotActionPicker.tray === null` in SpoolBuddy) lost their assign affordance entirely: a physically empty slot has no spool to attach an inventory record to, and offering the action there only led to users assigning the wrong spool to a slot the printer hadn't actually loaded yet — assignment now requires a loaded slot. The `i18n.inventory.noManualSpools` key (whose copy talked specifically about "manually added spools") was renamed to `inventory.noAvailableSpools` with new copy ("No spools available. Add a spool to your inventory or unassign one from another slot first.") since the empty-state premise changed; localised across all 8 languages with full translations. 5 net-new frontend tests in `__tests__/components/FilamentHoverCard.test.tsx` (assign/unassign buttons render for `vendor: 'Bambu Lab'`, non-BL vendors unchanged, EmptySlotHoverCard renders no assign affordance, configure button still works on empty slots) plus the existing `AssignSpoolModal.test.tsx` "filters out BL spools" expectation was inverted to match the new contract and the empty-state test reworked to exercise the only remaining trigger (every spool taken by another slot).
|
||||
- **Inventory: "Delete Tag" button renamed to "Clear RFID Tag"** ([#729](https://github.com/maziggy/bambuddy/issues/729) follow-up) — The reporter mistook the button for a taxonomy-tag delete (it actually clears the RFID tag UID/UUID off the spool record so the row can be re-attached to a different physical spool). Renaming it to "Clear RFID Tag" + the success toast to "RFID tag cleared" removes the ambiguity. No behaviour change. Localised across all 8 UI languages with full translations.
|
||||
|
||||
- **Nozzle icon on the dual-nozzle status card** ([#1115](https://github.com/maziggy/bambuddy/issues/1115)) — the dual-nozzle active-extruder card on the printer status bar was the only card in that row without a theme icon (the Nozzle/Bed/Chamber temperature cards all carry a thermometer icon), which left the row looking visually uneven on H2D / H2S / H2C. Adds a small schematic nozzle icon (filament body + heater block + tip) above the L/R diameter labels, styled in amber-400 to match the card's active-extruder accent. SVG design contributed by @m4rtini2.
|
||||
|
|
@ -31,6 +32,8 @@ All notable changes to Bambuddy will be documented in this file.
|
|||
- **Per-request trace ID column on every log line, plumbed through HTTP access log + application logs + response headers** — Builds on the new uvicorn-access-log-into-bambuddy.log change below: the access line tells you *who* called an endpoint, but until now there was no way to tie that line to the application records emitted on the server side while handling that request. A new FastAPI middleware (`trace_id_middleware` in `main.py`, sourced from `backend.app.core.trace`) stamps each request with a fresh 8-char hex ID (or honours a sane inbound `X-Trace-Id` header for cross-system correlation), stores it in a `ContextVar` so any code in the request's call stack can read it, echoes it on the response as `X-Trace-Id`, and a new `TraceIDFilter` injects it into every `LogRecord` so the format string `[%(trace_id)s]` resolves to the right ID for the right request. ContextVars (rather than `request.state`) are the right plumbing here because asyncio copies the current context into every `asyncio.create_task`, so background work spawned from inside a request inherits the trace ID without explicit threading; the logging filter has no access to the FastAPI request object regardless. Records emitted outside any request scope (startup, MQTT callbacks, scheduler) get a stable `-` placeholder so the column stays visually aligned and missing values are obvious in `grep`. Inbound `X-Trace-Id` is hard-validated against a strict whitelist (`[A-Za-z0-9_-]+`, max 64 chars) before being honoured — a hostile or buggy caller cannot smuggle log-injection payloads (newlines, control chars, megabyte blobs) into `bambuddy.log` via the trace-ID column; values that fail the gate silently trigger a freshly minted server-side ID rather than failing the request. Middleware is decorated AFTER `auth_middleware` on purpose: Starlette stacks `@app.middleware` decorators LIFO so the last-decorated runs first inbound, making trace stamp the OUTERMOST layer — auth log lines and every record emitted on the way down to and back from the route handler all carry the same ID. Output now looks like `2026-04-26 09:51:39,152 INFO [uvicorn.access] [a4f3b1e7] 192.168.1.42:54812 - "POST /api/v1/printers/1/print/stop HTTP/1.1" 200` paired with the route handler's `2026-04-26 09:51:39,158 INFO [bambu_mqtt] [a4f3b1e7] [SERIAL] Sent stop print command` — one `grep a4f3b1e7` away from the full causality chain. 30 new tests across `tests/unit/test_trace.py` (placeholder when no request scope, filter copies ContextVar value onto records, ID propagates into spawned tasks via asyncio context copy, concurrent requests don't leak IDs into each other, generator produces unique hex IDs, hostile payloads rejected by validator, max-length boundary, dash/underscore variants accepted) plus `tests/integration/test_trace_middleware.py` (X-Trace-Id header echoed on response, body and header IDs match, each request gets a unique ID, generator format stays short hex, safe inbound IDs honoured, hostile inbound IDs replaced, overlong inbound IDs replaced, ContextVar reset cleanly after request).
|
||||
|
||||
### Fixed
|
||||
- **SpoolBuddy AMS page: re-assigning a just-unassigned spool sometimes showed an empty picker** ([#1133](https://github.com/maziggy/bambuddy/issues/1133) follow-up) — Reported live during the rollout of the #1133 picker change: unassigning a Bambu PLA Metal spool from SpoolBuddy and re-opening the picker showed "no spools available" — the just-freed spool was missing. The investigation surfaced four distinct causes that all needed addressing for the picker to stay correct, plus a deployment-side cause that prevented any of the fixes from reaching the live kiosk. **(1) Dual cache-key shapes for spool assignments**: `SpoolBuddyAmsPage` keys by `['spool-assignments', selectedPrinterId]` while the shared `AssignSpoolModal` keys by `['spool-assignments']`, and `SpoolBuddyAmsPage.unassignMutation.onSuccess` only invalidated the printerId-keyed one, leaving the modal's unkeyed cache stale. Both invalidate calls (mutation success + modal-close handler) now hit both keys; collapsing the two key shapes into one is intentionally deferred since the dual-key pattern predates this change and shows up in 6 components. **(2) Toggle wasn't a real escape hatch**: the existing "Show all spools" toggle's label *said* it would help when a spool was hidden but only bypassed the material/profile filter, not the assignment-elsewhere gate. It now bypasses BOTH filters, making it a real escape hatch (the backend's `assign_spool` is upsert-per-(printer/ams/tray), so picking a currently-taken spool just creates a second assignment row — foot-gun for normal flows but exactly the recovery path this toggle is for). **(3) Cross-component cache pollution**: `['inventory-spools']` was used as a query key by 5+ components calling `getSpools()` with different `includeArchived` arguments — React Query treated them as one query and served whichever response landed first, so a SpoolBuddy component priming the cache with `getSpools(false)` could hide spools from the modal that wasn't yet present at that fetch time. The modal now uses its own dedicated key `['inventory-spools', 'assign-modal']` + `getSpools(true)` so it's never at the mercy of someone else's cache state. **(4) Empty-state had no diagnostic surface**: when the picker showed "No spools available" there was no way to tell *why* — was the fetch empty? Were spools archived? All assigned elsewhere? A small counter `X fetched · Y archived · Z assigned to other slots` now renders in the empty state so future reports of this kind are immediately answerable from a screenshot rather than requiring devtools digging. **(5) Browser holding stale JS forever**: `index.html` was being served without `Cache-Control` headers, so Chromium's heuristic-cache freshness window kept the OLD HTML "fresh" for days across browser restarts. The OLD HTML referenced an OLD content-hashed bundle, which was also still in disk cache, so the kiosk kept running pre-deploy JS *no matter how many times* its Chromium was restarted or cache-cleared — the persistent profile would re-seed the cache from disk on next start. Backend now sends `Cache-Control: no-cache, must-revalidate` on both `/` and the SPA catch-all that serve `index.html`; service worker `CACHE_NAME` bumped from `bambuddy-v25` to `bambuddy-v26` so any client that does eventually re-fetch `sw.js` invalidates its CacheStorage; and `spoolbuddy/install/install.sh` now generates the kiosk launcher with `--user-data-dir=/tmp/spoolbuddy-kiosk-userdata` plus a pre-launch `rm -rf` so every kiosk restart starts from a clean slate (the kiosk has no per-user state worth persisting — auth token is in the URL query, not a stored cookie). 6 net-new tests across `AssignSpoolModal.test.tsx` (toggle escape-hatch behavior) and `tests/integration/test_static_html_cache_headers.py` (Cache-Control directive on root + SPA catch-all routes, no leak onto API routes). Reproduced end-to-end on an H2D + dual AMS + SpoolBuddy display: unassign Bambu PLA Metal Iridium Gold Metallic from slot B4 → reopen picker → spool now visible without browser intervention.
|
||||
|
||||
- **Plate-clear button stayed visible after the API cleared `awaiting_plate_clear` outside the printer-card click path** ([#1128](https://github.com/maziggy/bambuddy/issues/1128)) — `awaiting_plate_clear` is a Bambuddy-side flag, not a printer-side one, so toggling it does not produce an MQTT push from the printer. Commit 4e86e8c added the flag to the `printer_status` payload so MQTT-driven broadcasts (e.g. when a print finishes and on_print_complete sets the flag to True alongside a state transition to FINISH) carry it correctly. The reverse transition didn't get the same treatment: `POST /printers/{id}/clear-plate` mutated `PrinterManager._awaiting_plate_clear` and persisted to the DB, but emitted no `printer_status` WebSocket update — and the in-`main.py` status-change broadcaster's `status_key` deduplication intentionally excludes Bambuddy-side flags, so even a coincidentally-arriving MQTT push wouldn't reflect the change. The "Mark plate as cleared" button on the printer card disappeared "immediately" after a click only because the React Query cache was being optimistically updated client-side; clearing the flag through any other route (an admin script, a second tab, an automation hitting the endpoint directly, the scheduler at `print_scheduler.py:1844` when dispatching the next queued print) silently left every UI subscriber but the originating tab stale until a coincidental status refresh. Centralised the broadcast in `PrinterManager.set_awaiting_plate_clear` itself rather than at each call site, so every current AND future caller is covered without remembering to wire it up: a new `_broadcast_status_change(printer_id)` private coroutine is scheduled alongside the existing `_persist_awaiting_plate_clear` whenever the flag flips under a running event loop. The broadcast lazy-imports `ws_manager` to keep `printer_manager.py` clean of application-layer infra at module-import time, short-circuits when `get_status` returns `None` (printer disconnected — the next reconnect produces a fresh push anyway), and swallows `ws_manager.send_printer_status` failures so the persistence path can complete even if the WS layer is temporarily unavailable. The same hook is now in place for any other Bambuddy-side flag that gets added to `printer_state_to_dict` later — they'll all need to broadcast their own changes for the same reason. 8 new regression tests in `test_printer_manager_status_broadcast.py`: schedules-on-True/False/loop-running/no-loop/loop-stopped contracts, `_broadcast_status_change` happy path with payload assertion, skip-when-no-state, swallow-WS-errors, and an end-to-end live-loop test that fires `set_awaiting_plate_clear(False)` and asserts a broadcast lands with `awaiting_plate_clear: false` in the payload. Existing 24 tests in `test_scheduler_clear_plate.py` continue to pass unchanged because they instantiate `PrinterManager()` without attaching a loop (sync unit-test path) — the new `_schedule_async` call short-circuits on the same loop check the existing persistence call already used. Thanks to @EdwardChamberlain for the precise root-cause analysis (down to the exact line and the suggested `ws_manager.send_printer_status()` fix).
|
||||
|
||||
- **Uvicorn HTTP access log was missing from `bambuddy.log`, leaving rogue server-state changes untraceable** — When an HTTP endpoint that mutates server state fires unexpectedly (the canonical example: a print spontaneously stopping mid-job because something hit `POST /printers/{id}/print/stop`), the only on-disk trail was Bambuddy's own application log — which by design only records the *outbound* MQTT publish (`Sent stop print command`), not the *inbound* HTTP call that triggered it. The result was an unsolvable mystery on 2026-04-26: prints stopping with no preceding Bambuddy-side log line, no way to identify the caller, and the rotated container stdout already gone by the time the support pack was generated. Root cause: uvicorn ships its `access` logger with `propagate=False` by default, so the existing `RotatingFileHandler` attached to root never received those records. `main.py` now attaches the same file handler directly to `logging.getLogger("uvicorn.access")` and applies a new `WriteRequestsOnlyFilter` (`backend/app/core/logging_filters.py`) that keeps `POST` / `PUT` / `PATCH` / `DELETE` and drops `GET` / `HEAD` / `OPTIONS`. Status polls, camera streams, snapshot fetches, websocket upgrades, and CORS preflights account for the bulk of access traffic on a running install and none of them can change server state on their own — dropping them keeps `bambuddy.log` focused on lines that matter for incident triage without churning the 5 MB rotation window faster than it's useful. Filter anchors on the `" `+verb+` ` pattern uvicorn's format string guarantees, so a literal `"POST"` substring inside a URL (e.g. `GET /api/posts/POST_123`) cannot false-match. The filter lives in its own module so the test suite can import it without pulling in `main.py`'s entire startup graph. 13 new tests in `test_logging_filters.py` cover all four write verbs being kept, GET/HEAD/OPTIONS being dropped, two URL-contains-verb-substring false-match guards, empty/unrelated-line/idempotency edge cases. Output now looks like `2026-04-26 09:23:14,690 INFO [uvicorn.access] 192.168.1.42:54812 - "POST /api/v1/printers/1/print/stop HTTP/1.1" 200` — one `grep "POST.*stop"` away from "who triggered this".
|
||||
|
|
|
|||
|
|
@ -4767,7 +4767,7 @@ async def serve_frontend():
|
|||
"""Serve the React frontend."""
|
||||
index_file = app_settings.static_dir / "index.html"
|
||||
if index_file.exists():
|
||||
return FileResponse(index_file)
|
||||
return FileResponse(index_file, headers=_HTML_CACHE_HEADERS)
|
||||
return {
|
||||
"message": "Bambuddy API",
|
||||
"docs": "/docs",
|
||||
|
|
@ -4775,6 +4775,19 @@ async def serve_frontend():
|
|||
}
|
||||
|
||||
|
||||
# index.html must always be revalidated — Vite emits content-hashed JS/CSS
|
||||
# bundles (e.g. `index-JRaF_JhW.js`), so the JS itself is safe to cache
|
||||
# forever, but the HTML wrapping it is the only file that knows which hash
|
||||
# is current. Without explicit cache-control headers Chromium decides
|
||||
# heuristically (typically 10% of the time since Last-Modified) and on
|
||||
# long-running kiosks happily serves stale HTML across browser restarts.
|
||||
# That stale HTML references an old bundle hash, the old bundle is also
|
||||
# in the disk cache, and the user ends up running pre-update JS forever
|
||||
# without ever knowing why. ``no-cache`` (revalidate every time, but a
|
||||
# 304 is cheap) is the correct setting for an SPA's entry HTML.
|
||||
_HTML_CACHE_HEADERS = {"Cache-Control": "no-cache, must-revalidate"}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint."""
|
||||
|
|
@ -4860,6 +4873,6 @@ async def serve_spa(full_path: str):
|
|||
|
||||
index_file = app_settings.static_dir / "index.html"
|
||||
if index_file.exists():
|
||||
return FileResponse(index_file)
|
||||
return FileResponse(index_file, headers=_HTML_CACHE_HEADERS)
|
||||
|
||||
return {"error": "Frontend not built"}
|
||||
|
|
|
|||
90
backend/tests/integration/test_static_html_cache_headers.py
Normal file
90
backend/tests/integration/test_static_html_cache_headers.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"""Tests for the SPA index.html cache-control behaviour.
|
||||
|
||||
Background: Vite emits content-hashed JS/CSS bundle filenames (e.g.
|
||||
``index-JRaF_JhW.js``), so those assets are safe to cache forever — the
|
||||
hash changes when their content changes. The wrapping HTML, however, is
|
||||
the only file that knows which hash is current. Without explicit cache
|
||||
directives, Chromium falls back to heuristic caching (typically 10% of
|
||||
the time since Last-Modified) and on long-running kiosks happily serves
|
||||
stale HTML across browser restarts. That stale HTML references an old
|
||||
bundle hash, which is also still in disk cache, so the kiosk runs
|
||||
pre-deploy JS indefinitely without ever knowing why.
|
||||
|
||||
Reproduced in the wild during the #1133 rollout — the SpoolBuddy
|
||||
display kept serving the pre-fix picker for hours after every
|
||||
cache-clear attempt because Chromium would re-seed its cache from
|
||||
disk on next start. Fixed by sending ``no-cache, must-revalidate`` on
|
||||
the two routes that serve ``index.html``.
|
||||
|
||||
These tests pin that behaviour so it can't silently regress (e.g. a
|
||||
later PR adding a third index.html serve route forgetting the headers,
|
||||
or someone tightening the policy to ``max-age=N`` and breaking deploys
|
||||
in subtle ways).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
# index.html is served by two distinct routes:
|
||||
# - "/" — root entry
|
||||
# - the SPA catch-all (any unrecognised path that isn't /api/)
|
||||
# Both must carry the same headers; testing both individually is the
|
||||
# only guard against one being added later without the other.
|
||||
HTML_ROUTES = [
|
||||
pytest.param("/", id="root"),
|
||||
# Catch-all routes a path like /spoolbuddy/ to index.html. The trailing
|
||||
# slash matters — without it FastAPI redirects, which would skip the
|
||||
# cache-control middleware. Tested as a real-world client URL.
|
||||
pytest.param("/spoolbuddy/", id="spa-catchall-spoolbuddy"),
|
||||
pytest.param("/printers", id="spa-catchall-printers"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(("path",), HTML_ROUTES)
|
||||
async def test_index_html_emits_no_cache_directive(async_client: AsyncClient, path: str):
|
||||
"""Every index.html serve must emit ``Cache-Control: no-cache,
|
||||
must-revalidate`` — kiosks rely on this to pick up new builds without
|
||||
operator intervention."""
|
||||
response = await async_client.get(path)
|
||||
|
||||
# Both serve routes should return 200 with HTML content type.
|
||||
assert response.status_code == 200, f"Expected 200 for {path}, got {response.status_code}: {response.text[:200]}"
|
||||
assert response.headers.get("content-type", "").startswith("text/html"), (
|
||||
f"{path} returned non-HTML content-type: {response.headers.get('content-type')}"
|
||||
)
|
||||
|
||||
# The Cache-Control header is the actual contract under test.
|
||||
cache_control = response.headers.get("cache-control", "")
|
||||
assert "no-cache" in cache_control, (
|
||||
f"{path} missing 'no-cache' in Cache-Control header (got: {cache_control!r}). "
|
||||
f"Without this kiosks serve stale HTML across browser restarts and never "
|
||||
f"pick up new builds."
|
||||
)
|
||||
assert "must-revalidate" in cache_control, (
|
||||
f"{path} missing 'must-revalidate' in Cache-Control header (got: {cache_control!r}). "
|
||||
f"This belt-and-braces directive prevents stale-while-revalidate-style "
|
||||
f"intermediaries from serving cached HTML even when it's expired."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_routes_unaffected_by_html_cache_headers(async_client: AsyncClient):
|
||||
"""Defensive: the cache-control directive must NOT leak onto API
|
||||
responses. API responses set their own headers (or none at all) per
|
||||
endpoint; a global ``no-cache`` would silently disable the React
|
||||
Query cache wins we depend on for snappy UI updates."""
|
||||
response = await async_client.get("/api/v1/printers")
|
||||
|
||||
# We don't care about success/failure here — just that no cache
|
||||
# directive was inherited from the HTML serve path. (The endpoint
|
||||
# itself may 401/403 depending on auth state in the test fixture
|
||||
# which is fine; what matters is the response shape.)
|
||||
cache_control = response.headers.get("cache-control", "")
|
||||
assert "no-cache" not in cache_control or "private" in cache_control, (
|
||||
f"API route /api/v1/printers leaked HTML cache-control: {cache_control!r}. "
|
||||
f"If a 'no-cache' directive is intentional on an API endpoint it should be "
|
||||
f"set per-route, not inherited from the SPA HTML path."
|
||||
)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// Bambuddy Service Worker
|
||||
const CACHE_NAME = 'bambuddy-v25';
|
||||
const CACHE_NAME = 'bambuddy-v26';
|
||||
const STATIC_CACHE = 'bambuddy-static-v25';
|
||||
|
||||
// Static assets to cache on install
|
||||
|
|
|
|||
|
|
@ -1210,12 +1210,43 @@ for _i in \$(seq 1 60); do
|
|||
sleep 1
|
||||
done
|
||||
|
||||
# Ephemeral user-data-dir under /tmp + wipe on every launch.
|
||||
#
|
||||
# The kiosk has no per-user state worth persisting (the auth token is in
|
||||
# the URL query, not a stored cookie), but the default profile at
|
||||
# ~/.config/chromium was accumulating two specific kinds of state across
|
||||
# reboots that broke deploys badly:
|
||||
#
|
||||
# 1. HTTP disk cache holding old index.html across browser restarts.
|
||||
# Chromium's heuristic-cache freshness window kept the old HTML
|
||||
# "fresh" for days, which referenced an old content-hashed bundle,
|
||||
# so newly deployed code never reached the running tab even after
|
||||
# pkill+relaunch. Reproduced in the wild during the #1133 rollout
|
||||
# — the kiosk kept showing the pre-fix picker for hours after every
|
||||
# cache-clear attempt because the persistent profile would re-seed
|
||||
# the cache from disk on next start.
|
||||
# 2. A stuck Service Worker registration, which intercepted requests
|
||||
# with its own cache layer (CacheStorage), independent of the HTTP
|
||||
# cache. Even after \`rm -rf Default/Cache/*\` the SW could replay
|
||||
# stale responses from CacheStorage until explicitly unregistered.
|
||||
#
|
||||
# Wiping the user-data-dir on every launch is the simplest, most
|
||||
# bulletproof escape hatch — every kiosk restart is now functionally
|
||||
# equivalent to a private-window first-load. Future deploys propagate
|
||||
# automatically: the next chromium launch picks up the latest bundle
|
||||
# without any extra tooling. Trade-off is a slightly slower first paint
|
||||
# (no warm cache) and zero offline support, neither of which matter for
|
||||
# a single-purpose kiosk facing a backend on the same LAN.
|
||||
USER_DATA_DIR="/tmp/spoolbuddy-kiosk-userdata"
|
||||
rm -rf "\$USER_DATA_DIR"
|
||||
|
||||
exec chromium --kiosk --no-first-run --disable-infobars \
|
||||
--disable-session-crashed-bubble --disable-features=TranslateUI \
|
||||
--noerrdialogs --disable-component-update \
|
||||
--overscroll-history-navigation=0 \
|
||||
--ozone-platform=wayland \
|
||||
--disable-crash-reporter --disable-breakpad \
|
||||
--user-data-dir="\$USER_DATA_DIR" \
|
||||
"\$kiosk_url"
|
||||
EOF
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-D1m4OtUR.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-JRaF_JhW.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-telVPl_h.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Bambuddy Service Worker
|
||||
const CACHE_NAME = 'bambuddy-v25';
|
||||
const CACHE_NAME = 'bambuddy-v26';
|
||||
const STATIC_CACHE = 'bambuddy-static-v25';
|
||||
|
||||
// Static assets to cache on install
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue