mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 11:25:48 +02:00
54af3146a366fd10fb4b8c9e8280fb1ae1dbcd27
794
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
df5aa04df1 |
Pool AMS backup spools in the print dialog's filament check
The dialog weighed each plate against the spool in the slot it mapped to and knew nothing about AMS Filament Backup, so a two-plate job needing 1441 g of ABS was refused against a 1000 g spool while the identical full spool in the next slot went uncounted. The dispatcher has pooled matching spools since #1762 and would have run the print -- "Print anyway" was always the right answer to this warning. The rule for which spools back each other up now lives in one place: build_slot_materials() in filament_deficit, which the dispatcher's pool and the new slot_materials half of GET /printers/{id}/inventory-remain both draw on. The dialog groups on the keys it is handed rather than resolving spools a second time, which is what let the two answers drift apart, and which also gives the check to Spoolman users -- it read the internal inventory only, so in Spoolman mode it approved everything. Where a pool really is short the warning quotes the pooled totals, since the per-slot figure reads as a contradiction next to a full peer spool. |
||
|
|
e93f1b43c3 |
chore(settings): drop the Slicer Bundles notice and rebalance the columns
Bundle import was withdrawn in 0.2.5 and the panel was kept behind as a static notice pointing at the alternatives. It has been on screen for several releases, it was shown to everyone running the slicer sidecar whether or not they had ever imported a bundle, and it was a card in Settings -> Queue & Dispatch that could not be acted on. Component, render site and all thirteen locales' strings are gone; no slicing behaviour is touched. Four docstrings still described the removed feature as a live fallback: SliceModal was said to fall back to "the user's uploaded Slicer Bundles" when a preset carries no compatible_printers. There is no bundle model and no bundle endpoint left -- the actual fallback is the @BBL <code> printer-model registry, which is what SliceModal has been doing since Removing the card left the left column of that tab noticeably longer, so G-code Injection moves to the foot of the right column. The card is unchanged and keeps its card-gcode anchor, so settings search still jumps to it. |
||
|
|
b63adb8151 |
fix(virtual-printer): wrap the card header instead of overflowing it (#2808)
Every item in the collapsed header was flex-shrink-0, so the row was as wide as its contents and the Card doesn't clip -- the remote-interface IP and the enable toggle painted outside the card border. The name's `truncate` couldn't save it: a flex item defaults to min-width:auto, so it never shrank below its text (`flex-shrink-0 truncate` on the target name was self-cancelling for the same reason). Move the metadata into a flex-1 min-w-0 flex-wrap group so it wraps to a second line, and keep the chevron, dot and toggle outside it. Wrapping rather than truncating: the bind and remote-interface addresses are what the page exists to show. Needs three things at once, hence the report -- both IPs set (Bambuddy and printer on different subnets), a target named "Printer at <ip>" from discovery, and the 3-column card grid. overflow-hidden is scoped to this card, not added to Card: half the cards in the app render menus that deliberately paint outside their bounds. |
||
|
|
57190b10ba |
Stop offering server-side slicing for STEP files
The Slice action appeared on .step / .stp and the endpoint accepted the job, but neither slicer can load one from its command line -- both answer "Unknown file format. Input file must have .stl, .obj, .amf(.xml) extension." So the file was read, converted and uploaded before failing as "The input model file to the slicer can not be parsed", which reads as a corrupt model rather than an unsupported format. The endpoint refuses a STEP up front with a message saying to export it as STL or 3MF, and the Slice and pipeline buttons no longer appear on one. Open in Slicer is unchanged and still hands STEP to the desktop application, which opens it fine -- that was always the working path. isSliceableFilename (desktop) and isApiSliceableFilename (sidecar) are now separate predicates so the two cannot drift back together. |
||
|
|
08df660f6c |
Replace the embedded G-code viewer with the slicer's own renderer
Sliced files previewed through a vendored copy of PrettyGCode in an iframe. It drew each move as a screen-space line -- a line has no thickness in the scene, so it cannot occlude the layer behind it, which is why prints came out stringy and shimmered where layers crossed. Being a separate app in a frame, it could be neither themed nor translated, and carried its own machinery for detecting a proxy refusing the embed. Now built on libvgcode, the renderer OrcaSlicer draws its own preview with, vendored from three-slicer (AGPL, same as us). It takes the THREE namespace as an argument and imports nothing, so it runs on our 0.181 rather than the 0.160 its package pins. The parser is ours; upstream renders its own kernel's output and ships no G-code parser at all. Two things it has to get right, both found by checking a real plate rather than assuming: - BambuStudio does not use the OrcaSlicer/PrusaSlicer annotations. It writes "; FEATURE:", "; LINE_WIDTH:", "; CHANGE_LAYER" and "; Z_HEIGHT:", not ";TYPE:", ";WIDTH:" and ";LAYER_CHANGE". Reading only the latter showed a 52-layer print as 23,165 layers in one colour, because with no layer marker recognised every travel Z-hop split a layer and every segment took the fallback feature. - It emits a tenth of its moves as G2/G3 arcs -- 706 extruding ones in a single plate. Ignoring them punched holes through curved walls and tree supports. Arcs with no X/Y are the helical travel lift and lay down nothing, so they interpolate as travels. Four colour modes: filament (default, from the AMS slots the file was sliced with), feature, layer height, line width. Speed, fan and temperature are deliberately absent -- upstream derives those from settings rather than the toolpath, and guesses dressed as measurements are worse than an honest omission. The parser now carries the data to do them properly later. Legend entries are switches. Hiding removes the records before the mesh is built rather than recolouring them: the shader packs colour into a single float with no alpha, so there is no transparent to set, and removal is the useful behaviour anyway -- a hidden support stops occluding what it covered. The scene is built once and only the toolpath rebuilds. Doing otherwise constructed a new WebGLRenderer on every render, because the buildVolume default is an object literal and so a fresh identity each time; browsers cap live WebGL contexts and drop the oldest, which blanked the canvas after a few interactions. utils/framing.ts goes with the iframe, along with six now-orphaned strings in all 13 locales. src/lib/vendor is excluded from eslint -- acting on findings in vendored code makes it impossible to re-copy on the next upstream release. |
||
|
|
2d584e02a4 |
Render the previews properly instead of sketching them
Model preview ------------- The camera distance came from `maxDim * 1.8`, which accounts for neither the camera's field of view nor the viewport's aspect ratio, so a tall narrow panel was framed as though it were square -- the model sat in the middle with a screenful of dead space above it. Solved from the bounding sphere against both fields of view instead, so it fills the frame at any panel shape. Near/far now scale to the subject rather than staying at the 0.1/10000 defaults. Lighting was two directional lamps over 0.6 flat ambient on a Phong material: every surface facing the same way got an identical colour, which is what flattened models into silhouettes. Now a MeshStandard material lit by a generated RoomEnvironment through PMREMGenerator, with ACES tone mapping so the lit side of a saturated filament colour doesn't clip to white and drain the hue. Added a contact shadow. Two things would have made it silently draw nothing: the build plate is an unlit MeshBasicMaterial and cannot receive shadows, so the catcher is a separate ShadowMaterial plane; and three's default directional shadow camera is a +/-5 unit box, which nothing on a 256mm bed falls inside. The PMREM render target is disposed on unmount -- it is GPU memory the collector cannot reclaim, and this viewer is opened and closed repeatedly from the file manager. Device pixel ratio is capped at 2; a 3x phone screen was quadrupling fragment load for no visible gain. G-code preview -------------- Switched gcode-preview from `lineWidth: 2` to `renderTubes`. A 2px screen-space line has no thickness in the scene, so it cannot occlude the layer behind it -- hence the stringy surface and the shimmer where layers overlap. Tubes are built from real extrusion width and height, so the print occludes itself. The flag is marked experimental upstream, and the 0.42 extrusion width is a hardcoded default that is right for a 0.4 nozzle and wrong for a 0.6. Both are worth revisiting if this holds up in use. Modal ----- Removed the G-code tab. G-code has its own full-page viewer, and a preview of a model is a different question from a preview of a print. That left dead weight behind it: the render branch, the GcodeViewer import, the has_gcode capability (still computed, never read), the Code2 icon, and two orphaned strings in all 13 locales. One test was repurposed to assert the tab is absent so it cannot creep back; two others only exercised that tab's disabled state and went with it. |
||
|
|
b6027b7138 |
Say why a preset's values are unavailable, not just that they are
The settings panel collapsed four causes into one message -- "the picked preset's own values could not be read" -- with no indication of what to do about it. The overwhelmingly common cause has an obvious fix, and it isn't an edge case: an install pulls its sidecar as SIDECAR_TAG:-latest regardless of which Bambuddy channel it is on, so a current Bambuddy talking to a sidecar that predates POST /profiles/resolve is the normal state, not a misconfiguration. Those users would have seen an amber warning on every slice with nothing pointing at the sidecar image. resolve_profile now returns ResolvedProfile(values, reason) instead of None for everything, the route passes the reason through, and the panel picks its message from it: sidecar_outdated -> name the fix: update the sidecar image sidecar_unavailable -> the sidecar did not answer not_configured -> no sidecar is configured preset_unresolved -> the previous generic wording A request that fails outright maps to sidecar_unavailable, since a backend we cannot reach and a sidecar that will not answer are the same thing from the dialog. Every variant still ends with "anything you don't change still uses the preset" -- that reassurance is the point of the notice, and it is true whichever way the lookup failed. Tests pin the distinction rather than just the happy path: a 404 and a 500 must produce different reasons, and each panel case asserts both that its own message appears and that the "update the sidecar image" line does not leak into the others. |
||
|
|
f421bb8160 |
Show the picked preset's real values in the process-settings panel
The panel baselined every field on the option schema's compiled-in
defaults, so a preset setting a 0.42mm line width displayed 0 -- the C++
default meaning "derive from the nozzle". Every field was affected; the
Line width group just made it obvious.
Bambuddy cannot answer this itself. A standard-tier pick is only an
{inherits: ...} stub on our side, and local/cloud presets are deltas whose
remainder lives in the profile tree bundled inside the running sidecar.
The values now come from the sidecar's POST /profiles/resolve, which runs
the same resolver /slice does against the same profiles, so what the panel
shows cannot disagree with what a slice produces. Deliberately not the
local orca_profiles resolver: it walks OrcaSlicer's published tree, which
can differ from the image actually installed.
An untouched field shows the preset's value and reverting returns to it.
isModified compares against that baseline too, so fields the preset moved
off the C++ default are no longer flagged as user edits, and values nobody
typed are no longer sent. When the values can't be read -- sidecar offline
or older than the endpoint -- the panel falls back to schema defaults and
says so rather than presenting them as the preset's.
Row layout, from screenshots:
- The control column is anchored to the right edge at a fixed width. It
had been packed left after a fixed label column, leaving the values
stranded mid-container with dead space beside them.
- Units are no longer truncated to "mm o...". The cap fitted the common
"mm" but not "mm or %" or "mm/s² or %".
- The "from file" tick moved ahead of the control it qualifies; it used to
sit past the unit at the row's right edge, reading as unrelated.
Both the unit and the control keep fixed widths, and the tick's slot is
reserved on rows without one -- sizing any of them to content makes each
row's input land at a different x and the column comes out ragged.
Also fixes a field that could not be cleared: emptying a free-text input
dropped the key, so it snapped back to the baseline and retyping appended
to it ("0.42" + "0.5" = "0.420.5"). The number branch was fixed earlier;
the text branch -- coFloatOrPercent, coString, the vector types -- was
not, and the regression test used a number input so it never caught it.
Requires a sidecar built from orca-slicer-api 4b664b7 or later. Older
images 404 the endpoint, which is handled as the fallback above.
|
||
|
|
c384911f7c | Sync | ||
|
|
2368445378 | Changed layout | ||
|
|
f0500578bd |
Edit the full print-parameter set from the slice dialog
Slicing from Bambuddy meant taking a process preset as-is; any change meant a round trip through Bambu Studio. The slice dialog now carries OrcaSlicer's full process tree -- pages, groups, labels, tooltips, ranges and defaults extracted from the slicer's own sources. Enable/disable rules are evaluated from the slicer's own enable_if expressions via a recursive-descent interpreter (no eval, CSP), with enum comparisons validated against each option's declared values. Anything undecidable leaves the field editable rather than greyed. Overrides apply after the source's support config (#1881) and the designer's carried tweaks (#2622), so an explicit choice always wins; an untouched panel sends the same request as before. Adds slice_engine as a separate setting from preferred_slicer -- where slicing runs is a different axis from which binary the sidecar drives. Only the sidecar engine is registered, so no picker renders yet. |
||
|
|
328bac450a |
Stop auto-drying re-arming into a threshold it can never reach (#2770)
An H2D armed five 12-hour drying cycles inside four hours, one of them six seconds after the previous one ended, and none ran more than a couple of hours. Two things combine. The firmware ends a cycle when it decides the filament is dry rather than when the clock runs out, and reports no fault doing it -- across this printer's history the run length tracks how wet the spools were, from nearly the full 12 hours starting at 32% down to minutes once the unit sat at 10-13%. That part is the AMS doing its job. The loop is ours. An AMS reports higher relative humidity while it is warm than once it has cooled: the same unit read 10-13% cold and 15-20% through every cycle. With the threshold at 14% the reading at the moment a cycle ended was always still above it, so the next 30-second pass armed another 12-hour cycle. Nothing counted, nothing waited, and it only stopped when the box finally cooled enough to read 13%. Auto-drying now waits 30 minutes after a cycle ends before arming another on the same unit, and gives up on a unit after two consecutive cycles that bring the reading no lower -- logging why and sending a new notification, on by default because it reports that Bambuddy has stopped acting. Progress is judged against the lowest reading any cycle on that unit has ended at, not against the threshold, so a genuinely wet spool in a humid room coming down 40-37-35 keeps drying however far it still is from the target; comparing against the best so far rather than the previous end stops a sensor wobbling by one point reading as progress every other cycle. The suspension lifts by itself once the reading falls below the threshold. Neither guard can stop a running cycle, and a cycle Bambuddy cut short for a print, or that the user stopped by hand, is not counted against the unit -- so a farm that dries between queue jobs is unaffected. The threshold field now warns below 20%, and every cycle end logs the unit's temperature and humidity, which is what made this diagnosable. The same bundle showed unrelated tasks failing with "database is locked", each inside a 30.000-second Discord connect timeout. Alarms are raised from inside the loop that records sensor history, at a point where the new rows are added but not committed; the first read in the notification path flushed them to satisfy itself, opening a write transaction, and the provider was then contacted over the network with that transaction still open. SQLite allows one writer and 30 seconds outlives the 15-second busy timeout, so every other write in that window failed. The two reads that run before a provider is contacted no longer flush the caller's pending work, and the connect timeout is 5 seconds rather than 30 -- the body keeps the full 30, so image uploads on a slow uplink are unaffected. SQLite only; Postgres has no single-writer limit. |
||
|
|
04009a5c6a | Post work PR #1448 | ||
|
|
406cf71149 | Merge branch 'dev' into feature/billing | ||
|
|
82d6d98266 |
Restore the pointer cursor on interactive controls (#2791)
Hovering most of Bambuddy gave an arrow rather than a hand. Not everywhere,
which is what made it read as sloppiness rather than a bug: the update pill was
inert while the buttons beside it were fine, a bed or nozzle tile responded but
the history-graph button in its corner did not, and dropdowns went either way
with no pattern behind it.
The pattern was there. Tailwind v3's Preflight set `button { cursor: pointer }`.
v4 dropped it to match the browser default, which for a button is `default`.
Bambuddy has been on v4 since the frontend was built, and `src/index.css` never
had a base layer restoring it, so a button only looked clickable where someone
had written `cursor-pointer` by hand. 15 of 934 had. 0 of 149 selects, and 19
of 130 checkbox/radio inputs. The 233 ad-hoc `cursor-pointer` usages are why it
looked arbitrary instead of uniformly broken.
One `@layer base` rule now covers button, select, checkbox, radio, summary and
[role=button]. base sits below utilities, so `cursor-not-allowed` and the
`disabled:cursor-*` variants still win; the `:not(:disabled)` guard catches the
disabled controls that carry no such utility. Verified against the built bundle
rather than the source -- the rule lands inside @layer base, and
`.cursor-not-allowed` is emitted after it.
Click-outside backdrops are deliberately excluded. 90 of the 96 remaining
onClick divs are `fixed inset-0` overlays; a full-screen sheet advertising
itself as a button is worse than one that says nothing. Of the rest, 50 are
stopPropagation wrappers and 3 are the temperature tiles, which already set the
cursor through `statusControlClass` -- which is exactly why those tiles worked
while the button nested inside them did not. That left two real ones: Card, now
conditional on an onClick actually being passed, and the queue card, whose
existing `sm:cursor-default` kept the desktop intent.
Separately, from the same report. FilamentHoverCard draws the slot menu twice,
and the two paths had drifted into opposite orders: Configure above Assign Spool
on an empty slot, the reverse on a filled one, so the menu reshuffled itself
depending on whether the slot held filament. Both now lead with the spool
action. Tests assert the order on each path, so one can no longer move without
the other -- checked by reinstating the old order and confirming the empty-slot
test fails.
Those buttons also used justify-center, which centred each label independently
and left the icons in a ragged column; they are justify-start now. Their hover
was a 10% opacity step that was very hard to see, now 20%. And the favourites
star previews yellow on hover, suppressed when the user lacks archives:update.
|
||
|
|
a9e23910fc | Merge branch 'dev' into feature/billing | ||
|
|
dd1d40b0d4 |
implemented pr (worth fixing) feedback
update commit |
||
|
|
306b9ba7fd |
Accept Forgejo tokens scoped to a single repository (#2775)
ForgejoBackend.test_connection asked GET /user who the token belonged to
before asking whether the token could reach the repository, and treated a 403
there as fatal. A Forgejo v15 repository-scoped token may only carry
read/write on issues and repositories, so it 403s on /user -- and was rejected
despite reaching its own repository fine, which is all a backup needs: the push
path uses the Contents API and restore reads commits, trees and blobs, all
under /repos/{owner}/{repo}. That /user call was the only one in the whole
provider layer.
The probe stays, because a 401 from it is genuinely conclusive and names a bad
token before the repo call has to guess -- Forgejo v15+ hides a private repo
behind 404 rather than 403, so the repo call cannot always tell those apart.
Every other status now falls through to the repo check.
Two additions keep the messages as sharp as before: the repo call's own 401 is
mapped to "Invalid access token" instead of a generic API error, and the 404
names write:repository and the scoped-to-another-repository case, mentioning a
possibly-invalid token only when /user did not confirm the identity.
The token hint under the field was one shared string reading "fine-grained
token with Contents read/write" -- GitHub's advice, shown to Gitea, Forgejo and
GitLab users too. It is now per provider via PROVIDER_TOKEN_HINT_I18N_KEY,
following the existing repo-URL placeholder map, translated in all 13 locales.
Tests pin the repository-scoped token connecting, a transient /user status not
blocking the repo call, both 404 wordings, and the repo-call 401; a frontend
test switches providers and asserts the hint follows.
|
||
|
|
b5163b94f8 |
fix(backup): report the categories a failed restore already committed (#2656)
The service reports what landed on a part-way failure -- categories commit as they finish, so results names the ones on disk -- and the modal gated the whole result panel on success, so it showed the failure message and dropped them. The cache invalidation was inside that same branch, which is the half that mattered: a run that committed the settings category and then failed left the app rendering pre-restore settings, with no reload and no re-read, which is the failure the modal's own reload-on-close exists to prevent. Gate on what was written instead. A refusal that never reached a category still carries an empty results and still keeps the form, so the mutex and backup-in-flight cases are unchanged. A partial does not read as a success: the tick becomes a warning and a line says the listed categories are the ones on disk. --- fix(backup): keep the local owner when the backup names one we cannot resolve (#2656) An owner the backup names but this instance has no user for was written as NULL, and overwrite is a blanket setattr -- so restoring over a local archive that had a perfectly good owner took it away, which is the 404-for-its-own- owner failure this column is carried across to fix. Resolving by username widened the trigger from a stale id to any user renamed since the backup. It is the same state as an absent key: the backup has not told us who owns this. So it takes the same action -- the column is not written at all. Overwrite keeps the local owner, insert lands ownerless with the note, and an explicit null still writes, so overwrite still means "match the backup". The notes move to the insert path with it. On overwrite nothing was taken away, so there is nothing to warn about, which is the rule the absent-key case already follows. |
||
|
|
bb25e36510 | Merge branch 'dev' into feature/2656-restore-from-github | ||
|
|
43bf854bd5 | Merge remote-tracking branch 'upstream/dev' into feature/billing | ||
|
|
cd004df817 |
Show Home Assistant sensors on the printer card (#1148, #448)
Binds binary_sensor and reading-carrying sensor entities to a printer and renders their state on its card, worded by Home Assistant's device_class. Optional per-sensor alert condition drives a notification on the transition into the alert state and an opt-in interlock that holds queued prints while alerting -- a hold with a readable waiting_reason, never a failure, and only ever on a sensor that was read successfully. Sensors get their own table rather than a wider entity pattern on SmartPlug: get_smart_plug_by_printer would otherwise hand the card's power button a door contact to switch. The hold is passed to the model matcher directly rather than merged into busy_printers: _check_auto_drying reads that set as "is currently printing" and would put an idle-but-held printer down the mid-print drying path. The notification_providers migration spells its default FALSE, not 0 -- Postgres rejects an integer default for a boolean and _safe_execute swallows the error. |
||
|
|
fb4c130bb2 |
fix(slicer): drop the legacy library:read from the slice gate (#2725)
The desktop handoff accepted library:read alongside read_all/read_own, on the reasoning that default groups do not carry it and requiring it would lock out Operators and Viewers. The permission grants nothing in that position: the slicer-token endpoint gates on require_ownership_permission(LIBRARY_READ_ALL, LIBRARY_READ_OWN), and neither that dependency nor User.has_permission expands the legacy name, so a group holding only library:read gets a 403 there. It cannot reach the File Manager to try, either - GET /library/folders gates on the same pair - and the library:read -> library:read_own migration in core/database.py runs only over the groups named in DEFAULT_GROUPS, so a custom role that still carries it stays stuck rather than being upgraded. custom role that still carries it stays stuck rather than being upgraded. Accepting it only enabled a menu item the server refuses, and the failure is indistinguishable from "no slicer installed" once the catch hands the unauthenticated URL over. Removed, with the comment recording the reason so the next reader does not re-add it, and a test that pins it. --- refactor(slicer): share one sliceable-file-type rule (#2725) The File Manager and the 3D preview decide the same thing about the same file and each held its own list of extensions - which is how they came to disagree, offering a desktop handoff for an STL whose own preview showed "Open in Slicer" greyed out. Making the two lists identical fixed the symptom and left the drift, so SLICEABLE_FILE_TYPES now lives in utils/slicer.ts with isSliceableFileType for a stored file_type and isSliceableFilename for a name. The filename form still rules out the compound extensions explicitly, since .gcode.3mf ends with .3mf; the type form does not need to, because classify_file_type stores that one whole. Both test files mocked the whole slicer module, which would have replaced the new predicates with undefined - switched to importOriginal so only openInSlicer is stubbed. That is the better shape regardless: the tests now exercise the rule the component runs instead of a copy declared beside them. Carries the rebuilt bundle. The CSS hash moves with it - the split button introduces Tailwind classes the previous build had no reason to emit. |
||
|
|
af7908ca1e | Improve local slicer selection with code review suggestions, adding fail toast and handle (legacy) read permissions | ||
|
|
0cf5b8da41 |
fix(backup): tell a restore apart from a backup in Backup History (#2656)
A restore writes a `github_backup_logs` row too — same table, same status values, and it already carried `trigger: 'restore'`, which the API already returned. The history table rendered date / status / commit only, so the row read as a successful backup dated now while "Last backup" said something else: `last_backup_at` is only stamped by an actual backup, and the two disagreeing is alarming with nothing on screen to explain it. Adds the Type column the trigger was always there to fill. Unknown values fall back to the raw string rather than rendering blank, matching the `backup.pathCheck.*` lookup a few hundred lines up — a trigger kind added later shows up as itself instead of vanishing. Backend unchanged: it has recorded this correctly since the restore path was written. Three tests, all three failing without the column. 13 locales in parity at 5776 leaves — pt-BR takes "Backup manual" rather than the parenthesised form because "Backup (manual)" is identical to en, which the parity check counts as untranslated. Bundle rebuilt: `index-DhOfNgMz.js` → `index-CadgB7UN.js`. It also picks up the `archivesOwnerUnknown` leaves from the previous commit, which changed i18n without rebuilding. |
||
|
|
df6656a0a9 |
fix(backup): drop the settings-pin workaround, upstream fixed the cause (#2656)
This modal carried two workarounds for #2716: `onSuccess` deliberately did not invalidate `['settings']`, and a query-cache subscription pinned the entry to the pre-restore copy for as long as the result panel was up. Both existed because SettingsPage's debounced auto-save diffed its `localSettings` form state against the live cache, so any refetch of a restored settings row -- this modal's, a window refocus, a reconnect, or any of the ~30 other observers of the key -- read as an edit and PATCHed the pre-restore values back over the restore about 500 ms later. `43cb216a` on dev fixed that. The page now keeps a server baseline and reconciles a moved snapshot field by field: an untouched field adopts the server's value instead of overwriting it. The restore no longer needs an exception, and maziggy explicitly invited dropping it. A commit on top rather than a rebase-drop of `21bb5afc`: later commits touch this file, and the workaround was right when it was written. This says so. The reload on close stays -- it was never one of the two workarounds. Its stated reason was, though, and it was the #2716 bug, so it is restated for what it actually buys: invalidating `['settings']` only resyncs what reads that query, and the interface language, currency and auth toggles are read on boot. Tests: "never invalidates the settings query" inverts; the pin test and its control go with the pin. The reload pair stays. 28 -> 26 tests in this file. Bundle rebuilt: index-CCCWDEkl.js -> index-CHCEEMgx.js, carrying this, J1's locale leaf and the reworded ack caveat. CSS hash unchanged. |
||
|
|
27e97bc979 |
fix(backup): disclose the K-profile exception before the restore, not after (#2656)
With overwrite off, the confirmation said "Missing entries are added; existing entries stay as they are." For archives, spools and settings that is true -- _apply threads the flag into all three. _restore_kprofiles takes no overwrite parameter at all, and deliberately: writing a slot is always an overwrite on the printer, so it resolves the live cali_idx and publishes extrusion_cali_set either way, replacing whatever calibration that slot currently holds. The behaviour is right and the backend does say so, but it says so as a kprofilesAlwaysOverwrite note -- which only reaches the user in the result panel, after an MQTT send that cannot be taken back. The one screen that explains overwrite-off stated the opposite. So the fix is on the frontend, where the mismatch is. One new leaf, kprofilesOverwriteCaveat, in all 13 locales, rendered in two places: appended to the overwrite-off confirmation when kprofiles is among the selected categories, and beside the K-profiles row itself as soon as it is ticked, which is the same screen as the toggle whose promise it qualifies. Neither appears with overwrite on, where nothing is promising otherwise. Tests: 2 that fail pre-fix (the caveat beside the row, and inside the confirmation the user clicks through) and 2 controls (a spools-only restore keeps the plain message; overwrite-on keeps the strong one and adds nothing). Frontend suite 2601 -> 2605 tests across 195 files. Bundle rebuilt for this and for H1's new preview detail string: index-DPSSa0iw.js -> index-Dg68T-ox.js. CSS hash unchanged. |
||
|
|
945f50a1ab |
fix(backup): stop overwrite writing a spool's other tag key (#2656)
Four of the review's smaller items. E6, the substantive one. tag_uid and tray_uuid are both in the overwrite setattr loop, so a spool matched on one key got the backup's *other* key written onto it. Neither column has a unique constraint (models/spool.py, and no unique index in the migrations), so nothing errors — a duplicate tag simply appears, after which _find_spool's .scalars().first() is non-deterministic and an AMS tag lookup resolves to an arbitrary one of the two spools. The same loop could also clear a tag the user had scanned since the backup was taken, when the backup entry held None. _find_spool now reports which key matched, and _guard_tag_overwrite drops a tag column from the write when the incoming value is empty and the local row has one (the backup predates the scan, so the local tag is the newer fact) or when another local spool already holds it. Announced in the tally the way the archive un-delete case already announces itself, rather than done silently — the spoolTagKept locale key landed with the rest of the i18n block last commit. E5. The Restore button is hidden without github:restore. All three endpoints are gated on it server-side, so the modal 403s on its first preview; offering the button is offering an action that cannot work. Button only — the card stays visible, since configuring backups is a separate permission — and hasPermission returns true with auth off, so a single-user instance is unaffected. E3. models/github_backup.py: the trigger comment said manual/scheduled; this PR added a third value. E4. ha_token_from_env: recommending no change, with the reasoning recorded as a test rather than left in a review thread. It is built only in the settings GET response, is absent from AppSettingsUpdate, and is therefore never a Settings row — it cannot reach a backup, so an allowlist entry would be dead code. Worse, a name-shaped exception to a belt-and-braces denylist is a live hole: an attacker-authored settings/app_settings.json could get a *token*-named row written by choosing that name. 4 unit tests and 1 frontend test that fail against this commit's parent, plus 4 controls: a free tag is still written, an unchanged tag is not reported as kept, an insert is unaffected, and the button still shows with auth disabled. |
||
|
|
af8d14d796 |
i18n(backup): make the restore notes and preview details translatable (#2656)
A German user got a translated modal with "Not present in this backup commit" in
the middle of it. Every tally note and preview caveat was a server-built English
sentence rendered verbatim.
Follows the backup.pathCheck contract already in use one card down in the same
component, deliberately rather than inventing a second convention: the server
sends a `code` plus typed `params` and carries the English along as the
fallback, and the client renders
`t(`...${code}`, { ...params, defaultValue: message })`. The defaultValue arm is
what keeps a newer backend's unfamiliar code readable instead of printing the
raw key — covered by its own test.
Shapes:
- notes: list[str] -> list[GitHubRestoreNote] {code, params, message}. Breaking,
but the field is unreleased in this same PR.
- GitHubRestorePreviewCategory gains detail_code / detail_params; `detail` stays
as the English fallback.
- _CategoryTally.note(code, message, **params), deduped on (code, params) rather
than on the rendered text, so two offline printers both keep their names. The
20-note cap is unchanged.
28 new leaves across 13 locales: 20 notes.* and 8 details.*. `noData` collapses
the four per-category "No X data in this backup" strings into one, since the
category heading already renders beside it. Counts use single-form {{count}} in
the existing "N record(s)" style rather than i18next plural suffixes — nothing in
this block uses _one/_other and the parity script has extra rules for them.
Parity holds at 5737 leaves in all 13 locales.
Deliberately out of scope, and worth saying so rather than leaving it to look
like an oversight: result.message, the commit-picker subject lines and the HTTP
error strings stay English. Those also originate in the provider backends, so
code-ifying them widens the diff well past the restore service.
spoolTagKept is added here with the rest of the locale churn but is not emitted
until the next commit, so the 13-locale change lands once.
|