mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
fix(frontend): emit relative asset paths so SPA loads under any subpath (#1195)
Vite's default base of '/' baked absolute asset URLs into the built
index.html (/assets/..., /manifest.json, /img/..., /sw-register.js),
so any path-prefixed reverse proxy (Traefik, nginx subpath, Cloudflare
Tunnel with path routing) served the SPA as a blank white page — the
browser requested assets from the host root and got HTML or text/plain
back, triggering MIME-mismatch errors on every stylesheet/script.
Set base: '' in vite.config.ts so the HTML transform emits relative
URLs everywhere. Update public/sw-register.js to register('sw.js')
(relative) so SW scope auto-pins to whatever subpath the document
loaded from.
Out of scope: API_BASE in client.ts is still absolute. The supported
HA embedding path remains Webpage panel + TRUSTED_FRAME_ORIGINS, not
HA Ingress (subpath-aware SPA bootstrapping has too many failure modes
around PWA scope, push subscriptions, and deep-link reloads to take on
in core). Documented explicitly in docker.md.
Reported by @Spegeli, follow-up to #1167.
This commit is contained in:
@@ -15,6 +15,8 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **Filament Track Switch (FTS) support — print modal filament dropdown is no longer empty when an X2D / H2D has the FTS accessory installed** ([#1162](https://github.com/maziggy/bambuddy/issues/1162), reported by @mkavalecz) — When the FTS accessory is installed the printer's MQTT changes one nibble of the per-AMS `info` bitmask: bits 8-11 flip from a fixed extruder ID (0x0 / 0x1) to `0xE` ("uninitialized"), because the AMS is no longer wired to a single nozzle — the FTS dynamically routes any slot to either extruder. Bambuddy's MQTT parser already skipped 0xE entries when building `ams_extruder_map` (matching BambuStudio's reading for boot-time transient state), so with the FTS installed the map ended up empty and the print modal's filament dropdown — which filters by `extruderId === nozzle_id` to prevent cross-nozzle assignment ("position of left hotend is abnormal" failures) — filtered out *every* loaded slot. Net effect: empty Filament Mapping dropdown on every dual-nozzle print with the FTS, even when the AMS was fully loaded with the right material. Detection comes from a new MQTT field — `print.device.fila_switch` — which is non-null only when the accessory is installed; it carries the routing topology as two arrays: `in[track] = currently fed slot (-1 = empty)` and `out[track] = extruder this track terminates at`. The fix surfaces this through a new `FilaSwitchState` dataclass on `PrinterState` (`installed`, `in_slots`, `out_extruders`, `stat`, `info`) and the equivalent `FilaSwitchResponse` Pydantic schema on the `GET /printers/{id}/status` route. Frontend (`useFilamentMapping.ts` + `FilamentMapping.tsx`) skips the per-extruder filter when `printerStatus.fila_switch?.installed === true` so any compatible AMS slot can satisfy any nozzle's filament requirement, since the FTS handles the routing. Slots currently fed into a track also get a routing badge in the dropdown — `[L]` or `[R]` — so the user can tell at a glance which slot the FTS is currently routing where (idle slots get no badge: they can be routed to either extruder on demand). The hard "no cross-nozzle assignment" filter on real dual-nozzle printers without the FTS stays untouched (still trips the same way it always has — `fila_switch == null` keeps the existing behaviour). 4 backend tests in `test_bambu_mqtt.py::TestFilamentTrackSwitchDetection` (default-not-installed, detect-from-MQTT-using-the-reporter's-bundle, no-fila_switch-field-stays-not-installed, missing-in-out-arrays-don't-crash) and 2 frontend tests in `useFilamentMapping.test.ts` (FTS-active drops the nozzle filter; explicit `fila_switch: null` keeps the filter applied). Upstream fila_switch payloads with anything other than the documented shape are tolerated — `installed` flips on the *presence* of the field, the routing arrays default to empty lists if missing, and the dropdown skips the badge for slots not currently in `in_slots`.
|
||||
|
||||
### Fixed
|
||||
- **Frontend served behind a path-prefixed reverse proxy (e.g. `/bambuddy/` on Traefik / nginx / Cloudflare Tunnel) loaded a blank page** ([#1195](https://github.com/maziggy/bambuddy/issues/1195), reported by @Spegeli, follow-up to [#1167](https://github.com/maziggy/bambuddy/issues/1167)) — Vite's default `base: '/'` emits absolute asset URLs in the built `index.html` (`/assets/index-*.js`, `/assets/index-*.css`, `/manifest.json`, `/img/...`, `/sw-register.js`), which assumes the SPA is always served at the host root. Behind any path-prefixed reverse proxy — Traefik with a path prefix, nginx `location /bambuddy/`, Cloudflare Tunnel with path routing, Synology / Unraid reverse-proxy panels — the browser then requests those absolute paths from the host root, the proxy doesn't see them, and the upstream serves either a 404 or HTML for an unknown path with `Content-Type: text/plain`/`text/html`; the browser logs `Refused to apply style from '.../assets/index-*.css' because its MIME type is 'text/plain'` and renders a blank white page. Two-line fix: `frontend/vite.config.ts` sets `base: ''` so Vite's HTML transform rewrites every absolute asset reference to relative (`./assets/...`, `./manifest.json`, `./img/...`, `./sw-register.js`) — these resolve correctly against whatever subpath the document was served from. `frontend/public/sw-register.js` is a public-dir file Vite copies as-is, so its `navigator.serviceWorker.register('/sw.js')` call is changed to `register('sw.js')` (relative); the SW scope is automatically pinned to whatever subpath the document loaded from, which is exactly what every reverse-proxy-at-subpath user wants. Net effect: an `https://example.com/bambuddy/` deployment now loads correctly without any frontend rebuild on the user's side. **Out of scope for this change:** runtime API base detection — `API_BASE = '/api/v1'` in `frontend/src/api/client.ts` is still absolute, so API calls still go to the host root. This is intentional. The fix above closes the immediate "blank page" report; making the API base, React Router basename, PWA manifest scope, and service-worker scope all subpath-aware would mean rewriting how the SPA bootstraps and would touch PWA-install state, push-notification subscriptions, and deep-link reload semantics. The supported way to embed Bambuddy in Home Assistant remains the **Webpage panel + `TRUSTED_FRAME_ORIGINS`** path documented in the wiki — Bambuddy reachable on a stable URL (HTTP for HTTP-only HA, HTTPS via your own reverse proxy for HTTPS HA / Nabu Casa / custom-domain), iframe-embedded via the HA dashboard. HA Ingress / addon-based subpath embedding (which would require the runtime path detection above) is not supported by core. Documented explicitly in `docker.md` so users hit the right pattern first.
|
||||
|
||||
- **iframe embedding from trusted origins (e.g. Home Assistant Webpage panel) no longer blocked** ([#1191](https://github.com/maziggy/bambuddy/issues/1191), reported by @azurusnova) — Bambuddy ships strict anti-clickjacking headers (`X-Frame-Options: SAMEORIGIN` and CSP `frame-ancestors 'none'`) by default, which protects internet-exposed deployments from being embedded by hostile sites. But it also broke a documented integration path: Home Assistant's Webpage dashboard panel embeds Bambuddy via `<iframe>` on a different origin (HA on `:8123`, Bambuddy on `:8000`), and the SAMEORIGIN value is port-strict, so even same-LAN trusted setups got "refused to connect". A new `TRUSTED_FRAME_ORIGINS` env var takes a comma-separated list of `scheme://host[:port]` origins; when set, the middleware drops `X-Frame-Options` (modern browsers honor `frame-ancestors`, and the legacy `ALLOW-FROM <url>` syntax is deprecated and inconsistent across vendors) and the CSP `frame-ancestors` directive becomes `'self' <origin> <origin>...`. The default — empty env var — keeps the strict `'none'` behavior, so Docker / bare-metal users without HA see no behavioural change. Origin validation happens at startup: only `http://` and `https://` are accepted, paths/query/fragments/wildcards are rejected with a warning (one bad entry doesn't take the deployment down — it's just dropped from the allowlist). The `gcode-viewer` route's `frame-ancestors 'self'` (same-origin embed for the in-app gcode preview iframe) also includes the allowlist when configured, so HA users embedding Bambuddy can still open the gcode viewer modal. 16 new tests in `test_security_headers.py`: 12 unit tests for the env-var parser (empty / unset / single / multiple / whitespace / empty-segment / non-http scheme dropped / missing host dropped / path dropped / query+fragment dropped / wildcard dropped / trailing-slash kept) and 4 integration tests for the middleware (default-strict emits SAMEORIGIN + 'none', allowlist relaxes CSP and drops X-Frame-Options, /docs branch also honors the allowlist, other security headers like X-Content-Type-Options and Referrer-Policy are unaffected in both modes). Documented in the Docker env-var reference page on the wiki and in `.env.example`.
|
||||
|
||||
- **Virtual Printer queue mode auto-dispatched onto the wrong colour when multiple compatible printers were available** ([#1188](https://github.com/maziggy/bambuddy/issues/1188), reported by @EdwardChamberlain) — Sending a sliced 3MF to a queue-mode VP via Orca / Studio with auto-dispatch on caused Bambuddy to schedule the job onto a printer of the right model but the wrong loaded filament: a print sliced for matte white PLA would land on a printer with no white loaded, and the printer would start the job using whatever was the closest available match. Edward's diagnosis was exact (`virtual_printer/manager.py:325-326`): the manual /api/v1/print-queue/ POST flow extracts the 3MF's per-slot filament requirements at queue-add time and writes `required_filament_types`, `filament_overrides`, and `ams_mapping` on the resulting `PrintQueueItem`, so the scheduler's color-match enforcement (`print_scheduler.py:512` — keys on `filament_overrides[].force_color_match === true`) actually runs. The VP queue-write path (`_add_to_print_queue`) skipped all of that and built a bare `PrintQueueItem` with only `printer_id`, `target_model`, `archive_id`, `plate_id`, `position`, `status`, `manual_start`. Net effect: the scheduler reached the model-only-matching fallback and accepted the first available printer of the target model regardless of loaded colour, exactly as he described. **Fix:** the scheduler's existing `_get_filament_requirements` 3MF parser is extracted into a shared helper (`backend/app/services/filament_requirements.py:extract_filament_requirements`) so the VP path can reuse it at upload time. The VP's `_add_to_print_queue` now calls that helper after archiving and populates `required_filament_types` unconditionally (cheap; helps the scheduler reject obvious type mismatches even without `force_color_match`); and writes `filament_overrides` with `force_color_match: true` per consumed slot when a new per-VP setting `queue_force_color_match` is on. Default is **off** to preserve current behaviour for upgraders — a fresh-install user who wants the bug-free behaviour flips the toggle once on the VP card; an existing user gets exactly the model-only-matching they had before until they opt in. Auto-dispatch onto the wrong material happens loudly enough that anyone affected can find the toggle. **Why default-off** rather than default-on: existing automation that relies on "send to queue VP, get printed somewhere" without caring about colour shouldn't silently start blocking on colour matching after an upgrade. The toggle has clear UI copy (`virtualPrinter.queueForceColorMatch`) explaining the trade-off. **Defence in depth:** a malformed or unparseable 3MF (e.g. fake bytes from a misconfigured upload tool) leaves both fields None and the scheduler falls back to model-only matching, matching pre-fix behaviour for the unhappy path. The scheduler itself is unchanged — it already handled `force_color_match` correctly when the field was populated; the bug was purely the VP path not populating it. **Schema:** one nullable column `virtual_printers.queue_force_color_match BOOLEAN DEFAULT 0/FALSE` (Postgres-safe) added via the existing `_safe_execute` migration pattern. **API:** `VirtualPrinterCreate` and `VirtualPrinterUpdate` Pydantic schemas + `_vp_to_dict` response shape carry `queue_force_color_match`, the create + update routes wire it through to the model, and `VirtualPrinterInstance` constructor + `multiVirtualPrinterApi` TypeScript client mirror the field. **UI:** new toggle on `VirtualPrinterCard` rendered only when `mode === 'print_queue'` (parallels the existing `auto_dispatch` toggle's mode-gating), with `pendingAction` state for the in-flight indicator. **i18n:** new `virtualPrinter.queueForceColorMatch.{title,description}` keys in all 8 locales — English fully translated, German fully translated, the other 6 locales seeded with English copy pending native translation (matches the project's existing flow for newly-added user-facing features). 11 new tests: 8 in `test_filament_requirements.py` covering the extracted parser end-to-end (per-slot dicts, zero-use slots filtered, plate filtering, no-plate flat-walk fallback, unparseable / missing / config-less files, sorted output); 3 in `test_virtual_printer.py::TestVirtualPrinterInstance` covering the VP write path (setting-off → only `required_filament_types` populated; setting-on → `filament_overrides` populated with `force_color_match: true` per slot; unparseable 3MF → both fields None, no crash). Existing scheduler tests still pass against the refactored helper (verified end-to-end across the scheduler / virtual_printer / print_queue / filament test suites — 479 tests). Edward's "out of scope nice-to-have" suggestion of a "Requires Color Match" pill on queue cards is deferred to a follow-up so this PR stays scoped to his repro.
|
||||
|
||||
@@ -10,7 +10,7 @@ if ('serviceWorker' in navigator) {
|
||||
});
|
||||
} else {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
navigator.serviceWorker.register('sw.js')
|
||||
.then((registration) => {
|
||||
console.log('SW registered:', registration.scope);
|
||||
})
|
||||
|
||||
@@ -74,6 +74,10 @@ function serveGcodeViewer() {
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
// Empty base emits relative asset URLs (./assets/... instead of /assets/...)
|
||||
// so the built SPA loads correctly when served at any subpath — HA Ingress,
|
||||
// nginx/Traefik path prefix, Cloudflare Tunnel path routing, etc. (#1195).
|
||||
base: '',
|
||||
plugins: [react(), serveGcodeViewer()],
|
||||
build: {
|
||||
outDir: '../static',
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+8
-8
@@ -17,17 +17,17 @@
|
||||
<meta name="apple-mobile-web-app-title" content="Bambuddy" />
|
||||
|
||||
<!-- Manifest -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="./img/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="./img/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./img/apple-touch-icon.png" />
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-CwcBz1oz.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Cw7zekS6.css">
|
||||
<link rel="apple-touch-startup-image" href="./img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="./assets/index-CwcBz1oz.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-Cw7zekS6.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
@@ -35,6 +35,6 @@
|
||||
<!-- Service Worker Registration (skip on SpoolBuddy kiosk).
|
||||
Kept as an external file so the CSP `script-src 'self'` covers it
|
||||
without needing 'unsafe-inline' or per-build hashes. -->
|
||||
<script src="/sw-register.js"></script>
|
||||
<script src="./sw-register.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -10,7 +10,7 @@ if ('serviceWorker' in navigator) {
|
||||
});
|
||||
} else {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
navigator.serviceWorker.register('sw.js')
|
||||
.then((registration) => {
|
||||
console.log('SW registered:', registration.scope);
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user