Commit Graph
1071 Commits
Author SHA1 Message Date
maziggy 4888485a54 fix(camera): redact credentials, contain failures, and stop the external-camera test claiming a connection it never opened
Review follow-ups on the external-camera capture coalescing.

The coalescing was transplanted from camera.py, which is keyed by printer IP
and so has nothing to hide in a log line. These keys carry the camera URL, and
an RTSP camera URL routinely embeds user:pass@ - so the five new log lines
printed the password, one of them at warning level, where it reaches support
bundles. All five now go through _log_key(), which redacts before truncating:
slicing first can cut the URL short of the @ the pattern anchors on and leave
the password intact, which is why every other URL log in the module already
does it in that order.

_capture_frame_uncoalesced gained the blanket catch its camera.py counterpart
has. That is load-bearing once captures are shared: the wrapper hands one
task's outcome to every caller waiting on it and can only give a follower its
own turn for an outcome it recognises, so an escaping exception reached all of
them at once and none retried - one caller's failure becoming N. The per-type
helpers catch narrowly (aiohttp.ClientError / OSError / timeouts), so the
guarantee belongs here rather than resting on their coverage. CancelledError
is re-raised ahead of it, since the wrapper distinguishes a cancelled leader
from a failed one.

test_connection reports whether it shared a capture. It reaches capture_frame
like any other consumer, so a test landing while Obico is polling got that
frame back and answered "connected" for a connection it never made - the one
answer a connection test must not give silently. It still shares rather than
forcing its own capture, because forcing one would open the second handle to a
single-reader device that this whole mechanism exists to prevent. The response
carries `coalesced`, which also gives capture_in_flight() the consumer its
camera.py counterpart has in the Diagnose tool, and the Test button says
"shared with a capture already running" instead of a bare success.

Tests 12 -> 20: an unexpected error reported as a failed capture, a raising
leader whose follower still gets a frame, the three coalesced states, and
redaction on each log line that can carry a URL. The raising-leader test
patches _capture_rtsp_frame rather than _capture_frame_uncoalesced, since a
stand-in installed in the latter's place sits above the catch and would test
the wrapper against a shape it can no longer be handed.
2026-07-31 08:47:57 +02:00
maziggy ac3e3cc60f fix(printers): don't retract a fan kit on a partial airduct frame
device.airduct is pushed field by field - the modeCur handler reads it with
an "in" check for that reason - so a frame can carry parts without carrying
every fan. Absence in that list is what tells us a kit is not fitted, and
taken from a truncated frame it made both accessory badges vanish mid-print
and started rejecting fan=aux2 on a printer that has the fan.

A parts list now counts as a full inventory only when it carries ids 1 (part
cooling) and 2 (aux). Neither is optional on a machine that reports an
airduct at all, and both appear in every layout in the support-package
archive - P2S base 1,2 / P2S+kit 1,2,3 / X2D 1,2,3,10 / H2C,H2D,H2S 1,2,3,6.
Anything narrower is a diff frame: its speeds are applied, presence is left
alone. Presence can still be added from a partial frame; only retraction
needs the full list, so a kit that really is removed still disappears.

Also compose showChamberFan from both model lists rather than branching
between them, so the P2S/X2D entries in MODELS_WITH_CHAMBER_FAN stay
reachable instead of reading as dead, and note in the fan-speed docstring
that the aux2 gate also rejects between connect and the first airduct push.
2026-07-30 18:04:38 +02:00
maziggy 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.
2026-07-30 17:34:26 +02:00
maziggy 83142c726c fix(slice): report a finished slice once, not once per queued poll
setInterval does not await an async callback. Slicing a large project
blocks the backend for seconds, so poll ticks piled up behind one stalled
request, each holding a snapshot taken while the job was still active.
They resolved together, and every one of them ran the completion path —
one toast and two query invalidations each. A 20s stall against the 1.5s
interval produced 13 "Sliced X" toasts from a single slice.

