mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 07:25:44 +02:00
58ea7a360d1a2ee2b7f88d10941c35cf67771d8b
823
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
58ea7a360d |
refactor(models): break the schema cycle that backup and restore sort through
print_archives.library_file_id -> library_files.folder_id -> library_folders.archive_id -> print_archives. Three nullable SET NULL links, each reasonable alone, that together made a loop metadata.sorted_tables could not sort: it dropped those edges, warned on every backup and every restore, and could return an order placing a child before its parent -- which once imported library_files ahead of library_folders and killed a restore on a ForeignKeyViolation. The restore no longer depends on that order (it strips every foreign key before importing and adds them back after), but the backup export sorts the same way, and the warning ends with "may raise an error in a future release" -- which would break backup and restore on one upgrade. Marking one edge use_alter removes it from the sort graph, not from the database: PostgreSQL emits it as ALTER TABLE ADD CONSTRAINT, as it already did for every constraint on these three tables, and SQLite inlines it into CREATE TABLE, so ON DELETE SET NULL holds on both. Verified against PostgreSQL 16 and SQLite. |
||
|
|
89d94796ee |
fix(queue): keep the filament override when a model job moves to one printer (issue #3133)
Switching an "Any P2S" job to a specific P2S cleared its filament override: "Specific Printer" empties the target model and the reset effect counted that as a model change. Printer mode also matched trays against the 3MF's colours, never sent the override, and left the old one on the row. The reset now compares against the last model actually targeted, so the switch keeps the override while a real model or plate change still clears it. Printer-mode tray matching (single, per-plate, multi-printer and the selector's per-printer editor) runs against the requirements with the overrides applied, mirroring the scheduler's _apply_filament_overrides; an entry naming the slot's own filament is not a swap and keeps its tray_info_idx. Printer-mode submits carry the user's overrides, and the create endpoint stores them for a printer-targeted job, so a dispatch-time recompute of an unresolved mapping looks for the same filament. Saving re-attaches the tray_info_idx an unchanged entry already had, so a virtual printer's force-colour PLA-variant pin (#2650) survives an edit in either assignment mode. The printer card's compatibility filter skips printer-targeted jobs: it mirrors the model scheduler, and hiding a job on filament would hide it from the printer it is going to run on. |
||
|
|
f98381f3d1 |
fix(queue): send the copy count for a cross-model print (issue #3101)
Selecting sliced files for two printer models and asking for 25 copies queued one item. The queue emptied as soon as it dispatched and the Batches tab stayed empty, because no batch is created at quantity 1. A multi-plate file moves the run count off the modal's Quantity field onto a stepper beside each plate (#342), hiding the field. The cross-model submit (#671) posts that field, which in this combination nothing can set, so it stayed at its initial 1. The modal read "19 runs in total" above a button that queued one. Per-plate steppers do not fit a cross-model job: its plate is chosen per candidate, in the alternatives list, so there is one number to give. Exclude cross-model from the per-plate mode and the global field comes back. Drop the plate selector in that mode too. Its choice never reached the request; it only keyed the filament-requirements query, so picking plate 3 for a candidate while plate 1 stayed ticked above produced overrides computed from a plate the job would not print. That query now follows the primary file's own dropdown. Dispatch needed nothing -- it already gives each copy its own candidate rows -- but naming did. A cross-model job carries neither archive_id nor library_file_id, because the candidates are the files, so both branches that name a batch missed and every such order would have read "Batch" in the tab the reporter went looking in. Name it after the first candidate. The existing cross-model tests all mock a single-plate file, which is why the pair was never covered; the multi-plate case is added. |
||
|
|
885c4156ae |
fix(inventory): enable Clear RFID Tag for a tray-UUID-only spool (issue #3109)
The button gated on tag_uid alone. A spool linked by its 32-character Bambu tray UUID carries none -- Bambuddy splits a stored tag by length, so a 32-char value becomes tray_uuid and tag_uid stays empty. In Spoolman mode that is every Bambu Lab spool synced from the AMS; on the reporter's instance, 35 of 39 tagged spools, none of which could have its tag cleared from the dialog. The documented workaround was to edit extra.tag in Spoolman's own interface. Everything around the button already treated those spools as tagged. The Tag ID column renders whichever identifier is present, and the payload the button sends nulls both fields -- which both inventory modes honour: the built-in PATCH applies them through exclude_unset, and the Spoolman route keys its tag-removal branch off either field being explicitly null. Either identifier now enables it, and clearing still removes both. |
||
|
|
4a85e033c0 |
fix(finance): show the currency the install is configured for (issue #3123)
The Finance page was the only surface in Bambuddy that read its currency from a data row rather than the `currency` setting, and it fell back to EUR where every other page falls back to USD. One variable drives every amount on that page, so the personal balance, the cost-center budgets and the whole transaction list were wrong together on any install not set to euros. It now takes the configured currency from /settings/ui-flags, which is readable by anyone who can see Finance -- /settings needs SETTINGS_READ, which a cost_centers:read_own user does not have. The backend was the other half. Of the four places that settle on a currency, three wrote a hardcoded "EUR": the wallet the API mints on demand, the wallet a print charge mints when none exists, and the balance returned for a user with no wallet row at all. All four now go through one resolver, which lives beside the rest of the balance logic. The wallet's currency column is removed outright rather than merely ignored. An install has one currency and nothing here converts between them, so a per-wallet copy could only ever drift from the setting -- and a column nothing reads is a trap for whoever finds it next. A startup migration drops it on both SQLite and PostgreSQL, after the raw CREATE TABLE that would otherwise re-add it on an install whose finance tables predate the ORM. SQLite builds older than 3.35 have no DROP COLUMN and keep it, harmlessly, since it has a default and no reader. Saving settings now invalidates the ui-flags query too. Nothing did, so a changed currency sat behind that query's staleTime before showing up. The sponsor prompt's own EUR fallback is now USD, matching AppSettings. |
||
|
|
3a5f802cdc |
fix(diagnostics): read the subnet the host is actually on (issue #3092)
The Network subnet check told the reporter that 192.168.98.170 and 192.168.96.9 were on different networks and to go configure routing between them. They are four hundred addresses apart inside one 192.168.96.0/22 LAN. An IPv4 address does not carry its prefix, and the check supplied /24 for both sides. That is the most common LAN and not the only one, and the guess is wrong in both directions: it splits a /22 and it merges a /25. Read the prefix off the interface that owns the address instead. find_local_ipv4_network() enumerates every interface, including the ones EXCLUDED_INTERFACE_PREFIXES hides. That list keeps docker0 and friends out of the Virtual Printer's bind dropdown; here the caller is asking about an address the kernel has already picked as a route source, and answering "unknown" because it sits on a bridge would be a worse answer than the truth. When nothing claims the address the check skips, which is what it always did with no host IP at all -- it must not assert a split it cannot see. The same check chose which of Bambuddy's own addresses to compare by probing a route toward 10.255.255.255, which on a multi-homed host is not the interface the printer is on. It asks for the route toward the printer now. On a two-NIC dev box that alone was warning about a printer sitting on the second card's own subnet. The probe takes IPv4 literals only. connect() on a name would resolve it on the event loop, and _same_subnet rejects names anyway, so nothing is lost. Resolving the prefix shells out to `ip -j addr show`, so it moves off the loop too. ----- fix(diagnostics): name the container engine instead of asking about Docker (issue #3092) "Not running in Docker - not applicable", said to a Bambuddy inside a Podman container. It reads as "you are on bare metal", and it sent the reporter looking for his problem somewhere else. Podman runs Bambuddy in exactly the two shapes Docker does, and the shape is the thing that breaks printer discovery and the Virtual Printer. detect_container_runtime() names the engine -- Docker, Podman, Kubernetes, containerd, LXC, or a container it cannot place -- and the check became Container network mode. is_running_in_docker() is deliberately left alone rather than rewritten on top of it. Three callers key real behaviour off that flag, and one of them switches the Add Printer flow from SSDP to subnet scanning. SSDP works for a host-networked Podman container, so answering True there would take a working feature away to fix a sentence. Widening it is a separate decision from naming the engine, so it is made separately. Mode detection keeps the original signal first, which also makes the Docker path incapable of regressing: a Docker host always has a docker0, so a container that sees one shares its namespace, and the new rules can only turn a warning into a pass. That signal says nothing about Podman, which creates no such interface on a host running no bridge containers -- which is how host networking came to be reported as bridge. The general form of the same idea answers for Podman: an interface whose iflink equals its ifindex was created in this namespace, and a NAT-networked container only ever receives one end of a veth pair. tun/tap is skipped, because a container may run its own WireGuard and that tun is native to a namespace it is not evidence of. The interface also has to be the one the kernel just named -- sysfs is namespace-tagged but a bind-mounted host /sys is not, and reading a colliding name's numbers would be reading another namespace's answer. What is still unreadable now says so and suggests host networking if discovery is failing, rather than guessing bridge and telling a healthy install to recreate itself. An LXC or LXD system container is named and told the question does not apply: it is on the LAN like a small virtual machine, so there is no network mode to recommend -- and its subnet check still runs. An engine we cannot name is a sentinel the frontend localizes, not a word interpolated into thirteen other languages. The support bundle carries the engine name beside the Docker flag, so the next report of this shape is answerable from the bundle. |
||
|
|
e4a9ef4550 |
fix(spoolbuddy): show the colour name the rest of Bambuddy shows (issue #3090)
SpoolBuddy said "Unknown color" under a correctly-coloured swatch for spools the inventory page names without trouble. The name was never in the spool record. Bambu's RFID tags frequently carry no readable colour name -- some carry an internal code instead -- so Bambuddy has always resolved the swatch's own hex against the colour catalog, and the kiosk was rendering the empty column. Exactly one SpoolBuddy file already did it right, which is what marks this as an inconsistency rather than a kiosk simplification. Route every SpoolBuddy colour-name display through resolveSpoolColorName, which also stops the spools that do carry a code from showing "A06-D0" at the user. The write-tag edit form keeps the raw stored value on purpose: offering a derived name for editing invites the user to save it as though they had typed it. Spoolman has no colour-name field at all, so _map_spoolman_spool puts the spool's subtype there and sets color_name_is_synthesized. That flag now travels on the tag-matched broadcast, and resolveSpoolColorName takes a third argument to honour it -- a synthesised name loses to the catalog and survives only as a last resort. Spoolman installs were reading "Silk+" as a colour on the Inventory page and the AMS hover card too, so those call sites pass the flag as well. Searching by a colour you can read on screen now finds it, in the kiosk and in Bambuddy: the shared inventory filter matches the resolved name as well as the stored one. That makes the filter depend on the catalog, which loads asynchronously, so the three memoised call sites take its version as a dependency -- without that, a query typed before the catalog arrives keeps its empty result and reproduces the very symptom being fixed. The fallback label was hardcoded English in components that already import useTranslation; it is now spoolbuddy.spool.unknownColor in all 14 |
||
|
|
48244bcfcf |
fix(queue): tell a pinned queue item why it is waiting (issue #3074)
A job queued as "Any X1C" explains itself when it cannot start: the model-based branch builds a reason for every candidate printer and puts it on the row, so the queue shows "Busy: X1C-01" or "Waiting for filament: X1C-02 (needs PETG)". The same job pinned to one printer showed nothing. It sat at Pending with waiting_reason NULL for as long as that printer was busy, which from the outside is indistinguishable from a queue that has stopped working -- the reporter watched fourteen minutes of it while his X1C ran a print he had started from its own screen. The fixed-printer branch had six ways out and none of them wrote the field. The sensor interlock (#1148) was its only writer, and it cleared the field up front on every pass where no sensor was holding the printer, so NULL was not an oversight on those paths but a guarantee. Every exit now writes, through one helper. The reasons reuse the model-based branch's vocabulary so _is_busy_only() keeps deciding what is worth a notification: a printer that is printing, drying, or working through the item ahead of this one reads as "Busy: <printer>" and stays silent, because it resolves itself. A printer that is off with no Auto On plug, and one whose plug could not switch it on, are worth saying. A finished plate nobody has acknowledged is split out from plain busy and named as itself. _is_printer_idle() returns the same plain False for that and for a running print, but they are not the same thing to the person looking at the queue: one clears itself and the other needs somebody to walk over to the printer. That notification fires on the transition into asking, where a busy-only reason counts as not asking. Testing whether the item was waiting at all -- which is what the model-based branch does -- would never fire it here: nobody's queue goes straight from idle to an unconfirmed plate, it waits behind the print first. The cost is that a printer dropping offline, returning busy and dropping again asks twice rather than once. The interlock stays silent. It has never sent this notification, and a change about what the queue displays is not the place to start. Clearing the field up front is gone with it. It existed so a shut door could not leave "Waiting on Enclosure Door" standing while the printer stayed busy with something else, and the new rule carries that guarantee instead -- whichever exit runs next overwrites it, and the dispatch path clears it. Two paths clear it that the report did not mention. A staged item and a future-scheduled one skip before this branch and never reach it again, so anything written on an earlier pass would outlive its condition for the life of the row. That includes the filament-deficit check, which stages the item itself. The notification is wrapped: a queue that cannot say why it is waiting is the bug being fixed, and a queue that stops dispatching because a provider timed out would be a worse one. On the frontend, the queue timeline drops any pending item carrying a reason, on the grounds that such an item will not auto-dispatch. That held while only the model-based branch wrote the field; "Busy: <printer>" is now the commonest reason there is, and it describes the very chain the timeline forecasts, so the rule would have emptied the view for anyone whose queue is pinned. It now asks whether the reason needs the user, via a small shared reader of the same shape the scheduler encodes. Which job goes out, and when, is unchanged: running the previous scheduler and this one over the same 768 states dispatches the same items in the same order with the same statuses, across 1452 rows that now carry a reason. |
||
|
|
30e530a881 |
fix(archives): let Items Printed go to 0 for a ruined plate (issue #3051)
A jam can destroy everything on the plate while the printer still reports the job as a success, so the honest count of usable parts is zero. The edit dialog floored the field at one, and a project's completed-items count sums that column, so there was no way to record that a job produced nothing. The floor was in the dialog only; the API stored whatever it was given, which also meant a negative count was accepted and would have subtracted from the project totals. The column is now bounded at zero instead. Filament Trends counted prints as `quantity || 1`, which would have read a deliberate 0 as "unset" and charged the ruined plate as one print while the project page counted none. |
||
|
|
309e64b8a2 |
fix(ams): resolve a slot's K profile by index when the printer does not file per hotend (issue #3044)
An X2D with two AMS 2 Pro, one per hotend, showed a K value on every slot of the first and nothing on any slot of the second. Configure Slot was worse than blank there: the picker offered no matching profile, the slot read as though nothing were bound, and choosing one changed nothing the user could see. Both symptoms are one rule. A calibration index can mean two different profiles on a dual-nozzle machine -- on the maintainer's H2C, index 16 is the left hotend's black PLA at K=0.018 and 15 is the right's at K=0.020 -- so the index is resolved against the slot's own hotend, and a miss shows nothing rather than the other nozzle's number. That is right whenever the printer files its calibrations per hotend. This one files them per filament: the second AMS's slots point at the same entries as the first, every entry tagged with one extruder, and requiring a match found nothing at all. The hotend now has to appear in the table the printer actually sent before it is used to narrow anything. Where it does not, the index stands on its own, which is what BambuStudio does for this same card -- AMSItem.cpp resolves it through get_pa_k_n_value_by_cali_idx, matching cali_idx and nothing else. Where it does, nothing changes: the H2C case still blanks rather than borrowing, and the other hotend's profiles stay reachable under Other K profiles. The relaxed path still refuses an answer when the candidates disagree on a value. The premise that the table is always numbered per nozzle had been written into three comments and two layers of code; it is corrected where it appears. Alongside it, in the same picker: the K-profile options rendered the hotend suffix twice in the matching group and three times under Other, so every option on a dual-nozzle printer read "... . Left . Left". |
||
|
|
f5cdc86689 |
fix(queue): chain the Timeline in the scheduler's order, not queue position (issue #3043)
Turning Shortest Job First on reordered the pending list and the scheduler, and left the Timeline drawing the pre-SJF queue for good. It chained each swimlane's bars by queue position alone and was never told the setting existed -- so the one view whose whole job is to say when each print will run was the one view answering for an order the scheduler had no intention of using. Bars now chain in the order the scheduler will dispatch: jumped items first, then shortest print time with an unknown duration last, then position. The starvation guard is visible there too, so a long print that has finally come up reads as next rather than staying buried behind every short job on the lane. Three surfaces claim to show queue order -- the pending list, the "if started now" ETA, and the Timeline -- and each had its own copy of the comparator, which is how one of them came to be missing a whole clause. They now share one, written against the scheduler's ORDER BY. Grouping came along with it: the pending list folded a model name down to its first character to build a lane key, so Any X1C and Any X2D both landed on -88 (as did Any P1S and Any P1P) and the two lanes interleaved into a single run of rows. Backend untouched. The scheduler was dispatching correctly the whole time; only the drawing of it was wrong. |
||
|
|
069ee8fc87 |
fix(queue): skip preheat entirely when no loaded filament wants a chamber (issue #3041)
Preheat & Heat Soak delayed every PLA print by five to seven minutes and gave nothing back. The filament map correctly derived a chamber target of 0, and the chamber phase correctly skipped -- but the stage then heated the bed, waited for it, and held the full soak anyway, because the soak had no idea it was holding for a chamber nobody asked for. The print's own G-code sets the bed the moment it starts, so the bed phase only moved the warm-up ahead of the FTP upload instead of overlapping with it. A 0 that comes out of the filament map now skips the stage before any command goes out. The one thing the skip still does is put the airduct flap back to cooling on the models that have one -- an H2D left in heating mode by the ABS job before it would otherwise cook the PLA that follows, and that costs one MQTT command and no waiting. Explicit instructions are untouched. A chamber target of 0 typed into a print's own override still heats the bed and runs the soak, which is what the queue documentation has always promised it does, as does forcing a print's Preheat override to On. Prints that want chamber heat are unaffected, including the P1S/P1P/A1 tier where the bed and the soak timer are the whole mechanism. The existing unit tests all ran with soak_seconds=0, which is why the production default was never exercised; the PLA test now runs at the real default and asserts nothing is dispatched and nothing is slept. Surfaced in the UI on the way through: the Settings hint claimed the derived 0 skipped "the chamber phase", and the per-print chamber override field said nothing about a typed 0 meaning bed-only -- a user reaching for 0 to turn preheat off got the delay instead. |
||
|
|
e2493132bf |
fix(slicer): offer the desktop handoff only for formats the target slicer accepts (issue #3029)
The Slice action and Open in Slicer offered .stl, .step and .stp alongside .3mf, on the assumption that a slicer which opens an STL from its own File menu will open one from a link. Bambu Studio does not. Every bambustudio:// and bambustudioopen:// URL reaches one import path that refuses any filename which is not .3mf, and refuses it before fetching anything: "Download failed, unknown file format." The message names the format, so the failure reads as a broken model rather than an unsupported handoff. OrcaSlicer has no such limit. Only MakerWorld links take its equivalent path; a link to the user's own Bambuddy goes to its general downloader, which does not inspect the extension. So the format list becomes per slicer. isSliceableFilename and isSliceableFileType take the target as a required argument -- a default would quietly reintroduce the handoff that cannot work -- and an unrecognised value falls back to the Bambu Studio list, which is where openInSlicer sends anything that is not exactly 'orcaslicer'. The File Manager offers Slice only when the configured desktop slicer will take the file. The 3D preview reaches both slicers from one split button, so instead of hiding it promotes whichever can take the file to the primary action, naming that slicer when it is not the configured one; the split collapses to a plain button when no alternative is left. The sidecar path is untouched: with Use Slicer API on, Slice still takes STL and 3MF whatever the desktop target is. |
||
|
|
816f073a9e |
fix(auth): decouple media routes from the camera stream token (issue #3025)
Thirteen routes with nothing to do with a camera took the camera stream
token as their credential -- library and archive thumbnails, plate
previews and plate thumbnails, timelapses, print photos, archive QR
codes, project covers, print-log thumbnails, printer covers and
external-link icons. A browser cannot put an Authorization header on an
<img src>, so these need a credential that fits in the URL, and the
camera token was the only one that existed. Minting one costs
camera:view, so a user granted library access to their own files got a
grid of broken images until they were also handed the live camera.
Adds a media token: minted by POST /auth/media-token behind plain
authentication, and identified -- it records the principal the way the
websocket token does rather than being anonymous the way the camera
token is. Each route now gates on the permission and ownership rules of
the resource it serves, through the same _ensure_*_visible helpers its
header-authenticated siblings already use. The three camera routes keep
the camera token, and require_camera_stream_token_if_auth_enabled now
documents that it is for those only.
The media dependencies accept ordinary Authorization / X-API-Key headers
as well as ?token=, delegating that path to the existing checkers, so
API-key scope rules and the per-printer allowlist are unchanged.
Long-lived camera_stream, camwall and overlay tokens are deliberately
not accepted on the media routes -- those are handed to kiosks, walls
and Home Assistant to display video. The cam wall, streaming overlay and
kiosk views use only the three camera routes and are unaffected.
Frontend: withMediaToken alongside withStreamToken, and
useStreamTokenSync fetches a media token for every signed-in user while
asking for a camera token only when the user can mint one, which also
stops the 403 that fired on every page load for everyone else.
Also fixed, same class:
- /printers/{id}/files/plate-thumbnail/{i} is rendered in an <img> but
had a header-only guard, so the file manager's plate thumbnails 401'd
whenever auth was enabled. It now takes a media token too.
- getProjectCoverImageUrl returned a URL ending in ?token=, and the
project edit dialog appended its own ?v= cache-buster after it, so the
second ? landed inside the token value. The version is now a parameter
applied before the token.
Tests: 15 integration tests for the token boundary, permission
enforcement and per-row scoping; 10 frontend tests for the URL split and
the two-query hook. test_cover_image_get_uses_stream_token_gate is
renamed and repointed at the media gate -- what it pins, that the
credential has to fit in a URL, is unchanged.
|
||
|
|
93eeb05264 |
fix(auth): let the sidebar read install flags without settings:read (issue #3023)
cost_centers:read_own exists so a non-admin can see their own wallet, balance and cost-centre spend, and the Finance page honoured it -- typing the URL worked and rendered their balance. The sidebar never offered the entry. It decides whether to show Finance by reading billing_enabled from GET /settings, which requires SETTINGS_READ. A non-admin gets 403 there, so the value arrived undefined, `undefined !== true` held, and the entry was hidden from precisely the users the permission was written for. The permission map and the route guard were both already right; only discovery was broken. Three more fields came from that same 403, and one of them failed the other way up. The Notifications gate tests `=== false`, which undefined never satisfies, so an administrator who switched user notifications off still left the entry showing to the non-admins it governs. Nobody reported that one, and no administrator could have reproduced either: administrators can read /settings. The remaining two were quieter -- the sponsor prompt fell back to EUR whatever the install uses, and the update check ran where it had been turned off. SETTINGS_READ cannot be the price of knowing whether billing is on. It also grants sight of the SMTP, LDAP and MQTT credentials, which is the reason /settings/ui-preferences exists at all. So: a second endpoint, GET /settings/ui-flags, carrying those four fields and asking only that the caller be signed in, via the existing require_auth_if_enabled. Layout drops its /settings query altogether, which closes the class rather than the two instances that happened to be visible. Deliberately not four more fields on /ui-preferences. That endpoint is served to anyone at all on the recorded grounds that its contents are "public defaults that ship with the app" (test_route_auth_coverage.py), and its field set is pinned by a test written to make anyone adding to it stop and think. These fields are not defaults -- they say how this deployment is configured -- so they get their own endpoint at their own trust level instead of stretching that charter to fit them. require_auth_if_enabled also keeps the auth-disabled case that /ui-preferences was ungated for: "works when there is no auth" and "readable by anyone" are different statements, and conflating them is what put a settings read in front of a permission that never needed one. Twelve tests. Backend pins that the operator can read the flags, that the same operator still gets 403 from /settings, that an anonymous caller is refused when auth is on, that it answers when auth is off, the exact field set, that no credential ever appears, and that the public endpoint did not quietly gain these fields. Frontend pins Finance visible for cost_centers:read_own with /settings returning 403, and Notifications hidden when the flag is off -- each waiting on a positive signal before asserting an absence, so the negative cases cannot pass before the query resolves. Reported by @lonix, who traced it to the queryKey and the route gate. |
||
|
|
f3b1c59169 |
fix(orca-cloud): close the HTTP client when an authenticated build fails
OrcaCloudService owns an httpx client from construction, and every path in _build_authenticated_service after that point can raise: no stored refresh token, a rejected refresh, an unreachable Orca, and the token-rotation write. On success the caller closes the client. On failure nobody is ever handed it, so all four paths leaked one into the connection pool. That went unnoticed while the only callers were routes, where the trigger is a person retrying a broken sign-in a handful of times. It stopped being harmless in |
||
|
|
9434875fa1 |
fix(ams): resolve a custom filament's own id from every preset source (issue #3003)
A custom filament profile reaches an AMS slot as itself through exactly one field, tray_info_idx, and every source we can read that id from was reading it from the wrong place or not reading it at all. Bambu Cloud returns a preset's own filament_id either on the response envelope or inside the preset JSON under `setting`, and only the envelope was read. Presets of the second shape fell through to the base_id branch and reached the slicer as the Bambu filament they inherit from. filament_type next door already handled both spreads; filament_id now does too. Orca Cloud was absent from the resolver entirely. A spool stores the bare profile UUID, which matched no branch and fell through normalize_slicer_filament -- a function that passes anything it does not recognise straight through -- so a 36-character UUID went into the field. Orca profiles carry their own filament_id in the slicer JSON that OrcaProfileDetail already exposes under `setting`, so the lookup is the same one the Bambu branch does. It is best-effort: no pairing, a dead token or a missing orca_cloud:auth permission degrades to the fallback rather than failing the assignment, and it passes clear_on_auth_failure=False because a background caller cannot tell a real revocation from a lost refresh-rotation race. configure_ams_slot sent the cloud setting_id as tray_info_idx when it found no real filament id. That field is 8 characters on the printer -- exactly the width of a local preset id, less than half a cloud one. Measured on the reporter's A1: sent PFUS9ddc938fe3ab8f, the tray read back PFUS9DDC, acknowledged as a success. The slot then resolved to nothing, so the slicer showed Generic anyway and the calibration table, keyed by the same field, lost the slot. It now falls back to the slot's existing filament id or the generic for the material, and the route's guard was aligned with the resolver's so both refuse the same four shapes from one shared definition. This reverses the contract #1053 pinned. Six tests asserted that the PFUS belonged in tray_info_idx; the A1 capture shows it never worked, so they were rewritten with the measurement in their docstrings. Verified against 874 AMS trays across twelve models in the support archive: 92 already carry a custom "P" + 7 hex filament id, which is what confirms the mechanism works and this is a lookup failure rather than a platform limit. No tray on any model carries a setting_id, so a profile with no filament_id of its own still cannot be told apart from its base. |
||
|
|
e9daa2124e |
Match slicer presets on what they declare, not what they are named (issue #2982)
The internal slicer picked PETG for a PLA plate and an A1 process for a P1S. Both come from the sidecar's bundled-profile listing, fixed in the sidecar repo; this is the consuming half plus the hardening that keeps an older sidecar degrading rather than breaking. Standard-tier presets now carry the compatible_printers the sidecar reports. That list is the only truthful account of which printer a preset belongs to, because the bundle ships no process preset named after a P1S, an X1, an X1E or an H2D Pro -- all ten of the P1S's are named "@BBL X1C" and name the P1S only in that list. Reading the printer out of the preset NAME therefore made a P1S look like it had no compatible process at all: all 198 hid behind "Show all" and the auto-pick fell through to an alphabetically-first 0.06mm Fine @BBL A1 0.2 nozzle the CLI refused. A P1S now gets 0.20mm Standard @BBL X1C and 73 filaments instead of 4. An older sidecar reports nothing here, which leaves the name matcher in place -- degraded as before, not broken. Material is now a hard partition in the filament pre-pick rather than a +10 bonus. A preset stating a different material than the plate asks for is the wrong preset, not a worse one: wrong nozzle temperature, wrong bed temperature, wrong flow. A preset stating NO material stays eligible -- unknown is not wrong, and 32 shipped profiles genuinely have none. The same rule reaches the retain path, which held a slot on printer-compatibility alone and so cemented a wrong-material pick through every re-pick. A preset the user chose themselves is exempt: printing PETG on a plate a designer labelled PLA is a legitimate thing to do, and this rule exists to correct the auto-pick, not to overrule the user. Two more, both found while tracing this and neither reported: Among process presets equally valid for the selected printer, the one nearest a 0.2mm layer height now wins. Within a tier the list is alphabetical and Bambu's naming puts the finest height first, so every slice that did not name its own process silently got 0.08mm Extra Fine on an X1 Carbon and 0.06mm Fine on an A1 mini -- correct presets, nobody's default. Ties break toward the coarser, faster height; a name with no readable height is still pickable when it is the only candidate; a process the 3MF named still wins outright. H2DP is aliased to H2D Pro, the same shape as the A1M rename in #1649 -- the bundle spells the model one way in preset names and another in the printer preset, so an H2D Pro classified all 198 processes as another printer's. Deliberately narrow: H2DP and a plain H2D are different machines and must not collapse. A dropdown the printer filter would empty now shows the unfiltered list instead. That state was reachable for four printer models and told the user nothing; a visible preset for the wrong printer can be changed, an empty dropdown cannot. Verified against live Orca 2.4.2 and BambuStudio 02.08.02.61 sidecars over the real 1156- and 1792-profile trees: every one of the eight printer models tested now auto-picks a 0.20mm process for its own printer, a PLA plate draws a PLA preset and a PETG plate a PETG one. Each change was confirmed to fail its tests when reverted. |
||
|
|
699fc419fe |
Paint an AMS slot card with the spool's colours, not the tray's (issue #2967)
A Ziro "Colorful Mist" -- yellow, cyan and pink, effect Tri Color -- hovered on the printer card as a single flat pink rectangle. A printer reports exactly one tray_color hex per tray and nothing else, so telemetry cannot describe a gradient or a surface effect and never will. The header now paints the bound spool's own swatch whenever that spool declares extra colour stops or an effect, through buildFilamentBackground -- the builder the Inventory swatches already use, so the two surfaces cannot drift apart. A plain single-colour spool keeps the flat backgroundColor it has always had, and a slot with nothing bound is untouched, so the common case goes nowhere near the gradient path. The gate is "any stop at all", not "more than one". buildColorLayer ignores rgba the moment stops exist, so a one-stop spool renders that stop rather than the slot hex; skipping it would leave this card showing a different colour from the Inventory row for the same spool, which is the class of disagreement the shared builder exists to prevent. isLightColor now tests the colour actually on screen. Once the spool's swatch is painted the base is no longer the slot hex -- a single stop replaces it outright, and an effect-only spool paints the spool's own rgba -- so testing the slot hex would pick the text colour for a background that is not there. Above one band no single hex can decide legibility, and the name sits dead centre where a multi-stop background is likeliest to change under it. So a genuinely multi-band header puts the name on the same scrim the vendor badge already uses. One stop, or an effect over one colour, still vendor badge already uses. One stop, or an effect over one colour, still leaves a real base colour to test and keeps the contrast rule it had. Spoolman mode gains the gradient in the process. Spoolman has held the stops in filament.multi_color_hexes all along and the label renderer has been reading them for releases, but _map_spoolman_spool never returned them -- so the identical roll registered in Spoolman rendered flat while the internally-managed one did not. Both now share one parser rather than reading the same field two ways. What stays asymmetric is Spoolman's own limitation, and it is pinned by a test rather than left to be rediscovered: Spoolman has no field for a surface effect at all. Its only neighbouring field, multi_color_direction, says how the stops are laid out, not that the roll is silk or glitter. effect_type is therefore None for a Spoolman spool instead of guessed at, and silk/sparkle/wood remain internal-only. The other two halves of the report -- the header naming the colour "White" instead of "Colorful Mist", and the print dialog offering "A3: PLA (White)" -- were already fixed on dev by #2875 and by the slot-naming change that landed the day after this was filed. Neither is in 1.2.5.3, which is what the reporter is running. |
||
|
|
4f10d15584 |
Record the colour a slice is actually printed in (issue #2977)
Every file the internal slicer produced came back with filament_colour = #00AE42 whatever filament was picked: a green plate thumbnail, green metadata, and a "Color mismatch" in the Print dialog against the AMS slot the job had just been correctly mapped to. A colour is not a property of a filament preset in either slicer. It belongs to the project, and Bambu Studio and OrcaSlicer set it from the plate in their GUIs -- so nothing was attached to the preset Bambuddy sends by name, and the CLI fell back to its own compiled-in default, which is Bambu green. None of the shipped BBL filament profiles define filament_colour; the whole tree has zero occurrences. default_filament_colour is not the answer on its own. Measured against a 02.08.02.61 sidecar, a profile carrying only that still slices to filament_colour ["#00AE42"] -- Bambu Studio consumes it in the GUI when a project is created, not in --load-filaments. So it is read and rewritten as filament_colour, which the same sidecar does honour: the reporter's exact triplet returns ["#E8B00C"] when patched this way, and the colour lands in both project_settings.config and slice_info.config, which is what the thumbnail and the AMS mapping actually read. Each filament row in the slice dialog gets a colour control, and the resolved value flows through a chain: the user's pick, then the preset's own default_filament_colour, then the colour that slot was designed with in the source 3MF. A slot with none of the three is left untouched rather than given a guess. The designed colour is read from project_settings.config, not slice_info.config. The latter records what the file was last sliced with, which for a source that never carried a colour is #00AE42 itself -- measured -- so using it would have been circular. The control is offered on single-filament sources too, because an STL, and equally a mesh-only 3MF exported from CAD, has no colour anywhere else to inherit. That is the case this exists for, and it took three shapes to make it visible: a swatch in the label row was identical to the read-only dot multi-colour rows have carried for releases, and adding the hex beside it only made it look like a caption. It now sits beside the dropdown, styled like it and the same height, with the swatch and hex wrapped in one label bound to the input so a click anywhere on it opens the picker. An untouched slot with no designed colour submits an empty string rather than the control's displayed default. A sent colour outranks the preset's own, so pinning the placeholder would silently discard the real colour of an imported OrcaSlicer profile that carries one. Slicer Pipelines pick up the same chain without carrying a colour of their own. Also adds a warning for a defect found while investigating this: a filament preset whose name the sidecar's bundle cannot resolve is not rejected. The CLI inherits nothing, falls back to its defaults for every field, and returns a well-formed success -- measured, an unresolvable name slices as filament_type ["PLA"] at nozzle_temperature ["200"] with filament_ids [""] and filament_vendor ["(Undefined)"], so a PETG preset a sidecar image predates prints at PLA temperatures with no diagnostic anywhere. Both signals are required together, which keeps it off the two legitimate lookalikes: a hand-written profile that never named a vendor still carries a real filament id, and a user's own cloud preset carries a vendor while legitimately having no bundled id. The file is kept rather than refused, unlike the missing start G-code of whoever can see the temperatures. |
||
|
|
5211fd4575 |
Store a failure reason in one vocabulary, not three (issue #2974)
failure_reason was written three different ways and nothing reconciled
them. derive_failure_reason wrote English display labels ("Layer shift"),
older builds of the archive editor wrote the translated label in whatever
locale that user was running, and the two stale-archive paths wrote
English prose sentences. All three reach one column -- the archive PATCH
has mirrored the field onto the latest print-log entry since #1444 -- and
the Failure Analysis widget groups on the raw value, so one real cause
occupied several buckets. On a live install before this landed:
print_log_entries held 91 rows reading "User cancelled" beside 1 reading
"userCancelled".
In an English UI those two render as the same words twice with different
counts, which is why nobody spotted it. In any other locale one of them
stays English, because a stored label has no key for t() to resolve. The
editor was worse than cosmetic about it: its reverse lookup compared the
stored value against t() in the current locale, so for a non-English user
nothing matched and the dropdown opened empty over an archive that
plainly showed a reason.
The keys were already canonical and already enforced.
_FAILURE_REASON_KEYS in api/routes/print_log.py rejects anything else
with a 400 and explains why in its own comment -- the widget renders
values back through t(), so an unrecognised one surfaces as a raw string.
derive_failure_reason had simply never been held to that rule. It now
produces keys, and the cancel branch returns userCancelled.
The two "Stale - ..." sentences become one new noStatusUpdate key. Both
describe the same observation, that no end-of-print status ever arrived;
which of the two situations occurred is already carried by status --
cancelled at the stale-cleanup site, the reconciled outcome at the
reconnect site -- so collapsing them loses nothing and gives Statistics
one bucket instead of two sentences that could never be translated. It
had to enter the vocabulary rather than merely be tolerated, because the
editor discards any value it does not recognise.
Existing rows are converted by a startup migration folding 168 historical
labels onto the 12 keys across both columns. It is exact rather than a
guess: every label across all 14 locales resolves to exactly one key,
with no collisions. The map is a frozen snapshot rather than something
read from the locale files at run time -- it maps what was written
historically, so regenerating it from the current translations would
silently stop recognising the very rows it exists to convert. A value
outside the map is left alone; guessing would be worse than leaving one
honest string in its own bucket. There is no one-shot settings flag, on
purpose: the statement only matches values in the map and a key is never
a label, so it is self-terminating, and a flag would permanently skip
anyone who restores an older database.
The last part is a data-loss bug that was not in the report. The editor's
fallback to '' was not merely a wrong-looking dropdown -- the empty
selection was then saved over the stored text, so opening the editor on
an archive whose reason was free text and pressing Save destroyed the
classification. An unrecognised value now keeps its own option and
survives a save.
|
||
|
|
b3c67c6943 |
Keep a lookbehind Safari 16 cannot parse out of the bundle (issue #2971)
An iPhone on iOS 16 loaded nothing at all -- no error, no partial render, just white, over LAN IP and over an HTTPS domain alike, while the same install was fine on Android, macOS, Windows and Linux. remark-gfm, added in v1.2.5 for the folder README panel, reaches mdast-util-gfm-autolink-literal, whose module body carries a lookbehind assertion. Safari did not support lookbehind until 16.4. A regex literal is validated when its module is compiled, not when the function holding it runs, so this was never going to fail as a broken README panel. FolderReadmePanel -> FileManagerPage -> App is a plain static import chain, the regex landed in the entry chunk, and the browser refused to compile all 10 MB of it. Nothing executed, so nothing rendered. v1.2.4 is the last release that loads on those iOS versions. The panel now renders GFM through a locally composed plugin holding four of remark-gfm's five sub-extensions -- tables, strikethrough, task lists, footnotes -- and omitting autolink literals, the only one carrying the lookbehind. Composing rather than configuring is forced by the bug: importing remark-gfm at all is what breaks the page, so no runtime option could have reached it. Parity was measured rather than assumed. Serialized ASTs against real remark-gfm over a 34-case corpus, position data included, are identical in 29; the five that differ are exactly the autolink cases, where the only change is link -> text with table and list structure intact. Across 26 hostile inputs -- NUL bytes, a BOM, an RTL override, a lone surrogate, combining marks, a 200 KB line, 500 stacked tables, 60-deep nesting, malformed and ragged tables -- neither implementation throws and none diverge, and applying the plugin twice is idempotent for both. The visible cost is that a bare https://example.com or foo@example.com typed into a folder README no longer links itself; [text](url) and <https://example.com> are core markdown and still do. The wiki claimed "links all render" and now says which. remark-gfm, mdast-util-gfm and micromark-extension-gfm leave the dependency tree and their eight surviving sub-extensions are declared directly, at ranges equal to or tighter than the ^2.0.0 those two packages declared, so the resolution surface did not widen. The bundle is 23 KB smaller. Vite's build.target governs syntax lowering and esbuild does not rewrite regular expressions -- measured, a lookbehind builds silently under safari15, safari16.0 and es2020 alike, which is how this shipped and then sat unnoticed for two months. So the guard is a real check rather than a compiler setting: npm run build now ends in check-browser-baseline.mjs, which scans the emitted bundles for syntax Safari 16.0 cannot parse and fails with the offending snippet. It is scoped to parse-time failures only -- a missing runtime API breaks one feature, while one of these takes down the whole app and has no graceful degradation to fall back on. Verified firing on the stale bundle before the rebuild, and running correctly inside the Docker frontend stage where only frontend/ is copied. Seven renderer tests pin both halves of the trade: each surviving GFM feature still renders, and both forms of autolinking stay off on purpose so a future dependency bump cannot quietly bring the lookbehind back. |
||
|
|
d5c7047765 |
Name an AMS slot after the spool assigned to it
The print dialog described every slot from the printer's own telemetry, and
a printer cannot describe a spool it did not sell: a tray record carries no
brand field, tray_sub_brands is left empty for anything that is not a Bambu
spool, and the colour arrives as a bare hex the client resolves against
Bambu's own colour catalogue. A Devil Design PLA Basic Orange assigned in
Bambuddy therefore read as "PLA (Sunflower Yellow)" -- Bambu sell a
Sunflower Yellow at the same FEC600 -- while the printer card, which reads
the assignment, named it correctly. Two views of one slot, disagreeing.
GET /printers/{id}/inventory-remain now carries each bound slot's brand,
material, subtype, colour name and hex alongside the pooling key it already
sent, and the dialog prefers that over telemetry. The fallback is per field,
not all or nothing, so a spool with no stored colour name still gets the
catalogue lookup it had before while its brand and subtype come from the
binding. Resolved server-side because the identity rule differs per
inventory mode -- brand is a column in internal mode and a nested vendor in
Spoolman's, where the subtype is the filament name with its material prefix
stripped and the colour name has a three-step read order Spoolman has no
field for. Spoolman's synthesised colour name, which falls back to the
subtype, is withheld rather than rendered as "PLA Basic (Basic)".
Matching is deliberately untouched and still runs on the printer's
telemetry. The auto-assignment, the colour-mismatch test and the mapping
that actually gets dispatched all read type, colour hex and tray_info_idx,
so renaming a slot cannot make the panel and the dispatcher draw different
conclusions from it. The payload is re-read on every open of the dialog: it
names the slots now, and a spool assigned moments earlier would otherwise
keep its old name for the rest of the thirty-second stale window. Done at
the two readers rather than by invalidating the key from each of the
eighteen places a binding or a spool can change, half of which are internal
paths and half Spoolman ones -- covering some would make freshness depend on
which mode you run.
Two hardening fixes fall out of putting a mapper on this path.
build_slot_materials runs before every queue start through
compute_deficit_for_queue_item, and _map_spoolman_spool walks a dozen nested
fields off the wire, any of which arriving as the wrong type raises
AttributeError rather than ValueError. Naming a slot must never cost a
dispatch, so that call fails soft to no name. The same inputs also reached
_material_identity_spoolman and _normalize_color_for_id, which have always
been on this path and would fail a queue start on a Spoolman record whose
filament is not a dict or whose color_hex is a number; both now read those
as "nothing to pool with". Behaviour for well-formed input is unchanged --
the guards only intercept types that previously raised -- so no pooling key
moves and AMS Filament Backup is untouched.
|
||
|
|
e5a18bf58b |
Key a K profile on its nozzle's flow type
A printer files each calibration under a nozzle id of the form HH00-0.4 (high flow) or HS00-0.4 (standard) and can hold both for one diameter -- a maintainer's H2D carries 102 high-flow entries against 6 standard -- because the same filament reads a different K through each. Nothing read that, so a standard-flow profile could be selected for a high-flow nozzle and vice versa. The flow is now stored with the profile, shown against each option in the picker, and checked before a stored profile is applied. Two spellings have to agree for that: a calibration entry says HH00-0.4 while the fitted nozzle reports HH01, so the comparison is two characters rather than four -- the trailing digits are a hardware variant the calibration table normalises to 00. Unknown flow on either side matches anything, which is what it has to do. Every profile stored before this has none. And an X1C declares none on any profile at all -- probed live, all eight come back with an empty nozzle id, against a four-digit cali_idx and a populated setting_id -- even though the machine really does take either nozzle. supports_nozzle_flow_type is therefore the wrong thing to gate on: it returns True for an X1C, and treating that silence as Standard would have dropped every X1C profile the moment a high-flow nozzle was fitted. What the printer's own table declares per profile is the test. NozzleInfo.nozzle_type carries two vocabularies by printer generation -- the nozzle material on legacy printers, the flow code on H2 -- and the comment claiming only the former is corrected. Anything that is not HH or HS reads as unknown, which is what makes the material spelling harmless. Storing both flows for one hotend and diameter is deliberately not done: spoolman_k_profile is UNIQUE on (spool, printer, extruder, diameter) with no flow column, and allowing a second row in internal mode alone would break inventory-mode parity. The picker marks a profile whose flow does not match what is fitted instead of letting it look configured while doing nothing. |
||
|
|
a7b563334e |
Configure a spool's filament preset and K profile per nozzle
A slicer preset is bound to a printer model: "Bambu PLA Basic @BBL X1C" is not the same preset as "@BBL H2C", and Bambu names a nozzle size in it as well. A spool carried exactly one, which was right until the same spool was used on a second machine -- the AMS slot on the other one was then configured with a preset that machine has no profile for. K profiles had the matching gap from the other side: the tables have always been keyed per hotend, but the picker could not express it. spool_filament_preset and its Spoolman twin store the exceptions, keyed (spool, printer_model, nozzle_diameter). Model rather than printer because the preset is a property of the model -- "@BBL X1C" is the same preset on every X1C, and asking per machine would mean picking the identical value twice. K profiles stay on printer_id, because a K value is measured on one physical hotend and two machines of the same model legitimately differ. Resolution is exact (model, diameter) -> (model, "") -> the spool's own preset, so a spool nobody has configured behaves exactly as it did before. The form writes one row per nozzle size and never the "" row; that level is kept for API clients wanting one value to cover a model. Both halves cover every standard nozzle size rather than the size currently fitted, because a spool is configured once and nozzles get swapped. The PA Profile tab becomes a Printers tab: a model list beside a detail pane holding a preset row per size and a K-profile grid of size by hotend. Each model is offered only the presets that name it, through the same matcher the Configure AMS Slot modal filters with, which moves out of that component into utils/slicerPrinterMatch. Presets whose name identifies no model -- most user-authored and OrcaSlicer ones -- stay offered everywhere, as does whatever is already selected, so a saved override cannot vanish from the control that shows it. Every preset carries an origin badge in the wording and colours that modal already uses. Every path that configures a slot now respects both: manual assign in either inventory mode, RFID auto-assign, the Spoolman tag link, the re-fire when a slot goes empty to loaded, the re-apply after a calibration-table refresh, and the re-selection when a Filament Track Switch moves an AMS to the other nozzle. Which nozzle a slot feeds, and how wide it is, was worked out independently in seven of those places, each reading nozzles[0] for every slot on the machine -- correct on a single-nozzle printer and on a dual-nozzle printer with matching nozzles, wrong the moment two sizes are fitted. That resolution is now services/slot_nozzle. Which array entry belongs to which hotend is no longer inferred. Measured on an H2D fitted with a 0.4 high flow on the left and a 0.6 on the right, nozzles[0] reads the right hotend, so the array is indexed by extruder id and the H2/X2 parser's convention is the one that holds. The legacy parser's opposite convention never governs a real dual-nozzle machine: every model in DUAL_NOZZLE_MODELS reports device.nozzle.info, and left_nozzle_diameter appears in no log or wire capture. Two comments that said otherwise were wrong and are fixed; amsHelpers' code was right all along and only its comment lied. Four defects surfaced while wiring it, all pre-existing except the last. The picker identified a chosen calibration by cali_idx alone, and the printer numbers its calibration table per nozzle -- on a dual-nozzle machine the same index exists on both hotends meaning different things, so saving could persist the other hotend's K value and diameter; SpoolBuddy's write-tag page carried a verbatim copy and gets the same fix. RFID auto-assign chose a K profile with no extruder test at all, so a spool calibrated on both hotends had a coin toss decide which pressure-advance value the slot got, on the path that runs unattended every time a Bambu spool is loaded. The Spoolman tag-link path resolved no preset whatsoever, configuring every linked slot with a generic material id and discarding a preset set in inventory -- the same defect #1713 fixed on the assign path, one function over. And an FTS inlet move re-selected K for nozzle 0 rather than for the nozzle the AMS had just been moved to. The last one is new here: a per-model override can be a cloud USER preset, whose PFUS-prefixed id the slicer rejects, and passing it straight into extrusion_cali_sel would silently lose the K-profile link. Reached the printer only where such an override exists, which is why nothing in the suite caught it. printer_safe_filament_id falls through to the spool's own preset and then the tray's RFID value instead. Reading a printer's calibration table asks for one nozzle size at a time. H2-series firmware answers only the first one or two of a concurrent burst of extrusion_cali_get and silently drops the rest, each dropped request costing a five-second timeout before its retry: measured at 11 and 23 seconds on an H2C and an H2D for four parallel requests, against roughly one second in series. An X1C answers all four at once, which is why this only ever surfaced on dual-diameter printers. Printers themselves are read in parallel -- separate machines are separate connections. The Configure AMS Slot dialog opens on the spool's own configured values, falling back to the slot's last manual configuration and then the tray's RFID data. The spool form is wider for the two-pane layout, colour, weight, cost and location move to their own tab in two columns, and a printer card in expanded view lists every fitted nozzle size rather than the first entry alone. |
||
|
|
9500c046c0 |
Ask which nozzle to feed when a Filament Track Switch is fitted
Load and Unload in the AMS slot menu did nothing on an H2C with the switch fitted. The ams_change_filament command carries an optional extruder_id and Bambuddy never sent it. That is correct on every printer without the switch, and is what BambuStudio does there too -- each AMS is wired to one hotend, so the firmware works the target out for itself and an explicit value would only be a guess at something it already knows. Fit the switch and every AMS is bound to one of its two inlets instead, either hotend is reachable from any slot, and a command naming neither leaves the firmware nothing to act on. It was discarded in silence. Load now asks which hotend to feed, on the same terms as Bambu Studio: no preselection, so a stray Enter cannot feed the wrong one, and the hotend already fed from that very slot greyed out. Printers without a switch send a byte-identical command and still load in one click. A switch fitted but not yet set up -- any AMS still unassigned to an inlet -- refuses the load up front rather than publishing one the firmware will drop, mirroring DevFilaSwitch::IsReady, which likewise demands a switcher position on every AMS. Unload was addressed at the same time. It was aimed with tray_now, a single value for the whole printer, so on any dual-nozzle machine with both hotends loaded it unloaded whichever that field happened to name regardless of which slot's menu was used. It now names the slot and resolves the holding hotend from device.extruder.info, previously read for temperatures only. That resolution is gated on the printer having reported two extruders: single-nozzle machines do send the block, but nobody has read a single-nozzle snow value off the wire, and staking every X1C, P1S and A1 unload on an unverified encoding buys nothing where tray_now is already unambiguous. Both new state fields ride the WebSocket and are in the broadcast key, and both are computed in the REST status route as well -- that response is what the page has before any push arrives, and leaving them at their defaults would have told a correctly set-up machine that its switch was not set up. Verified on H2C-1, AMS-A slot 3: loaded and unloaded from each hotend in turn, all four correct. Covered by 18 MQTT unit tests, 4 status-dict tests, 7 integration tests and 6 component tests. Two known stragglers, both deliberately left alone. Load on an AMS-HT slot has never worked -- an HT unit is addressed by its unit id rather than ams*4+slot, which these endpoints do not accept -- so unload there keeps the printer-wide form it always used instead of gaining a slot it cannot name. And a slot's K-profile still follows the AMS's plumbing rather than the nozzle just loaded, so loading to the far hotend leaves the other one's calibration bound; that is the same per-nozzle problem the filament and K-profile redesign is scoped to fix. |
||
|
|
196fcaf15b |
Keep the last run a batch order can re-queue a plate from
An order produces what it still owes by cloning an existing queue item for the same plate. That row is the only record of the printer target, AMS mapping and print options the user chose, so deleting the last one left the order reporting work outstanding that nothing could produce, and no way to close it out: Cancel was hidden unless there were pending items to cancel, which by then there were none. Deleting an order's last surviving run for a plate now cancels it instead. A cancelled run does not satisfy a target, so the order still owes the print and can still make it. A completed run is exempt and still deletes outright -- rewriting it as cancelled would falsify what the order produced. Also: the response reports per plate whether anything is left to clone, so the card explains a stranded plate rather than offering a button that can only fail; dispatch skips a stranded plate instead of aborting the whole order; and Cancel is offered for any active order. |
||
|
|
a68933cb2f | Allow Avery label sheets to start at an unused position (#2879) (#2918) | ||
|
|
73912d4f05 | Let a clear spool stay clear on the way to Spoolman (issue #2912) (#2924) | ||
|
|
54af3146a3 | [Feature]: Bind Home Assistant sensors to storage locations (dryboxes/bins) (#2827) | ||
|
|
5dd7bd213f |
Read a print's destination from the report topic, not just the request one (issue #1820)
current_project_url was assigned in exactly one place, _handle_request_message, and _on_message calls that only for the request topic. A print started from the printer's own screen publishes nothing there, so the field stayed None for the one case the storage verdict exists for: the file is already in the printer's model library under /userdata/model/history/, which port 990 does not serve. The verdict then fell through to the sdcard flag, and @ojimpo's H2S reports that flag true -- its "card" is the internal eMMC -- so every such print ran the full sweep before giving up. He measured one: 16 filename-and-directory attempts over 22 FTPS connections, 18 of them refused, 6.4 seconds, then a fallback archive holding a name and nothing else. The printer does announce where the file lives, as an unsolicited project_file *response* on the report topic about two seconds before gcode_state reaches PREPARE. _process_message now reads the url off it, gated on result SUCCESS and a non-empty value so a refused dispatch cannot name a file that was never written. Reading it there rather than only at the request topic also covers an install neither of us had in view: some brokers refuse the request-topic subscription, and on those no print of any kind had ever populated the field. The new branch captures state and nothing else. The "External project_file payload" diagnostic stays with the request-topic handler: our own dispatch is echoed on both topics, the request-topic echo lands first and clears _own_project_file_key, so reusing the diagnostic here would have logged every Bambuddy-started print as somebody else's. A test pins that. What the print names is now what gets tried -- the five directories a copy could be in, rather than the ~110 connections that cannot succeed. The probe is still worth running: an H2S keeps recently used jobs under /cache and archives them in full while they last, which is why the reporter's two prints on the same day behaved differently. Slicer-sent prints are unchanged. The banner no longer describes a step that never happened. With no reason recorded, a blank archive fell back to the original wording -- "Store sent files on external storage" is off in your slicer -- which on that printer is on, and which the internal-storage wording from #2780 already explains would not help on an H2. The archive that most needed that explanation was the only one that could not be given it. So file:///userdata/ now earns its own reason, internal_history, separate from the brtc://emmc dispatch case. A dispatch chose internal storage and can be aimed elsewhere; a print of a file that was already there had no dispatch at all, and telling that operator to pick External in Send names a dialog they never opened. The banner and the connection diagnostic both read the verdict's reason rather than a fixed one, so the two surfaces cannot give the same printer different advice. Thirteen locales, and a wiki section the banner links to. ----- Read the K-profile selection when the mutation runs, not when it is captured Configure Slot sends cali_idx from selectedKProfile, and the mutation read it through its own closure. React Query hands a mutation its options from an effect, so a click landing between a commit and that effect flushing runs the previous render's mutationFn -- one that captured the selection as it was before the K-profile query resolved. The payload then carries cali_idx -1 and the printer binds the default 0.020 instead of the calibrated K, while the dialog shows the right profile selected throughout. It surfaced as an intermittent failure of the per-nozzle K-profile test, about one full-suite run in six. Reproducing it with staggered query resolution showed the divergence directly: the select element held the correct profile immediately before and after the click, and the payload still carried -1. That test's slot is the most exposed case in the file -- a right-hotend slot carrying the left hotend's index, where the "keep showing the active profile" safety net cannot repair an empty recompute. The selection now goes through a ref written during render, so the mutation resolves it at execute time. An effect would have inherited the same flush ordering this exists to escape. The K value and the profile's ids travel in the same payload and had the same exposure, so they move with it. Measured over a staggered-resolution grid: 2 failures in 15 runs before, 0 in 12 after. api.getSlicerPrinterModels was also missing from the test file's mock, so that query ran with no query function and rejected in all 37 tests -- mocked now, though on its own it changed nothing, which is how the ref was confirmed as the fix rather than assumed. |
||
|
|
06e5114a03 |
Do not report a printer as Safe when nothing is checking it (issue #2952)
The printer card's AI badge collapsed every class that was not Warning or Failure into green Safe, and the service reported `safe` whenever it had no verdict. The state entry is created when a monitored print is first seen -- before the first snapshot, let alone the first inference -- so a rejected ML API token, an unreachable ML API, a failed capture and an unset External URL all rendered as a healthy watched print: green Safe at score 0.000. For a safety feature that is the worst failure mode available: it asserts the print is being watched exactly when it is not. The reporter read that badge and concluded the loop had never started. It had been calling the ML API every ten seconds and being turned away with a 401 -- invisible because Obico's auth layer rejects a bad token before its request log sees it, and because successful checks log nothing there either. Add two honest states. Not checking (amber) when the last poll produced no result, carrying the reason; Starting while a monitored print waits for its first result. Score and frame count are withheld while not checking, since 0.000 beside "Not checking" reads as a measurement rather than its absence. The reason is per printer, so a card names its own problem rather than whichever printer failed most recently, and stays behind settings:read because it can quote configured URLs -- the badge state does not, because whether a print is watched is not configuration. An unrecognised class now falls back to Starting, not Safe. Test Connection saves the form before probing, so a green result describes the configuration the loop actually runs with rather than what is typed in the boxes. |
||
|
|
b1f5ec9642 |
Let one checkbox say where a slice's settings come from (issue #2942)
Two features in the slice dialog read as one. "Use the file's built-in settings" slices a 3MF the way its designer set it up, ignoring the picked profiles. The per-option "from file" ticks beside each setting carry the designer's individual deviations onto the profile you picked, and those arrived pre-ticked whatever the checkbox said. So a slice run deliberately without the file's settings still took sixteen values out of it -- the reporter's log names them, enable_support and support_type among them, landing on a process preset they had chosen on purpose. The ticks now follow the checkbox. Off, nothing comes out of the file until it is asked for by name; on, every setting the file changed shows ticked, because on that path the file really does drive the whole slice. Taking the designer's work in bulk is still one click, from a line at the top of the panel that says how many settings the file changed -- it is the only way left to reach them without hunting for chips across six pages of 348 options -- and it still leaves the machine-tuned keys and the two that are the picked preset for a per-key decision. The panel greys out options the slicer's own rules switch off, and it was evaluating those rules against what the user had typed alone, falling back to the compiled-in schema defaults for the rest. A preset with supports on therefore read as enable_support: false and greyed out the whole Support page while the slice ran supports. A greyed row greyed its tick too, which is how the reporter's screenshot shows a support type marked "from file", applied to the slice, and impossible to clear. The rules now see what the slice will actually run with: the preset's values, the file's values for the keys that are on, and anything typed on top. The tick is no longer gated on those rules at all -- it answers a different question, not whether an option is in play but where its value comes from. Underneath both, the support carry-over ran outside the ticks entirely, lifting four keys out of any 3MF that had supports on with nothing on screen able to decline. It now stands down for the keys that were offered and turned down, which the request can say for the first time: an empty design_overrides list means the caller was shown the file's settings and took none, where no list at all is a caller that predates the choice. That distinction is what keeps the carry whole for sources with no deviations to tick, an OrcaSlicer export among them, rather than trading one silent default for another. Worth knowing: a Bambu Studio file with supports enabled no longer switches supports on for you. Tick Enable support, or the checkbox above the panel. Measured against the reporter's own sixteen keys, and covered by backend and frontend tests -- reverting any one of the three changes fails tests. |
||
|
|
cbbdab86f9 |
Say which blue a colour mismatch is about (issue #2941)
A print was refused a colour match between two filaments the dialog itself labelled "Blue". The comparison was right: the slicer profile asked for a near-pure #0028FF and the slot held Bambu's navy #0A2989, 118 apart in the blue channel alone and a CIEDE2000 distance of 15, where 1 is a just-noticeable difference. Nothing on screen said so. A hex that misses the colour catalogue is named by a coarse family bucket, so both sides resolved to the same word, and the warning sat between two identical labels with nothing to reconcile it against. Read as a broken matcher, which is what the report said, and a fair reading of what was shown. Where both sides of a mismatch carry the same name they are now qualified by their hex, and the tooltip names them together: "Same type, different color: needs Blue (#0028FF), slot has Blue (#0A2989)". Names that already differ are left alone -- the hex is noise once the words separate them -- and a side with no name falls back to its hex rather than growing an empty bracket. The comparison is untouched. It was correct, and its tolerance is not something to widen on one report: a difference that size would start matching navy to cyan, and eligibility is the same rule the queue scheduler dispatches on, so loosening it would change which spool gets printed rather than only which warning gets shown. This issue was about being able to see why the warning fired, and a test pins that it still fires. The strings around it were hardcoded English: the panel's status line, the required-filament tooltip, the auto-matched marker, the slot placeholder, the mismatch detail and the type-not-found message. All nine now go through translation, in all thirteen locales. Two of them -- "Same type, different color" and "Filament type not loaded" -- turned out to have had translations sitting in every locale file the whole time, unused, while the component rendered the English literal beside them. The parity gate could not have caught any of this: keys that were never added have nothing to compare against. |
||
|
|
87e0a4c3b6 |
Name a spool by its subtype on the slot it is assigned to
A spool's subtype is half of what it is called: "PLA" and "PLA Wood" are
different filaments. The AMS slot's hover card built the assigned-spool
line out of brand, material and colour name and left the subtype out, so
a roll of Bambu PLA Wood Classic Birch in an H2C's A4 was announced as
"Bambu Lab PLA - Classic Birch".
Everything else named it correctly at the same moment -- the RFID read,
the inventory row, the slot's own profile line, which is built from the
spool's slicer preset rather than reassembled, and Bambu Studio -- so the
one wrong line read like a bad tag read rather than a display fault.
It was not only the render. The card's assignedSpool prop had no subtype
field at all, and the six places the printer card fills it in -- regular
AMS, AMS-HT and external spool, each in both Spoolman and internal-
inventory mode -- never passed one, so the value could not reach the
component. The field is required rather than optional, which is what
stops the next call site from quietly omitting it; that omission is the
whole of this bug.
Three more surfaces rebuilt the name the same way and are fixed with it:
the SpoolBuddy AMS slot panel in both inventory modes, and the write-tag
confirmation. Every other place a spool is named -- the assign dialogs,
the inventory cards, the forecast rows, the label picker -- already
included the subtype, so these four were the outliers.
This is the display-side half of #2902, which stopped the backend
reducing a filled or foamed filament onto its base material. The card was
doing the same thing to the same spools, one layer further out.
|
||
|
|
55cc64c87d | Add printer video downloads and range selection (#2853) | ||
|
|
4e40a5022c |
Register a Spoolman extra field with its own write (issue #2903)
Spoolman rejects a spool whose extra dict carries a key it has not been told about, answering 400 "Unknown extra field tag.". Bambuddy keeps the tray UUID in extra.tag, so that key has to exist before the first spool is created. Registration ran from three hand-maintained lists that fire when the integration is set up -- the connect route, startup, and two inline blocks in the inventory routes. Enabling Spoolman from the Settings page reaches none of them, so the first AMS sync on a fresh Spoolman failed on every slot while vendor and filament creation succeeded. Neither fix suggested on the issue is quite the right shape. Adding the block to PUT /settings/spoolman fixes this path and makes a third copy of a list that has already drifted -- it would still omit bambu_color_name. Ensuring at the sync entry point leaves the other four tag writers alone: linking and unlinking a tag, and both inventory edit paths. So the registration moved to the write. create_spool, update_spool and update_spool_full each register the keys of the extra dict they are about to send, once per client, before sending. Every tag writer funnels through one of the three, merge_spool_extra included. This closes the class rather than the instance: a write that carries a key is a write that registers it, and bambu_color_name shows why that matters -- it never made it into the connect or startup lists at all, and works today only because two call sites remembered it by hand. Best-effort, deliberately. ensure_extra_field already logs and returns False rather than raising, so a registration that fails leaves the write to be attempted and to report exactly what it reported before. Failures are not memoised either, so a Spoolman that was merely restarting gets another try on the next write. The older blocks stay. They are redundant now, but the inventory routes' inline calls are pinned by tests that assert them against a mocked client, where the funnel cannot run. The status endpoint is the other half. The Connect button would have registered the fields, and the reason nobody reaches it is that GET /spoolman/status reported "connected" whenever an earlier request had left a client object behind. Roughly twenty call sites build one lazily, and saving the Settings page builds one as a side effect of syncing locations, so the flag turned on which page had been opened rather than on anything about Spoolman. The UI reads it twice -- Connect only while disconnected, the sync section only while connected -- so those two controls landed in states the user cannot explain. It now asks the Spoolman that is configured, including the stale-URL check every other route already does, and does not probe at all when the integration is switched off, which used to let a leftover client report a disabled Spoolman as connected. That leaves nothing for Disconnect to do, so it is gone. Spoolman is a stateless HTTP API with no session to close; the button dropped the client object, the next request rebuilt it lazily, and the status flipped back on its own within the 30s poll -- an action that looked like it worked and then quietly undid itself. The enable toggle owns turning the integration off. Connect stays as what it always was in practice, a way to re-check a Spoolman that is not answering, and is shown only then. Its translation keys are left in place; only the control is removed. Resolving the client there means the status poll can now fail in ways a read-only check could not, so it no longer reports failure by failing. Replacing a client closes the previous one and httpx's aclose() is not guaranteed not to raise; a poll that runs every 30 seconds answering 500 is worse than one answering what is true either way, which is that Spoolman could not be reached. The SSRF rejection keeps its own message rather than folding into the general one -- a URL the guard refuses is the admin's to correct, and that is only actionable if the log says so. Tests drive the real client against a fake Spoolman that enforces the unknown-extra-field rule rather than mocking it away, so each one fails against the old code for the reason the reporter's install did. The concurrency test needed the fake to be async: MockTransport answers without suspending, so the first version ran each request to completion in turn and passed just as happily with the lock removed. |
||
|
|
e4aab7b44b |
Leave archived projects out of the pickers that file work (issue #2888)
The reporter opens a project per job and archives it when the job is done, so the Project dropdown in Edit Archive listed five live projects behind thirty-odd finished ones, in one unscrolled run with nothing to tell them apart. Archived is the state that means "put this away", so that is what the pickers now drop: the Edit Archive dropdown, the pending-uploads panel, the bulk Add to Project dialog and the File Manager's folder link. Completed stays. It says the work is finished, not that it should be hidden, and filing a reprint under a finished project is ordinary. The Archives right-click submenu had the opposite bug and offered active projects only, so a completed project was reachable from the edit dialog and not from the menu next to it. All five surfaces share one rule now. Whatever a thing is already filed under survives the filter whatever its status. A select holding a value that matches none of its options is reset by the browser to the first one, and here that reads "No project" -- an archive sitting in an archived project would have said in as many words that it was filed nowhere. The stored id does survive an untouched save; it is the field that lies. The parent-project picker is deliberately untouched. A finished or archived project is still a legal parent, and its own comment says so. Fixed alongside it, and reported separately: the status tabs counted only the projects the selected filter had already let through, so every tab but the current one counted zero and dropped its badge. Switching tabs moved the number rather than showing four of them. They are counted from the unfiltered list the page already fetches for the sub-project captions -- same query key, no extra request. The new rule is one function with its own unit tests, since five callers now depend on it reading the same way. Each half is pinned separately: dropping the filter, dropping the kept id, restoring active-only, and counting from the filtered list each fail their own tests and nothing else. |
||
|
|
3916db822c |
Stop the G-code preview from sizing the box that sizes it (issue #2887)
Opening a 3D Preview from Archives left an empty white pane with the legend and the layer slider drawn over it, and the page's scrollbar shrank for as long as it stayed open -- about 190px of page height a second, with no limit. The viewer appends its canvas into the very element it measures with clientWidth/clientHeight and watches with a ResizeObserver. setSize writes each new size onto the canvas as inline style, and three.js leaves the canvas display:inline, so the line box adds descender space on top of the height just set. Where that element takes its height from its contents, the canvas sizes the box that sizes the canvas and gains a fixed 33px every round -- the reporter measured container = canvas + 33 on every sample. The page gave it no height to take instead. The viewer pane is flex-1 min-h-0, which divides nothing unless the column above it is a definite height, and h-full is a percentage resolved against a main area whose own height comes from a min-height -- a floor, not a size. So it fell through to the content, and the content was the canvas. Nothing was ever drawn because of the same loop, not a second fault: every observer callback reallocated and cleared the frame buffer, and an antialiased render of what had grown to roughly 18 megapixels never finished before the next one arrived. The data path was fine throughout, which the legend and the 1..57 layer slider both prove -- they are built from the parsed toolpath. The canvas is now positioned out of flow, so it cannot contribute to the height of the element that measures it on any page, and that element takes a definite height from the pane around it rather than a percentage. display:block goes on too, for the case where something overrides the positioning. The page is sized from the viewport the way the File Manager page already was. Either change alone stops the growth, but the structural one alone would trade it for a collapsed pane: an out-of-flow canvas contributes nothing to content height, so with no definite height above it the pane becomes clientHeight || 1. They belong together. The same viewer in the File Manager dialog was never affected -- a dialog gives it a fixed height, so neither fault could arise there. jsdom does no layout, so the loop cannot be reproduced in a test. The structure that forbids it can: the new cases assert the canvas is out of flow, that the measured element is definite-height rather than h-full, and that the pane stays positioned so inset-0 resolves against it. Reverting either change fails exactly those. |
||
|
|
3901043238 |
Name an AMS slot's colour by its material, not its hex alone (issue #2875)
A hex is not one colour in Bambu's range. #FFFFFF is Jade White in PLA Basic, Ivory White in PLA Matte and plain White in six other materials; popover resolved its title from the hex alone, against a map that keeps one name per hex, so an ivory Matte spool read "Jade White" while the profile line beside it correctly read Matte Ivory. /inventory/colors/map now carries the names collapsing loses, keyed "<material>|<hex>". An entry is emitted only when it recovers a name the same manufacturer's own range lost -- 11 of them against the 608 colours in the shipped catalog. Both halves matter: a name equal to the flat answer is weight, and a name from another brand is not a recovery, it would put Prusament's "Pristine White" on every generic white PLA slot. A slot with a spool assigned from Inventory is titled with that spool's own colour name: it is the roll the user said is in there. Bambu internal codes are still rejected as non-names (#857). |
||
|
|
7e77bf5833 |
Take the layer height from the plate that actually printed
The archive card, the library file details and the slice dialog all read the layer height from a 3MF's project_settings.config. That records the project's settings and can still describe an earlier process or another plate; the plate's own G-code - what the printer executes - was never consulted for it, because the parser read the first 4KB, enough for the layer count in the header block but not for the config block that carries layer_height 14-25KB in. A print running at 0.08 on the H2C archived as 0.2 with the layer count from the same file correct beside it. The plate G-code now wins wherever the two disagree, and the plate that was printed is the one read - the header parse used to take the first gcode entry in the zip regardless of which plate the archive was for. Source 3MFs, which carry no G-code, keep the project value as before. --- Stop carrying a file's layer height over the preset you picked Bambuddy carries a designer's process deviations across a re-slice (#2622) and pre-ticked every one that was not machine-coupled. layer_height is one MakerWorld projects routinely carry, so picking "0.08mm High Quality" for a file whose designer had moved layer height to 0.2 sliced at 0.2 while the dropdown still read 0.08 - the same 0.2 the settings panel showed, tagged "from file". Layer height and first layer height are now classified preset_defining and treated like the machine-coupled keys: offered, never pre-selected. The flag travels on DesignOverride so the modal and the backend agree, and the panel's badge names the conflict and shows the preset's own value next to the file's, so ticking one is a deliberate choice. |
||
|
|
d227d42272 |
Let a print with no 3MF be given its filament weight (issue #1820)
When the sliced file stays somewhere Bambuddy cannot read, the archive is built from the printer's report alone and carries no weight. Nothing could supply one afterwards: rescan reads the figure out of the 3MF, and that archive has no file to read. The reporter's H2S print left 46.16 g on the spool with nothing recording it, and he corrected Spoolman by hand. Edit Archive now has a Filament used (g) field. It is written to the archive's most recent run as well, because the Projects roll-up and the Prometheus counter sum PrintLogEntry rather than the cards - correcting only the archive would fix the display and leave every aggregate reading the old figure, or none at all. But not over a figure the run measured for itself. A run's grams come from the tracked spool delta when there is one and only fall back to copying the archive's estimate when there is not, so mirroring unconditionally would overwrite a measurement with a typed estimate. The mirror now takes a run that has no figure, or one holding exactly what this archive held - which also makes the undo complete, since clearing the archive clears the copy it made and leaves a measured run alone. The field is text rather than a number input. A number input reports an empty string for anything the browser judges malformed, a decimal comma in a locale that does not expect one included, and that reads here as "the user cleared it" - it would have wiped a good figure while the field still showed what was typed. Filtering on the way in keeps what is displayed and what would be sent the same string, and clamps it to the range the API accepts: this modal has no error surface, so a refused save looks like nothing happened at all. Saving also invalidates the archive's runs query. The Print Log this modal renders at its top reads them separately and kept serving the pre-edit row, so a correction looked like it had not taken - true for the status and failure-reason mirrors since #1444 as well. Second half of the same report: the internal-storage probe (#2856) logged which file it found but not where. On a printer that keeps uploads for weeks - the reporter has months of them in /cache - a reprint of a name that was re-sliced but never re-sent can match an older copy, and without the directory that mismatch is invisible rather than merely rare. The download helper returns the path that served the file instead of a bare flag; every caller only ever tested it for truth. |
||
|
|
90fac7b529 |
Keep card and row actions reachable without a hover-capable pointer (issue #2865)
Tailwind v4 compiles group-hover: inside @media (hover: hover) - the shipped CSS has .group-hover\:opacity-100 sitting in exactly that block. On a touch-only device the media query never matches, so the rule that reveals the control is not merely never triggered: it is never applied. A control written as opacity-0 group-hover:opacity-100 is invisible for good. The reporter's iPhone screenshot shows the project card with nothing where the "..." belongs, and Edit and Delete live only there. So the hiding half is what has to depend on the pointer, not the revealing half. A can-hover variant carries the query - the same one the history thumbnail preview has used since it was written - and the six controls behind it become can-hover:opacity-0 group-hover:opacity-100. Without a hover-capable pointer no rule hides them and they simply render; with one, nothing changes. Every reveal selector is specificity (0,2,0) against the hider's (0,1,0), so which one wins does not depend on where they land in the stylesheet. Six controls were affected: the project card menu, the File Manager's folder actions (reachable by accident today - the "wrap names" toggle drops the hover class), duplicate preset, rename and delete tag, delete print photo, delete plate reference. Six more already tried to handle this, by viewport width under 768px on the Archives cards and the File Manager's file cards. That covers a phone and misses an iPad in landscape, which is touch-only at 1024px. They move to the capability check and useIsMobile goes with them, along with the isMobile prop threaded into FileCard. opacity-0 also leaves a button focusable while invisible, so tabbing through a card stopped on a control nobody could see. Focus now reveals them, through group-focus-within on the wrappers and focus-visible on the standalone buttons - neither is hover-gated. jsdom does not evaluate media queries, so the tests pin the class contract instead: a bare opacity-0 is the defect, because it applies unconditionally while everything that undoes it does not. The decorative hover reveals are left alone - the archive hash badge, the project name overlay, the thumbnail preview, the swatch tooltips. Nothing is unreachable there, only unseen. |
||
|
|
c5e0055864 |
Track which nozzle each AMS feeds through the Filament Track Switch
With a switch fitted, an AMS is not wired to a nozzle any more. It is plumbed into one of the switch's two inlets and reaches both nozzles through it, so every unit reports its extruder as "not fixed" (0xE) and ams_extruder_map comes back empty. Bambuddy had nothing to fall back on but the AMS unit number, so AMS-A was badged R and AMS-B was badged L purely because their unit ids are 0 and 1, a third unit got no badge at all, and every one of those labels was wrong. The binding needed no new telemetry. BambuStudio reads it out of bits 24-27 of the same AMS info string we already parse for the type and the extruder id -- 0 is In-B, 1 is In-A -- and it only means anything when a switch is installed, because without one 0xE really does mean an uninitialised unit. That gates the read, which forced the switch block to be parsed before the AMS block: _handle_ams_data runs early in _process_message and _update_state only much later, so the binding was lost on every frame that carried both. The badge letters stay L and R, In-A reading as L, with the inlet named in full in the tooltip -- the letter is the inlet's position and not a claim about which nozzle that AMS feeds, since the switch can route either inlet to either outlet. Both views update live now. The switch fields were missing from the WebSocket payload, and the frontend shallow-merges each push over its cached status, so an absent field kept whatever the last full fetch left behind. The broadcast dedup key had no term for them either, so "Join IN-B" on the printer screen moved nothing: the binding is not in the tray component of that key, and not in the AMS change-hash, which covers tray fields only and must stay that way because it drives Spoolman sync. The rest of this is the calibration half, which is where it actually bites. K-profiles are calibrated per nozzle and the printer numbers its calibration table per nozzle too, so entry 16 exists on both hotends and means a different profile on each. A tray holds exactly one index. Move an AMS to the other inlet and every configured slot in it silently keeps pointing at the old hotend's table -- measured on the maintainer's H2C, a black PLA calibrated 0.018 left and 0.020 right stayed on the left profile after the move, and a manual RFID re-read only re-asserted the same wrong one. Bambu has not solved this either; their AMS dialog carries "TODO: fila_switcher broken the connection of ams->extruder" above the line that decides which nozzle's profiles to offer. Three copies of "which extruder is this slot on" each ended in else 0, which on a switch machine filed every profile under the right-hand nozzle. They now share one resolver that returns None for "unknown", because unknown and extruder 0 are very different answers on a dual-nozzle machine and conflating them is what bound a left-nozzle profile to a slot sitting on the right. The per-slot K value on the printer card had the same confusion from the other direction: its lookup was keyed on cali_idx alone, so one nozzle's K silently overwrote the other's. Moving an AMS now re-selects each configured slot's counterpart profile for the nozzle it has arrived on. Only the calibration binding moves, and only for slots whose spool already has a profile for that nozzle: configuring a slot is a deliberate preparation step, so a slot we know nothing about, or a spool calibrated on one hotend only, is left exactly as the operator set it. Nothing fires on the first sighting of a binding either, since every reconnect learns them afresh and re-applying there would overwrite a choice made by hand. Configure Slot resolves against the slot's own nozzle throughout. Option identity carries the extruder, so a filament calibrated on both hotends gives two distinguishable entries instead of two that collapse into whichever the printer listed first; options name the hotend; matches are scoped to the nozzle the slot feeds, with the other hotend's profiles still reachable under Other K profiles; and the slot's active index is resolved as a pair rather than followed into the wrong table. Inlet to nozzle is one table, In-A to the left hotend and In-B to the right, measured rather than assumed -- fila_switch.out cannot be used for it, reporting [1, 1] unchanged across a 90-second capture, both outlets claiming the same extruder. The print dialog picks up the same inlet labelling in its slot dropdown, replacing a left/right hint that never rendered because it matched snow-encoded values against global tray ids; decoding it correctly would not have saved it, since the firmware never reports which inlet is currently paired with which outlet. The dialog also notes when every filament a print needs sits behind one inlet, which is legal but slow -- a change between two filaments on the same inlet retracts the outgoing spool all the way back to its AMS, where a change across the two only retracts as far as the switch. Assigning an AMS to an inlet remains printer-side. BambuStudio can read that binding and has no command to write it, so there is no wire format to copy. |
||
|
|
7a42e0a7e5 |
Show which Filament Track Switch inlet each AMS feeds
With a switch fitted, an AMS is not wired to a nozzle any more. It is plumbed into one of the switch's two inlets and reaches both nozzles through it, so every unit reports its extruder as "not fixed" (0xE) and ams_extruder_map comes back empty on these machines. The printer card had nothing to fall back on but the AMS unit number, so AMS-A was badged R and AMS-B was badged L purely because their unit ids are 0 and 1, a third unit got no badge at all, and every one of those labels was wrong. The SpoolBuddy assign modal had the same fallback in a worse form, mapping anything that was not extruder 1 to R. The binding turned out to need no new telemetry. BambuStudio reads it out of bits 24-27 of the same AMS info string we already parse for the AMS type and the extruder id -- 0 is In-B, 1 is In-A -- and it is only meaningful when a switch is installed, because without one 0xE really does mean an uninitialised unit and those bits carry nothing. That gates the read, which in turn forced the switch block to be parsed before the AMS block: _handle_ams_data runs early in _process_message and _update_state only much later, so the binding was lost on every frame that carried both. _parse_fila_switch is split out and called first, and left in _update_state as well so that stays a complete absorb step. The badge keeps L and R rather than A and B, because the lettering is familiar and matches the physical layout. It is a different colour from the plain nozzle badge, and its tooltip names the inlet in full, since the letter is the inlet's position and not a claim about which nozzle that AMS feeds -- the switch can route either inlet to either outlet. An AMS still reporting a real extruder id keeps its ordinary badge, which BambuStudio also treats as authoritative over any switch binding, and a switch that has been fitted but not yet set up on the printer shows nothing rather than a guess. The print dialog's slot dropdown gets the same label. It replaces a left/right hint that never once rendered: ftsExtruderForSlot compared snow-encoded in[] values against global tray ids and could not match. Decoding it correctly would not have saved it -- the firmware reports which slot sits in each inlet and which nozzle each outlet feeds, but never which inlet is currently paired with which outlet, so no per-slot nozzle can be derived. That function is gone rather than fixed. The dialog also points out when every filament a print needs sits behind one inlet. Bambu's own guidance is that this is legal but slow: a change between two filaments on the same inlet retracts the outgoing spool all the way back to its AMS before the next can be fed up the shared tube, where a change across the two inlets only retracts as far as the switch. All on one inlet means every change in the job takes the slow path, and moving a single spool fixes it. So it advises, it does not block. Both views update live. Two things were stopping that. fila_switch and ams_switch_inlet were absent from printer_state_to_dict, and the frontend shallow-merges each WebSocket push over its cached status, so a field the push omits keeps whatever the last full fetch left behind. And the broadcast dedup key had no term for either, so "Join IN-B" on the printer screen moved nothing: the binding is not in the tray component of that key, and it is not in the AMS change-hash either, which covers tray fields only and must stay that way because it drives Spoolman sync. Assigning an AMS to an inlet remains printer-side. BambuStudio can read the binding and has no command to write it -- its switch class is parse and getters only, and the recommended-arrangement popup draws and publishes nothing -- so there is no wire format for us to copy. Adding the two fields to PrinterState broke four test modules whose SimpleNamespace stubs predate them. The stubs are fixed rather than the production reads made defensive: the real dataclass always carries both, and a getattr in the dedup key would silently stop tracking the field if it were ever renamed. |
||
|
|
47a37618a0 |
Let a busy or offline printer take a dropped file (#2849)
Dragging a sliced file onto a printer card refused the drop unless the
printer was connected and neither RUNNING nor PAUSE. The overlay went red
with "Printer busy", handleCardDrop returned early, and the file was
discarded with no toast and nothing uploaded. The card's Print button was
hidden by the same condition, so both routes into "Print from Printer
Card" closed at once and the way through was the File Manager, uploading
and queueing by hand.
The gate never described a real constraint. Every print Bambuddy sends
becomes a queue item; dropping onto an idle printer only looks instant
because the scheduler dispatches it on the next pass. Busy is a timing
difference, not a different path. The modal has always passed
disableBusy={false} to PrinterSelector, and asapToastShouldPromiseLaterStart
exists precisely to say "this will start later" when the target cannot
take it now. cleanup_library_after_dispatch is a print_queue column
consumed at dispatch, not on close, so the transient upload survives
however long the item waits.
Offline is included for the same reason: the queue dispatches when the
printer comes back, so a machine that is powered down can be given work.
The overlay now says which one is happening -- "Drop to print" when the
job would start immediately, "Drop to queue" when it would wait, covering
a print in progress, a paused job, an AMS mid-cycle, a plate not yet
cleared, and a printer that is offline. The predicate behind that wording
is the one the modal already used for its own later-start notice, lifted
out of PrintModal into utils/printer as isPrinterCurrentlyDispatchable so
the card cannot promise something the modal contradicts a second later.
The drop is also gated on the permissions the flow actually exercises. It
uploads to the library and creates a queue item, so library:upload and
queue:create -- the pair the Print button beside it has always checked.
printers:control, which it checked before and never uses, meant someone
holding that alone got the file uploaded and then rejected by the queue,
leaving a library row behind with nothing pointing at it. The refusal now
names whichever of the two is missing instead of claiming the printer is
busy.
printers.cannotPrint is dropped in favour of printers.dropToQueue across
all 13 locales; its text was both unused and, after this, wrong.
The Print button stays inside the expanded-card block, so S-size cards
still show the drop zone and no button, exactly as before.
|
||
|
|
713a85d114 |
Let a bug-report capture outlive the panel that started it (#2847)
Step 2 asks the user to reproduce the problem, and the panel sits over the part of the app they have to reach to do it. Closing it was the obvious move and it was wrong in two different ways, picked by timing alone. Reopen inside five minutes and the reset-on-open effect put you on an empty step 1 while the server stayed at DEBUG, with nothing left in the flow that could stop it -- only Stop & Submit ever did. Leave it closed and the cap fired behind you: the panel is hidden but mounted, so the timer kept running, stopped logging and filed the report with no window open and no confirmation. A capture is now written down -- description, email, was_debug and a start timestamp -- and the reset skips a run in progress, so the panel reopens on the step it left. The disc turns amber while a capture is going and Layout marks the compact header's button and offers Resume report on the debug-logging banner, since a run started there ends at that panel's button and not at the System page's raw toggle. If the cap fires while the panel is closed, it opens first, so the submission happens in front of the user. Elapsed is measured against the start time rather than counted in ticks, which a background tab throttles hard enough that five minutes was not five minutes. That timestamp also lets a run survive a reload, which matters because reloading is an ordinary step in reproducing a bug: on mount a stored run is reconciled against /support/debug-logging and resumed. One that outlived the cap unattended is not resumed and not filed -- an hour-old description is not a report anyone still expects -- but its log level is put back, which is what stayed wrong indefinitely before. The screenshot is deliberately not persisted: a 1920px JPEG runs to hundreds of kilobytes against an origin-wide budget, and it survives a close either way. |
||
|
|
3954d3a7e6 |
Choose which rack nozzle each filament prints from on an H2C (#1784)
The Vortek rack holds six hotends, and a multi-colour plate is sliced to
use a different one per colour so it can skip the purge. Which of the six
each colour takes is not in the 3MF. The same plate, sliced and sent twice
from Bambu Studio with a different choice each time, produces two files
that differ only in rounding in the last digit of a few extrusion figures
-- the filament grouping, the toolchange stream, the 120 nozzle-change
markers and project_settings.config are all identical. The choice travels
only in the dispatched nozzle_mapping.
Bambuddy had no way to state it, so those plates went out with no nozzle
assignment at all and the printer chose for itself. That is what levelled
on one hotend and printed with another, millimetres above the plate.
Every rack-bound filament now carries a position picker beside its AMS
slot dropdown, listing all six with the nozzle each holds. An empty
position, or one holding the wrong diameter or flow type, is shown greyed
out with the reason rather than hidden, so someone looking for position 4
finds it. The choice is per filament *group* rather than per slot, because
a group is one hotend: two filaments the slicer grouped together share it
and cannot point at different positions.
Nothing has to be picked. Positions are assigned automatically, preferring
one already loaded with that colour, which on the plate this was built
against reproduces Bambu Studio's own pick exactly.
A nozzle currently picked up onto the carriage is offered too. The
firmware drops its rack position from the report entirely rather than
sending a placeholder (#943), and refusing it would rule out the position
most likely to be wanted -- the one the last print left mounted. Only
recoverable when exactly one position is missing; two gaps are genuinely
ambiguous and stay unavailable.
Positions are re-checked at dispatch, not just when queued, because the
rack can be re-loaded in between. The two failure modes differ on purpose:
an explicitly chosen position that no longer fits stops the print, names
what the position now holds, and deletes the uploaded file from the SD
card so it cannot be started by hand either -- an operator who named a
hotend must not silently get a different one. An automatic assignment that
cannot be made instead falls back to letting the firmware choose, which is
what happened before any of this existed.
The pick is stored as {group: position} rather than as the expanded
nozzle_mapping, though that is what goes on the wire. That column means
"Bambu Studio decided, forward verbatim", and only the group-and-position
form can be re-checked against what is actually mounted at dispatch.
The existing multi-rack refusal in extract_nozzle_mapping_from_3mf stays.
It still guards the #2800 fallback, which can only ever name one rack id.
Measured on the maintainer's H2C: rack position n is physical nozzle id
15 + n, confirmed by cross-referencing two captured dispatches against
Bambu Studio's own dialog. extruder_max_nozzle_count names which carriage
is the rack straight from the file, and is read rather than assumed -- a
fourth independent confirmation of the carriage indices fixed in
|
||
|
|
e6842e1d3c |
Rank near-colour matches by how they look, and share one filament type table (#2804)
Three follow-ups to #2804, all bearing on one decision: which spool a print uses when the exact colour is not loaded. Colour ranking is now perceptual. The ranking added in #2804 measured RGB distance, which rates a colour by how far apart the numbers are rather than how far apart they look, and it overweights blue badly enough to invert the answer: against a required #1E4821 green, a purple #38202F is the nearer of two eligible spools by RGB and four times the further once measured properly. Both sides now use CIEDE2000 -- perceptual_color_distance in backend/app/utils/color_utils.py and colorDistance in amsHelpers.ts, kept structurally identical so they can be read side by side. Verified against the Sharma/Wu/Dalal published reference set, all 31 pairs to 1e-4, and the two implementations agree to within 1e-9 across 800 sampled pairs. Eligibility is untouched, still the per-channel RGB box, so this only reorders spools that already qualified. Type matching now agrees between the interface and the scheduler. Bambu firmware treats PA-CF, PA12-CF and PAHT-CF as one material and the scheduler has always matched them accordingly, but the interface compared raw type strings and called that same pairing a mismatch. The badge contradicted what the printer was about to do, and the manual override picker, which groups by canonical type, offered the very spool the badge then rejected. The fifteen comparison sites in useFilamentMapping.ts, useMultiPrinterFilamentMapping.ts and PrinterSelector.tsx now call filamentTypesCompatible. The pipeline pre-flight reads the matcher's table instead of its own copy. That copy had drifted into disagreeing in both directions: it aliased PLA Basic to PLA where the matcher never has, so a run could clear the check and then fail to map its slots, and it lacked the nylon grouping, so it flagged runs the matcher handles without complaint. A check whose job is to predict dispatch is wrong whenever it disagrees with dispatch, whichever way it leans, so it and the scheduler now both read backend/app/utils/filament_types.py. That canonicaliser deliberately does not strip surrounding whitespace. It looks like a free improvement, but it would collapse a junk tray_type to "" just as a 3MF declaring no filament type yields "", and a typeless requirement would start matching a junk-typed tray instead of reporting the slot unmapped. Padded type strings are worth handling on their own terms, with that case addressed. One behaviour change outside the ranking: the pre-flight is stricter for a printer reporting a product name such as "PLA Basic" where the generic material belongs, which it now flags rather than passes. Rare in practice, since the printer reports material and product name in separate fields, and it is the answer the matcher would give. Nothing about which spool a print actually uses changed outside the colour ranking itself. Adds 203 backend and 6 frontend tests. The #2804 tie-break test now uses identical colours: two colours at equal RGB distance are not perceptually tied, which is rather the point. |
||
|
|
4a65abe228 |
Tell the browser which colour scheme the page is in
The parts of a form control the browser draws itself -- a number input's stepper, a date field's calendar button and popup, a select's dropdown, scrollbars, the autofill tint -- were painted in the light appearance on every theme. Bambuddy switches theme by swapping CSS variables under a `dark` class, which the browser cannot see, so it assumed the page was light and matched the steppers to a white background that was not there. Declaring color-scheme alongside the variables fixes all of them at once, in both directions. It goes on `.dark` rather than the per-palette classes because the kiosk sets `dark` on the root element directly. Three date and time fields had been pinned to dark by hand to work around this and no longer need to be; being pinned, they were wrong under the light theme anyway. |