mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
fix(frontend): revert base: '' so deep SPA routes load their assets on initial navigation (issue #1221)
PR #1195 (d6a31393) set Vite's base: '' to emit relative asset URLs
in the built index.html, intended as a partial improvement for
path-prefixed reverse proxies. The relative URLs broke every deep
SPA route on initial load: popup windows, direct URL paste, page
refresh on /camera/<id>, /projects/<id>, /groups/<id>/edit,
/external/<id>, /files/trash, and the SpoolBuddy kiosk paths.
Browser resolves ./assets/index-XXX.js against the document URL.
For /camera/<id>, that gives /camera/assets/index-XXX.js — the SPA
catch-all returns index.html (text/html) for that path, and modern
browsers refuse to execute HTML as a JS module under
X-Content-Type-Options: nosniff. Hence the empty-popup symptom
reported on #1221 across P1S / P2S / X1 / Docker / git / Chrome /
Firefox / Brave / Safari, plus the quieter "blank page on refresh"
on every other deep route.
Reverts the two PR #1195 lines: removes base: '' from
vite.config.ts (Vite default '/' restored, emitting absolute asset
URLs /assets/..., /manifest.json, /sw-register.js) and reverts
register('sw.js') to register('/sw.js') in public/sw-register.js.
PR #1195's class of bug — path-prefixed reverse proxy users serving
Bambuddy at a subpath — was already explicitly closed as wontfix in
that thread because supporting it requires subpath-aware
bootstrapping (API_BASE, React Router basename, PWA manifest scope,
SW scope) for every user forever. The supported alternative for that
audience stays as documented in the #1195 closing comment: NPM
(Nginx Proxy Manager) addon + Cloudflare Tunnel at a real domain
with HTTPS, then HA Webpage panel embedding via
TRUSTED_FRAME_ORIGINS — that path doesn't depend on base: '' at all.
The trade-off is intentional: revert reaches every user impacted by
deep-route initial-load bugs (much larger population than
path-prefixed proxy users), in exchange for an already-wontfixed
subpath-proxy regression that has a working alternative.
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **Slicer Bundle (.bbscfg) import — pick presets from a stored bundle instead of resolving cloud/local/standard PresetRefs every slice** — Closes the long tail of preset-resolution corner cases (cloud presets behind login, "from User" sentinel handling, the `# `-prefix clone trick, dangling `inherits` on renamed parents, etc.) by letting users upload a BambuStudio "Printer Preset Bundle" (`.bbscfg`) once per printer and pick from it for every subsequent slice. **Service layer (`backend/app/services/slicer_api.py`):** `BundleSummary` / `BundleNotFoundError` types, `import_bundle` / `list_bundles` / `get_bundle` / `delete_bundle` methods, `slice_with_bundle` which posts `/slice` with bundle id + per-category preset names instead of the JSON triplet. **Routes (`/api/v1/slicer/bundles`, all gated on `Permission.LIBRARY_UPLOAD`):** `POST` / `GET` / `GET :id` / `DELETE :id`. All routes proxy via `_resolve_slicer_api_url` so they follow the user's `preferred_slicer` setting (bambu_studio vs orcaslicer). Status-code mapping treats sidecar 4xx as 400, `BundleNotFoundError` as 404, sidecar unreachable as 503, and sidecar 5xx as 502. **Preview-slice (`backend/app/services/slice_preview.py::get_preview_filaments`)** picks up optional `bundle_id` + `printer_name` + `process_name` + `filament_names` params and routes through `slice_with_bundle` when set; the cache key picks up a bundle-context fingerprint so different bundle picks on the same file occupy distinct entries — gram numbers in the preview now match what the real print will produce instead of being derived from the file's embedded process settings (which can drift from the triplet the actual slice would use). The `library.py` and `archives.py` `/filament-requirements` routes forward the new params. **Dispatch (`SliceRequest.bundle: SliceBundleSpec`):** when set, `_run_slicer_with_fallback` skips `resolve_preset_ref` and calls `slice_with_bundle`; the validator skips the preset-required check so bundle-only requests validate. 3MF + bundle CLI 5xx still falls back to the embedded-settings slice path (`used_embedded_settings=True` surfaces in the response), and sidecar 404 (unknown bundle / preset name) maps to 400. **Frontend SliceModal Bundle tier:** new "Slicer bundle" picker at the top of the modal, rendered only when at least one bundle is imported (`GET /slicer/bundles` non-empty). Selecting a bundle replaces cloud / local / standard preset dropdowns with bundle-scoped pickers (process + per-slot filament names from the bundle) — printer is implicit (each `.bbscfg` has exactly one). "None" leaves the modal on the original preset-triplet path. Submit routes through `SliceRequest.bundle` so the backend skips PresetRef resolution and asks the sidecar to materialise the JSON triplet from the stored bundle by name. **Frontend types:** `SliceBundleSpec` + `bundle?: SliceBundleSpec` on `SliceRequest`; `getLibraryFileFilamentRequirements` / `getArchiveFilamentRequirements` accept an optional 4th-arg bundle context object. The orca-slicer-api fork's bundle endpoints (shipped on `bambuddy/bundle-import`) are the server side of this — see the slicer-api sidecar docker-compose for the matching versions.
|
||||
|
||||
### Fixed
|
||||
- **Filament profile picker truncated long names with no way to see the nozzle suffix** ([#1237](https://github.com/maziggy/bambuddy/issues/1237), reported by @basziee) — In the Configure AMS Slot modal, profile names like `SUNLU PETG GLOW IN THE DARK GEN2 @Bambu Lab H2C 0.4 nozzle` were visually truncated mid-name, hiding the `@<printer> <nozzle>` suffix. With several near-identical entries differing only in nozzle size, users had to open browser dev tools to tell them apart. **Fix:** the preset row now expands inline on hover — `truncate` stays as the default (so the list keeps its compact one-line shape) but `group-hover:whitespace-normal group-hover:break-all` flips it to a wrapped multi-line view the moment the cursor enters the row, so the nozzle suffix is readable instantly without waiting on the browser's title-tooltip delay. The parent button gets `group` to drive the hover. The native `title={preset.name}` is also added as a belt-and-braces fallback for assistive tech and touch devices where `:hover` doesn't fire. Same pattern in both the desktop and mobile layouts of `ConfigureAmsSlotModal.tsx`. No new dependencies. **Test:** new `ConfigureAmsSlotModal.test.tsx` regression assertion that the rendered preset span carries `title=<full name>` plus the `truncate` and `group-hover:whitespace-normal` classes, and the parent button has `group` — so a future refactor that drops any of those fails CI.
|
||||
- **Camera preview popup opened to a blank page; deep-route refresh and direct URL load broken** ([#1221](https://github.com/maziggy/bambuddy/issues/1221), reported by @enjoylifenow / @Haeckan / @elit3ge / @jc21) — Clicking "open camera in new window" from the printer card opened a popup that rendered as an empty white page across P1S / P2S / X1 series, every install method (Docker / git clone), every browser (Chrome / Firefox / Brave / Safari), starting with the daily build of 2026-05-05. **Cause:** PR #1195 (`d6a31393`, "fix(frontend): emit relative asset paths so SPA loads under any subpath") set `base: ''` in `vite.config.ts` to support path-prefixed reverse proxies (HA Ingress, nginx subpath, Cloudflare Tunnel path routing). With that, the built `index.html` references its bundle and stylesheet via relative URLs (`./assets/index-XXX.js`, `./sw-register.js`). When the popup opened at `/camera/<id>`, the browser resolved `./assets/index-XXX.js` against the current document URL — which doesn't end in a slash, so the URL parser treated `<id>` as a file and `/camera/` as the directory, giving `/camera/assets/index-XXX.js`. The backend's SPA catch-all returned `index.html` (text/html) for that request, and modern browsers refuse to execute HTML as a JS module under `X-Content-Type-Options: nosniff`, so the popup loaded the document but never the bundle. Same break hit any deep route on initial load — direct URL paste / refresh on `/camera/:printerId`, `/projects/:id`, `/groups/:id/edit`, `/files/trash`, `/external/:id`, and the SpoolBuddy kiosk's `/spoolbuddy/ams` if loaded directly — manifesting as a quiet "blank page on refresh" that users worked around by navigating from the home page. The console error gives it away: `Loading module … was blocked because of a disallowed MIME type ("text/html")`. **Fix:** revert PR #1195's `vite.config.ts` and `sw-register.js` changes — `base: ''` is removed (Vite default `'/'` restored), and `navigator.serviceWorker.register('sw.js')` reverts to `register('/sw.js')`. The built `index.html` now emits absolute asset URLs (`/assets/...`, `/manifest.json`, `/sw-register.js`) which resolve against host root regardless of document URL, so deep routes load their assets correctly on initial navigation. PR #1195's class of bug — path-prefixed reverse proxy users serving Bambuddy at a subpath — was already explicitly closed as wontfix in that thread because supporting it requires subpath-aware bootstrapping (API_BASE, React Router basename, PWA manifest scope, service-worker scope, push-subscription scope) for every user forever. The supported workaround for that audience is documented: NPM (Nginx Proxy Manager) addon + Cloudflare Tunnel at a real domain with HTTPS, then HA Webpage panel embedding via `TRUSTED_FRAME_ORIGINS` — that path doesn't depend on `base: ''` at all. The trade-off here is intentional: revert reaches every user impacted by deep-route initial-load bugs (much larger population than path-prefixed proxy users), in exchange for an already-wontfixed subpath proxy regression that has a working alternative. ([#1237](https://github.com/maziggy/bambuddy/issues/1237), reported by @basziee) — In the Configure AMS Slot modal, profile names like `SUNLU PETG GLOW IN THE DARK GEN2 @Bambu Lab H2C 0.4 nozzle` were visually truncated mid-name, hiding the `@<printer> <nozzle>` suffix. With several near-identical entries differing only in nozzle size, users had to open browser dev tools to tell them apart. **Fix:** the preset row now expands inline on hover — `truncate` stays as the default (so the list keeps its compact one-line shape) but `group-hover:whitespace-normal group-hover:break-all` flips it to a wrapped multi-line view the moment the cursor enters the row, so the nozzle suffix is readable instantly without waiting on the browser's title-tooltip delay. The parent button gets `group` to drive the hover. The native `title={preset.name}` is also added as a belt-and-braces fallback for assistive tech and touch devices where `:hover` doesn't fire. Same pattern in both the desktop and mobile layouts of `ConfigureAmsSlotModal.tsx`. No new dependencies. **Test:** new `ConfigureAmsSlotModal.test.tsx` regression assertion that the rendered preset span carries `title=<full name>` plus the `truncate` and `group-hover:whitespace-normal` classes, and the parent button has `group` — so a future refactor that drops any of those fails CI.
|
||||
- **Filament usage double-counted when AMS auto-falls-back to a same-material spool** ([#957](https://github.com/maziggy/bambuddy/issues/957)) — When one spool ran out mid-print and the AMS transparently switched to a sibling slot loaded with the same material, the usage tracker credited the originally-mapped spool with the full 3MF estimate AND added the fallback spool's remain%-delta on top — so a 78 g print could show as 78 g + 60 g = 138 g consumed across the two spools, leaving the empty spool's recorded weight beyond its label weight (the symptom the original report flagged on a 1209 g spool reading "1188.30 g used" while the new spool only got a 30 g credit). Two interacting bugs: (1) the tray-change recorder in `bambu_mqtt.py` gated on `state in ("RUNNING", "PAUSE")` literal strings, and P2S firmware briefly transitions out of RUNNING during the AMS swap, so the switch was never appended to `tray_change_log`; (2) the usage-tracker splitting branch in `usage_tracker.py` was gated on `not slot_to_tray`, so even when the tray-change log was populated the splitting code only ran for prints where the slicer's mapping had not been captured — i.e. never on the actual fallback case. **Fix:** the `bambu_mqtt.py` gate now keys on the print-lifecycle flags (`_was_running and not _completion_triggered`) so any tray change between print start and completion is captured regardless of the momentary `gcode_state` string. The `usage_tracker.py` gate is split so `tray_change_log` evidence with > 1 entries always takes over from `slot_to_tray`, treating the per-segment per-layer gcode usage as the source of truth when the printer actually fed from multiple trays. Path 2 (AMS remain%-delta fallback) then naturally skips both trays because they're already in `handled_trays` after splitting, eliminating the double-credit. **Tests:** new `test_tray_change_recorded_during_intermediate_state` and `test_tray_change_not_recorded_after_completion` in `test_bambu_mqtt.py` exercising the new gate; new `test_tray_switch_overrides_print_cmd_mapping` in `test_usage_tracker.py` pinning that with `ams_mapping=[0]` set and `tray_change_log=[(0,0),(1,30)]` the splitter produces two segments summing to the 3MF estimate (no double-count) and adds both `(0,0)` and `(0,1)` to `handled_trays`.
|
||||
- **3D Preview returned `{"detail":"Not Found"}` in Docker installs** ([#1218](https://github.com/maziggy/bambuddy/issues/1218)) — The embedded GCode viewer's static assets (`gcode_viewer/`) were not copied into the production Docker image, so clicking "3D Preview" on any archive loaded an iframe at `/gcode-viewer/?archive=<id>` that returned a bare FastAPI 404 — Firefox / Chrome rendered the JSON response inside the iframe area while the outer Bambuddy layout looked normal, masking the failure unless the user actually inspected the iframe. The Vite production build doesn't stage `gcode_viewer/` into `static/` either (the dev server serves it via a `configureServer` middleware that's dev-only), and the only integration test for the route accepted `404` as a valid outcome ("`assert response.status_code in (200, 404)`") so CI never caught the missing files. Affected every Docker build since the embedded viewer landed in 0.2.4b1 (commit `3adce435`, 2026-04-22). **Fix:** `Dockerfile` now copies the `gcode_viewer/` directory alongside the React build output. **Defence in depth:** `backend/app/main.py` logs an ERROR at startup when `_gcode_viewer_dir / "index.html"` is missing so future packaging gaps surface in `docker logs` and the support bundle instead of as silent runtime 404s. **Test guard:** `backend/tests/integration/test_gcode_viewer.py` adds `test_gcode_viewer_index_served_when_assets_present` which skips when the directory is intentionally absent (unit-test environments) but asserts `200 OK` + a non-empty HTML body when the assets do exist on disk — so a future broken `COPY` fails CI loudly rather than continuing to ship a broken image.
|
||||
- **Slice button no longer enabled before the preview slice resolves** — Until the preview slice (or embedded-metadata read for already-sliced 3MFs) returned the per-plate filament list, the SliceModal rendered a synthetic single-slot fallback so the auto-pick had something to bind against. That made the Slice button enabled the moment the modal opened, even before the slicer had told us which AMS slots the plate actually consumes — clicking would dispatch against opaque defaults and the real-life print would either pick the wrong filament or fail with a slot-mismatch error after the fact. Adds `filamentReqsQuery.isSuccess` to the `isReady` chain so the button stays disabled while the preview slice is in flight (or before the backend's `/filament-requirements` call settles for sliced files) and flips to enabled the moment the real slot list lands and auto-pick fills it.
|
||||
|
||||
@@ -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,10 +74,15 @@ 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: '',
|
||||
// Default base ('/') emits absolute asset URLs (/assets/...). Required so
|
||||
// deep SPA routes (camera popup at /camera/<id>, /projects/<id>, kiosk
|
||||
// /spoolbuddy/ams, refresh on any nested route) resolve their <script>
|
||||
// and <link> tags to /assets/... instead of /<route-prefix>/assets/...,
|
||||
// which the SPA fallback would otherwise return as text/html and the
|
||||
// browser would refuse to execute (#1221). The earlier `base: ''` partial
|
||||
// fix for subpath reverse proxies (#1195, wontfix) is reverted — that
|
||||
// audience uses NPM + Cloudflare Tunnel at a real domain per the
|
||||
// documented workaround, which doesn't depend on this setting.
|
||||
plugins: [react(), serveGcodeViewer()],
|
||||
build: {
|
||||
outDir: '../static',
|
||||
|
||||
+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-BofTUuqf.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CacBES2t.css">
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-BofTUuqf.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CacBES2t.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);
|
||||
})
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
docker exec bambuddy python3 -c '
|
||||
import socket
|
||||
host = "192.168.255.133"
|
||||
ports = [80, 322, 990, 2024, 2025, 2026, 3000, 3002, 6000, 8883]
|
||||
for p in ports:
|
||||
s = socket.socket()
|
||||
s.settimeout(3)
|
||||
try:
|
||||
s.connect((host, p))
|
||||
print(f"{p} open")
|
||||
except OSError as e:
|
||||
print(f"{p} closed ({e.__class__.__name__}: {e})")
|
||||
finally:
|
||||
s.close()
|
||||
'
|
||||
Reference in New Issue
Block a user