Commit Graph
3402 Commits
Author SHA1 Message Date
MartinNYHC 30d26efb3d Merge branch 'dev' into feature/ukrainian-localization 2026-07-28 15:13:34 +02:00
maziggy 9d549050f7 fix(cloud): complete the CSRF handshake on Bambu Cloud TOTP sign-in (#2696)
Signing in to Bambu Cloud with an authenticator-app account failed every
time with "Invalid code", whatever the code was. Bambu Lab added double-
submit CSRF protection to the bambulab.com web origin - which is where,
and only where, this service posts the two-factor code. The endpoint
refused the request with 403 "CSRF error: missing_cookie" before it ever
evaluated the code, and Bambuddy reported that refusal as a bad code.

Verified against the live endpoint with a deliberately invalid key: a
bare POST returns missing_cookie; GET /api/csrf mints a bbl_csrf_token
cookie; a POST carrying only the cookie returns missing_header; a POST
carrying the cookie plus an x-bbl-csrf-token header reaches application
logic. Landing on the sign-in page first - the intuitive fix - does not
help, as that page sets only Cloudflare's __cf_bm. Of five header
spellings tried, only x-bbl-csrf-token is accepted, so the tests pin it.

verify_totp now performs that handshake against the same origin it will
post to (bambulab.cn for the China region - a token minted by the global
site is a cookie the .cn endpoint never issued), and declines to submit
the code at all when no token can be obtained rather than burning the
user's 30-second TOTP window on a request that is certain to be refused.
A CSRF refusal now also says the code was never checked instead of
masquerading as a wrong code, which is what sent the reporter chasing
clock drift and leading-zero parsing.

Only TOTP sign-ins were affected. Every other cloud call, the email-code
two-factor path included, goes to api.bambulab.com, which is not gated,
and existing stored tokens were unaffected throughout.

The region-routing test's MockTransport needed teaching about the
handshake: it returns one canned response for every request and set no
cookie, so the fix correctly refused to POST and the test lost the URL it
asserts on. It now mints a token for /api/csrf and additionally checks
the handshake stays on the .cn origin.
2026-07-28 15:05:28 +02:00
maziggy d0d658f6a0 Updated CHANGELOG 2026-07-28 15:05:22 +02:00
maziggy 8551e32f14 feat(slicer): keep the designer's print settings when re-slicing for another printer (#2622)
Published models often deviate from the stock Bambu profile on purpose -
five walls, 100% infill, a 0.1mm first layer. Re-slicing one for a
different printer discarded all of it: the picked process preset
overrides the file's embedded settings, and that override is precisely
what makes cross-printer re-slicing work, so it cannot just be dropped.
"Slice as designed" (#2611) does not help - it is all-or-nothing and
only offered when the picked printer already matches the design's
target.

The deviation list does not have to be computed. Bambu Studio writes it
into the 3MF as different_settings_to_system, laid out as
[process, *filaments, printer] - verified against real files at 2, 3 and
4 filament slots. The parser refuses any file whose array length
contradicts its own filament count rather than guessing an index, since
reading the printer slot as the process slot would carry the designer's
machine_start_gcode onto a foreign printer.

The slice dialog now lists exactly which print settings the author
changed and what each was set to, with a checkbox per setting. Design
intent - wall count, infill, layer and first-layer height, supports,
seam, brim, ironing - is ticked by default. Printer-specific values -
speeds, accelerations, jerk, fans, temperatures, prime-tower geometry -
are listed with a badge but start unticked: tuned for the author's
machine, they can be merely wrong on the target or outside the range its
profile accepts, which fails the slice outright.

Only ticked keys are sent, and only keys the source actually flags as
changed are applied. Values are written into the outgoing process JSON,
the same mechanism the support carry-over has used since #1881: for a
Standard preset pick that JSON is an inherits stub, so the patch is the
child in the chain and wins over the flattened parent. Process slot
only - filament picks are honoured as chosen.

The wiki's "this is not a settings merge" note under Slice as designed
described the gap this closes; rewritten to point at the new panel.

Translated in all locales; wiki updated. Covered by backend and frontend
tests.
2026-07-28 14:49:50 +02:00
Olexandr 4cca2b5929 fix(i18n): localize Ukrainian weight unit 2026-07-28 12:42:28 +00:00
Olexandr d5a02390e9 fix(i18n): refine Ukrainian sign-in wording 2026-07-28 12:41:01 +00:00
Olexandr 077d2ab941 Add Ukrainian localization 2026-07-28 12:41:01 +00:00
maziggy 4e46ba071f feat(printers): show remaining time, ETA and layers on the size-S card (#2674)
Size S exists for one job: watching a whole fleet on a single screen. It
rendered the printer name, a status pip and a progress bar - every other
block on the card is gated behind the expanded view - so it could not
answer the question that view is for, "which printer finishes first".
Dropping to S to fit more printers meant losing the information you
dropped down to compare.

The compact card now carries one line of metrics under the progress bar
while a print is running: remaining time, ETA in the configured
12/24-hour format, and layer progress. These are the values the Medium
card already shows, rendered with the same formatters and the same ETA
styling so the two sizes read alike. Each value is omitted individually
when the printer does not report it, and the row holds its height when
nothing is printing so cards do not shift as prints start and finish.
Card dimensions and grid density are otherwise unchanged.

Frontend only. Wiki updated. Covered by tests - which required teaching
the new test file to mock localStorage.getItem, since the harness
replaces localStorage with bare vi.fn() stubs and setItem is a no-op;
without that the page falls back to its size-M default and the compact
branch never renders.
2026-07-28 14:28:07 +02:00
maziggy 8fd1f884dc feat(mqtt): publish the plate-clear gate and add a notification for it (#2525)
When a print reaches a terminal state Bambuddy holds the queue until
someone confirms the build plate is clear. That gate was visible only in
the Web UI: the printer's own MQTT push reports nothing beyond RUNNING,
PAUSE, FAILED, FINISH and IDLE, so an external automation could not tell
"finished" from "finished and still waiting for a human".

The per-printer status topic now carries an awaiting_plate_clear field,
and every transition is additionally published on a new retained topic,
bambuddy/printers/{serial}/plate_clear. Retained, and published from the
flag itself rather than from printer telemetry: a subscriber learns the
state of every printer the moment it connects, and the state stays
correct after Auto Off powers a printer down - telemetry stops there,
which would otherwise leave the status topic frozen at false.

Publishing is edge-triggered. The queue clears the gate on every
dispatch whether or not it was up, and no subscriber should see a
"plate cleared" for a plate that was never dirty. Persistence and the
WebSocket broadcast stay unconditional; they are idempotent and predate
this.

A matching Plate Clear Required notification event was added, off by
default on every provider because it fires after every print at the
same moment as the print-complete alert. Only the rising edge notifies.
Acknowledging still goes through POST /printers/{id}/clear-plate.

Two tests in test_printer_manager_status_broadcast.py asserted
_schedule_async.call_count == 2 for the setter. The new emission makes
it three on a transition, so they now assert that the persist and
broadcast coroutines are actually scheduled - which is the contract

Translated in all locales; wiki updated. Covered by backend and
frontend tests.
2026-07-28 13:36:55 +02:00
maziggy f4f76e0121 fix(inventory): allow editing and duplicating stock spools without a slicer preset (#1905)
A spool created by Quick Add, a CSV import or an RFID scan has no slicer
preset, brand or subtype. Reopening it in Edit Spool demanded all three
before anything could be saved, so changing its storage location, cost
or notes was impossible - and Copy Spool had the same gate with no Quick
Add toggle to waive it. The preset you were then forced to pick auto-
filled material, brand and subtype from the preset name, silently
rewriting a hand-entered manufacturer (Elegoo -> Generic) so the spool
no longer appeared where it had been filed.

Editing and copying now require only what the backend requires: the
material. Preset, brand and subtype stay fully visible and editable -
nothing is hidden the way Quick Add hides it - and the required-field
markers no longer advertise a rule that isn't enforced. Selecting a
preset fills only fields that are still empty or that a previously
selected preset had filled, so values the user (or the saved spool)
provided survive; switching between presets still replaces what the
earlier one contributed.

The brand and material dropdowns also no longer filter themselves down
to the brand/material pairs known to the color catalog and slicer
presets. Elegoo is catalogued only for PLA, which made a real product
like Elegoo ASA look impossible to enter. Both lists now always offer
everything known, with paired entries ranked first under Suggested and
the rest under All, and a spool's own custom brand or material is always
present in its own dropdown. The SpoolBuddy write-tag form shares these
fields and gets the same treatment.

Lastly the Quick Add layout no longer leaks out of create mode: quick-
adding a spool and then opening Edit left the edit form in the reduced
layout with no toggle to leave it, because the toggle is create-only.

Frontend only. Translated in all locales; wiki updated. Covered by
validation and form-interaction tests.
2026-07-28 13:09:16 +02:00
maziggy af7874546a feat(projects): per-file print progress and complete-sets tracking (#1897)
Projects made of many distinct files that each need N prints (e.g. 13
plates x 10 sets = 130 prints) only had aggregate progress. Finding out
"how many times have I printed plate_7?" meant reading the Activity
Timeline line by line, unusable at 130 events.

Projects now take an optional Copies per File target. Every printable
file in the project's linked folders shows an X / N badge with a mini
progress bar (gray not started, amber in progress, green done), and the
progress card gains a Complete Sets bar - the minimum per-file count,
i.e. how many finished assemblies can be shipped right now. Without the
target, printable files show a plain printed-count badge.

Counting matches the aggregate project stats: completed runs only,
served by a new /projects/{id}/file-progress endpoint. Runs attribute
to a file via a new library_file_id stamp on queue-dispatched archives,
falling back to content hash and then filename for historical rows.

Also fixed: files queued from a project-linked File Manager folder now
inherit that project, so their prints count toward project statistics -
previously only prints started from the project page were attributed.

Test-harness fix along the way: the test suite's get_db override never
committed, unlike production get_db, so endpoints relying on the
request-scoped commit silently lost their writes in tests. The override
now mirrors production commit/rollback semantics.
2026-07-28 12:46:31 +02:00
maziggy 1fb6978ee1 feat(library): let users delete empty folders (#1781)
Library folders have no ownership tracking, so folder deletion was
gated entirely behind library:delete_all - a user with
library:delete_own could create folders and delete their own files,
but the emptied folder sat there until an admin removed it.

Users with library:delete_own can now delete folders that are truly
empty: no subfolders and no files, including trashed ones - folder
deletion cascades, so removing a folder that holds another user's
trashed file would silently break trash restore. External folders
(operator-configured mounts) and folders linked to a project or
archive still require library:delete_all even when empty, since
deleting them affects more than the folder itself. The bulk-delete
endpoint applies the same rule instead of skipping all folders for
non-admin users.

The folder tree's Delete entry enables accordingly and shows a
"You can only delete empty folders" hint on non-empty folders. The
backend stays authoritative - a folder that only contains trashed
files is invisible in the tree but still refuses deletion.
2026-07-28 12:05:00 +02:00
maziggy eae5359fbc feat(printers): show AI failure detection state on printer cards (#1546)
The live Obico classification was only visible under Settings ->
Failure Detection, so tracking how detection matched an ongoing print
meant flipping between the Printers screen and Settings.

Each printer card's badge row now shows an AI badge whenever detection
is enabled for that printer, like the other health badges: gray Idle
while no print is being watched, then green Safe, amber Warning, or
red Failure while a print is actively monitored. The tooltip carries
the current smoothed score; clicking jumps to the full detection
status and history in Settings. Printers excluded from the monitored
subset show no badge.

Served by a new lightweight /obico/printer-status endpoint readable
with printer permissions alone - it exposes only the enabled flag, the
monitored-printer set, and per-printer classification, keeping ML URL
and other configuration behind the existing settings-gated endpoint.
2026-07-28 11:37:36 +02:00
maziggy 62ba751278 feat(notifications): Bark notification provider (#1495)
Bark is the open-source, account-free iOS push app (self-hostable
via bark-server). Configure with just the device key from the app;
the server URL defaults to the official api.day.app relay and
accepts a self-hosted instance. Optional settings: notification
Group, Sound, and iOS Interruption Level - Time Sensitive breaks
through scheduled summaries, Critical bypasses Silent mode and
Focus, Passive delivers silently.

bark-server can wrap failures in an HTTP 200 body ({"code": 400}),
so the sender checks the body code as well as the HTTP status.
Unknown interruption levels are dropped rather than forwarded.
2026-07-28 10:58:08 +02:00
maziggy 49f9d7120d feat(notifications): custom data fields for Home Assistant notify services (#1441)
When an HA notification provider targets a notify service (e.g.
notify.mobile_app_myphone), a new optional Data (JSON) field is
forwarded as the service call's nested "data" object - the same
place HA automations put mobile push options like priority, ttl,
channel, and group. ttl: 0 + priority: high make Android pushes
arrive immediately; channel gives printer alerts their own sound.

JSON rather than key=value lines so numbers stay numbers and nested
options work. Validated on both ends: the UI rejects malformed JSON
before saving, and the sender fails loudly instead of posting a
half-built payload. Only included when configured - the default
persistent_notification.create path is unchanged, as its schema
rejects unknown keys.
2026-07-28 10:44:40 +02:00
maziggy d68724c689 feat(stats): energy usage in cost records and trends (#1432)
The Most Expensive record on the Statistics page ranked prints by
filament cost alone, ignoring the per-print energy cost Bambuddy
already measures via an attached smart plug. It now ranks by
filament + measured energy cost; prints without smart-plug data
compete on filament cost alone, as before.

Filament Trends gains an Energy Over Time chart: kWh per day (per
hour for ranges of a week or less, per week for long ranges), with
the range's total kWh and energy cost in the header. The chart only
renders when the selected range contains measured energy data, so
setups without smart plugs see no change.

The /archives/slim stats feed now carries each run's energy_kwh /
energy_cost from print_log_entries. Translated in all locales.
Covered by backend and frontend tests.
2026-07-28 10:27:45 +02:00
maziggy ef2865821c Updated README 2026-07-27 14:58:42 +02:00
maziggy 9bb5a1b999 fix(inventory): PA-Profil picker fetches K-profiles across all installed nozzles (#2618)
The Edit Spool "PA-Profil" tab and the SpoolBuddy write-tag page fetched a
printer's calibrations with getKProfiles(printer.id), which defaults the nozzle
filter to 0.4. The printer/MQTT layer filters strictly by that diameter, so on
a multi-nozzle printer a same-filament 0.6mm K-profile was never retrieved and
the picker showed only the 0.4mm entry ("1 match"). (The AMS-Slot config dialog
was already fixed in #1899; these two pickers were not.)

Add installedNozzleDiameters(status) and a shared fetchPrinterCalibrations()
that queries every reported nozzle diameter and merges the results, falling
back to 0.4 when the printer hasn't reported nozzle hardware. Each profile row
now shows a nozzle-diameter badge so identically-named profiles are distinct.
2026-07-27 13:08:57 +02:00
maziggy da33ba64d1 fix(backup): parse subpath-hosted Gitea/Forgejo repository URLs (#2642)
Gitea/Forgejo served under a ROOT_URL path prefix (e.g. https://host/gitea)
place repos at /<prefix>/owner/repo. The Gitea backend assumed a host-root
layout: parse_repo_url required exactly two path segments (so subpath URLs
raised "Cannot parse repository URL") and get_api_base dropped the prefix,
yielding https://host/api/v1 instead of https://host/gitea/api/v1.

Treat the final two path segments as owner/repo and keep leading segments as
a base-path prefix; derive the API base as {scheme}://{host}{prefix}/api/v1.
Root-hosted instances are unaffected. Forgejo inherits the fix; GitHub/GitLab
are untouched.
2026-07-27 12:53:43 +02:00
maziggy 98b2900812 Updated BACKERS 2026-07-27 12:34:29 +02:00
maziggy 20abfb3b4d Updated CHANGELOG 2026-07-27 12:28:49 +02:00
maziggy d90b91604f chore(deps): patch postcss + brace-expansion; pin react-router 7.18.1 with a documented audit exception
- postcss 8.5.15 -> 8.5.23 (GHSA-r28c-9q8g-f849, source-map path traversal)
- brace-expansion override ^5.0.7 -> ^5.0.8 (GHSA-mh99-v99m-4gvg, DoS)

react-router: pin react-router-dom to exact 7.18.1 (direct dep) and react-router
to 7.18.1 via overrides (transitive). 7.18.1 is the most-patched 7.x -- it clears
14 advisories that older 7.x releases carry, several reachable from a SPA (open-
redirect XSS in Link/useNavigate, route-matching DoS). The one remaining advisory,
GHSA-qwww-vcr4-c8h2, is RSC-mode-only; Bambuddy is a Vite SPA using BrowserRouter
with no RSC runtime (@react-router/server not installed), so the path is
unreachable. The only version that fully clears npm audit is the 8.3.0 major
(no react-router-dom 8.x exists; it needs migrating 50 import sites plus a React
peer bump), deferred as its own change.

Because a version pin can't stop npm from reporting the theoretical 7.11.0
downgrade as fixAvailable, the ci.yml (hard) and security.yml (nightly issue)
audit gates gain a narrow, documented allowlist keyed on the GHSA id. It resolves
the react-router-dom -> react-router advisory chain and stays fail-closed: a
different advisory on react-router still fails the gate, and an isSemVerMajor
guard drops the exemption the moment a non-major fix ships, forcing us to take it.
2026-07-27 12:27:36 +02:00
maziggy 0bc98beac5 fix(queue): paginate History with Show more instead of a hard 50-cap (#2682)
The Print Queue History tab reported the full count in its header (e.g.
"History (311 items)") but the row builder hard-sliced the list to
items.slice(0, 50) with no control to load the rest, so everything past the
50th finished print was unreachable. The whole history is already loaded
client-side (the queue endpoint has no limit) and sorted -- it just wasn't
drawn.

History now renders progressively: the first page (50) plus a "Show more"
button and a "Showing X of Y" count that loads the next page until the full
list is on screen. The visible count resets to one page only on a deliberate
re-sort or location-filter change -- not on the periodic queue poll, which
produces a fresh array each tick and would otherwise collapse an expanded
view mid-scroll.

Frontend-only; batch grouping and per-row actions unchanged. Two new i18n
keys across all 12 locales. Covered by a test asserting the 50-row cap, the
count label, and that Show more reveals the remainder. Wiki updated.
2026-07-27 12:04:57 +02:00
maziggy 561e94b755 fix(logs): redact LDAP Distinguished Names from support bundle (#2681)
With LDAP auth in use, the debug log carried the full user DN on successful
auth -- e.g. "(DN: CN=Joe Schmoe,CN=Users,DC=ad,DC=example,DC=com, ...)". A DN's
leaf CN is the user's real name, PII on par with the email address already
redacted, and it passed straight into an uploaded support bundle. The log
sanitizer (shared by the support bundle and the in-app bug report) had no DN
pattern; DNs also leak via ldap3 exception strings and group-mapping logs.

- sanitize_log_content: redact LDAP DNs to [DN] -- a run of >=2 attr=value RDN
  components (CN/OU/DC/UID/...). The value class excludes <>;+ (RFC 4514 requires
  them escaped in a value) so the final comma-unbounded component doesn't swallow
  trailing log text such as "-> GroupName". Ordinary key=value lines are untouched.
- ldap_service: stop logging the raw DN on successful auth (username + group
  count suffices), keeping the PII off disk even before bundle sanitization.
2026-07-27 11:53:59 +02:00
maziggy 7c83316797 fix(camera): reap leaked ffmpeg for external USB/RTSP streams (#2675)
Closing an external USB (V4L2) camera view abruptly could leave its ffmpeg
running and holding /dev/videoN open -- LED stuck on, and reopening the view
failed or took 10-30s fighting for exclusive device access. Same class of leak
as #776 (built-in RTSP path), but the external path was never wired into that
fix: external streams registered into none of the _active_streams /
_disconnect_events / spawned-PID registries, so /camera/stop returned
{"stopped": 0} for a live USB stream and the orphan janitor's /proc net matched
only rtsp(s)://bblp: cmdlines. Cleanup ran only via the stream generator's own
finally, which an abrupt disconnect can skip.

- Thread an on_process callback + stop_event through generate_mjpeg_stream into
  _stream_usb / _stream_rtsp; register the process before the startup probe so a
  process that hangs on a locked device (not just one that exits) is reapable.
- Register external streams into the shared registries under a unique
  {printer_id}-ext-{token} id so /camera/stop and cleanup_orphaned_streams find
  and kill them; stop_event prevents the reconnect loops from respawning.
- Extend the /proc safety-net scan to also match USB (-f v4l2) ffmpeg, excluding
  still-active streams and unrelated ffmpeg.
2026-07-27 11:35:27 +02:00
maziggy 8646c40957 fix(slicer): reject invalid sidecar output instead of storing a corrupt slice (#2671)
The slice client only checked the sidecar's HTTP status, not its body. When the
sidecar -- or a reverse proxy in front of it -- returned 200 OK with a body that
wasn't a real 3MF (a stock/misconfigured sidecar, a proxy error page, a truncated
response, or an OrcaSlicer/Bambu Studio CLI crash emitting no output), Bambuddy
wrote that tiny blob to a .gcode.3mf, stored it as a valid sliced file (the
3MF-parse failure was swallowed as "no thumbnail"), and let it be queued and FTP'd
to the printer -- producing the ~28-byte files that "did nothing" and then failed
at print time. Separately, a genuine 413 comes from the proxy in front of the
sidecar rejecting the multi-MB upload (model + profiles), so raising the body
limit on the wrong proxy layer had no effect.

- Factor the duplicated status handling in slice_with_profiles /
  slice_without_profiles into one _handle_slice_response.
- When a 3MF export was requested, validate the body is a real ZIP; otherwise
  raise SlicerApiServerError with an actionable message instead of persisting
  a corrupt file.
- Special-case 413 with a message naming client_max_body_size on the proxy
  directly in front of the sidecar (Cloudflare cap noted).
2026-07-27 11:18:54 +02:00
maziggy 1bdd7d224a fix(library): sort File Manager by real filesystem mtime, recursively (#2680)
The folder tree's "sort by recent activity" and the file pane's date sort
put external (mapped/NAS) files in a near-random order instead of ls -t's
newest-first. Nothing captured the files' on-disk mtime: the sort keyed off
the DB updated_at/created_at, which for a bulk external scan is the same
scan instant for every row, so a whole block tied and sorted arbitrarily;
only rows Bambuddy had later touched individually looked "partially right."
The tree also bubbled up only immediate child-file activity, so a file added
deep in a subtree never lifted its parent folders.

- Add nullable fs_modified_at to LibraryFile and LibraryFolder (dialect-
  branched migration, mirroring the #2615 dispatching_at pattern).
- External scan records each file's and directory's real os.stat().st_mtime
  and refreshes it on every re-scan, so a file edited over the mount
  re-sorts and existing installs backfill on the next scan.
- list_folders computes each folder's activity as a recursive newest-
  descendant roll-up (post-order), so a fresh deep file lifts every ancestor.
- Folder tree sort and the file pane's date sort now use the real mtime,
  falling back to created_at for managed uploads with none.
- New toolbar toggle shows/hides each item's last-modified date in the right
  pane (grid + list), with strings in all locales.

Store the mtime as naive UTC to match the other timestamp columns so activity
comparisons never mix naive and aware values on either dialect. Covered by
integration tests (mtime capture, re-scan refresh, deep-file recursive bubble,
folder mtime) and a frontend test proving fs_modified_at is preferred over
created_at.
2026-07-27 11:01:43 +02:00
maziggy 13183e365a fix(ams): clear empty AMS-HT slot via its real tray_exist_bits position (#2670)
After #2594 every empty-slot clearing path skipped AMS-HT units, so a removed
HT spool never cleared on the printer card while Bambu Studio correctly showed
Empty. Root cause: the HT presence bit is packed as a single consecutive bit in
tray_exist_bits at 16 + (ams_id - 128) (HT-A=16, HT-B=17, ...), not the regular
ams_id*4 position -- so the bitmask cleanup, which skipped id>=128, never
touched it. The HT's state field is firmware-variant (loaded reports 11 on H2D,
9 on the #2594 firmware) and it keeps echoing stale tray_type after removal, so
the bitmask is the only reliable, firmware-independent signal. Confirmed against
a live H2D capture (loaded=0x10f7f, empty=0xf7f) and the OrcaSlicer
DevFilaSystem.cpp reference (is_exists = tray_exist_bits >> (16 + (ams_id-128))).

- apply_tray_exist_bits: handle AMS-HT (128-135) at 16+(ams_id-128) instead of
  skipping it; clears an empty HT and, because a loaded HT keeps its bit set,
  never wrongly clears one (keeps the #2594 fix intact). Ids outside the known
  regular (0-15) and HT (128-135) ranges are left untouched rather than guessed.
- Build the AMS change-hash from the merged state, not the raw payload, so a
  removal signalled only by the bitmask (firmware still echoing tray_type) still
  flips the hash and fires on_ams_change to unbind the spool_assignment row.
- Emit the exists presence bit in the websocket status serializer (REST already
  did) so the card renders "Empty" instead of "?" where state is ambiguous.
2026-07-27 10:34:56 +02:00
maziggy aa443c6e83 fix(print): keep filament gram usage visible when the name is long (#2669)
In the Print dialog's Filament Mapping, each required filament shows its
name and the grams the job needs, e.g. "Bambu PLA Basic (281.2g)". Name and
grams shared one fixed-width column with truncate on the whole string, so a
long name pushed the "(...g)" off the end and clipped it -- partially on a
wide screen, entirely in mobile portrait. The gram usage is the number that
matters (does the spool have enough left?), so it shouldn't be the part that
gets dropped.

Pin the gram usage (shrink-0, whitespace-nowrap) and let only the name
truncate, with the full name on hover. Applied to both the Specific-Printer
(FilamentMapping) and Any-model (PrinterSelector) panels. Layout only.
2026-07-27 09:22:14 +02:00
maziggy 1234f0830a fix(mqtt): don't let K-profile responses clobber the nozzle size (#2663)
Fetching K-profiles probes every nozzle size in turn (extrusion_cali_get
for 0.2/0.4/0.6/0.8mm), and each response echoes the requested diameter at
the top level. _process_message passed every "print" message -- including
these responses -- to _update_state, which treats a top-level
nozzle_diameter as the installed hardware. So the last size probed (0.8)
overwrote the real nozzle in memory; a genuine status push corrected it and
the next K-profile fetch broke it again, which is why it flickered between
0.8, empty and the correct 0.4. Since 1.2.5 the #1899 mismatch guard then
refused to dispatch, failing prints with a bogus "printer has 0.8mm".

Handle extrusion_cali_get responses only via the K-profile parser and skip
_update_state for them, mirroring the existing get_accessories guard. The
nozzle size now comes solely from the real status push. In-memory only --
affected printers self-correct on the next push after updating.
2026-07-27 09:15:34 +02:00
maziggy db6d306b35 fix(queue): mobile tap-to-reorder with up/down arrows (#2667)
The print queue couldn't be reordered on a phone. In portrait the drag
grip and selection checkbox are hidden below the sm breakpoint, so there
was no reorder affordance at all; in landscape the grip shows but it
carried touch-action: manipulation while the only dnd-kit sensor is a
PointerSensor with an 8px distance, so touch gestures scrolled instead of
starting a drag. Reordering was effectively mouse-only.

Add tap-friendly up/down arrow buttons to pending rows on mobile (shown
below sm, where the drag handle is hidden). They move a row one step among
its siblings -- standalone items, whole batches, and items within a batch,
in both the flat and per-printer layouts -- and persist through the same
POST /queue/reorder path as drag. Arrows appear only in the manual
"position" sort (shortest-job-first off) where position has meaning, are
gated on queue:reorder, and the first row's up / last row's down render
disabled. Also switch the desktop drag handle's touch-action to none so
mouse-style drag works on touch (landscape phones, tablets). Reuses the
existing queue.moveUp / queue.moveDown translations.
2026-07-27 09:02:36 +02:00
maziggy 6530a6af06 fix(library): send camera stream token for 3D Preview plate thumbnails (#2661)
The File Manager 3D Preview dialog (ModelViewerModal) rendered plate
thumbnails with the raw thumbnail_url. The plate-thumbnail endpoints are
gated behind a camera stream token passed as ?token= (an <img> can't send
an Authorization header), so with auth enabled the browser fetched without
a token and got 401 "Valid camera stream token required" — broken image
icons for every plate. The Slice dialog's picker (PlatePickerModal) and the
Print modal's PlateSelector already append the token via withStreamToken(),
which is why the same file's thumbnails showed there.

Wrap the thumbnail src in withStreamToken(), matching the other two call
sites. The token is synced app-wide and withStreamToken() is a no-op when
auth is off, so non-auth setups are unchanged.
2026-07-27 08:27:05 +02:00
maziggy 800c45536e fix(scheduler): pin the force-color variant when selecting the AMS slot (#2650)
Follow-up to 0f203ce: force color match now picks the right AMS slot, not
just the right printer. The slot mapper cleared tray_info_idx when applying
an override, so on a printer holding two same-colour PLA spools of different
variants (Basic GFA00 / Matte GFA01 / Silk GFA06) it could map to the wrong
one. It now keeps the variant for force_color_match overrides (both the 3MF
and no-3MF fallback paths) so the matcher pins the matching tray, and falls
back to type+colour when that variant isn't loaded. A manual filament swap
(a preference override) still clears the idx so it matches the swapped-in
spool rather than the old one.

The printer-card queue-compatibility hint applies the same variant rule.
2026-07-24 13:13:04 +02:00
maziggy 0f203ce7ca fix(scheduler): force color match now distinguishes PLA variants (#2650)
Force color match dispatched onto the wrong PLA sub-variant: a job sliced
for White PLA Matte was treated as an exact match by printers loaded with
White PLA Basic or Silk+, because Bambu reports every variant as
tray_type "PLA" and the distinction lives only in tray_info_idx
(GFA00=Basic, GFA01=Matte, GFA06=Silk).

Two places dropped the field: the VP queue built each force override
without the parsed tray_info_idx, and _get_missing_force_color_slots
compared loaded trays on (type, colour) only.

Carry tray_info_idx into the override and require it to match when both
the override and a candidate tray have one; a blank idx on either side
(custom/third-party spools, older 3MFs) falls back to the historical
type+colour behaviour, so those setups are unaffected.
2026-07-24 12:54:22 +02:00
maziggy 30e4577838 Bumped version 2026-07-24 12:32:36 +02:00
maziggy 559571a35f Bumped version 2026-07-24 12:08:46 +02:00
maziggy 7c875b88ff chore(bandit): annotate tri-state migration SQL as a B608 false positive
The bed_levelling/flow_cali/nozzle_offset_cali boolean->tristate migration
builds two UPDATE statements with an f-string interpolating the column
name. Bandit flags these as B608 (SQL injection) at medium severity, which
failed the release-gate scan in test_security.sh.

The interpolated _col only ever iterates the hardcoded _tristate_cols tuple,
never user input, and SQL identifiers can't be passed as bound parameters.
Suppress with `# nosec B608` (matching the existing settings.py convention)
plus an inline rationale. No behavior change.
2026-07-24 11:24:25 +02:00
maziggy 29e78e85d5 test(launcher): skip repo-root launcher checks when files aren't shipped
test_launcher_shutdown_config.py and test_systemd_backup_paths.py read
repo-root launcher/config files (Dockerfile, docker-compose.yml,
deploy/bambuddy.service, install/install.sh, installers/windows/...,
spoolbuddy/install/install.sh). The Docker test image built from
Dockerfile.test copies only backend/, pyproject.toml, gcode_viewer/ and
requirements, so all 15 tests failed in test_docker.sh with "launcher
moved or was removed" — the files simply aren't in the image.

Guard both modules with skipif on frontend/package.json, which is present
in every source checkout but never in the test image. Native runs
(test_backend.sh, every commit) still execute the tests in full and catch
a genuinely moved/deleted launcher; the release-gate Docker run skips them
instead of failing on files it deliberately doesn't ship.
2026-07-24 11:17:19 +02:00
maziggy a898de3bb7 Bumped version 2026-07-24 11:10:11 +02:00
maziggy a273cd3eec security(frontend): bump react-router-dom to 7.18.1 for patched release
Moves react-router-dom/react-router 7.16.0 -> 7.18.1, off the range
flagged by GHSA-wrjc-x8rr-h8h6 (open redirect via backslash in Link/
useNavigate), GHSA-h8fp-f39c-q6mh (RSC), and GHSA-337j-9hxr-rhxg (SSR
hydration). The latter two need RSC/SSR, neither of which this
client-only SPA uses; the open-redirect one is the only reachable path
(post-login redirect), already guarded by sanitizeRedirectTarget.

Stays within the existing ^7.16.0 caret, no new transitive deps. Rebuilt
the static bundle. npm audit now reports 0 vulnerabilities.
2026-07-24 10:55:55 +02:00
maziggy 83ac5b361c Security fix (security-issue #6) 2026-07-24 09:15:03 +02:00
maziggy 154c6591d8 Updated BACKERS 2026-07-23 09:14:11 +02:00
maziggy 4f5bbde7de security(frontend): bump linkify-it and dompurify to patched releases
npm audit flagged both against the production dependency tree, and the
Frontend Security job fails on any fixable high-severity finding there
(FIXABLE HIGH: linkify-it).

linkify-it 5.0.1 -> 5.0.2 (GHSA-v245-v573-v5vm, high, CVSS 7.5) fixes a
quadratic-complexity DoS in the mailto: validator scan loop. It reaches us
only through prosemirror-markdown inside @tiptap/pm; the editor's own
autolinking uses linkifyjs, which is a different package and unaffected.
Nothing under frontend/src/ imports prosemirror-markdown or markdown-it and
neither appears in the production bundle, so the vulnerable code is tree-
shaken out and no running install was exposed.

dompurify 3.4.11 -> 3.4.12 (GHSA-c2j3-45gr-mqc4, low) fixes a
CUSTOM_ELEMENT_HANDLING bypass of afterSanitizeElements for allowed custom
elements. DOMPurify is shipped, but we never set CUSTOM_ELEMENT_HANDLING and
register no afterSanitizeElements hook, so the bypass has no precondition;
ProjectPageModal additionally passes a strict ALLOWED_TAGS/ALLOWED_ATTR
allowlist.

Both patched versions already satisfy the ranges their parents declare, so
this is a lockfile-only change - no overrides entry needed, package.json
untouched. npm audit reports zero vulnerabilities, npm run build is clean,
and all 2423 frontend tests pass.
2026-07-22 15:43:26 +02:00
maziggy 657958a67d Updated CHANGELOG 2026-07-22 15:43:07 +02:00
maziggy 21c6182c8d fix(ams): close the slot popup before it covers the filament dialog (#2631)
Tapping Configure on an AMS slot left the slot popup standing on top of
the filament type/colour dialog it had just opened, so both layers were
on screen at once.

The popup is portaled at z-[60] so it can escape the stacking contexts
sibling printer cards create on the dashboard (#1336), which also puts
it above ConfigureAmsSlotModal and LinkSpoolModal at z-50. Nothing
dismissed it: it is hidden only by the pointer leaving it, and a touch
device never sends that after the tap that opened it. On desktop the
next mouse movement cleared it, which is why this is a tablet report.

FilamentHoverCard and EmptySlotHoverCard now dismiss themselves before
running any action that opens a dialog or navigates away - Configure,
Assign Spool, Unassign Spool, and both Open in Inventory links. The
dismissal clears the pending timer as well, so a queued open cannot
resurrect the card over the dialog.

Actions that report progress inside the popup are unchanged: RFID
re-read, Load and Unload render their spinner there, and Copy UUID its
confirmation tick.
2026-07-22 14:04:56 +02:00
maziggy 41ad1d65c7 feat(skip-objects): select items directly on the build plate
Pairs the top-down plate preview with the slicer's per-object pick mask
(Metadata/pick_N.png), whose pixel colours encode the same identify_id the
firmware's skip command takes, so a click resolves to a real object rather
than an inferred bounding box. Several objects can be selected before one
confirmation; selected and already-skipped items are highlighted on the
plate; the checklist stays available when no mask exists.

view=pick serves only the active plate's mask and 404s otherwise, unlike
every other view. A render returned in a mask's place would be decoded as
object IDs — dark pixels yield small integers that collide with real ones —
and a click would then skip an arbitrary object, mid-print, irreversibly.
The 404 is what tells the UI to fall back to the checklist.

Click mapping goes through the contained rect, since the canvas paints at
mask resolution under object-contain; clicks on a letterbox bar are rejected
rather than clamped onto whichever object touches the border. Confirming
names the object when one is selected and counts them when several are,
which is what plates of identically-named clones need.

No printer-control command path was added or changed; the layer, permission
and existing skip-command guards are untouched.
2026-07-22 12:32:14 +02:00
maziggy 2c038097a0 Updated BACKERS 2026-07-22 10:45:23 +02:00
maziggy fdd6ec416f fix(slicer): classify filament profiles by their real printer scope, not just their name (#2628 follow-up)
Slicing for a P2S failed with "filament preset Bambu PLA Basic @BBL X1C 0.2
nozzle (slot 1) is not compatible with printer Bambu Lab P2S 0.4 nozzle" —
naming a profile shown nowhere in the dialog. The picked profile was
"Overture PLA Matte @0.2", whose inheritance chain roots in that X1C profile.

The dialog classifies a profile by its compatible_printers list and falls back
to reading the printer out of its name. That name carries no model, and the
list — present on the imported copy — is not shipped by every source: Bambu
Cloud omits it deliberately (rate limits), and Orca Cloud shipped it but
Bambuddy only mined filament type and colour from the same content.

Orca Cloud entries now carry their own compatible_printers, and the existing
same-name enrichment bridge carries the list onto entries that lack one, in
both directions between the cloud tiers. A bare "@<size>" name tag is read as
a nozzle size as a last resort: it can rule a printer out but never rules one
in, and implausible values are ignored rather than guessed at.
2026-07-22 10:44:05 +02:00
maziggy 56accd24de fix(smart-plugs): don't blank printer state when an accessory plug switches off (#2629)
An end-of-print auto-off on a plug that powers a filter fan marked the linked
printer offline and forced its state to "unknown". The mark was unrecoverable:
connected heals on the next MQTT message but state does not (only frames
carrying gcode_state rewrite it, and steady-state push_status frames are
partial), so the printer stayed "unknown" until a manual Force Refresh and the
queue never dispatched to it again.

The offline mark is now an explicit presumption: mark_power_off records the
state it overwrites and _on_message undoes it as soon as the printer sends
another report on its own topic, since inbound traffic proves the power was
never cut. A reconnect discards the saved state, so a genuine power cut is
unaffected. Each plug also gains a controls_printer_power flag (default true,
backfilled) that gates all five power-off paths, and the queue's power-on step
now picks the flagged plug instead of whichever linked plug came first.
2026-07-22 09:57:27 +02:00
maziggy 286eb9bf4a security(docker): raise pip floor to 26.1.2 to match advisory fix version
The image upgraded pip to >=26.1, but PYSEC-2026-196's fix is specifically
26.1.2 (PYSEC-2026-2875/2876 are fixed in 26.1). The old floor could resolve
26.1.0/26.1.1, which are still vulnerable to PYSEC-2026-196. --upgrade already
grabbed the latest in practice; this makes the pin match the advisory exactly.
2026-07-21 12:54:17 +02:00