diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9bd706032..b41fd524c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -19,12 +19,11 @@ repos:
rev: v5.0.0
hooks:
- id: trailing-whitespace
- # Exclude static/ (build output) and gcode_viewer/ (vendored third-party
- # assets — see gcode_viewer/VENDORED.md) so whitespace normalisation
+ # Exclude static/ (build output) so whitespace normalisation
# doesn't drift the files away from upstream.
- exclude: ^(static/|gcode_viewer/)
+ exclude: ^static/
- id: end-of-file-fixer
- exclude: ^(static/|gcode_viewer/)
+ exclude: ^static/
- id: check-yaml
- id: check-json
exclude: ^(static/|frontend/tsconfig\.)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5da81275d..c3d1ec56a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ All notable changes to Bambuddy will be documented in this file.
- **The slice dialog can edit the full print-parameter set, not just pick a preset** — Slicing from Bambuddy meant taking a process preset exactly as it came. Anything beyond that — one more wall for a bracket, supports for a single overhang, slower outer walls on a part that keeps scarring — meant going back to Bambu Studio, editing there, and re-exporting. The slice dialog now has a **Process settings** section carrying the whole tree: the same pages, groups and ordering the desktop slicer shows under Print Settings, with the same labels, tooltips, ranges and defaults, because they are extracted from the slicer's own sources rather than hand-picked. The dialog itself widens to make room: on a reasonably sized screen it now uses two columns, with every "what am I slicing with" decision — pipeline, printer, process, filaments, bed type, layout passes — kept together on the left and the settings panel given a column of its own on the right, open and ready rather than folded away. Narrower screens keep the single column and the collapsed panel. The settings a source file's designer changed (#2622) now live in this panel too, marked *from file* against the options they belong to instead of in a separate list further up the dialog — so there is one place that shows what a slice will actually use. Machine-coupled ones stay flagged and unticked as before, anything the panel has no entry for is listed by name rather than quietly dropped, and typing your own value still wins. Switching on "Use the file's built-in settings" greys the panel out rather than removing it, so the dialog does not appear to lose a feature when that toggle is flipped — it stays visible, says why it is inactive, and applies nothing. Options that select *which* filament prints a feature — support base and interface, and the per-region pickers for walls, infill and surfaces — list the filaments you actually picked on the left rather than asking for a slot number, so "support interface" can be set to the PVA in slot 2 by name. Defaults and ranges are read out of the slicer's C++ initialisers, so a few arrived in source form — the whole Line width group showed "0." rather than "0" — and those are now cleaned as the data is generated instead of being papered over at display time. Every field starts from the values your picked process preset actually sets, fetched by flattening it through the slicer sidecar — the same resolver that does the slicing, so the numbers cannot disagree with what a slice produces. A field you never touch shows the preset's value, and reverting returns to it. Where those values cannot be read the panel falls back to the slicer's own defaults and says why — a sidecar older than the feature, one that did not answer, or none configured at all — rather than presenting the defaults as if they were your preset's. The first is much the most likely, because the sidecar image is pulled independently of your Bambuddy version, so that message names the fix outright: update the sidecar image. It behaves the way the desktop one does. **Simple / Advanced / Expert** matches the slicer's own visibility tiers, search reaches across every page at once, changed settings are marked and individually revertable, and settings the slicer itself disables in your current configuration are greyed out — infill options with infill at zero, ironing options with ironing off — because Bambuddy evaluates the slicer's own enable rules rather than approximating them. Where a rule cannot be decided with certainty the setting stays editable, on the grounds that a missing control looks like a bug while a redundant one is merely ignored. Edits apply to one slice, are not saved into a preset, and are written after the source file's support configuration and any carried designer settings, so an explicit choice is never silently overridden; an untouched panel produces exactly the request it did before. Parameter names and descriptions are in English even where the rest of Bambuddy is not — several hundred strings lifted verbatim from the slicer, which is a separate job from translating Bambuddy's own interface. The dialog's own wording is translated in all locales. Wiki updated, covered by backend and frontend tests.
### Changed
+- **The G-code preview is now the slicer's own renderer** — Sliced files previewed through an embedded copy of a third-party viewer, shown in an iframe. It drew each move as a screen-space line, so a print came out stringy and shimmered wherever layers crossed; it coloured by filament slot only; and being a separate app inside a frame, it could be neither themed nor translated, and needed its own machinery to detect a proxy refusing the embed. It has been replaced by Bambuddy's own viewer built on **libvgcode**, the renderer OrcaSlicer draws its own preview with, so extrusions are solid volumes that occlude one another and a print reads the way it does on the desktop. Colour by **filament** — the default, showing the print in the colours you actually assigned — or by **feature**, where walls, infill, supports, bridges and the prime tower each take the slicer's own colour, or by **layer height** or **line width** on a graduated scale. Every entry in the legend is a switch: click a filament or a feature to take it out of the view, which genuinely removes it rather than hiding it behind what it was covering, so you can look inside a part without its supports in the way. The layer slider has both ends, so a band of layers can be isolated rather than only a top capped, and travel moves can be shown. Reading the file needed real care: BambuStudio annotates its G-code quite differently from OrcaSlicer, and it emits a tenth of its moves as arcs — reading only the one dialect showed a 52-layer print as 23,165 layers in a single colour, and ignoring the arcs punched holes through every curved wall and tree support. Both are handled, along with the helical travel lifts that look like arcs but lay down nothing. Covered by frontend tests.
- **The 3D and G-code previews are rendered properly rather than sketched** — The model preview drew every surface with the same flat shading, so a print read as a coloured silhouette with no form, and it sat marooned in the middle of the frame with a screenful of empty space above it. The framing was the plainer bug: the camera distance came from a fixed multiple of the model's largest dimension, which takes no account of the camera's field of view or the shape of the panel it is drawn in, so a tall narrow preview was framed as though it were square. It is now solved against the model's bounding sphere and both fields of view, and fills the frame whatever the panel's proportions. The model itself is lit by a generated environment rather than two lamps and a wash of ambient light, which is what gives a curved surface a gradient across it instead of one flat tone, and it now casts a contact shadow so it looks like it is resting on the plate rather than pasted in front of it. The G-code preview drew each move as a two-pixel line, which is why a sliced model came out stringy and shimmered where layers crossed — a line has no thickness in the scene, so it cannot hide the layer behind it. Moves are now drawn as solid extrusions with real width and height, and the print occludes itself the way it does in a desktop slicer. The modal's second tab is gone: G-code already has its own full-page viewer, and a preview of a model is a different question from a preview of a print. Covered by frontend tests.
- **The slice dialog's process and filament lists now leave out presets that belong to another printer** — They were already sorted by compatibility, but a preset for a different Bambu model still appeared, demoted to an "Other printers" group at the bottom of the dropdown. With a large cloud filament library that group is most of the list, so the filtering was doing little for the thing it was meant to help: finding the profile you actually want. Those presets are now held back, with the label reporting how many ("3 hidden") next to a **Show all** link that brings them back for that one dropdown. Two things are never hidden. A preset with no detectable printer — a custom or renamed profile — stays in the list, because absence of evidence is not evidence of incompatibility and hiding those would make people's own imported profiles vanish. And whatever is currently selected stays visible even when the list is collapsed, so a deliberate cross-printer pick, or one restored from a pipeline, is never silently discarded by being dropped from the options. Re-slicing for another printer remains fully supported, so this is a default view rather than a restriction. Fixing this also corrected a screen-reader bug in those dropdowns: the controls sat inside the label wrapping the select, which handed them the entire label as their spoken name. A separate defect surfaced alongside it — the filter did nothing at all when the selected printer was a preset you had edited, because BambuStudio names those copies with a leading "# " and the matcher did not know to look past it. On such a printer every preset read as "compatibility unknown", and profiles listing their compatible printers by name could be ruled out against the very printer they were cloned from.
- **The MQTT debug log now records the commands sent to a printer, not only what it reports back** — **Printer → Debug → MQTT** captured one side of the conversation. Bambuddy listens on both of a printer's topics, but the one carrying commands returned before anything was written to the log, so a capture could show every status push the printer made and nothing it was ever told — including the commands Bambu Studio sends over the local network, which is the only place they can be observed at all. Those now appear alongside Bambuddy's own, grouped under the outgoing filter. It is what lets a question like "which value does Studio put in this field?" be answered from a user's capture instead of guessed at, and it is why #2774 could not be taken further. Commands Bambuddy sends appear twice, once as it publishes and once as the broker echoes it back, and the pair is itself evidence the command reached the broker. Logging is off until switched on, as before. Covered by backend tests.
diff --git a/Dockerfile b/Dockerfile
index be7293fb6..3072eb2c7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -72,15 +72,6 @@ COPY .git/HEAD ./.git/HEAD
# Copy built frontend from builder stage
COPY --from=frontend-builder /app/static ./static
-# Copy embedded GCode viewer static assets (PrettyGCode + Bambuddy adapter).
-# Served by the explicit @app.get("/gcode-viewer/{...}") routes in main.py,
-# which resolve files under (static_dir.parent / "gcode_viewer") = /app/gcode_viewer/.
-# Without this COPY the routes return a bare 404 at request time and the 3D
-# Preview iframe shows {"detail":"Not Found"} (see #1218). The directory is
-# vendored third-party JS — the Vite build does NOT stage it into static/,
-# the dev server serves it via a configureServer middleware that's dev-only.
-COPY gcode_viewer/ ./gcode_viewer/
-
# Create data directories. Ownership is normalised at startup by the
# entrypoint (chowns to PUID:PGID and drops privileges via gosu before
# exec'ing the app), so we don't need a chmod 777 hack here — that was
diff --git a/Dockerfile.test b/Dockerfile.test
index cf782eb51..02461b749 100644
--- a/Dockerfile.test
+++ b/Dockerfile.test
@@ -23,11 +23,6 @@ RUN --mount=type=cache,target=/root/.cache/pip \
COPY backend/ ./backend/
COPY pyproject.toml ./
-# Embedded GCode viewer assets — required so the @app.get("/gcode-viewer/...")
-# packaging-regression test in tests/integration/test_gcode_viewer.py actually
-# runs instead of pytest-skipping with "index.html not present". Path matches
-# the production Dockerfile (static_dir.parent / "gcode_viewer" = /app/gcode_viewer/).
-COPY gcode_viewer/ ./gcode_viewer/
# Create necessary directories
RUN mkdir -p /app/data /app/logs /app/archive
diff --git a/backend/app/main.py b/backend/app/main.py
index 1e3c7d117..9b206c077 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -1,7 +1,6 @@
import asyncio
import json
import logging
-import mimetypes as _mimetypes
import os
import posixpath
import secrets
@@ -8063,7 +8062,8 @@ def _frame_ancestors(default_value: str) -> str:
``default_value`` is the strict directive used when the operator has not
configured ``TRUSTED_FRAME_ORIGINS`` — typically ``'none'`` (catch-all and
- docs) or ``'self'`` (gcode-viewer, served same-origin). When trusted origins
+ docs) or ``'self'`` (the streaming overlay, embedded same-origin by the
+ Settings URL builder's preview). When trusted origins
are configured, ``'self'`` is always included so same-origin embedding never
breaks even if an operator forgets to add their own origin to the list.
"""
@@ -8103,23 +8103,7 @@ async def security_headers_middleware(request, call_next):
# - img-src data: / blob:: base64 thumbnails and Blob-URL timelapse previews.
# - media-src blob:: timelapse video player uses Blob URLs.
# - font-src data:: some icon fonts are embedded as data URIs.
- if request.url.path.startswith("/gcode-viewer"):
- # The gcode viewer is embedded in an iframe served by this same origin,
- # so frame-ancestors must allow 'self'. prettygcode.js also uses eval()
- # internally, so script-src needs 'unsafe-eval'.
- response.headers["Content-Security-Policy"] = (
- "default-src 'self'; "
- "script-src 'self' 'unsafe-eval'; "
- "style-src 'self' 'unsafe-inline'; "
- "img-src 'self' data: blob:; "
- "media-src 'self' blob:; "
- "connect-src 'self' ws: wss:; "
- "font-src 'self' data:; "
- "object-src 'none'; "
- "base-uri 'self'; "
- "frame-src 'self' http: https:; " + _frame_ancestors("'self'")
- )
- elif request.url.path in ("/docs", "/redoc", "/docs/oauth2-redirect"):
+ if request.url.path in ("/docs", "/redoc", "/docs/oauth2-redirect"):
# FastAPI's built-in Swagger UI / ReDoc pages load assets from
# cdn.jsdelivr.net and bootstrap with an inline
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-