Only one poll round is now in flight at a time, which also stops queueing
requests against a backend that is already saturated. Completion is
recorded once per job id, and a round still awaiting a response when the
effect tears down now returns instead of acting.
2026-07-30 17:14:07 +02:00
maziggy 9ef06449ef fix(filament): a unique preset match no longer counts as a colour match (#2687)
The Filament Mapping panel reported "(Ready)" with a green tick for a slot
where the slice wanted dark red and the auto-matched tray held dark green.
Manually picking that same tray reported the mismatch correctly, which is
what made it obvious something was inconsistent.

Auto-match ranks candidates by tray_info_idx first, and a uniquely-matching
preset was accepted as definitive on the premise "same preset = same spool =
same colour". The preset names the variant, not the spool: GFA00 is PLA
Basic, GFA01 PLA Matte, GFA17 PLA Translucent, in every colour Bambu sells.
The reporter's own bundle has eight GFA00 trays in eight colours. With one
Matte spool loaded, every Matte requirement idx-matched it and the colour
comparison was never reached - which is why this surfaced on PLA Matte and
not on Basic, where several spools are usually loaded and the match falls
through to the branch that does compare colours.

The verdict now comes from the tray that was selected rather than from which
rule selected it, and both branches share one comparison so they cannot
drift apart again. Selection is unchanged - the right variant still wins per
mismatch and the slot stays selected.

A requirement with no colour at all is treated as satisfied rather than
mismatched; 3MFs that omit it parse to "" and there is nothing to disagree
with. That also affects the manual branch, which used to flag it.

No dispatch change: _get_missing_force_color_slots already required an exact
colour, so force colour match was gated correctly throughout.
2026-07-30 09:26:46 +02:00
maziggy 9ef03067f7 feat(file-manager): show last activity on folder rows via the existing date toggle (issue #2680)
Follow-up to #2680: the calendar toggle only put dates on the file pane, so
the folder tree had no way to show the timestamp it was already sorting on.
FolderTreeItem now takes showModified and renders latest_activity_at under
the folder name, threaded through the recursive call so nested folders get it
too. No backend change - the field was already on the wire from the sort fix.

Folders are labelled "last activity", not "last modified", and get their own
i18n key. The value is the newest timestamp among the folder, its files and
everything below it, so a folder can read as newer than its own directory
mtime - calling that "modified" would look like a fresh instance of the
ls -lt mismatch the issue was originally about. Folders with no activity
render nothing rather than an Invalid Date placeholder.

The name span moved into a flex column so the second line does not disturb
the row's link badge, file count or kebab menu. That broke a folder-delete
test that reached the row via parentElement, now fixed to use closest().

Separately, the #996 collapse describe left an implementation on the
module-global localStorage.getItem mock, which silently collapsed the folder
tree for every describe after it. It resets in afterEach now; without that,
any later test asserting on nested folders fails for reasons unrelated to
what it is testing.
2026-07-30 09:09:47 +02:00
maziggy 24322c71cb fix(tab-progress): drop the redundant status poll and quieten the test suite
The hook is mounted globally in WebSocketProvider, so refetchInterval on its
per-printer status queries added one request per printer every 30s on every
page. The Printers page already runs that fallback on the same query key, and
useWebSocket writes ['printerStatus', id] straight into the cache, so the poll
bought nothing outside the Printers page and cost a request per printer per
tab everywhere else.

Also captures document.title at mount instead of restoring to a hardcoded
'Bambuddy', so the default no longer has to be kept in sync with index.html.

jsdom has no canvas backend, so getContext('2d') logged a "Not implemented"
jsdomError with a full React stack on every run of the hook's tests, and the
favicon branch bailed on the null context and went untested. Stubbing
getContext/toDataURL removes the noise and lets the ring code run, so the
favicon swap and the restore-on-toggle-off path are now asserted.
2026-07-29 14:38:18 +02:00
maziggy 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.
2026-07-29 14:10:38 +02:00
maziggy c11bcecfd8 fix(i18n): list Ukrainian after Russian in the language picker
The uk locale was inserted before ru in i18n/index.ts - in the import block,
the resources map and availableLanguages. Locales are appended to those lists
as they land (tr, then ru on 2026-07-19, then uk on 2026-07-28), and
SettingsPage renders availableLanguages in array order, so the picker showed
Ukrainian above Russian while every other entry followed the order it was
added.

Moves uk to the end of all three lists. SUPPORTED_LNGS is sorted
alphabetically and already had uk in the correct position, so it is unchanged.

Frontend-only, no behaviour change beyond the picker's row order.
2026-07-29 13:22:24 +02:00
maziggy 91269f14fe fix(mqtt): report why a printer refused the connection instead of looping silently
A printer with a wrong access code gave no explanation anywhere. The connect
callback's failure branch was a bare `state.connected = False`, discarding the
CONNACK reason code the printer had just sent, so the only trace was paho's
follow-up disconnect -- logged every 30 seconds as "rc=Unspecified error",
which is exactly what a powered-off printer produces. In the report behind this
fix one of three printers had been in that loop for the whole capture, and
neither the log nor the support bundle could say why.

Bambu speaks MQTT 3.1.1, whose CONNACK return codes 4 and 5 paho maps onto
reason codes 134 and 135. Both are now logged with the printer's own reason
string and, for those two, the remedy: the access code is regenerated whenever
LAN Only or Developer Mode is toggled, so it has to be re-read from the screen.
The access code itself is never logged -- it would land in every bundle.

The reason is kept on the client as a stable slug and plumbed through
test_connection into the connection diagnostic, which now distinguishes two
cases it previously conflated. "The printer refused our credentials" is
asserted only when the printer said so; when all Bambuddy knows is that there
is no session, the text hedges and names the alternatives (rebooting, or
already at its limit of simultaneous connections). The old wording claimed the
access code was most likely wrong in both cases.

Frontend needed no change -- ConnectionDiagnostic already renders
`<status>_<reason>` variants with fallback to the plain per-status text, so an
unrecognised slug degrades to today's wording rather than a missing key.
2026-07-29 09:02:39 +02:00
maziggy c3cee54c0c fix(i18n): bring the Ukrainian locale up to parity and drop two English strings
The Ukrainian locale was authored before #2622 landed and merged dev in
without picking up the five slice.designSettings* keys that came with it, so
check:i18n failed at 5675 leaves against en's 5680. This is invisible on the
PR because ci.yml only triggers for pull requests targeting main, and this one
targets dev - the only checks that ran were the security workflow's.

Adds the five missing keys, following the file's own convention of rendering
"designer" as "автора" as it already does in slice.useEmbeddedHint.

Two values were still English and the parity gate could not see them, because
check 4 only fires on leaves byte-identical to en: profiles.localProfiles.
pressureAdvance and inventory.paProfileTab both read "Pressure Advance (PA)"
where en has "Pressure Advance" and "PA Profile". The added "(PA)" was enough
to walk past the equality test. Both are now translated, the second as a tab
label matching the Russian. Inline mentions of the term in profiles.subtitle
and kProfilesDescription stay in English, which is what ru does too.

failureDetection.mlUrl is translated rather than allow-listed, and the dead
'{{weight}} г' entry is dropped from UK_COGNATES - that list matches against
the English value, so an entry spelled in Ukrainian could never fire, and
since the unit is now localized no entry is needed at all.

Four straight apostrophes normalized to the typographic form used by the
other 186 in the file. Locale count in the README and the two wiki pages
corrected from 11 to 13; it had already been wrong, omitting Russian.
2026-07-28 15:27:04 +02:00
MartinNYHC 30d26efb3d Merge branch 'dev' into feature/ukrainian-localization 2026-07-28 15:13:34 +02:00
maziggy 8551e32f14 feat(slicer): keep the designer's print settings when re-slicing for another printer (#2622)
Published models often deviate from the stock Bambu profile on purpose -
five walls, 100% infill, a 0.1mm first layer. Re-slicing one for a
different printer discarded all of it: the picked process preset
overrides the file's embedded settings, and that override is precisely
what makes cross-printer re-slicing work, so it cannot just be dropped.
"Slice as designed" (#2611) does not help - it is all-or-nothing and
only offered when the picked printer already matches the design's
target.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Add installedNozzleDiameters(status) and a shared fetchPrinterCalibrations()
that queries every reported nozzle diameter and merges the results, falling
back to 0.4 when the printer hasn't reported nozzle hardware. Each profile row
now shows a nozzle-diameter badge so identically-named profiles are distinct.
2026-07-27 13:08:57 +02:00
maziggy 0bc98beac5 fix(queue): paginate History with Show more instead of a hard 50-cap (#2682)
The Print Queue History tab reported the full count in its header (e.g.
"History (311 items)") but the row builder hard-sliced the list to
items.slice(0, 50) with no control to load the rest, so everything past the
50th finished print was unreachable. The whole history is already loaded
client-side (the queue endpoint has no limit) and sorted -- it just wasn't
drawn.

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

Frontend-only; batch grouping and per-row actions unchanged. Two new i18n
keys across all 12 locales. Covered by a test asserting the 50-row cap, the
count label, and that Show more reveals the remainder. Wiki updated.
2026-07-27 12:04:57 +02:00
maziggy 1bdd7d224a fix(library): sort File Manager by real filesystem mtime, recursively (#2680)
The folder tree's "sort by recent activity" and the file pane's date sort
put external (mapped/NAS) files in a near-random order instead of ls -t's
newest-first. Nothing captured the files' on-disk mtime: the sort keyed off
the DB updated_at/created_at, which for a bulk external scan is the same
scan instant for every row, so a whole block tied and sorted arbitrarily;
only rows Bambuddy had later touched individually looked "partially right."
The tree also bubbled up only immediate child-file activity, so a file added
deep in a subtree never lifted its parent folders.

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

Store the mtime as naive UTC to match the other timestamp columns so activity
comparisons never mix naive and aware values on either dialect. Covered by
integration tests (mtime capture, re-scan refresh, deep-file recursive bubble,
folder mtime) and a frontend test proving fs_modified_at is preferred over
created_at.
2026-07-27 11:01:43 +02:00
maziggy aa443c6e83 fix(print): keep filament gram usage visible when the name is long (#2669)
In the Print dialog's Filament Mapping, each required filament shows its
name and the grams the job needs, e.g. "Bambu PLA Basic (281.2g)". Name and
grams shared one fixed-width column with truncate on the whole string, so a
long name pushed the "(...g)" off the end and clipped it -- partially on a
wide screen, entirely in mobile portrait. The gram usage is the number that
matters (does the spool have enough left?), so it shouldn't be the part that
gets dropped.

Pin the gram usage (shrink-0, whitespace-nowrap) and let only the name
truncate, with the full name on hover. Applied to both the Specific-Printer
(FilamentMapping) and Any-model (PrinterSelector) panels. Layout only.
2026-07-27 09:22:14 +02:00
maziggy db6d306b35 fix(queue): mobile tap-to-reorder with up/down arrows (#2667)
The print queue couldn't be reordered on a phone. In portrait the drag
grip and selection checkbox are hidden below the sm breakpoint, so there
was no reorder affordance at all; in landscape the grip shows but it
carried touch-action: manipulation while the only dnd-kit sensor is a
PointerSensor with an 8px distance, so touch gestures scrolled instead of
starting a drag. Reordering was effectively mouse-only.

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

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

The printer-card queue-compatibility hint applies the same variant rule.
2026-07-24 13:13:04 +02:00
maziggy a273cd3eec security(frontend): bump react-router-dom to 7.18.1 for patched release
Moves react-router-dom/react-router 7.16.0 -> 7.18.1, off the range
flagged by GHSA-wrjc-x8rr-h8h6 (open redirect via backslash in Link/
useNavigate), GHSA-h8fp-f39c-q6mh (RSC), and GHSA-337j-9hxr-rhxg (SSR
hydration). The latter two need RSC/SSR, neither of which this
client-only SPA uses; the open-redirect one is the only reachable path
(post-login redirect), already guarded by sanitizeRedirectTarget.

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

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

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

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

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

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

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

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

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

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

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

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

The offline mark is now an explicit presumption: mark_power_off records the
state it overwrites and _on_message undoes it as soon as the printer sends
another report on its own topic, since inbound traffic proves the power was
never cut. A reconnect discards the saved state, so a genuine power cut is
unaffected. Each plug also gains a controls_printer_power flag (default true,
backfilled) that gates all five power-off paths, and the queue's power-on step
now picks the flagged plug instead of whichever linked plug came first.
2026-07-22 09:57:27 +02:00
maziggy fb11adc8fb fix(a2l): normalise AMS Lite unit 16->6 so slots load and deduct (#a2l-am-unit-16)
The A2L reports its 4-slot AMS Lite as unit id 16, but its slot-presence
bitmasks sit at bit base 24 (id 6) and it reports tray_now as a local 0-3
slot. Fed the raw id 16, the ams_id*4+slot convention probed bits 64-67
(always zero) and marked loaded slots empty; the local tray_now was read as
global, so usage deducted from the wrong spool (or not at all); and the
ams_id<=7 DB constraint rejected id-16 Spoolman links.

Normalise the Lite 16->6 at the MQTT ingest boundary so global tray ids land
at 24-27 - matching the firmware's own bit base, working with every existing
ams_id*4+slot consumer, colliding with nothing, and passing the DB
constraint. Globalise tray_now to 24+slot, widen the valid-tray guards, label
the unit "AMS Lite", and build the confirmed ams_mapping2 {ams_id:16,
slot_id:0-3} / flat 0-3 for dispatch. Outbound slot commands translate 6->16
on the wire via a single helper. Self-scoping: only unit id 16 is touched, so
all other printers/AMS types are unaffected. One uncaptured wire field (the
physical global tray on load/cali) is extrapolated and isolated to the helper.
2026-07-21 10:21:11 +02:00
maziggy 2e74f2ad41 feat(ams): confirm spool assignments landed instead of fire-and-forget (#2582)
Assigning a spool to an AMS tray pushed ams_filament_setting +
extrusion_cali_sel and reported success immediately, whether or not the
tray accepted it. A silently-dropped assignment never surfaced, and since
a print only deducts from the spool on the exact tray it pulls from, it
also recorded no filament usage - which made the whole thing feel random.

Read the AMS telemetry back after every assign (inventory assign_spool and
the Configure Slot modal) and toast the outcome: loaded when the tray
echoes the pushed tray_info_idx, a warning when the filament loaded but the
K-profile (cali_idx) did not, or not-confirmed after ~30s. Verification
uses the periodic per-tray push (the command ack hardcodes sequence_id 0
and can't be correlated); an on-demand pushall is nudged so it lands
quickly. Covers regular AMS, AMS-HT and external slots; stays silent rather
than inventing a failure if the printer goes quiet. The read-back check
runs on every AMS push because the change-hash excludes tray_info_idx.
2026-07-21 08:46:02 +02:00
maziggy 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
2026-07-20 17:55:56 +02:00
maziggy 8192dce65c . 2026-07-20 14:13:30 +02:00
maziggy 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.
2026-07-20 13:41:41 +02:00
maziggy 258db95483 fix(overlay): authenticate the OBS overlay with a token when login is enabled (#2613)
The /overlay/{id} route renders without a login, but everything it draws is
auth-gated: printer status and name (PRINTERS_READ), one setting (SETTINGS_READ),
and the camera stream (a camera-stream token). A signed-in browser rides its JWT
from local storage; OBS is a fresh browser with no session, so the overlay stayed
blank whenever authentication was enabled. Cloudflare/remote access was never the
cause -- an incognito window fails identically.

Give the overlay a self-contained kiosk-token mode, mirroring the Cam Wall:

- New `overlay` long-lived-token scope, kept separate from `camwall`: the overlay
  names the printed file on screen, which a Cam Wall token is trusted never to
  expose, so folding it in would silently widen every existing wall token.
- New token-authed GET /printers/{id}/overlay-status returning exactly the fields
  the overlay draws and nothing else; added to the auth-middleware allowlist so it
  reaches its own RequireOverlayTokenIfAuthEnabled gate.
- StreamOverlayPage reads ?token= and, in that mode, authenticates its status and
  camera calls with the token and skips the WebSocket (the 2s poll is the feed).
  The logged-in path is unchanged.
- Token-mint UI (Settings > API Keys) offers the scope with a ready-made
  /overlay/{id}?token= URL copied once on creation.
2026-07-20 13:04:35 +02:00
maziggy 4ef6772c51 fix(ui): keep the progress toast on-screen in the installed iPhone PWA (#2612)
The dispatch progress toast is a fixed 420px wide and the toast viewport is
anchored 80px from the right (to clear the bug-report bubble). On a 390px-wide
phone that overflows the left edge by ~110px, so in the Home-Screen PWA the
toast was clipped off the left, with text bleeding past the edge.

Cap every toast to a viewport-relative max-width (calc(100vw - 6rem - safe-area
insets)) so it can't exceed the screen; desktop keeps the 420px. Make the
viewport position safe-area-aware (env(safe-area-inset-*) on bottom/right) so an
installed PWA clears the home indicator and a landscape notch, and add
min-w-0/shrink-0 to the per-job filename row so long names truncate instead of
widening the toast at the narrower phone width.

Frontend-only; no backend, schema, or i18n change. Covered by a test pinning the
width cap; the suppression test's viewport lookup moved to a stable data-testid.
2026-07-20 11:05:39 +02:00
maziggy 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)
2026-07-20 10:50:15 +02:00
maziggy 83a7b75b14 fix(queue): persist selected plate to the archive; reconcile archive on offline stop (#2603)
A print queued from a specific plate of a multi-plate 3MF showed as Plate 1
in Print History after cancellation: the archive derives its plate from the
filename, but a whole multi-plate 3MF uploads under one name with no plate
suffix, so the parser defaulted to plate 1 and nothing copied the queue
item's plate_id onto the archive (which had no plate field).

Add a nullable print_archives.plate_id, copy it from the queue item at
dispatch (archive- and library-file paths), expose it in the archive API,
and render it in Print History. A startup backfill copies the plate onto
existing archives from their linked queue rows. Column add + backfill are
identical on SQLite and Postgres.

Also fix a related lifecycle bug: stopping a printing item while the printer
was offline left the linked archive stuck at "printing" (queue row
cancelled, but no MQTT completion ever arrives to reconcile the archive).
The offline-stop path now closes the archive out directly; the online path
still defers to the MQTT completion event.
2026-07-19 09:29:54 +02:00
maziggy 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).
2026-07-18 13:06:01 +02:00
maziggy 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.
2026-07-18 08:11:44 +02:00
maziggy 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.
2026-07-17 10:02:19 +02:00
maziggy 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.
2026-07-17 08:16:35 +02:00