Commit Graph
2476 Commits
Author SHA1 Message Date
maziggy e2e567f4dd fix(i18n): add settings.ldap.advanced key in all 8 locales (#1297)
The "Advanced" section header in LDAP settings was always
  rendering the hardcoded English fallback because the translation
  key was never defined. Added in en/de/fr/it/ja/pt-BR/zh-CN/zh-TW.
2026-05-12 16:38:56 +02:00
maziggy 8a6fcf5cbd fix(settings): expose UI rendering fields without requiring SETTINGS_READ (#1293)
The Clear Plate button (and 4 other features on the Printers page) read
  their state from /settings, which requires SETTINGS_READ. Granting that
  permission also adds the Settings nav item and leaks SMTP/LDAP/MQTT
  credentials — exactly what users were trying to avoid by giving an
  operator only printers:clear_plate.

  New /settings/ui-preferences endpoint returns a curated, opt-in subset
  of non-sensitive fields. Matches the existing /default-sidebar-order
  precedent. PrintersPage switched to the new endpoint; admin pages still
  use /settings for full access.
2026-05-12 16:30:28 +02:00
maziggy 5fea138f5d fix(auth): preserve manually-assigned groups across LDAP logins (#1292)
_sync_ldap_user used to replace user.groups entirely on every login,
  wiping manual admin assignments to groups outside the LDAP mapping.
  Now partitions on LDAP-managed group names (mapping values + default
  group) and only rebuilds that slice from LDAP truth. Manual assignments
  to non-managed groups are preserved; revocation in LDAP still
  propagates for managed groups.
2026-05-12 16:12:58 +02:00
maziggy 1d6e1b9e88 fix(inventory): persist storage_location for internal spools (#1291)
The column existed on the Spool ORM model but was missing from
  SpoolBase, SpoolUpdate, and SpoolResponse. Pydantic silently
  dropped writes and reads omitted the field, so the inventory
  table always showed "—" in the Storage Location column even
  after saving. Adding the field to the two schemas is enough —
  the update route already uses model_dump + setattr.
2026-05-12 16:02:24 +02:00
maziggy 82ce471124 Updated README 2026-05-12 15:15:26 +02:00
maziggy 301770d09f docs(changelog): add 0.2.5b1 entry for #1272 page-header unification 2026-05-12 14:03:11 +02:00
Ed 14a6d294f4 Merge pull request #1272 from EdwardChamberlain/feat-page-icons
[Feature] Update page icons for consistent UI style
2026-05-12 14:01:37 +02:00
maziggy 0c92a4d326 fix(vp): broadcast archive_created so Archives page refreshes live (#1282)
Real-printer prints broadcast archive_created from the MQTT print_start
  handler, which the Archives page listens for to invalidate its query
  cache. The VP file-receive paths created the archive in the DB but
  never emitted the event, so the new card only appeared after a tab
  switch triggered refetch-on-focus.

  Added a small _broadcast_archive_created helper on VirtualPrinterInstance
  and called it from _archive_file (immediate mode) and _add_to_print_queue
  (queue mode). Review mode is unaffected — it creates a PendingUpload,
  not a PrintArchive. Broadcast errors are swallowed at debug level so a
  transient WebSocket issue can't break the file-receive flow.
2026-05-12 13:36:32 +02:00
maziggy 2b747e96c9 Updated README 2026-05-12 13:24:08 +02:00
maziggy 5101cf8e37 Updated BACKERS.md 2026-05-12 12:20:51 +02:00
maziggy 59f7d736e3 Added BACKERS.md 2026-05-12 12:18:56 +02:00
maziggy 0d6171dc9a fix(vp): emit FINISH after FTP upload so Print-flow slicers unwedge (#1280)
Bambuddy's VP supports two slicer flows: Send (file upload only — what
  queue/immediate/review modes are designed for) and Print (file upload
  + start-print, intended for proxy mode). When a user clicks Print
  against a non-proxy mode the VP must still respond gracefully — the
  file is fine to receive, just the start-print never happens. Instead
  the slicer wedged at "Downloading...(0%)" and blocked the next
  dispatch with "The printer is busy with another print job".

  Cause: on_file_received transitioned gcode_state PREPARE -> IDLE
  directly. Print-flow slicers watch the state cycle and only release
  their in-flight-job lock on PREPARE -> ... -> FINISH (or FAILED).
  PREPARE -> IDLE looks like "printer abandoned my job" and keeps the
  prior job pinned in the slicer's memory.

  Fix: transition PREPARE -> FINISH with prepare_percent=100. The 1-Hz
  periodic status push broadcasts the new state to every connected
  slicer within a second. Send-flow slicers don't watch this state so
  the change is a no-op for them; Print-flow slicers see the FINISH
  they were waiting for and unwedge.
2026-05-12 10:19:34 +02:00
maziggy 7596725550 fix(mqtt): external-spool ams_filament_setting must use global tray_id (#1279)
ams_set_filament_setting and reset_ams_slot encoded the single-external
  case as {ams_id: 255, tray_id: 0, slot_id: 0}. The "LOCAL tray_id = 0"
  comment was a misread of the printer's response (which echoes the local
  slot position), not the request semantics.

  Captured BambuStudio -> X1C exchange shows the request encoding is
  {ams_id: 255, tray_id: 254, slot_id: 0} (global tray index in tray_id).
  The previous code's tray_id: 0 is what the P1S in #1279 rejects with
  result: "fail", which silently broke external-spool filament selection
  on every Bambu printer with no AMS or external spool in active use.

  Dual-external (H2D) branch was not in the captured exchange and is
  explicitly pinned at the legacy encoding pending a Studio -> H2D capture.
2026-05-12 09:44:20 +02:00
maziggy 4ee4bdb0d3 fix(archives): scan_timelapse picked stale video at false offset (#1278)
scan_timelapse's Strategy 2 matched filename timestamps against both
  archive.started_at and archive.completed_at across seven hypothesised tz
  offsets. The filename is always print-START time, so the end-time branch
  was a semantic mistake — and the dense offset set [0, +-1, +-7, +-8]
  let an unrelated video coincidentally land within minutes of any later
  archive at some offset.

  Extract Strategy 2 into _match_timelapse_by_timestamp(): compare only
  against start time, and refuse to auto-pick when the next-best different
  video is within a 15-minute ambiguity margin. The route then returns
  available_files and the frontend's existing manual-selection dialog
  takes over — which is the fallback the reporter explicitly asked for.

  Surfaces in LAN-Only mode where the printer can't reach NTP and its
  clock drifts (e.g. P2S filenames in CST while server is in UTC, the
  8h offset that exposed this bug).
2026-05-12 09:20:53 +02:00
maziggy 6fe00adb23 fix(spoolman): resolve -1 in ams_mapping to external spool (#1276)
BambuStudio encodes virtual tray IDs (254/255) as -1 in the flat
  ams_mapping array — a convention already documented in
  bambu_mqtt.py:start_print(). The spoolman tracking helper was treating
  -1 as "unmapped, use position-based default", which mapped slot_id=1
  to AMS tray 0 and credited external-spool prints to whatever Spoolman
  spool happened to be linked to AMS slot 0. The reporter's TPU prints
  on an H2S were credited to a PLA spool for ~49g over 4 prints before
  being noticed (regression of #853).

  When slot_to_tray[slot_id-1] == -1 and ams_trays contains 254/255,
  return the external tray ID directly. Prefers 254 over 255 (matches
  single-nozzle tray_now reporting + the vir_slot id=255->254 remap in
  bambu_mqtt.py:864). Legacy fall-through preserved for callers that
  don't pass ams_trays.

  Root cause investigation and patch by @ojimpo.
2026-05-12 09:07:49 +02:00
maziggy ae43ced0ef fix(vp): honor workflow default print options in queue-mode receive (#1235)
Prints sent from a slicer to a VP in print_queue mode arrived in the
  queue with bed_levelling / flow_cali / vibration_cali / layer_inspect /
  timelapse set to the SQLAlchemy column defaults, ignoring the user's
  workflow page settings entirely. The manual POST /print-queue endpoint
  reads these from the request body (frontend pulls them from settings
  before submitting), but manager._add_to_print_queue constructed the
  PrintQueueItem without touching any of those fields.

  Read default_bed_levelling and the other four settings via get_setting
  and pass them explicitly. _bool_setting helper handles the None ->
  AppSettings default fallback.
2026-05-12 08:59:14 +02:00
maziggy af52c4f2ff fix(spoolman): allow AMS-HT ams_id range in slot-assignment table (#1274)
H2C / H2D AMS-HT units report ams_id 128+ (one ams_id per unit, single
  tray), but spoolman_slot_assignments.ck_ams_id_range only admitted 0-7
  and 255. Every attempt to link a Spoolman spool to an AMS-HT slot died
  with `CHECK constraint failed: ck_ams_id_range`. The internal
  spool_assignment table has no such constraint and works fine.

  Widen the formula to (0-7) OR (128-191) OR 255 in the model, the
  CREATE TABLE DDL, and an idempotent in-place migration for existing
  installs (Postgres: DROP/ADD CONSTRAINT; SQLite: detect stale formula
  in sqlite_master, rebuild via _v2 rename pattern).
2026-05-12 08:48:30 +02:00
maziggy c097140e4c fix(camera): share broadcaster buffered frame with Obico + /camera/snapshot (#1271)
The MJPEG fan-out broadcaster from #1089 only solved viewer-side
  concurrency. Obico polling (every 5s) and the manual /camera/snapshot
  endpoint kept opening their own fresh RTSP sockets, which X1/H2/P2
  firmwares tolerated but X2D firmware 01.01.00.00 enforces strict
  single-connection on — every poll kicked the live stream.

  Add try_get_active_buffered_frame(printer_id): returns the broadcaster's
  last buffered frame when a viewer is connected, None otherwise. Obico
  and /camera/snapshot consult it before opening a fresh socket. When no
  viewer is active they fall through to the existing fresh-capture path.

  plate_detection and layer_timelapse intentionally not converted.
2026-05-12 08:36:08 +02:00
maziggy b334d7edc9 fix(spoolman): per-print 3MF tracking is the only weight writer (#1119)
Spoolman had two mutually-exclusive weight paths gated on the
  `disable_weight_sync` flag. The default (False) used AMS remain%
  x tray_weight auto-sync, which silently dropped non-BL spools
  because the AMS doesn't report tray_weight without RFID. The
  inventory_remaining fallback would have covered it, but the
  spool_assignment table it reads from is wiped on Spoolman
  activation, so non-BL spools got no weight updates at all.

  Match the internal Filament Inventory: per-print tracking always
  runs, AMS auto-sync no longer writes remaining_weight (it still
  maintains spool metadata and slot assignments). The setting
  becomes a no-op; left in the schema and UI for backwards compat.

  - store_print_data: drop the disable_weight_sync early return
  - sync_ams_tray callsites in main.py + routes/spoolman.py: force
    disable_weight_sync=True so weight is never written by AMS sync
  - new regression test confirming tracking runs with flag=false
2026-05-12 08:15:28 +02:00
maziggy 4b7df9f30b fix(usage-tracker): skip remain% fallback for trays not used by print (#1269)
The AMS remain% delta path charged every tray with a delta, not just
  trays involved in the print. Swapping a spool in an UNUSED slot mid-
  print made the slot report remain=0 (fresh spool, no tag), versus a
  print-start snapshot of 100%, so the originally-assigned spool got
  charged the full 1000g.

  Build print_used_keys from ams_mapping, tray_change_log, and
  tray_now_at_start, and skip fallback for trays not in that set.
  Legacy "scan every tray" behavior preserved when none of the three
  signals are present.
2026-05-12 07:47:19 +02:00
maziggy bf2b34ebcf fix(ui): round smart-plug live wattage on printer card (#1266)
Plugs reporting fractional watts (Kauf PLF12 / ESPHome via MQTT)
  overflowed the card width. SmartPlugCard and SwitchbarPopover
  already round the same field; only the printer-card badge was raw.
2026-05-12 07:37:13 +02:00
maziggy 359e4a778e Updated README 2026-05-11 16:46:09 +02:00
maziggy 71ac077c0c Bumped version 2026-05-11 14:52:24 +02:00
maziggy b444921021 test: drop racy printer_state_to_dict patch; assert structural shape
The patch on printer_state_to_dict raced against the broadcast coroutine
  under pytest-xdist's parallel workers. Mostly won locally, lost
  occasionally on CI — surfaced first as AttributeError on .kprofiles,
  then (after _fake_state was hardened) as a dict-content mismatch
  between the patched return value and the real 36-key dict.

  Fix: stop patching printer_state_to_dict; let it run for real against
  the complete _fake_state stub. Assertions now check the broadcast fired
  with the right printer_id and a dict containing awaiting_plate_clear,
  not the exact dict shape — that decouples the test from
  printer_state_to_dict's evolving body.
2026-05-11 14:08:23 +02:00
maziggy c36bfa24d4 test: harden _fake_state against printer_state_to_dict mock-leakage
The two TestBroadcastStatusChange / TestEndToEndUnderRunningLoop tests
  patch printer_state_to_dict to return a fixed dict, but on parallel
  xdist runners (CI's pytest -n 30) the patch occasionally didn't catch
  the call and the real function ran against the 4-field SimpleNamespace
  fake — first attr access (.kprofiles) AttributeError'd, swallowed by
  the try/except in _broadcast_status_change, send_status never awaited,
  the assertion failed.

  Filled _fake_state with every attribute the real printer_state_to_dict
  reads (iterables empty, scalars None, stg_cur=0 for the int comparison
  in get_derived_status_name). Test now passes whether or not the patch
  lands.
2026-05-11 13:58:16 +02:00
maziggy b4875fd5d6 fix(ci): ruff format spoolbuddy.py; raise SettingsPage test timeout
- backend/app/api/routes/spoolbuddy.py: re-formatted via ruff (lambda
    conditional wrapped in parens — the formatter check fires when the
    expression spans multiple lines without grouping)
  - frontend/src/__tests__/pages/SettingsPage.test.tsx: per-test timeout
    raised to 15s for the external_camera_snapshot_url PATCH test; the
    default 5s was tight enough on GitHub Actions runners that user.type()
    of the 49-char URL + 800ms debounce occasionally blew past it
2026-05-11 13:43:41 +02:00
maziggy 737c152243 fix(gcode_viewer): close CodeQL XSS + useless-escape alerts on PR #1263
- slider-shim.js: HTML-attribute-escape opts.id before interpolation
    (only caller passes a constant, but defends against future taint)
  - prettygcode.js: drop useless \\? escape inside [...] character class
2026-05-11 13:30:35 +02:00
maziggy 9f1188d711 Tool: Bandit B108
Severity: Warning ×4
  Issue: "Probable insecure usage of temp file/directory" — /tmp/<filename> literals used as synthetic DB field values in two integration tests
  Status: Fixed
  ────────────────────────────────────────
  Tool: CodeQL Python / JS
  Severity: Pending
  Issue: Still running on the head SHA
  Status: —
  ────────────────────────────────────────
  Tool: Trivy container scan
  Severity: Pending
  Issue: Still running
  Status: —
  ────────────────────────────────────────
  Tool: Bandit (Python Security Analysis)
  Severity: Pass
  Issue: The separate Bandit run on the changes already passes
  Status: ✓
2026-05-11 13:30:27 +02:00
maziggy 1c778e8a68 fix(security): bump pip to >=26.1 in Dockerfile (CVE-2026-6357)
The python:3.13-slim-trixie base image ships pip 26.0.1, which runs its
  self-update check after installing wheels — a malicious wheel that included
  a module name matching a deferred stdlib import (urllib, ssl, ...) could
  hijack the import inside the install step. GitHub code-scanning alert #778
  flagged this as medium-severity.

  Dockerfile now upgrades pip to >=26.1 immediately before the requirements.txt
  install, so the requirements install runs under the patched pip and the
  resulting dist-info metadata in the final image is the fixed version.
  No requirements.txt change — the floor is enforced at the image-build layer
  where the vulnerable copy actually lived.
2026-05-11 12:36:31 +02:00
maziggy a25177097d Post work PR #1255 2026-05-11 12:29:53 +02:00
BurntOutHylian 7afb303ffd feat(#1239): Update Gitea and Forgejo due to API changes from initial cut (#1255)
feat(#1239): first cut at Gitea backups silently failing after 1st run
feat(#1239): Added Token Scope for Forgejo edge case. Also included: test coverage for fixes
2026-05-11 12:27:41 +02:00
maziggy 90c3efece9 Housekeeping 2026-05-11 10:43:43 +02:00
maziggy dfd9fcedf4 fix(printer-card): confirm before HA entity toggle on printer card (#1260)
Smart plugs with "Show on Printer Card" enabled appear as a chip in the
  HA-entities row below the main plug controls. One click cut power to the
  printer instantly — including mid-print — while the main Off button right
  next to it already routes through a ConfirmModal. The HA-row chip was
  added later and skipped the same gating.

  Branch on entity type: script.* entities keep firing instantly (fire-once
  triggers, not power switches — confirming each click would be annoying),
  but switch/light/anything-else entities now open a ConfirmModal first.
  Reuses the same variant="danger" + running-print warning copy as the
  existing power-off confirmation when status.state === 'RUNNING'.
2026-05-11 08:26:51 +02:00
maziggy fed8f1f74f Added install_docker for Windows 11 2026-05-10 15:22:10 +02:00
maziggy 28c68feaeb Updated README 2026-05-10 13:58:48 +02:00
maziggy ba3dc613d1 Changed app defaults 2026-05-10 11:15:47 +02:00
maziggy 080176c6e5 fix(filament-mapping): X2D/H2D dual-nozzle without AMS lost
external-spool extruder routing (#1257)

  X2D with 0 AMS units and two external spools (Ext-L feeding left
  extruder, Ext-R feeding right) showed "Required filament type not
  found in printer" even when the matching filament was physically
  loaded. Cause: useFilamentMapping derived dual-nozzle status from
  ams_extruder_map being non-empty -- that map is populated from AMS
  info bits, so dual-nozzle printers without AMS got an empty map
  and hasDualNozzle=false. External spools then fell through to
  extruderId=undefined, and the nozzle-aware filter rejected every
  candidate because undefined !== 0/1.

  Prefer the hardware-reported printerStatus.nozzles array length as
  the dual-nozzle signal -- populated regardless of AMS configuration
  -- and keep the ams_extruder_map branch as fallback for older
  firmware that might not surface nozzles. Affects all dual-nozzle
  printers running without AMS: X2D, H2D, X2 Pro.

  Regression test pins both layers the bug straddled --
  buildLoadedFilaments extruderId assignment per external spool, and
  computeAmsMapping picking the correct external for a per-nozzle
  requirement -- so a future change that re-breaks either fails CI.
2026-05-10 10:15:07 +02:00
maziggy 79d54a8d53 feat(archives): build-plate icon on cards + uniform printer/model line (#1253)
Show an OrcaSlicer-style bed icon in the archive card's printer-name row
  indicating which build plate the print was sliced for (Cool /
  Cool SuperTack / Engineering / High Temp / Textured PEI / Smooth PEI),
  with the full plate name in the hover tooltip. Closes the gap where
  users had to remember which plate matched a re-print or open the
  source 3MF in a slicer just to read the bed setting.

  Card row also unified: archives with a real Bambuddy-printer
  association used to render "H2D-1 GCODE ..." while slicer-only uploads
  rendered "Sliced for X1C GCODE ..." -- same line, two different shapes.
  Drop the "Sliced for " prefix so both render as a uniform
  "<name-or-model> [bed-icon] GCODE <hash>" row, scanning identically
  regardless of provenance.

  Backend: new bed_type column on print_archives (idempotent ALTER TABLE
  migration; SQLite + Postgres safe). Populated from curr_bed_type in
  Metadata/slice_info.config (per-plate, authoritative -- that's what
  got sent to the printer for the exported plate) with a fallback to
  project_settings.config for older 3MF shapes. Wired through both
  archive_to_response() (the hand-rolled dict converter that bypasses
  from_attributes -- easy to miss) and the /rescan endpoint, so old
  archives can be re-parsed via the existing per-archive Rescan button.

  Backfill script (scripts/backfill_archive_bed_type.py, --dry-run
  supported) re-opens every NULL archive's 3MF on disk to populate the
  column. Auto-loads .env from project root before importing backend
  modules (config.py reads DATABASE_URL from os.environ at import time,
  not from pydantic-settings at Settings() time) and prints the resolved
  DB URL with credentials redacted, so operators can confirm they're
  hitting the intended database -- Postgres or SQLite.

  Frontend: 6 OrcaSlicer-style PNGs ship in frontend/public/img/bed/ --
  under /img/ because that path is already statically mounted; a
  toplevel /bed-icons/ tried first hit the SPA catch-all and returned
  index.html as text/html. New utils/bedType.ts maps slicer strings
  case-insensitively, covering both Bambu Studio and OrcaSlicer naming
  variants for the same physical plate. Unmapped or NULL bed_type
  simply omits the icon, so cards stay clean for pre-feature archives.
2026-05-10 09:54:10 +02:00
maziggy 47dd4dd016 Updated README 2026-05-09 17:44:20 +02:00
maziggy 77bf53b7f1 fix(gcode-viewer): add in-app back button (was browser-back only)
Opening the GCode Viewer from a File Manager card or Archive card mounts
  GCodeViewerPage as a full-height iframe inside the Layout shell. The page
  rendered nothing but the iframe, so once the third-party viewer's UI took
  over the content area there was no in-app affordance to return to the
  originating list - only the browser's back button.

  Add a thin bar above the iframe with an ArrowLeft button. The label adapts
  to the entry point - "Back to Print Archives" when the URL carries
  ?archive=, "Back to File Manager" when it carries ?library_file=, generic
  "Back" otherwise. Click prefers navigate(-1) so the user lands back in
  their original list with scroll position and filters preserved; falls
  back to /archives or /files when the page was opened in a fresh tab and
  there's no SPA history to return to.

  New gcodeViewer.{back, backToArchives, backToFiles} i18n namespace added
  to all 8 locales with native translations.
2026-05-09 12:41:13 +02:00
maziggy 83a83ed724 feat(labels): add 40x30 mm template, hex colour code, bolder brand (issue #809 follow-up)
Three enhancements requested by @oliboehm after the V1 label-printing
  ship in #809:

  - New box_40x30 single-label template (common DK/Brother roll size,
    good for filament-bag and storage-bin labels). Routes through the
    existing roomy layout since height >= 20 mm.

  - Colour hex code (#RRGGBB, alpha-stripped, uppercase) rendered on
    every label - useful when several near-identical material/colour
    spools sit next to each other and the swatch alone isn't enough to
    tell them apart. Skipped silently when rgba is None or malformed.

  - Brand line bumped to Helvetica-Bold (was regular) and a couple of
    points larger on both layouts so it reads cleanly at arm's length.

  Wired through the SpoolLabelTemplate union, the modal's
  TEMPLATE_OPTIONS, and the inventory.labels.templates.box40x30 i18n
  key in all 8 locales (native translations for de/fr/it/ja/pt-BR/
  zh-CN/zh-TW). Modal regression test widened from 4 to 5 template
  buttons. Three new renderer tests pin the hex-code render, the
  hex-code skip on invalid rgba, and the bold-brand font reference.
2026-05-09 10:10:20 +02:00
maziggy a193145522 fix(archives): hide truncated "Re..." / "Sc..." button labels on narrow card widths (issue #1249)
The archive card's action row crams 6 buttons into one line: 2 labelled
  (Reprint + Schedule, or Slice when un-sliced) plus 4 icon-only utilities.
  The labelled buttons used `flex-1` to share whatever the icon buttons
  left over, with the label gated on `hidden sm:inline truncate`.

  Tailwind viewport breakpoints can't see the card width. The grid grows
  columns alongside viewport (md:2 lg:3 xl:4), so cards stay ~320-380 px
  wide regardless of breakpoint, and the labelled buttons end up with
  ~30 px of space — enough to render "Re..." / "Sc..." and not much else.

  Bump the label breakpoint sm: -> xl: so labels appear only at
  viewport >= 1280px where the cards actually have room. Below that,
  the buttons render icon-only and the existing title= attribute serves
  as the hover tooltip.
2026-05-09 09:53:25 +02:00
maziggy 04096620a9 Post work PR #1246 2026-05-09 09:44:50 +02:00
Miguel Ángel López Vicente 6a130c09d7 [Feature] Copy filament (#1246) 2026-05-09 09:40:21 +02:00
maziggy 30fe88a334 fix(inventory): show all per-printer/per-nozzle variants in spool form's Slicer Preset dropdown (issue #1248)
Two defects in buildFilamentOptions, surfaced together:

  1. The function was precedence-based — cloud presets short-circuited
     the local-presets branch, silently hiding any imported Local Profile
     while the user was logged into Bambu Cloud. The wiki documents the
     dropdown as "merged and deduplicated" across cloud + local + built-in.

  2. Cloud default presets and local presets were being collapsed by base
     name (everything after "@" stripped), so all P1S/X1C/A1 variants of
     "Bambu PLA Basic" rendered as a single row. The spool form is
     printer-agnostic by design, so the right semantic is to show every
     variant individually — the union across all printers — not collapse
     them. AMS Slot is per-printer (it filters), the spool form is
     union-of-all (it doesn't).

  Rewrote the merge to push each cloud setting_id and each LocalPreset row
  as its own FilamentOption with the full @printer suffix preserved in
  displayName. Built-in dedup against cloud setting_id is kept (mirrors
  ConfigureAmsSlotModal.tsx). Wired api.getBuiltinFilaments() into both
  callers. slicer_filament persistence is unchanged so existing spools
  keep slicing correctly.
2026-05-09 08:43:51 +02:00
maziggy f5ecc61cda fix(spoolbuddy): lower /update permission to INVENTORY_UPDATE so kiosk's own Settings -> Update button works
The kiosk's Settings -> Update Daemon button returned "API keys cannot
  be used for administrative operations" because POST /spoolbuddy/devices/
  {id}/update was gated on Permission.SETTINGS_UPDATE, and SETTINGS_UPDATE
  is in the _APIKEY_DENIED_PERMISSIONS deny-list introduced by PR #1241.
  Every kiosk-side request tripped the deny-list before the API key's
  scope set (Read / Print Queue / Control / Legacy) was even consulted.

  Same root cause as the four QuickMenu System buttons fixed in 0.2.4b3
  (Restart Daemon / Restart Browser / Reboot / Shutdown). Missed /update
  in that audit on the reasoning "replaces the daemon binary, different
  threat surface" — but that's wrong: restart_daemon already replaces
  the running daemon process, so daemon-replacement is not a step up in
  blast radius. The SSH update is also strictly scoped to the one device
  the operator physically controls (git fetch + pip install + systemctl
  restart on that host) — same threat profile as the system commands
  already running on INVENTORY_UPDATE.

  Lower /spoolbuddy/devices/{id}/update from SETTINGS_UPDATE to
  INVENTORY_UPDATE so it aligns with the rest of the kiosk-scoped routes
  (calibration/tare, display, cancel-write, system/command,
  system/command-result, update-status). The main Bambuddy in-app updater
  at POST /api/v1/updates/apply keeps SETTINGS_UPDATE — that one runs on
  the Bambuddy host and is correctly fenced behind the deny-list.
2026-05-08 14:28:41 +02:00
maziggy 3f58fc74b4 fix(http): RFC 6266-encode Content-Disposition so non-ASCII filenames don't crash response (issue #1245)
Reported by @1000Delta. The printer file download (and three sibling
  endpoints) raised UnicodeEncodeError: 'latin-1' codec can't encode
  characters... on any filename outside U+0000..U+00FF (Chinese,
  Japanese, Arabic, accented Latin), because the route pushed `filename`
  straight into Content-Disposition: attachment; filename="...".
  Starlette/uvicorn encodes response headers as latin-1, so the assignment
  crashed at write-time.

  New backend/app/utils/http.py::build_content_disposition emits both an
  ASCII-stripped legacy filename="..." fallback and an RFC 5987
  filename*=UTF-8''<percent-encoded> parameter. Every modern browser
  prefers the *= form, so the original Unicode filename round-trips
  through Save-As intact.

  Same shape was latent in three siblings and fixed in the same PR
  (no deferred follow-ups): archive QR endpoint (archive.print_name
  from 3MF metadata), project ZIP export (project.name — the existing
  isalnum() sanitiser passes non-ASCII through), and the PDF label
  streamer (latent today, callers ASCII-only but the helper hardens it).
2026-05-08 14:06:40 +02:00
maziggy 91fa9cd432 Housekeeping 2026-05-08 13:33:04 +02:00
maziggy 829bc2dd6a Bumped version 2026-05-08 12:49:12 +02:00
maziggy a7b3e01b86 chore(deps): bump python-multipart floor 0.0.26→0.0.27 (CVE-2026-42561) 2026-05-08 12:45:17 +02:00