Commit Graph
2498 Commits
Author SHA1 Message Date
maziggy ccf985abfc feat(slicing): build-plate override in the SliceModal (#1337)
Slicing an STL via the integrated slicer always defaulted to whatever
  curr_bed_type lived in the chosen process preset (typically "Cool
  Plate"), which the slicer CLI rejected for high-temp filaments with
  "Plate 1: Cool Plate does not support filament 1". The user had no
  way to switch plates without cloning the preset in BambuStudio.

  The Slice modal now exposes a Build plate dropdown with the six
  canonical BambuStudio / OrcaSlicer plates (Cool Plate, Cool Plate
  SuperTack, Engineering Plate, High Temp Plate, Textured PEI Plate,
  Smooth PEI Plate) plus an "Auto (use process preset)" option that
  preserves the previous behavior. Positioned between Process profile
  and Filament rows so a long filament list never pushes it off the
  modal's scrolled viewport, and always enabled regardless of whether
  the user picked a Printer Preset Bundle.

  A new bed_type field on SliceRequest flows through both dispatch
  paths:
  - Resolved-preset path: _patch_process_bed_type overwrites
    curr_bed_type on the process JSON before forwarding to the sidecar.
    Works end-to-end today, no sidecar change needed.
  - Bundle dispatch path: slice_with_bundle adds a bedType form field
    to the sidecar multipart. The sidecar (maziggy/orca-slicer-api
    fork) needs a matching change to honor it as --curr_bed_type on
    the CLI invocation; until then the field is silently ignored and
    the slice runs with the bundle's default plate.
2026-05-14 15:28:41 +02:00
maziggy b51ef33472 fix(inventory): apply catalog color's gradient + effect, not just hex (#1340)
Picking a color preset from the catalog only copied color_name and
  rgba onto the spool — extra_colors (gradient stops) and effect_type
  (sparkle / wood / etc.) were silently dropped at three layers above
  the API: the SpoolFormModal state shape, the CatalogDisplayColor
  mapping in ColorSection, and the selectColor handler itself. All
  three widened to carry both fields through.

  Picking a catalog swatch now writes both fields from the entry, so
  solid presets cleanly replace previous gradients. Recent-colors and
  the hardcoded-fallback palette stay as plain hex pickers — they
  don't touch extras/effect since they aren't full presets.

  Also fixed the en-US `colour` → `color` drift in 8 locale files
  that the reporter flagged.
2026-05-14 14:52:18 +02:00
maziggy f45aaea97c fix(inventory): assign to AMS slot on firmwares that never report state=11 (#1322)
A1 Mini BMCU (01.07.02.00) and P1S Standard AMS (00.00.06.75) always
  report tray.state=3, even for loaded configured slots. The empty-slot
  detection preferred state==11 with tray_type as a fallback only when
  state was absent, so every assign was classified as empty and MQTT
  was skipped — both for "assign to unconfigured slot" and the secondary
  "PETG over a PLA-configured slot won't reconfigure" symptom.

  Empty-slot detection in the assign route and the on_ams_change replay
  now treats the slot as loaded when EITHER state==11 OR tray_type is
  non-empty. Reset-slot case (state=11 + tray_type="") still works
  through the first clause; configured slots on these firmwares now
  work through the second.

  Truly empty unconfigured slots (state!=11 + tray_type="") still hit
  the pending-config path, and the deferred publish now fires when the
  user later configures the slot in Bambu Studio (tray_type goes
  non-empty), since the replay uses the same disjunction.
2026-05-14 14:36:57 +02:00
maziggy 5c17cd4973 fix(inventory): restore Spoolman spool ID search + Unassign button (#1336)
Two regressions reported in #1336:

  1. spoolMatchesQuery did not include spool.id in the predicate, so
     typing a numeric Spoolman ID into the Assign Spool dialog or the
     Inventory page search returned no matches. Predicate now also
     tests String(spool.id).includes(q).

  2. The Unassign button in the spool edit modal was permanently
     disabled for Spoolman-mode spools. The modal only ever queried
     the legacy spool_assignments table (keyed by spool_id), but in
     Spoolman mode the assignment lives in spoolman_slot_assignments
     (keyed by spoolman_spool_id). Both the lookup query and the
     unassign mutation now branch on the spoolmanMode prop and call
     the Spoolman-flavored endpoints.
2026-05-14 14:19:12 +02:00
maziggy 4498156555 Updated BACKERS.md 2026-05-14 14:01:34 +02:00
maziggy 47c8d4d827 Updated README 2026-05-14 13:44:15 +02:00
maziggy 9400f2fab8 Updated BACKERS.md 2026-05-14 13:40:14 +02:00
maziggy fc58d28853 Updated BACKERS.md 2026-05-14 13:36:26 +02:00
maziggy db951de2c2 Updated CHANGELOG 2026-05-14 09:16:50 +02:00
a1d6fb22ae fix(spoolman): filter external library lookup by Bambu Lab manufacturer (#1330)
Bambuddy's external SpoolmanDB lookup in `_find_or_create_filament` matched
on material+color only, with no manufacturer filter. Because SpoolmanDB is a
multi-vendor catalog and entries are roughly ID-sorted, the first hit for
any common combination is almost always a competitor — `bambulab_pla_black_1000_175_n`
is the 15th entry for PLA + `#000000`. Bambu Lab RFID spools were being
labeled with competitor product names (`3DJAKE Black`, `3DXTECH™ Black`, etc).

Restrict the external-library loop to entries whose manufacturer is
`"Bambu Lab"` (with `id.startswith("bambulab_")` as a defensive fallback
for schema drift). When multiple Bambu Lab candidates exist, prefer the
entry whose `name` equals the AMS `tray_sub_brands` so `"PLA Basic"` wins
over generic `"Black"` when both are present. Forward `density` from the
chosen external entry so it is no longer overwritten by the PLA-default
1.24 in `create_filament`.

Six unit tests added: internal short-circuit preserved, non-Bambu external
entries skipped, PLA Basic > generic PLA tiebreaker, no-match fallback,
id-prefix defensive fallback, density propagation.

Fixes #1309

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: MartinNYHC <mz@v8w.de>
2026-05-14 09:14:24 +02:00
maziggy a2c9eef87c fix(safety): invert bed-jog Z direction on A1 / A1 Mini bed-slingers (#1334)
On A1 / A1 Mini, clicking the "Up" arrow on the printer-card bed-jog
  control sent the nozzle straight into the build plate. Reporter
  triggered it with the 50 mm step and crashed their nozzle.

  Root cause: the bed-jog UI was designed against the X1 / P1 / H2 family
  where the bed is the Z-axis and Bambu's firmware homes Z=0 at the top,
  so G1 Z- raises the bed toward the toolhead (decreases the nozzle-bed
  gap). The frontend maps "Up" to negative distance with that convention
  in mind.

  A1 / A1 Mini are bed-slingers: bed moves on Y, toolhead moves on X+Z,
  firmware uses standard cartesian Z (Z+ = toolhead up). On those models
  G1 Z-10 drives the toolhead DOWN 10 mm. There was no model
  classification at the bed-jog code path, so every printer got the same
  X1-convention G-code.

  Fix: new is_bed_slinger(model) helper in printer_manager (sibling to
  existing supports_chamber_temp / has_stg_cur_idle_bug, reuses the
  already-defined A1_MODELS frozenset which covers display names and
  internal codes N1 / N2S). The bed-jog route now inverts the signed
  distance before emitting G-code when the printer model is in that set,
  so UI "Up" semantics ("decrease nozzle-bed gap") stay consistent
  regardless of which physical part moves. Frontend untouched, single
  source of truth lives in the backend, keyed off the Printer.model
  column. Route Query description and docstring updated to spell out the
  new contract: distance is the gap adjustment, not the raw Z value.
2026-05-14 08:58:33 +02:00
maziggy b8e350c3bf fix(spoolman): persist color_name edits and stop form round-tripping the subtype synth fallback (#1319)
Editing a spool's color name on Spoolman-backed inventory appeared to
  accept the new value but the inventory list column and the next edit
  showed it back to the subtype. Three layers stacked to produce this:

  1. find_or_create_filament matches by material/name/color_hex/vendor —
     color_name is intentionally not part of the match key, but on a
     match it returned the existing filament's id unchanged, silently
     dropping the new value.
  2. The read helper falls back to subtype when filament.color_name is
     empty (kept on purpose: without it Spoolman installs that don't
     fill the field render every spool as "Unknown color").
  3. The edit form prefilled color_name from spool.color_name — which
     on those installs was the synth value. Changing subtype but not
     color_name silently round-tripped the OLD subtype back to Spoolman
     as if it were a real user-set color_name.

  Fixes:
  - find_or_create_filament now patches the matched filament's
    color_name via the existing patch_filament wrapper when the request
    differs. Parameter convention: None = don't touch, "" = explicit
    clear, any other string = set/update. A patch failure is logged but
    does not block the match.
  - The PATCH route uses model_fields_set to distinguish "field omitted"
    from "field explicitly set to null" (mirrors the existing
    storage_location pattern at the same site).
  - The map helper returns color_name_is_synthesized: bool. The edit
    form leaves the input blank when true, so the user sees the real
    stored state and can't accidentally round-trip the synth value back.
2026-05-14 08:27:06 +02:00
maziggy c519b73e29 Post work PR #1295 2026-05-13 13:27:47 +02:00
Sn0rrii 4d8dbc8336 fix(auth): cleanup orphan OIDC/MFA rows when user is deleted (#1285) (#1295)
fix(auth): cleanup orphan OIDC/MFA rows on user delete (#1285)

Three User-FK tables (user_oidc_links, user_totp, user_otp_codes)
declare ON DELETE CASCADE in their models, but SQLite ships with
PRAGMA foreign_keys=OFF (the project's existing pattern, mirrored
for APIKey in PR #1182). Without explicit DELETEs, deleting a user
on SQLite leaves orphan rows behind:
2026-05-13 13:23:21 +02:00
maziggy 52d6ac419a feat(support): record slicer CLI versions; harden sidecar update docs
Issue #1312 follow-up. Investigation traced the "Name cannot be empty"
  report to a sidecar image pre-dating the /profiles/bundle endpoint
  addition. Two changes so the next occurrence is self-diagnosable from
  the support bundle without a manual curl.

  Backend: new _fetch_slicer_health(url) helper does a 2s GET on /health,
  walks every non-dataPath key under checks looking for a version field
  (the wrapper labels both sidecars as checks.orcaslicer regardless of
  which CLI is bundled). _collect_slicer_api_info now exposes
  bambu_studio_version and orcaslicer_version. Strips trailing slash
  before appending /health to avoid double-slash 404s.

  Docs: bambuddy-wiki/docs/features/slicer-api.md gains a Quick Start
  callout that branch-built sidecars don't auto-update, a corrected
  /health troubleshooting entry (both "unknown" version and "orcaslicer"
  field name on bambu-studio-api are cosmetic wrapper bugs, not stale-
  image indicators), a new "Name cannot be empty" troubleshooting entry,
  and an Updating section that requires --no-cache --pull together
  (BuildKit caches the git context separately from layers, so --no-cache
  alone silently reuses the old checkout).
2026-05-13 11:38:36 +02:00
maziggy 1303cd2701 test(password): replace realistic fixtures with obviously-synthetic strings
GitGuardian flagged "Bambuddy1!" and "LongerP@ssw0rd!" in password.test.ts
  as potential leaked credentials. Replace with patterned placeholders
  ("Aa1!aaaa", "Aa1!Aa1!Aa1!") that still satisfy every complexity rule
  (upper/lower/digit/special, >=8 chars) but won't trip secret scanners.
2026-05-13 11:13:49 +02:00
maziggy 9cde2e37fd feat(slicer): log sidecar reject reason on bundle import failure (#1312)
The route mapped sidecar 4xx/5xx to HTTPException with detail but never
  logged it. Reporters seeing a 400 toast were giving us only the status
  code, not the reason, and the access-log line was all that landed in
  support bundles.

  Add WARNING-level logs on each error branch (400 SlicerInputError,
  503 SlicerApiUnavailableError, 502 SlicerApiError) with the sidecar's
  own message + the filename / byte count / configured URL. Next reporter
  on this code path produces a support bundle that contains the answer.
2026-05-13 10:43:13 +02:00
maziggy 1cf209d56b feat(support): audit bundle for new features; fix two leaks + slicer reachability
The settings-table passthrough auto-captured everything in `settings` (with
  sensitive-key redaction), but features storing config in dedicated tables
  were invisible. Triaging recent OIDC / 2FA / group bugs and the X1C slicer
  investigation needed data that wasn't in the bundle.

  New blocks in _collect_support_info:
    - auth: OIDC providers (cleartext names, no secrets), TOTP / OTP /
      API-key / long-lived-token / group counts
    - library: file / folder / external / trash / makerworld totals
    - inventory: spool + k-profile counts
    - queue: pending count, oldest pending age
    - maintenance: items total + enabled
    - integrations.github_backup: providers used + recent failures
    - integrations.slicer_api: enabled, URL source, reachability ping
    - per-printer obico_enabled flag

  Plus three smaller fixes caught testing against a real bundle:
    - mqtt_broker no longer leaks (broker keyword added)
    - virtual_printer_tailscale_auth_key no longer leaks (auth_key keyword
      + tskey- value-prefix safety net for future Tailscale settings)
    - slicer-API reachability check now mirrors the route's three-level URL
      precedence (DB → env var → default), instead of only looking at the
      DB setting. Previously returned null for every installation running
      the sidecar via env var or default port — i.e. most of them.
2026-05-13 10:35:45 +02:00
maziggy 82c90c6387 fix(mqtt): skip print-start fire on first RUNNING after Bambuddy startup (#1304)
Restarting Bambuddy mid-print misfired the plate-check + archive flow.
  The is_new_print guard treated _previous_gcode_state=None → RUNNING as
  a transition, but None just means we haven't seen any prior state yet —
  catch-up from a printer that was already running, not a fresh start.

  Add `_previous_gcode_state is not None` to the guard. _was_running still
  flips on unconditionally, so completion detection is unchanged. 3 tests
  that asserted the buggy behavior now seed an explicit prior state; new
  regression test pins the contract for the reporter's exact scenario.
2026-05-13 09:57:20 +02:00
maziggy d08183278f fix(users): show password rules in form + match FE check to BE (#1303)
Create/Edit User modal previously had no hint about backend password
  complexity, and the FE pre-check was only length>=6 — so any weak
  password got bounced as a bare "HTTP 422" toast after the round-trip.

  - New checkPasswordComplexity util mirroring backend's validator order
  - Helper text under password inputs in both modals
  - Submit disabled until every rule passes
  - client.ts 422 parser falls back to JSON.stringify(detail) when the
    mapped array is empty, so a bare status code never masks the real
    Pydantic detail again
  - i18n keys added in all 8 locales (EN/DE fully translated, others
    per project's English-seed convention)
  - 7 unit tests pinning the validator contract, including the
    reporter's "12345678" input
2026-05-13 09:50:42 +02:00
maziggy db308aa80b fix(library): defer external-scan STL thumbnails + Path coerce (#1299)
External scan hung on a 1200-subdir NAS because (1) every STL crashed
  with TypeError ('str / str') inside generate_stl_thumbnail and (2)
  thumbnail generation ran synchronously per file, so the FE timed out
  before db.commit() and nothing was persisted.

  stl_thumbnail.py now coerces inputs to Path defensively, and
  scan_external_folder defers STL thumbnail generation to a background
  asyncio task that opens its own session and processes each file
  post-commit. Subdirs appear in the sidebar immediately; thumbnails
  backfill over the next seconds/minutes.
2026-05-13 09:21:20 +02:00
maziggy a42ec820f6 fix(makerworld): point Open Cloud settings link to /profiles (#1300)
The "Open Cloud settings" link in the MakerWorld sign-in-required banner
  pointed at /settings?tab=cloud, but Settings has no cloud tab so the URL
  fell back to the General tab. Bambu Cloud login lives on /profiles, which
  already defaults its sub-tab to cloud.
2026-05-13 09:08:30 +02:00
maziggy e2e567f4dd fix(i18n): add settings.ldap.advanced key in all 8 locales (#1297)
The "Advanced" section header in LDAP settings was always
  rendering the hardcoded English fallback because the translation
  key was never defined. Added in en/de/fr/it/ja/pt-BR/zh-CN/zh-TW.
2026-05-12 16:38:56 +02:00
maziggy 8a6fcf5cbd fix(settings): expose UI rendering fields without requiring SETTINGS_READ (#1293)
The Clear Plate button (and 4 other features on the Printers page) read
  their state from /settings, which requires SETTINGS_READ. Granting that
  permission also adds the Settings nav item and leaks SMTP/LDAP/MQTT
  credentials — exactly what users were trying to avoid by giving an
  operator only printers:clear_plate.

  New /settings/ui-preferences endpoint returns a curated, opt-in subset
  of non-sensitive fields. Matches the existing /default-sidebar-order
  precedent. PrintersPage switched to the new endpoint; admin pages still
  use /settings for full access.
2026-05-12 16:30:28 +02:00
maziggy 5fea138f5d fix(auth): preserve manually-assigned groups across LDAP logins (#1292)
_sync_ldap_user used to replace user.groups entirely on every login,
  wiping manual admin assignments to groups outside the LDAP mapping.
  Now partitions on LDAP-managed group names (mapping values + default
  group) and only rebuilds that slice from LDAP truth. Manual assignments
  to non-managed groups are preserved; revocation in LDAP still
  propagates for managed groups.
2026-05-12 16:12:58 +02:00
maziggy 1d6e1b9e88 fix(inventory): persist storage_location for internal spools (#1291)
The column existed on the Spool ORM model but was missing from
  SpoolBase, SpoolUpdate, and SpoolResponse. Pydantic silently
  dropped writes and reads omitted the field, so the inventory
  table always showed "—" in the Storage Location column even
  after saving. Adding the field to the two schemas is enough —
  the update route already uses model_dump + setattr.
2026-05-12 16:02:24 +02:00
maziggy 82ce471124 Updated README 2026-05-12 15:15:26 +02:00
maziggy 301770d09f docs(changelog): add 0.2.5b1 entry for #1272 page-header unification 2026-05-12 14:03:11 +02:00
Ed 14a6d294f4 Merge pull request #1272 from EdwardChamberlain/feat-page-icons
[Feature] Update page icons for consistent UI style
2026-05-12 14:01:37 +02:00
maziggy 0c92a4d326 fix(vp): broadcast archive_created so Archives page refreshes live (#1282)
Real-printer prints broadcast archive_created from the MQTT print_start
  handler, which the Archives page listens for to invalidate its query
  cache. The VP file-receive paths created the archive in the DB but
  never emitted the event, so the new card only appeared after a tab
  switch triggered refetch-on-focus.

  Added a small _broadcast_archive_created helper on VirtualPrinterInstance
  and called it from _archive_file (immediate mode) and _add_to_print_queue
  (queue mode). Review mode is unaffected — it creates a PendingUpload,
  not a PrintArchive. Broadcast errors are swallowed at debug level so a
  transient WebSocket issue can't break the file-receive flow.
2026-05-12 13:36:32 +02:00
maziggy 2b747e96c9 Updated README 2026-05-12 13:24:08 +02:00
maziggy 5101cf8e37 Updated BACKERS.md 2026-05-12 12:20:51 +02:00
maziggy 59f7d736e3 Added BACKERS.md 2026-05-12 12:18:56 +02:00
maziggy 0d6171dc9a fix(vp): emit FINISH after FTP upload so Print-flow slicers unwedge (#1280)
Bambuddy's VP supports two slicer flows: Send (file upload only — what
  queue/immediate/review modes are designed for) and Print (file upload
  + start-print, intended for proxy mode). When a user clicks Print
  against a non-proxy mode the VP must still respond gracefully — the
  file is fine to receive, just the start-print never happens. Instead
  the slicer wedged at "Downloading...(0%)" and blocked the next
  dispatch with "The printer is busy with another print job".

  Cause: on_file_received transitioned gcode_state PREPARE -> IDLE
  directly. Print-flow slicers watch the state cycle and only release
  their in-flight-job lock on PREPARE -> ... -> FINISH (or FAILED).
  PREPARE -> IDLE looks like "printer abandoned my job" and keeps the
  prior job pinned in the slicer's memory.

  Fix: transition PREPARE -> FINISH with prepare_percent=100. The 1-Hz
  periodic status push broadcasts the new state to every connected
  slicer within a second. Send-flow slicers don't watch this state so
  the change is a no-op for them; Print-flow slicers see the FINISH
  they were waiting for and unwedge.
2026-05-12 10:19:34 +02:00
maziggy 7596725550 fix(mqtt): external-spool ams_filament_setting must use global tray_id (#1279)
ams_set_filament_setting and reset_ams_slot encoded the single-external
  case as {ams_id: 255, tray_id: 0, slot_id: 0}. The "LOCAL tray_id = 0"
  comment was a misread of the printer's response (which echoes the local
  slot position), not the request semantics.

  Captured BambuStudio -> X1C exchange shows the request encoding is
  {ams_id: 255, tray_id: 254, slot_id: 0} (global tray index in tray_id).
  The previous code's tray_id: 0 is what the P1S in #1279 rejects with
  result: "fail", which silently broke external-spool filament selection
  on every Bambu printer with no AMS or external spool in active use.

  Dual-external (H2D) branch was not in the captured exchange and is
  explicitly pinned at the legacy encoding pending a Studio -> H2D capture.
2026-05-12 09:44:20 +02:00
maziggy 4ee4bdb0d3 fix(archives): scan_timelapse picked stale video at false offset (#1278)
scan_timelapse's Strategy 2 matched filename timestamps against both
  archive.started_at and archive.completed_at across seven hypothesised tz
  offsets. The filename is always print-START time, so the end-time branch
  was a semantic mistake — and the dense offset set [0, +-1, +-7, +-8]
  let an unrelated video coincidentally land within minutes of any later
  archive at some offset.

  Extract Strategy 2 into _match_timelapse_by_timestamp(): compare only
  against start time, and refuse to auto-pick when the next-best different
  video is within a 15-minute ambiguity margin. The route then returns
  available_files and the frontend's existing manual-selection dialog
  takes over — which is the fallback the reporter explicitly asked for.

  Surfaces in LAN-Only mode where the printer can't reach NTP and its
  clock drifts (e.g. P2S filenames in CST while server is in UTC, the
  8h offset that exposed this bug).
2026-05-12 09:20:53 +02:00
maziggy 6fe00adb23 fix(spoolman): resolve -1 in ams_mapping to external spool (#1276)
BambuStudio encodes virtual tray IDs (254/255) as -1 in the flat
  ams_mapping array — a convention already documented in
  bambu_mqtt.py:start_print(). The spoolman tracking helper was treating
  -1 as "unmapped, use position-based default", which mapped slot_id=1
  to AMS tray 0 and credited external-spool prints to whatever Spoolman
  spool happened to be linked to AMS slot 0. The reporter's TPU prints
  on an H2S were credited to a PLA spool for ~49g over 4 prints before
  being noticed (regression of #853).

  When slot_to_tray[slot_id-1] == -1 and ams_trays contains 254/255,
  return the external tray ID directly. Prefers 254 over 255 (matches
  single-nozzle tray_now reporting + the vir_slot id=255->254 remap in
  bambu_mqtt.py:864). Legacy fall-through preserved for callers that
  don't pass ams_trays.

  Root cause investigation and patch by @ojimpo.
2026-05-12 09:07:49 +02:00
maziggy ae43ced0ef fix(vp): honor workflow default print options in queue-mode receive (#1235)
Prints sent from a slicer to a VP in print_queue mode arrived in the
  queue with bed_levelling / flow_cali / vibration_cali / layer_inspect /
  timelapse set to the SQLAlchemy column defaults, ignoring the user's
  workflow page settings entirely. The manual POST /print-queue endpoint
  reads these from the request body (frontend pulls them from settings
  before submitting), but manager._add_to_print_queue constructed the
  PrintQueueItem without touching any of those fields.

  Read default_bed_levelling and the other four settings via get_setting
  and pass them explicitly. _bool_setting helper handles the None ->
  AppSettings default fallback.
2026-05-12 08:59:14 +02:00
maziggy af52c4f2ff fix(spoolman): allow AMS-HT ams_id range in slot-assignment table (#1274)
H2C / H2D AMS-HT units report ams_id 128+ (one ams_id per unit, single
  tray), but spoolman_slot_assignments.ck_ams_id_range only admitted 0-7
  and 255. Every attempt to link a Spoolman spool to an AMS-HT slot died
  with `CHECK constraint failed: ck_ams_id_range`. The internal
  spool_assignment table has no such constraint and works fine.

  Widen the formula to (0-7) OR (128-191) OR 255 in the model, the
  CREATE TABLE DDL, and an idempotent in-place migration for existing
  installs (Postgres: DROP/ADD CONSTRAINT; SQLite: detect stale formula
  in sqlite_master, rebuild via _v2 rename pattern).
2026-05-12 08:48:30 +02:00
maziggy c097140e4c fix(camera): share broadcaster buffered frame with Obico + /camera/snapshot (#1271)
The MJPEG fan-out broadcaster from #1089 only solved viewer-side
  concurrency. Obico polling (every 5s) and the manual /camera/snapshot
  endpoint kept opening their own fresh RTSP sockets, which X1/H2/P2
  firmwares tolerated but X2D firmware 01.01.00.00 enforces strict
  single-connection on — every poll kicked the live stream.

  Add try_get_active_buffered_frame(printer_id): returns the broadcaster's
  last buffered frame when a viewer is connected, None otherwise. Obico
  and /camera/snapshot consult it before opening a fresh socket. When no
  viewer is active they fall through to the existing fresh-capture path.

  plate_detection and layer_timelapse intentionally not converted.
2026-05-12 08:36:08 +02:00
maziggy b334d7edc9 fix(spoolman): per-print 3MF tracking is the only weight writer (#1119)
Spoolman had two mutually-exclusive weight paths gated on the
  `disable_weight_sync` flag. The default (False) used AMS remain%
  x tray_weight auto-sync, which silently dropped non-BL spools
  because the AMS doesn't report tray_weight without RFID. The
  inventory_remaining fallback would have covered it, but the
  spool_assignment table it reads from is wiped on Spoolman
  activation, so non-BL spools got no weight updates at all.

  Match the internal Filament Inventory: per-print tracking always
  runs, AMS auto-sync no longer writes remaining_weight (it still
  maintains spool metadata and slot assignments). The setting
  becomes a no-op; left in the schema and UI for backwards compat.

  - store_print_data: drop the disable_weight_sync early return
  - sync_ams_tray callsites in main.py + routes/spoolman.py: force
    disable_weight_sync=True so weight is never written by AMS sync
  - new regression test confirming tracking runs with flag=false
2026-05-12 08:15:28 +02:00
maziggy 4b7df9f30b fix(usage-tracker): skip remain% fallback for trays not used by print (#1269)
The AMS remain% delta path charged every tray with a delta, not just
  trays involved in the print. Swapping a spool in an UNUSED slot mid-
  print made the slot report remain=0 (fresh spool, no tag), versus a
  print-start snapshot of 100%, so the originally-assigned spool got
  charged the full 1000g.

  Build print_used_keys from ams_mapping, tray_change_log, and
  tray_now_at_start, and skip fallback for trays not in that set.
  Legacy "scan every tray" behavior preserved when none of the three
  signals are present.
2026-05-12 07:47:19 +02:00
maziggy bf2b34ebcf fix(ui): round smart-plug live wattage on printer card (#1266)
Plugs reporting fractional watts (Kauf PLF12 / ESPHome via MQTT)
  overflowed the card width. SmartPlugCard and SwitchbarPopover
  already round the same field; only the printer-card badge was raw.
2026-05-12 07:37:13 +02:00
maziggy 359e4a778e Updated README 2026-05-11 16:46:09 +02:00
maziggy 71ac077c0c Bumped version 2026-05-11 14:52:24 +02:00
maziggy b444921021 test: drop racy printer_state_to_dict patch; assert structural shape
The patch on printer_state_to_dict raced against the broadcast coroutine
  under pytest-xdist's parallel workers. Mostly won locally, lost
  occasionally on CI — surfaced first as AttributeError on .kprofiles,
  then (after _fake_state was hardened) as a dict-content mismatch
  between the patched return value and the real 36-key dict.

  Fix: stop patching printer_state_to_dict; let it run for real against
  the complete _fake_state stub. Assertions now check the broadcast fired
  with the right printer_id and a dict containing awaiting_plate_clear,
  not the exact dict shape — that decouples the test from
  printer_state_to_dict's evolving body.
2026-05-11 14:08:23 +02:00
maziggy c36bfa24d4 test: harden _fake_state against printer_state_to_dict mock-leakage
The two TestBroadcastStatusChange / TestEndToEndUnderRunningLoop tests
  patch printer_state_to_dict to return a fixed dict, but on parallel
  xdist runners (CI's pytest -n 30) the patch occasionally didn't catch
  the call and the real function ran against the 4-field SimpleNamespace
  fake — first attr access (.kprofiles) AttributeError'd, swallowed by
  the try/except in _broadcast_status_change, send_status never awaited,
  the assertion failed.

  Filled _fake_state with every attribute the real printer_state_to_dict
  reads (iterables empty, scalars None, stg_cur=0 for the int comparison
  in get_derived_status_name). Test now passes whether or not the patch
  lands.
2026-05-11 13:58:16 +02:00
maziggy b4875fd5d6 fix(ci): ruff format spoolbuddy.py; raise SettingsPage test timeout
- backend/app/api/routes/spoolbuddy.py: re-formatted via ruff (lambda
    conditional wrapped in parens — the formatter check fires when the
    expression spans multiple lines without grouping)
  - frontend/src/__tests__/pages/SettingsPage.test.tsx: per-test timeout
    raised to 15s for the external_camera_snapshot_url PATCH test; the
    default 5s was tight enough on GitHub Actions runners that user.type()
    of the 49-char URL + 800ms debounce occasionally blew past it
2026-05-11 13:43:41 +02:00
maziggy 737c152243 fix(gcode_viewer): close CodeQL XSS + useless-escape alerts on PR #1263
- slider-shim.js: HTML-attribute-escape opts.id before interpolation
    (only caller passes a constant, but defends against future taint)
  - prettygcode.js: drop useless \\? escape inside [...] character class
2026-05-11 13:30:35 +02:00
maziggy 9f1188d711 Tool: Bandit B108
Severity: Warning ×4
  Issue: "Probable insecure usage of temp file/directory" — /tmp/<filename> literals used as synthetic DB field values in two integration tests
  Status: Fixed
  ────────────────────────────────────────
  Tool: CodeQL Python / JS
  Severity: Pending
  Issue: Still running on the head SHA
  Status: —
  ────────────────────────────────────────
  Tool: Trivy container scan
  Severity: Pending
  Issue: Still running
  Status: —
  ────────────────────────────────────────
  Tool: Bandit (Python Security Analysis)
  Severity: Pass
  Issue: The separate Bandit run on the changes already passes
  Status: ✓
2026-05-11 13:30:27 +02:00