mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-08 15:11:21 +02:00
2915d2221bb3a3af6376592cdff4b1e5da5ef401
725
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8b46006644 | Merge branch 'dev' into feature/oidc-env-config | ||
|
|
18938a10ee |
fix(kprofiles): stop reporting rejected K-profile writes as saved
Saving a K-profile was fire-and-forget. set_kprofiles_batch published and returned True, and the printer's extrusion_cali_set answer was logged at DEBUG and dropped, so a write the printer refused was reported to the user as saved (#2718, reporter @jmoore-skild). The reason it could not simply be gated on: the answer itself was wrong. Single-nozzle firmware returned result:"fail" with reason:"invalid tray_id" on writes that demonstrably applied. Measured against an X1C and an H2D over MQTT, the cause is the tray_id:-1 Bambuddy itself put in the payload. Sending three otherwise identical writes isolated it: tray_id:-1 fails, tray_id:0 succeeds, and cali_idx:-1 is accepted either way, so only that one field is at fault. The H2D ignores the value entirely; the X1C validates it, complains, and applies the write anyway. BambuStudio always sends a real tray_id and defaults it to 0 for a manually entered profile. With tray_id:0 the acknowledgement is honest, and the printer echoes back the sequence_id we sent -- confirmed for extrusion_cali_get, _set and _del on both printer classes -- so it can be matched to the write that caused it. Writes now return their sequence_id and the routes await the verdict, turning a real failure into an error that carries the printer's own reason. A printer that stays silent is still treated as success: no answer is not evidence of refusal, and firmware that never answers must not turn every save into an error. Raises the ack to INFO. It sat at DEBUG, so the one line that explains a failed save was absent from every support bundle -- the same reasoning that put ams_filament_drying at INFO for #1447. Also fixes extrusion_cali_set building its payload from str(self._sequence_id) without incrementing first, reusing the previous command's id. Harmless while nothing correlated on it, fatal now that the write path does. Adds supports_nozzle_flow_type() for the Standard / High Flow choice, which the K-Profiles UI previously showed as "Not reported by printer" -- not a value anyone can save. Most printers omit the nozzle identity from their calibration table entirely, and the slicer treats that as Standard rather than unknown; Bambuddy now does the same and keeps the choice editable. The field is hidden only where the model ships a single nozzle variant, using the slicer's own rule (len(nozzle_volume) // len(nozzle_diameter) > 1 over the machine preset) evaluated across every bundled Bambu profile. That puts only A1, A1 Mini and A2L on the hidden side -- it is not the single- versus-dual-nozzle split, since P1P, P1S, P2S, X1, X1C, X1E and H2S are all single-nozzle and all carry two variants. Editing a profile also no longer writes back an empty nozzle_id. Wiki records that on printers which omit the field the chosen flow type is discarded by the firmware and reads back as Standard, in Bambu Studio as well, so it does not get filed as a bug again. |
||
|
|
af282b3527 |
fix(kprofiles): populate the filament picker from all preset tiers (issue #2719)
Add K-Profile built its Filament dropdown from the profiles already on the printer, so on a printer with none the field was empty, required and unsatisfiable (#2719, reporter @jmoore-skild). The modal's own hint described the dead end: create the profile in Bambu Studio first. The dropdown now uses the app-wide lookup order -- local imported, Orca Cloud, Bambu Cloud, hardcoded built-in table -- same as the AMS slot picker and the SliceModal tier groups. The built-in table is compiled into the backend, so the list can never be empty: a new printer with no cloud account and nothing imported still gets a first profile. Not fixed the way the report suggested. Seeding from /printers/available-filaments would have offered only what happens to be in an AMS right now, which on the reported printer is nothing; its tray_info_idx is empty or a cloud user preset rather than a filament id; it aggregates across every printer of the same model; and it is gated on QUEUE_CREATE, which the K-Profiles page does not hold. The printer indexes its calibration table by filament_id, so the picked preset is reduced to one before anything is sent. Built-in entries and Bambu official cloud presets carry one; a cloud user preset needs its detail fetched (never base_id -- that collapses a custom preset onto its inherited generic, #1053); imported and Orca presets have no Bambu id at all and take the closest generic for their material, via the same table the AMS slot configure flow uses so the two agree. A filament that resolves to nothing is refused with a named error rather than written under a wrong id. Collapses duplicates from two separate causes. A cloud account carries one copy of each filament per printer model, and with the "@BBL <model>" suffix stripped for display those rows are indistinguishable -- deduped within each tier by resolved filament id, by display name for user presets that have none. Cloud setting_ids also carry a "_NN" variant suffix, so the built-in tier's already-covered check never matched and listed the same filament again; the bare id is now recorded alongside. Groups the options by source with an optgroup per tier, styled in index.css: browsers render optgroup labels small, grey and italic, which buries the one thing distinguishing a "Bambu PLA Basic" you imported from the one the built-in table ships. Drops the second getKProfiles(printer, "0.4") query that existed only to seed the old dropdown. It ran concurrently with the main fetch whenever a non-0.4mm nozzle was selected -- the two-requests-in-flight case that made K-profile fetches time out. --- fix(ui): cancel a dialog's deferred close when it unmounts The AMS slot configure and K-Profile dialogs hold a success state briefly and then close themselves -- 1.5s to 4s after the command goes out, so the printer has time to process it before the list refetches. Each did that with a bare setTimeout closing over setState and the parent's onClose, and nothing cancelled it. The timer therefore ran whether or not the dialog was still there. Dismissing it inside that window, or the printer card re-rendering underneath it, left a pending close that fired later and dismissed whatever dialog was open by then. It also threw outright when the surrounding environment was gone first: a test tearing down its DOM before the 1.5s elapsed produced "ReferenceError: window is not defined" out of react-dom's resolveUpdatePriority, reported as an unhandled error against a suite that otherwise passed. Routes all five through a useCancellableTimeout hook -- two in ConfigureAmsSlotModal, three in KProfileModal, the latter with the longest windows and so the widest exposure. Scheduling replaces any pending timer and unmounting clears it. |
||
|
|
a35ba8fa5f |
fix(kprofiles): read the nozzle diameter the printer actually sent (issue #1748)
Every K-profile came back as 0.4mm on printers running any other nozzle (#1748, reporters @Liquidmasl and @jmoore-skild). The printer puts nozzle_diameter on the extrusion_cali_get envelope only; the per-filament entries carry setting_id, filament_id, name, k_value, n_coef and cali_idx, and nothing else. The parser read the field per entry with a hardcoded "0.4" fallback, so the fallback fired on every profile of every response. The envelope value was already in scope, read into response_nozzle and used only to match the request. This never reproduced on H2D because that firmware does include the field per entry. Both construction sites are in the same handler, so the code path is shared; what differs is the payload, and every single-nozzle model omits it. The display was the least of it. Editing is delete-and-re-add on single-nozzle printers, and the dialog rebuilt nozzle_id and nozzle_diameter from its own greyed-out selects, so saving an untouched 0.6mm profile rewrote it on the printer as HH00-0.4. Deleting aimed extrusion_cali_del at the wrong nozzle the same way. Both now pass through what the printer reported. The cali_idx cascade in inventory.py, spoolman_inventory.py and spoolman.py matches on nozzle_diameter, so on a 0.6 or 0.8 nozzle it never found the printer-side entry and the assignment silently failed to stick -- that is the "cannot auto-map a K-profile" half of the report, fixed at the source without touching those three call sites. nozzle_id has no source in the payload at all, and state.nozzles carries material (hardened_steel), not flow, so it cannot honestly produce HH/HS. Rather than keep inventing one, the UI now says the printer did not report it: the card shows the diameter alone, the dialog shows "Not reported by printer", and the High Flow / Standard filter is hidden instead of being offered as a control that can only ever empty the list. Import stops stamping HH00 on profiles whose source reported none. Also correlates K-profile requests by sequence_id. Responses were matched by nozzle diameter through a single shared expectation slot, so a second request overwrote the first's and the first's valid answer was discarded as a mismatch -- the "Failed to get K-profiles after 3 attempts" in the same logs, with the printer having answered correctly both times. Pending state is now one entry per request, keyed by the id we already send, with the nozzle match kept as a fallback for firmware that does not echo it back. Fixes the flow-type select naming a new profile with the opposite label, which contradicted the identical expression 44 lines above it. |
||
|
|
21d61c3535 | Merge branch 'dev' into feature/oidc-env-config | ||
|
|
455a9e4ba7 |
fix(backup): collect cloud profiles from every connected account (#2717)
Enabling Cloud Profiles for a Git backup produced nothing, and said it had
worked. Two independent faults, either one sufficient.
The collector looked for a "setting" list. The Bambu Cloud listing endpoint
is keyed by preset type instead, each key holding private and public arrays,
so the loop body never executed once — and the entries carry no type of
their own either, which routes/cloud.py already knew: it takes the type from
the outer key and maps Bambu's "print" to process. Two bugs on one line.
It also asked build_authenticated_cloud for the credential store used when
authentication is disabled. With auth on, tokens live on User rows, so the
collector returned at "Cloud not authenticated" before ever reaching the bad
key. Every multi-user install was collecting from zero accounts.
Neither failure surfaced. backup_metadata.json recorded the configured flag
rather than the outcome, so it claimed cloud_profiles: true on runs that
wrote nothing, and the log read "Collected cloud profiles: 0 filament, 0
printer, 0 process" at INFO — which is exactly what a successful backup of
an empty account looks like.
Cloud profiles now come from every connected account across both clouds. The
toggle predates Orca Cloud entirely, and Orca has the same three preset
types, so both are collected and grouped the same way:
cloud_profiles/bambu/user-3/{filament,printer,process}.json
cloud_profiles/orca/user-3/{filament,printer,process}.json
Accounts are keyed by Bambuddy user id, "global" when auth is off. Never by
email: a backup repository can be public, and the Bambu listing's user_id is
dropped for the same reason. Both credential stores are read on every run,
because a Settings row survives someone enabling auth later and dropping it
would silently stop backing that account up.
Bambu costs one get_setting_detail per private preset. The listing is
metadata only, and without base_id and setting the backup is a list of names
that create_setting cannot rebuild from. Public presets are skipped — Bambu's
bundled catalogue is the same hundreds of entries for everyone, always
re-downloadable, not recreatable under your account, and would rewrite the
repository on every run. Orca needs no second call; its sync-pull carries
each profile's content inline. Where the Orca route drops a profile whose
content.type it cannot map, the backup writes it to other.json instead:
silently omitting a profile because Orca added a type is the same class of
bug as this one.
Failures are contained per account and per preset, and counted rather than
swallowed. A partial backup that looks complete is how this stayed invisible.
The metadata now reports what was collected, per cloud and per account, and a
run that collects nothing while the category is enabled warns with the reason
instead of an INFO line that reads like success.
The checkbox gated on the viewer's own Bambu sign-in, which is not the same
question as whether there is anything to back up — with auth enabled the
accounts belong to individual users, and an administrator who never signed
in personally saw the category disabled with plenty in scope. It now gates
on the total across both clouds and shows the counts. That comes from its
own endpoint rather than a field on /config, since /config answers null
until the first save and would disable the toggle during the very setup it
belongs to. Counts only, never identities.
One deliberate restraint. _build_authenticated_service clears stored
credentials when a refresh is rejected, which is right for a route — the
user is on the page and can pair again — and wrong for a scheduled job.
Orca reports every rejection with one composite reason ("unknown, expired,
revoked, or already used"), so a genuine revocation cannot be told apart
from a lost token-rotation race, and acting destructively on a signal that
cannot be disambiguated is the #2562 mistake in a different cloud. It also
gains nothing: the Profiles route hits the same failure and clears it then,
with the user present. Background callers now pass clear_on_auth_failure=
False and skip the account. A successful refresh is still persisted either
way — by that point the old token is consumed, so dropping the new pair
would break a working pairing for real.
Restore is not part of this. Nothing reads cloud_profiles/* yet; the format
carries base_id/setting for Bambu and content for Orca so that it can.
|
||
|
|
4f2c073a34 |
fix(vp): gate the slicer's AMS pick behind the toggle and scope its badges (#2700)
Round-3 review of the "Save AMS mapping" PR. The queue item's ams_mapping was set unconditionally, on the reasoning that honouring the slicer's own pick is a correctness fix rather than a feature. It is both. Storing a resolved mapping makes _ensure_ams_mapping return early, so _compute_ams_mapping_for_printer never runs — and that function is where prefer_lowest_filament lives, along with the AMS-filament-backup gate that qualifies it (#1766), the inventory-remain overrides, and the per-slot force-colour overrides. Every existing queue-mode VP pointed at a printer would have quietly lost all of it on upgrade, without a setting to turn it back on. So save_ams_mapping now gates the queue item too, not just the archive persistence. Off is exactly the old behaviour. The correctness case the PR was written for — two spools of the same red PLA, and the slot the user picked in the slicer thrown away — is still fixed, for anyone who asks for it. Force color match wins over it when both are on. Its only effect on a fixed-printer item is the filament_overrides written onto the queue item, and those are read inside the function a stored mapping skips, so the two toggles sitting next to each other on the same card silently cancelled. The dispatch now matches strictly, as asked, while the slicer's pick is still saved onto the archive — that is what the toggle's name promises, and a later reprint is a separate decision from this print. The queue-add fallback applies the same rule to a request that carries force-colour overrides. A mapping shorter than a plate's highest slot id cannot address that plate's own slots, and _ensure_ams_mapping would have kept it anyway, since it only rejects an all-unresolved one. Each plate now checks the length it needs and falls back to a computed mapping if the array does not reach. Bambu Studio sends a file-global array, so this normally never fires; it also means a multi-plate Send All degrades safely if that ever stops being true. The badges claimed more than they delivered. Both rendered whenever a saved mapping existed, ignoring which printer it belonged to, while the tooltips promised the reprint would reuse those exact spools — true only on the printer the trays were resolved against. The queue row's flag is now computed against that row's own printer, which is precisely when dispatch reuses the mapping, and the archive card names the printer instead of implying any of them will do. It hides itself when that printer no longer exists. Retranslated in all 13 locales. Frontend tests, which the PR had none of. The printer-scoping rule is now a pure function rather than an inline expression, covered for the mismatched printer, the no-printer-selected case that would otherwise compare undefined against undefined, and malformed extra_data. The toggle's undo bookkeeping is covered for unresolved slots, short mappings, and hand-made picks — preserved when the toggle never wrote that slot, replaced when it did, which is behaviour worth pinning either way. Also reverts all three queue-mode switches when a save fails, not just the new one; without it the card shows a setting the server rejected. |
||
|
|
4b4cb18a64 | Merge branch 'dev' into feature/save-ams-mapping-toggle | ||
|
|
284709f850 |
fix(projects): drop deleted prints from their project, and refresh the view (#2731)
Deleting a print that belonged to a project left it on the project page as a card with a missing thumbnail, and there was no way to remove it. Deleting a print is a soft delete by default (#1343): the files go from disk, the row stays so global Quick Stats keeps counting its filament, time and cost. Every other consumer filters those rows out. The projects module filtered none of them — the only deleted_at check in the whole file was for LibraryFile — so a deleted print kept its project_id and kept being listed, pointing at a thumbnail that no longer existed. The same broken previews appeared on the overview cards, and in the timeline, where the entry links to an archive that no longer opens. Unassigning was impossible because the only UI that can change a print's project lives on the Archives page, which correctly hides deleted prints: visible on the project, unreachable from anywhere. All eight project-scoped archive queries now filter, counts included. That last part is a deliberate divergence from #1343, where the whole point of the soft delete is that the contribution survives: a project is a piece of work with a definite membership, not a lifetime total, so a project that lists eleven prints must not claim twelve. The reasoning is recorded at the constant so nobody later "fixes" it back. remove_archives_from_project keeps working on hidden rows on purpose — it is the repair path for links written before this. The BOM print_name lookups are left alone; naming a since-deleted print is still correct. Two more consumers had the same gap. The CSV/Excel export handed back rows the interface says are gone — filtered at the base query, since the export is the list you are looking at saved to a file. Per-project failure analysis measured a failure rate against prints deleted from the project, and disagreed with the project's own numbers; only the project-scoped branch filters, global analysis still counts every run including orphans as #1390 established. Finally, the project page needed a manual reload to catch up. staleTime is 60s and the delete mutations invalidated only ['archives'], so a project visited within the minute served its cached copy, print still there. The project-assign mutations had the mirror-image bug: ['projects'] refreshed the overview cards but never ['project', id]. Both now go through one shared helper covering every project-derived key, as bare prefixes so all cached project ids are matched. |
||
|
|
5e2b7b53e6 |
fix(printers): surface the printer's own "command verification failed"
A P1S on firmware 01.10.00.00 rejected every control command and said so: HMS 0500-0500-0001-0007, "MQTT command verification failed". Bambuddy received that, dropped it, and reported a healthy printer instead. The frontend filtered it out. This code's meaning lives in attr's low half (0500) and code's high half (0001), both of which the MMMM_EEEE short form discards, so it collapsed to "0500_0007" — no catalog entry, no firmware actions, and filterKnownHMSErrors drops uncatalogued action-less errors. Catalog lookups now try full_code first, in both the description and the filter, and errors matched that way display the four-group code the printer's own screen shows. The remedy line is ours, not Bambu's: their wiki says to update Studio or Handy, which does not apply to a print sent from Bambuddy. The developer-mode probe made it worse. It read anything that was not an explicit refusal as confirmation, and this firmware answers the probe with an empty result while refusing everything else — so an inference drawn from a non-answer became "developer_mode: pass" in the support bundle of a printer that had not accepted a command all day. The probe now has three outcomes: explicit success enables, explicit verify-failure disables, anything else stays unknown and the diagnostic reports skip. The HMS is authoritative over that inference in both directions. It forces developer_mode False when present, and clears back to unknown when the printer stops reporting it, so enabling Developer Mode and restarting the printer is picked up without restarting Bambuddy. Dispatch no longer treats a refusal as a wedge. The watchdog latches the HMS across both phases and fails the item on the first attempt naming the code and the fix, rather than spending three uploads and 270s a lap to arrive at a message about SD cards. The check runs after the active-state exit in both phases, so a lingering HMS can never abort a print that is visibly running. Also: the "wrong or mis-cased serial number" hint no longer fires in the moment after a reconnect. _report_messages_since_connect is reset by _on_connect, so a reconnect landing microseconds before the staleness check leaves it at 0 for reasons that have nothing to do with the serial — this reporter's healthy printer was told to go check its serial 1 ms after reconnecting. |
||
|
|
11dc612bc4 |
feat(obico): authenticate to a token-protected ML API (#2733)
Obico's ml_api container takes an optional ML_API_TOKEN environment variable.
With it set, ml_api/auth.py answers a bare 401 to any request whose
Authorization header isn't "Bearer <token>"; with it unset it ignores the
header entirely. Bambuddy never sent one, so pointing it at a protected server
meant deleting the token there — which the reporter had set for their Home
Assistant integration and did not want to undo.
Settings -> Failure Detection gains an ML API Token field. When it is empty no
header is sent, so an unconfigured install's request stays byte-identical to
what shipped before the setting existed.
This failed in the worst possible way, and that is the more important half of
the change. Obico decorates /p/ with token_required but leaves /hc/ open. Test
Connection pinged /hc/, so it reported success against a server that was
rejecting every real detection call, the settings looked right, and detection
silently never ran. The only symptom was a generic "ML API call failed" buried
in the status card.
So the test now proves what it claims. After health passes it probes GET /p/
with no img parameter: the auth decorator runs before the handler, so 401 means
the token was rejected and 422 ("Invalid request params") means it was
accepted. No inference work is done either way. A probe that itself errors
reports the token as unknown rather than as working — the UI says it could not
be checked instead of claiming success.
The detection loop checks for 401 before raise_for_status, so a rejected token
is reported as a rejected token, naming the setting and the environment
variable, instead of surfacing "401 Unauthorized" with no hint of what to do.
The message never contains the token; a test pins that.
The setting name carries "token", so the support bundle's keyword redactor
masks it with no new rule. Resolving "field omitted" to the saved token is the
route's job, keeping test_connection a pure outbound call with no database
access.
Second fix, same issue: support bundles misreported which printers Obico
watches. The bundle split obico_enabled_printers on commas and read an empty
value as "no printers". The settings UI writes a JSON array, and empty means
*all* printers — the default — so a working Obico setup showed obico_enabled
false against every printer in its own bundle. That is the reporter's bundle
exactly, and it points anyone reading it at the wrong subsystem. The bundle now
parses the setting the way ObicoDetectionService does, keeps a comma fallback
for any install that stored the legacy shape, and factors in the global switch.
|
||
|
|
6844aa292f |
fix(ams): offer every K profile the printer holds for a generic filament preset (#2710)
The reporter's A1 mini has nine Flow Dynamics calibrations, all of them saved under Generic PLA and named after the spool's colour — "Dark Brown", "Glow", "Marble". Bambu Studio lists all nine for that slot. Configure AMS Slot offered one: the profile already bound to the slot. After a slot reset it offered none, leaving the slicer as the only way to assign a K value. Two independent faults, both tripped by picking a built-in generic preset. The filament-id match discarded Bambu's generic GFx99 ids as too broad. But the comparison already requires both sides to carry the same id, so that exclusion could only ever fire when the selected preset was itself the generic one — precisely the case where the match is right. The printer keeps one calibration table per filament id, so a slot on Generic PLA should offer everything calibrated under Generic PLA. Equal ids now match, generic or not. The name fallback was dead for the same presets: parsePresetName reads the leading "Generic" in "Generic PLA" as a manufacturer, which put the matcher into brand-gated mode and demanded the word GENERIC appear in the profile name. No real profile has it. "Generic" is no longer treated as a brand, so profiles still match on material when a printer reports no filament_id with its calibrations. The one profile that did appear came from the #1689 safety net that always surfaces the slot's active cali_idx — which is also why a reset slot, having no active profile, showed an empty list. Neither fix can be complete on its own, because profile names are free text and nothing ties "Marble" to a material. The picker now also lists every remaining profile on the printer under "Other K profiles on this printer", so a profile that exists can always be selected. Applying one from that group needs no new backend work: configure_ams_slot already realigns the slot's filament context to the chosen profile's, which is what makes the cali_idx stick. Options are keyed by name+k_value rather than the bare name, so two profiles sharing a name are no longer indistinguishable in the select. Both render blocks carry the change — the modal duplicates the picker for its full-screen variant. isMatchingCalibration gets the same generic-id rule for the spool form's PA suggester, with two guards. A new generic-id-to-material table means a PETG spool can never claim GFL99 profiles just because both sides stored a generic id (Nylon and PA compare as one material). And a spool that names its own brand keeps the stricter name path, so its suggestions stay brand-specific rather than becoming the printer's whole generic table. |
||
|
|
13c37ffe51 |
fix(vp): scope saved AMS mapping to the printer it was resolved against
Round-2 review fixes for #2700. Blocking: the toggle didn't actually gate the archive write. archive.py's promotion fired for any print_data carrying ams_mapping, but bambu_mqtt's request-topic interception captures ams_mapping unconditionally for every print source (slicer-direct LAN prints included). Since main.py's real-printer auto-archive path forwards the full MQTT payload as print_data, every archive on any install — VP or not — grew extra_data.slicer_ams_mapping. Fixed by replacing the print_data-sniffing with an explicit `slicer_ams_mapping` param on archive_print() that only the VP-queue path (already gated on save_ams_mapping) ever passes. Blocking: a saved mapping could get reused on a printer it was never resolved against — tray IDs only mean something relative to one printer's AMS layout. extra_data.slicer_ams_mapping is now stored as {mapping, printer_id} instead of a bare array: - add_to_queue's fallback only fires when the reprint's target printer_id matches the mapping's origin printer. - The frontend's archiveAmsMapping only surfaces (and the Mapping button only appears) when the print modal's selected printer matches too. - A model-based VP (target_printer_id=None, no MQTT bridge to any real printer) never stamps a mapping in the first place — there's no live AMS layout for the slicer to have resolved tray IDs against. Also from review: - Multi-plate archives now get the Mapping button too (the per-plate FilamentMapping loop was missing archiveAmsMapping entirely). - Added coverage for the previously-untested late-MQTT archive patch path (_restamp_recent_queue_item), including the model-based-VP skip case. - usingArchiveMapping now also resets on printer change, not just plate/archive (it already worked via the printer-scoping above, but is now an explicit dependency too). - The Mapping button's revert (OFF) now undoes only the slots it itself set, not every manual pick in scope — matches the comment above it. - Added a comment on why negative-value slots (external spool) are skipped rather than cleared when applying a saved mapping. |
||
|
|
bab1cfb906 |
feat(vp): per-VP "Save AMS mapping" toggle + reprint auto-apply
Lets a reprint reuse the AMS slot the slicer itself picked, instead of re-deriving one from the file's static type/color. When a Print Queue VP has "Save AMS mapping" on, the slicer's own live-resolved ams_mapping (from the project_file MQTT command) is persisted onto the archive as extra_data.slicer_ams_mapping. A later reprint can reuse it via a new "Mapping" button in the filament-mapping panel — one click snaps every slot to the saved pick, click again reverts to auto-match. Archive cards and queue rows get an "AMS mapping saved" badge so it's visible beforehand. add_to_queue also falls back to the saved mapping automatically when the caller sends no explicit ams_mapping (e.g. a plain reprint with no per-slot edits). The queue item's own ams_mapping (used for that dispatch) is still captured unconditionally whenever the slicer provides it — that part is a correctness fix, not gated behind the toggle. Only the archive persistence for future reprints is opt-in. Split out from the original combined PR per review: this half is genuinely opt-in and low-risk (#2684). The dispatch-time validation gate that keeps a stored mapping honest (#1308) changes behaviour for every existing user and will land as its own PR. Review fixes applied: - _extract_slicer_ams_mapping_json: dropped the unreachable `v is None` arm and rejected bool explicitly (isinstance(v, int) accepts bool). - Translated the Russian docstring text to English. - save_ams_mapping's model comment moved to a trailing comment on the column line, matching the file's convention. - usingArchiveMapping now resets when the plate or archive changes, so the Mapping button can't read ON against a mapping it never applied. - Translated "Click to change slot assignment" and "Re-read". - add_to_queue's fallback is now called out explicitly in code comments and covered by three new integration tests (fallback fires, explicit mapping wins, unrelated extra_data doesn't false-trigger). Closes #2684 |
||
|
|
c3448dae91 | Merge branch 'dev' into feature/oidc-env-config | ||
|
|
db538e43f1 |
fix(slice): give the slice modal one filament row per project slot (#2712)
The filament list is positional from the modal down to the CLI's filament_N.json parts, but for a source that already carries slice_info the requirements endpoint returns only the slots the plate consumes. A MakerWorld model declaring four filaments and painting with slot 4 alone therefore showed one dropdown, whose PETG pick the CLI bound to slot 1 — slot 4 sliced with the profile baked into the source, and the print came out PLA. The endpoint now takes full_slots, which widens that answer to every project slot with used_in_plate flags, and only the slice modal passes it. Print-time AMS matching shares the endpoint and keeps the used-only list, so it still asks for exactly the spools the job needs. |
||
|
|
ac2f4d058e |
fix(oidc): hide the icon buttons on the env-managed provider too
refresh-icon and remove-icon rendered outside the !provider.is_env_managed block, and both routes answer 409 for that provider -- so a click could only ever produce an error toast, which is the reason the comment right below them gives for hiding everything else. Its icon comes from BAMBUDDY_OIDC_ICON_URL and is re-applied on every boot. Reported by maziggy in review of #2625. |
||
|
|
6cda236dce |
feat(notifications): optional Telegram forum topic via message_thread_id (#1518)
Telegram groups with Topics enabled always received notifications in the General topic, since only Bot Token and Chat ID were configurable. Splitting notifications per printer meant running a separate chat for each one. The Telegram provider now takes an optional Forum Topic ID - the last number in a topic's link, t.me/c/1234567890/25 - and routes its messages there. Left empty, nothing changes. The value is coerced to an int once in _send_telegram and attached to both the sendMessage JSON body and the sendPhoto form data. That ordering matters: Telegram rejects a string message_thread_id in the JSON body while accepting one in the multipart call, so passing the raw form value through would have worked for thumbnail notifications and 400'd for plain-text ones. A non-numeric value is rejected in the form and again server-side before any request goes out. No migration - provider config is a JSON blob. Adds Forum Topic ID plus help text to the Telegram section of the provider dialog, translated in all 13 locales. Backend tests cover omitted / blank / int-typed / non-numeric values and both send paths; frontend tests cover the field being optional, absent for other providers, round-tripping on save, and blocking save on a bad value. |
||
|
|
05aebac67b |
feat(oidc): show the env-managed provider as locked in settings
The API answers 409 to any write against this provider, so offering edit, delete and the enable toggle would promise a change that cannot land -- the operator would click, see nothing happen, and have no way to tell why. The controls are hidden and a lock badge names the reason instead. Reuses settings.environmentManagedLabel, the string the Home Assistant env-managed fields already use: same situation, same wording, and no new key to keep in parity across eleven locales. is_env_managed is optional on the client type so a response from an older backend still type-checks. Refs #2593 |
||
|
|
8551e32f14 |
feat(slicer): keep the designer's print settings when re-slicing for another printer (#2622)
Published models often deviate from the stock Bambu profile on purpose - five walls, 100% infill, a 0.1mm first layer. Re-slicing one for a different printer discarded all of it: the picked process preset overrides the file's embedded settings, and that override is precisely what makes cross-printer re-slicing work, so it cannot just be dropped. "Slice as designed" (#2611) does not help - it is all-or-nothing and only offered when the picked printer already matches the design's target. The deviation list does not have to be computed. Bambu Studio writes it into the 3MF as different_settings_to_system, laid out as [process, *filaments, printer] - verified against real files at 2, 3 and 4 filament slots. The parser refuses any file whose array length contradicts its own filament count rather than guessing an index, since reading the printer slot as the process slot would carry the designer's machine_start_gcode onto a foreign printer. The slice dialog now lists exactly which print settings the author changed and what each was set to, with a checkbox per setting. Design intent - wall count, infill, layer and first-layer height, supports, seam, brim, ironing - is ticked by default. Printer-specific values - speeds, accelerations, jerk, fans, temperatures, prime-tower geometry - are listed with a badge but start unticked: tuned for the author's machine, they can be merely wrong on the target or outside the range its profile accepts, which fails the slice outright. Only ticked keys are sent, and only keys the source actually flags as changed are applied. Values are written into the outgoing process JSON, the same mechanism the support carry-over has used since #1881: for a Standard preset pick that JSON is an inherits stub, so the patch is the child in the chain and wins over the flattened parent. Process slot only - filament picks are honoured as chosen. The wiki's "this is not a settings merge" note under Slice as designed described the gap this closes; rewritten to point at the new panel. Translated in all locales; wiki updated. Covered by backend and frontend tests. |
||
|
|
8fd1f884dc |
feat(mqtt): publish the plate-clear gate and add a notification for it (#2525)
When a print reaches a terminal state Bambuddy holds the queue until
someone confirms the build plate is clear. That gate was visible only in
the Web UI: the printer's own MQTT push reports nothing beyond RUNNING,
PAUSE, FAILED, FINISH and IDLE, so an external automation could not tell
"finished" from "finished and still waiting for a human".
The per-printer status topic now carries an awaiting_plate_clear field,
and every transition is additionally published on a new retained topic,
bambuddy/printers/{serial}/plate_clear. Retained, and published from the
flag itself rather than from printer telemetry: a subscriber learns the
state of every printer the moment it connects, and the state stays
correct after Auto Off powers a printer down - telemetry stops there,
which would otherwise leave the status topic frozen at false.
Publishing is edge-triggered. The queue clears the gate on every
dispatch whether or not it was up, and no subscriber should see a
"plate cleared" for a plate that was never dirty. Persistence and the
WebSocket broadcast stay unconditional; they are idempotent and predate
this.
A matching Plate Clear Required notification event was added, off by
default on every provider because it fires after every print at the
same moment as the print-complete alert. Only the rising edge notifies.
Acknowledging still goes through POST /printers/{id}/clear-plate.
Two tests in test_printer_manager_status_broadcast.py asserted
_schedule_async.call_count == 2 for the setter. The new emission makes
it three on a transition, so they now assert that the persist and
broadcast coroutines are actually scheduled - which is the contract
Translated in all locales; wiki updated. Covered by backend and
frontend tests.
|
||
|
|
f4f76e0121 |
fix(inventory): allow editing and duplicating stock spools without a slicer preset (#1905)
A spool created by Quick Add, a CSV import or an RFID scan has no slicer preset, brand or subtype. Reopening it in Edit Spool demanded all three before anything could be saved, so changing its storage location, cost or notes was impossible - and Copy Spool had the same gate with no Quick Add toggle to waive it. The preset you were then forced to pick auto- filled material, brand and subtype from the preset name, silently rewriting a hand-entered manufacturer (Elegoo -> Generic) so the spool no longer appeared where it had been filed. Editing and copying now require only what the backend requires: the material. Preset, brand and subtype stay fully visible and editable - nothing is hidden the way Quick Add hides it - and the required-field markers no longer advertise a rule that isn't enforced. Selecting a preset fills only fields that are still empty or that a previously selected preset had filled, so values the user (or the saved spool) provided survive; switching between presets still replaces what the earlier one contributed. The brand and material dropdowns also no longer filter themselves down to the brand/material pairs known to the color catalog and slicer presets. Elegoo is catalogued only for PLA, which made a real product like Elegoo ASA look impossible to enter. Both lists now always offer everything known, with paired entries ranked first under Suggested and the rest under All, and a spool's own custom brand or material is always present in its own dropdown. The SpoolBuddy write-tag form shares these fields and gets the same treatment. Lastly the Quick Add layout no longer leaks out of create mode: quick- adding a spool and then opening Edit left the edit form in the reduced layout with no toggle to leave it, because the toggle is create-only. Frontend only. Translated in all locales; wiki updated. Covered by validation and form-interaction tests. |
||
|
|
eae5359fbc |
feat(printers): show AI failure detection state on printer cards (#1546)
The live Obico classification was only visible under Settings -> Failure Detection, so tracking how detection matched an ongoing print meant flipping between the Printers screen and Settings. Each printer card's badge row now shows an AI badge whenever detection is enabled for that printer, like the other health badges: gray Idle while no print is being watched, then green Safe, amber Warning, or red Failure while a print is actively monitored. The tooltip carries the current smoothed score; clicking jumps to the full detection status and history in Settings. Printers excluded from the monitored subset show no badge. Served by a new lightweight /obico/printer-status endpoint readable with printer permissions alone - it exposes only the enabled flag, the monitored-printer set, and per-printer classification, keeping ML URL and other configuration behind the existing settings-gated endpoint. |
||
|
|
62ba751278 |
feat(notifications): Bark notification provider (#1495)
Bark is the open-source, account-free iOS push app (self-hostable
via bark-server). Configure with just the device key from the app;
the server URL defaults to the official api.day.app relay and
accepts a self-hosted instance. Optional settings: notification
Group, Sound, and iOS Interruption Level - Time Sensitive breaks
through scheduled summaries, Critical bypasses Silent mode and
Focus, Passive delivers silently.
bark-server can wrap failures in an HTTP 200 body ({"code": 400}),
so the sender checks the body code as well as the HTTP status.
Unknown interruption levels are dropped rather than forwarded.
|
||
|
|
49f9d7120d |
feat(notifications): custom data fields for Home Assistant notify services (#1441)
When an HA notification provider targets a notify service (e.g. notify.mobile_app_myphone), a new optional Data (JSON) field is forwarded as the service call's nested "data" object - the same place HA automations put mobile push options like priority, ttl, channel, and group. ttl: 0 + priority: high make Android pushes arrive immediately; channel gives printer alerts their own sound. JSON rather than key=value lines so numbers stay numbers and nested options work. Validated on both ends: the UI rejects malformed JSON before saving, and the sender fails loudly instead of posting a half-built payload. Only included when configured - the default persistent_notification.create path is unchanged, as its schema rejects unknown keys. |
||
|
|
d68724c689 |
feat(stats): energy usage in cost records and trends (#1432)
The Most Expensive record on the Statistics page ranked prints by filament cost alone, ignoring the per-print energy cost Bambuddy already measures via an attached smart plug. It now ranks by filament + measured energy cost; prints without smart-plug data compete on filament cost alone, as before. Filament Trends gains an Energy Over Time chart: kWh per day (per hour for ranges of a week or less, per week for long ranges), with the range's total kWh and energy cost in the header. The chart only renders when the selected range contains measured energy data, so setups without smart plugs see no change. The /archives/slim stats feed now carries each run's energy_kwh / energy_cost from print_log_entries. Translated in all locales. Covered by backend and frontend tests. |
||
|
|
9bb5a1b999 |
fix(inventory): PA-Profil picker fetches K-profiles across all installed nozzles (#2618)
The Edit Spool "PA-Profil" tab and the SpoolBuddy write-tag page fetched a
printer's calibrations with getKProfiles(printer.id), which defaults the nozzle
filter to 0.4. The printer/MQTT layer filters strictly by that diameter, so on
a multi-nozzle printer a same-filament 0.6mm K-profile was never retrieved and
the picker showed only the 0.4mm entry ("1 match"). (The AMS-Slot config dialog
was already fixed in #1899; these two pickers were not.)
Add installedNozzleDiameters(status) and a shared fetchPrinterCalibrations()
that queries every reported nozzle diameter and merges the results, falling
back to 0.4 when the printer hasn't reported nozzle hardware. Each profile row
now shows a nozzle-diameter badge so identically-named profiles are distinct.
|
||
|
|
aa443c6e83 |
fix(print): keep filament gram usage visible when the name is long (#2669)
In the Print dialog's Filament Mapping, each required filament shows its name and the grams the job needs, e.g. "Bambu PLA Basic (281.2g)". Name and grams shared one fixed-width column with truncate on the whole string, so a long name pushed the "(...g)" off the end and clipped it -- partially on a wide screen, entirely in mobile portrait. The gram usage is the number that matters (does the spool have enough left?), so it shouldn't be the part that gets dropped. Pin the gram usage (shrink-0, whitespace-nowrap) and let only the name truncate, with the full name on hover. Applied to both the Specific-Printer (FilamentMapping) and Any-model (PrinterSelector) panels. Layout only. |
||
|
|
6530a6af06 |
fix(library): send camera stream token for 3D Preview plate thumbnails (#2661)
The File Manager 3D Preview dialog (ModelViewerModal) rendered plate thumbnails with the raw thumbnail_url. The plate-thumbnail endpoints are gated behind a camera stream token passed as ?token= (an <img> can't send an Authorization header), so with auth enabled the browser fetched without a token and got 401 "Valid camera stream token required" — broken image icons for every plate. The Slice dialog's picker (PlatePickerModal) and the Print modal's PlateSelector already append the token via withStreamToken(), which is why the same file's thumbnails showed there. Wrap the thumbnail src in withStreamToken(), matching the other two call sites. The token is synced app-wide and withStreamToken() is a no-op when auth is off, so non-auth setups are unchanged. |
||
|
|
800c45536e |
fix(scheduler): pin the force-color variant when selecting the AMS slot (#2650)
Follow-up to
|
||
|
|
21c6182c8d |
fix(ams): close the slot popup before it covers the filament dialog (#2631)
Tapping Configure on an AMS slot left the slot popup standing on top of the filament type/colour dialog it had just opened, so both layers were on screen at once. The popup is portaled at z-[60] so it can escape the stacking contexts sibling printer cards create on the dashboard (#1336), which also puts it above ConfigureAmsSlotModal and LinkSpoolModal at z-50. Nothing dismissed it: it is hidden only by the pointer leaving it, and a touch device never sends that after the tap that opened it. On desktop the next mouse movement cleared it, which is why this is a tablet report. FilamentHoverCard and EmptySlotHoverCard now dismiss themselves before running any action that opens a dialog or navigates away - Configure, Assign Spool, Unassign Spool, and both Open in Inventory links. The dismissal clears the pending timer as well, so a queued open cannot resurrect the card over the dialog. Actions that report progress inside the popup are unchanged: RFID re-read, Load and Unload render their spinner there, and Copy UUID its confirmation tick. |
||
|
|
41ad1d65c7 |
feat(skip-objects): select items directly on the build plate
Pairs the top-down plate preview with the slicer's per-object pick mask (Metadata/pick_N.png), whose pixel colours encode the same identify_id the firmware's skip command takes, so a click resolves to a real object rather than an inferred bounding box. Several objects can be selected before one confirmation; selected and already-skipped items are highlighted on the plate; the checklist stays available when no mask exists. view=pick serves only the active plate's mask and 404s otherwise, unlike every other view. A render returned in a mask's place would be decoded as object IDs — dark pixels yield small integers that collide with real ones — and a click would then skip an arbitrary object, mid-print, irreversibly. The 404 is what tells the UI to fall back to the checklist. Click mapping goes through the contained rect, since the canvas paints at mask resolution under object-contain; clicks on a letterbox bar are rejected rather than clamped onto whichever object touches the border. Confirming names the object when one is selected and counts them when several are, which is what plates of identically-named clones need. No printer-control command path was added or changed; the layer, permission and existing skip-command guards are untouched. |
||
|
|
56accd24de |
fix(smart-plugs): don't blank printer state when an accessory plug switches off (#2629)
An end-of-print auto-off on a plug that powers a filter fan marked the linked printer offline and forced its state to "unknown". The mark was unrecoverable: connected heals on the next MQTT message but state does not (only frames carrying gcode_state rewrite it, and steady-state push_status frames are partial), so the printer stayed "unknown" until a manual Force Refresh and the queue never dispatched to it again. The offline mark is now an explicit presumption: mark_power_off records the state it overwrites and _on_message undoes it as soon as the printer sends another report on its own topic, since inbound traffic proves the power was never cut. A reconnect discards the saved state, so a genuine power cut is unaffected. Each plug also gains a controls_printer_power flag (default true, backfilled) that gates all five power-off paths, and the queue's power-on step now picks the flagged plug instead of whichever linked plug came first. |
||
|
|
2e45893dd5 |
feat(print-options): add "Auto" state to bed levelling, flow & nozzle-offset calibration
Bed levelling, flow calibration, and nozzle-offset calibration were on/off only, so the sole way to run bed levelling was to force a full level before every print. Bambu Studio has always offered a third "Auto" state that lets the printer skip the calibration when it was done recently -- the state most users actually want. Make these three options tri-state (off/on/auto), defaulting to auto, and leave vibration/layer-inspect/timelapse as on/off (Bambu Studio exposes no auto for those). Wire encoding follows Bambu Studio's source exactly: each option sends a JSON bool (true only for "on") plus a companion int -- off=0, on=1, auto=2. The bool fields stay booleans (the #1478 H2S regression); only the companion int widened from {0,1} to {0,1,2}. #1721's observation that stage 8/39 stays queued when sending 2 is the auto contract (queued, skipped at runtime if recent), not a broken "off". - schemas: TriState = Literal[off/on/auto] with a BeforeValidator coercing legacy bool / 0-1 / true-false so old clients and un-migrated rows validate - model + migration: boolean columns -> String; SQLite via column affinity + data backfill, PostgreSQL via ALTER COLUMN TYPE guarded on information_schema (verified on both dialects); settings rows normalised true/false -> on/off - MQTT: start_print takes the tri-state strings and emits the paired bool+int - Virtual Printer: reconstructs the slicer's auto/on/off from the int companion (auto_bed_leveling / extrude_cali_flag) in both capture paths - frontend: CalibrationMode type; off/auto/on segmented controls in the print dialog, queue bulk-edit, and Settings -> Workflow; calibrationMode_* strings in all 11 locales |
||
|
|
585b1be054 |
fix(spoolbuddy): resolve react-simple-keyboard interop default so the kiosk keyboard renders (#2616)
Focusing any text field on a SpoolBuddy screen (inventory Search, or the Search / Color Name / Brand fields on write-tag New Spool) blanked the UI with React error #130 ("Element type is invalid ... but got: object"). It hit both internal and Spoolman inventories, so it was not data-specific. The SpoolBuddy shell mounts VirtualKeyboard, an on-screen keyboard that pops up on focusin for any input -- so every field on every SpoolBuddy page tripped it, while the main app (no on-screen keyboard) was fine. VirtualKeyboard imports the default export of react-simple-keyboard, a CommonJS package; under the current bundler's CJS->ESM interop that default resolves to the module namespace object ({ KeyboardReact, default }) rather than the component, so <Keyboard> renders an object as an element type and React throws. vitest's interop returns the real component, so it only manifested in the browser build -- a runtime, not a type, problem. Add a small resolveInteropDefault helper that unwraps such an interop-wrapped default: it returns the value as-is when already a usable element type (function/class, tag string, or a $$typeof-marked forwardRef/memo/lazy) and otherwise falls through to .default and named exports. VirtualKeyboard resolves the real component through it. |
||
|
|
c469aa3407 |
feat(slicer): add "slice as designed" mode honouring a 3MF's embedded settings (#2611)
Server-side slicing always applied the picked printer/process/filament triplet via --load-settings, which overrides the designer's embedded project_settings.config — so a MakerWorld model set up for 5 walls came out at the picked profile's default 2. That override is correct for re-slicing a design onto your own printer/AMS, but there was no way to slice a file the way its author configured it. SliceModal now offers a "Use the file's built-in settings" checkbox when the source 3MF carries embedded settings AND the picked printer matches the design's target model. It routes to the existing embedded-settings slice path (previously only a crash fallback), so walls/infill/filament come from the file. Ticking it locks all four preset dropdowns — printer included, since it's unused on this path and changing it would drop the match and hide the toggle. The printer-match gate stops embedded settings being honoured across models (wrong bed); there is no cross-printer re-targeting on this path. - schema: use_embedded_settings on SliceRequest - route: embedded_mode branch; crash-fallback guarded against re-running - frontend: gated checkbox locking all four dropdowns, resets on mismatch - 2 i18n keys across all 11 locales - tests: backend (flag skips triplet / ignored for STL) + frontend (toggle offered on match, locks dropdowns + sends flag / hidden on mismatch) |
||
|
|
e77e10896f |
feat(ams): name the expected slot when a paused print hits an AMS runout (#2587)
The firmware's runout HMS text says "insert into the same AMS slot", which is wrong under AMS Filament Backup: the firmware won't re-accept the depleted slot and advances to the next compatible one. Bambuddy parsed print.ams.tray_now only and dropped tray_tar/tray_pre, so the expected slot never reached the UI. Capture tray_tar/tray_pre on PrinterState and, while paused, resolve them to global tray IDs (expected_tray/previous_tray) on both the REST and WebSocket status payloads via a shared resolver: single-AMS passthrough, multi-AMS snow-mapping resolution, AMS-HT/external passthrough, and an honest null when the slot can't be placed. The AMS graphic highlights the expected slot (amber) and the ran-out slot (red); the HMS modal re-describes runout codes to name both, falling back to "check the printer" when unresolved. Runout copy translated in all 11 locales. Reporter @Jostxxl confirmed tray_pre=1/tray_tar=2 during the pause (ran out in Slot 2, printer expected Slot 3). |
||
|
|
47a2a77cd3 |
fix(filament): don't dispatch an unresolved AMS mapping to the external spool (#2589)
A P1S queue row with use_ams=true but ams_mapping=[-1] was silently printed with no AMS, starting against the empty external feed and pausing with a runout. Two faults combined: - start_print treated -1 (unresolved) the same as >=254 (explicit external) when deciding to force use_ams=False. Only genuine external now downgrades; -1 never does. - The scheduler trusted a stored [-1] as "already resolved" and passed it through. It now recomputes from live AMS trays whenever the stored mapping is entirely unresolved, and clears it if nothing matches rather than sending a doomed command. Frontend: the Print dialog no longer serializes an all-[-1] mapping while the printer status is still loading (the hook returns no mapping), and submit waits for AMS status with a "Waiting for AMS status" notice. Tests: new backend + frontend regression coverage; corrected one existing test that pinned the old [-1] -> use_ams=False behavior. |
||
|
|
1555fad539 |
fix(notifications): send Pushover retry/expire for Emergency priority (#2586)
Pushover rejects priority-2 (Emergency) messages unless they carry retry and expire. _send_pushover never sent them, so setting priority 2 always failed with Pushover's "retry and expire are required" error. Now at priority 2 we send retry/expire (default 60s/3600s, clamped to Pushover's 30-10800s range), surfaced as two provider fields shown only when priority is 2. Added PushoverConfig schema fields, i18n labels across all locales, and unit tests. |
||
|
|
00251fe808 |
feat(orca-cloud): pair via RFC 8628 device flow, replacing the paste-based sign-in
OrcaSlicer shipped a first-class external-app pairing API (OAuth 2.0 Device Authorization Grant), so the Supabase-PKCE copy-paste flow is replaced end to end. Connecting is now: click Connect, approve a short code on the Orca Cloud settings page, done — no redirect, no callback paste, no client secret, works from a LAN IP / localhost / behind a proxy. Backend: services/orca_cloud.py rewritten to device-code request + poll (the four RFC outcomes) + refresh_token grant + introspection + external sync pull; routes expose /device/start and /device/poll (device_code kept server-side in the reused orca_cloud_pending_* columns, no migration). Requests sync:read (read-only feature). Prod endpoint by default, ORCA_CLOUD_API_BASE overrides to staging. Wired the shared httpx client (fixes a per-request socket leak). Frontend: device-code connect UI + api client methods; all 11 locales updated. |
||
|
|
e97413edc7 |
fix(queue): enforce sliced-model compatibility on cross-model dispatch (#2578)
A queue item's "Any <model>" button labeled itself from the file's slice metadata while the scheduler used the row's target_model, so an X1C-sliced item targeting H2D showed "Any X1C" above "assign to first idle H2D". The mismatch itself was created silently: sliced-for metadata loads async, and switching to model mode before it arrived pre-selected the alphabetically first model (H2D on a mixed farm), after which the model dropdown hid itself. Nothing validated compatibility, so the scheduler would hand X1C G-code to an H2D. Frontend: never default the target silently, keep the dropdown visible in model mode (incompatible models disabled), label from the actual target, warn on mismatch, block submit when incompatible. Backend: new GCODE_COMPAT_FAMILIES table (X1/X1C/X1E/P1P/P1S interchange; everything else exact-match; missing metadata never blocks). Queue create and update reject incompatible targets with 400; the scheduler holds back pre-existing mismatched rows with an actionable waiting_reason instead of dispatching them. |
||
|
|
095d63b24a |
fix(print-modal): give each plate its own Filament Override, and stop
queueing plates we cannot map (#2552) The override panel disappeared for a multi-plate selection in Any [model] mode, but only once the dialog had been opened before -- which the reporter saw as "after the file was queued or printed". The filament requirements are keyed on the selected plate, which is null as soon as two plates are ticked. On a cold cache the modal cannot yet tell the file is multi-plate and fetches the whole file's requirements for one render; the panel rendered from that union. On a warm cache it knows from the first render, the whole-file fetch never runs, and the panel had nothing to render. Visibility was decided by a cache race, and the "working" case listed filaments from plates the user had not selected. Model mode now renders one panel per selected plate from that plate's own requirements, and each queued plate carries only the overrides for the slots it prints, so a colour forced on one plate no longer blocks another. Reviewing the per-plate machinery turned up four more holes, all closed here: a manual tray pick survived a change of printer, and a global tray id names a different spool on a different machine; a plate whose filaments could not be read was indistinguishable from one needing none and was queued with neither mapping nor forced colours, so Print now waits for every selected plate to answer and names the one it cannot read; the insufficient-filament check still weighed the whole file against a mapping the plates no longer use, and now follows what each plate dispatches, summing demand per tray; and the per-printer tray editor no longer appears for a multi-plate fan-out, where its choices were collected and then discarded. |
||
|
|
b5da9be794 |
fix(print-modal): map each plate on its own, and show the panel that does it (#2551)
Selecting several plates hid the filament mapping panel but did not stop the modal sending a mapping. With no single plate selected it fell back to the whole file's filament list -- the union of every plate -- and matched against that. Tray assignment is stateful, so where plate 1 prints red on slot 1 and plate 2 prints red on slot 2, slot 1 claimed the only red spool and slot 2 fell through to a type-only match on black. That one mapping went out with every plate, and the scheduler uses a stored mapping verbatim, so plate 2 printed in the wrong colour -- decided by a panel the user never saw. Fetch each selected plate's requirements and map them separately: one panel per plate, named after it, with its own tray overrides, and each queue item carries its own plate's mapping. A fan-out across several printers would be a panel per plate per printer, so those items carry no mapping and the scheduler maps each plate against the printer it picks. Model mode is unchanged -- no printer means no trays to map onto. The tray matcher existed twice and this needed a third caller, so extract it once and have both existing paths delegate; its 62 tests pass unchanged. The bug only reproduces with a realistic query cache -- the shared test harness sets gcTime: 0, which evicts the union and makes the modal look innocent -- so the new modal tests bring their own client. |
||
|
|
5bbfeefa65 |
fix(backup): diagnose an unwritable backup path instead of quoting errno 30 (#2544)
Nightly backups to a mounted NAS share ran from May and then stopped, failing with [Errno 30] Read-only file system. The reporter checked folder permissions -- correctly: the mount is gid=backup,dir_mode=0775, the service user is in that group, and his own shell writes to the share fine. Errno 30 is EROFS. A permission problem is errno 13. EROFS means the filesystem refused the write, and it refused because we told it to: our systemd unit ships ProtectSystem=strict, which mounts everything read-only inside the service's mount namespace and carves back out only ReadWritePaths=<install> <data> <logs>. A NAS share is not one of those three. Reads are unaffected -- which is why the UI happily listed his existing backups from the share while being unable to write a new one -- and his shell is outside the namespace entirely, so every check he could think to run said the directory was fine. Both installers write the unit file wholesale, so a ReadWritePaths line added by hand disappeared on the next install, taking the backups with it. They now back the old unit up (.bak-<timestamp>) and carry the operator's extra writable paths forward, reporting which ones they kept. The unit template documents the carve-out. The output directory is probed with a real write when it is saved and when the backup card loads, so an unwritable path is caught there rather than at 03:00 for a week. On failure the card names the cause and hands over the fix with the operator's path already in it (systemctl edit bambuddy -> ReadWritePaths=...), and a failed run reports the same diagnosis rather than the raw OSError. EROFS outside systemd, permission-denied, out-of-space, not-a-directory and missing are told apart, in all 11 locales. Docker: a backup path that is not bind-mounted is writable -- the write lands in the container's ephemeral layer and is lost on the next compose up. The probe compares the directory's device against the container root and warns, with the compose snippet that mounts it properly. |
||
|
|
aba00598bb |
fix(smart-plugs): read a REST plug's lifetime counter, and derive Today/Yesterday from it (issue #2539)
A Shelly reports one energy figure — aenergy.total, a lifetime counter in Wh that never resets. Bambuddy had a single REST energy field and filed whatever it found under "today", so the value never reset at midnight, and Yesterday and Total stayed at zero: get_energy() simply never set those keys. With `total` unpopulated, the hourly snapshot recorder skipped the plug, so the Statistics page's energy figure was zero as well, not just the Settings card. Split the REST energy config in two: rest_energy_path still means "used today", rest_energy_total_path means "lifetime counter". A Shelly has only the latter; a Tasmota behind a REST bridge has both; sharing a URL costs one fetch, not two. Then derive Today and Yesterday from that counter using the snapshots we were already taking: today = counter now - counter at the last local midnight; yesterday = the gap between the two previous midnights. Local midnight, not UTC — a UTC boundary rolls Today over at 02:00 in Berlin. The snapshot loop now ticks on the local hour so a reading lands on the boundary instead of up to an hour early. A counter that goes backwards (factory reset) reports nothing rather than a negative. Collateral, found while verifying on both engines: the smart-plug DateTime columns are naive UTC but the code wrote aware datetimes into them. SQLite drops the offset; asyncpg raises DataError. So on Postgres every snapshot capture raised inside the loop's except, and every status poll raised on last_checked — the whole subsystem was dead on the database we recommend for multi-printer installs. All plug timestamps are naive UTC now. Existing REST users with a cumulative path in the today field must move it to the new lifetime field; the form and wiki now name which counter each wants. |
||
|
|
d09db436c3 |
feat(camwall): serve the Cam Wall at /camwall, and on a token-authenticated kiosk
Cam Wall had no URL — the only way in was the toggle on the Printers page, so it could not be bookmarked, linked, or shown on a wall-mounted screen. Add a standalone /camwall route. Signed in, it is the wall as it was. For a TV or Pi with no login, it authenticates with a long-lived token in the URL. A kiosk needs the printer list and per-printer status, both of which sit behind PRINTERS_READ. Rather than widen camera_stream to cover GET /printers — whose response carries serial_number and ip_address, which have no business on a screen in a shared room — add a read-only feed at GET /api/v1/camwall/printers that serves only what a tile draws, and gate it on a new camwall token scope. The print filename is not served at all: a token wall renders the compact overlay, so the part on the bed is never named. The scope is separate rather than a widening: camera_stream tokens are already in the wild, minted to hand out video, and must not gain the ability to enumerate a fleet by name. camera_stream is refused by the feed; camwall passes the stream gate so its own tiles fill. Kiosk walls drop the settings popover and click-through entirely (not merely hidden — a passive screen must carry no focusable control it cannot act on), cap the overlay at compact, and poll rather than open a WebSocket. maxLive, interval and status can be set from the URL, clamped to the popover's ranges. |
||
|
|
f6c6cfbad3 |
fix(ams): show "?" not "Empty" for non-RFID spools using tray_exist_bits (#2527)
A spool with no readable RFID was reported by the standard AMS with an empty tray_type and state=9 — structurally identical to a truly-empty slot at the tray level — so the AMS card rendered it "Empty" while Bambu Studio correctly showed "?". The authoritative "a spool is physically here" signal is firmware's AMS-level tray_exist_bits bitmask (what Studio uses), but Bambuddy inferred emptiness from the per-tray state/tray_type. Confirmed from the reporter's bundle: tray_exist_bits=f (all four slots present) with tray_is_bbl_bits=5 (only slots 0,2 Bambu) — the present-but-non-Bambu slots were the ones shown Empty. Supersedes closed #1838. apply_tray_exist_bits() already parses the bitmask to clear stale fields on absent slots; it now also annotates each slot with an authoritative `exists` bool, gated behind a new annotate_exists flag so only the printer-card path sets it. The VP bridge leaves it off, so the `exists` key never reaches the slicer wire format. `exists` flows through the AMSTray schema/serialization to the frontend, where getEmptySlotKind() uses it: exists===true + no tray_type -> "?" (present, unconfigured), exists===false -> "Empty", exists absent -> the previous state=9/10 heuristic (AMS-HT and missing-bitmask paths unchanged). H2D/X1C already reported present-unknown slots with a non-9 state and took the "?" path; with the fix they reach it via `exists` and are unaffected. |
||
|
|
6127e30abf |
fix(diagnostic): skip external-storage check on P1S/P1P instead of fail (#2524)
P1-series printers have a MicroSD slot but no reachable control to enable "Store sent files on external storage": current P1 firmware (through 01.10.00.00) never publishes support_save_remote_print_file_to_storage, so the Bambu Studio toggle never renders, and the P1S has no screen — leaving store_to_sdcard stuck False with no way for the user to change it. The external_storage check reported a permanently-unresolvable fail. Add NO_REMOTE_STORAGE_TOGGLE_MODELS (P1S, P1P) + has_remote_storage_toggle(), kept distinct from the no-slot NO_EXTERNAL_STORAGE_MODELS. When a model has a slot but no reachable toggle and the option is off, the check now emits skip with params reason=unsupported_model rather than fail, and overall no longer escalates. A P1S reporting the option on still passes. Model-scoped and default-open, so X1/P2S/H2 (where the fail is actionable) are unaffected; if a future firmware surfaces the capability, drop the model and it reactivates. The frontend DiagnosticChecklist renders a reason-specific message variant (external_storage.skip_unsupported_model) so P1 users see an accurate explanation instead of the generic "needs a live MQTT connection" skip text. The fix propagates to the support-bundle diagnostic snapshot automatically. |
||
|
|
1603f52a07 |
Dock Folder README as a collapsible right rail instead of a top block (#2520)
The README panel rendered full-width above the file grid, pushing model files below the fold with no page scroll to get past it. On lg+ it now docks as a fixed-width right-hand column beside the list (own full-height scroll); on mobile it stacks on top and the page scrolls. Added a collapse toggle (thin strip / slim bar + one-click reopen) with the choice persisted to localStorage. New i18n keys readme.show/hide/label across 11 locales. |
||
|
|
917bfd7666 |
feat(labels): scannable QR on 203 dpi thermal printers + monochrome mode (#1870)
The 40x30 mm box label rendered its QR too densely for low-res thermal printers — the modules bled together and wouldn't scan. Two causes: the QR was 20% of inner width (~7.5 mm on the narrowest template, half of the others) and used ERROR_CORRECT_M. Fix adaptively so all templates benefit: give the roomy-layout QR a 12 mm minimum size (box_40x30 -> 12 mm, ~3.5 dots/module at 203 dpi) and switch label QRs to ERROR_CORRECT_L (same payload, chunkier modules; a label needs no M-level recovery). Keep the quiet-zone border at 2 — the size+L gains suffice without risking scans. Also add a Monochrome (black & white printer) option to the label dialog: drops the colour swatch (a useless grey block on B&W) and widens the text; the hex-code line still carries the colour. Threaded through the renderer, route, API client, and modal, with translations in all 11 locales. |