Commit Graph
907 Commits
Author SHA1 Message Date
maziggy b01b5e05fd Add tests, docs, and changelog for #920 + #932
Audit PRs #920 (printers search/filter) and #932 (print from project
  view) for regressions, i18n coverage, test gaps, and docs.

  No regressions found: existing callers of the changed signatures
  (archive_print, getLibraryFiles, filteredPrinters chain) are unaffected;
  i18n is complete in all 7 locales for both features.

  Backend tests (4 new, all passing):
  - test_list_files_by_project_id — bulk JOIN returns files across all
    linked folders, excludes unlinked ones
  - test_list_files_folder_id_takes_precedence_over_project_id — guards
    the documented precedence folder_id > project_id > include_root
  - test_add_to_queue_with_project_id — project_id is persisted on the
    queue row for later archive linkage
  - test_add_to_queue_invalid_project_id_returns_404 — regression guard
    for the validation pre-check on the queue path (mirrors the one on
    the direct-print path in library.py)
2026-04-10 13:09:21 +02:00
lietschaend f95b2acd7d Feature: print files directly from project view (closes #930) (#932)
* feat: print files directly from project view (closes #930)

Show printable files from linked library folders directly in the project
detail page, with Print Now and Add to Queue buttons per file. Removes
the detour through the File Manager for common reprint workflows.
2026-04-10 12:54:27 +02:00
maziggy b069b5217c y Fix virtual printer "Synchronizing device information" timeout in Orca (#927)
OrcaSlicer's "Send job" flow sat on "Synchronizing device information…"
  until it gave up, even though FTP upload worked when the user clicked
  "Send job anyway". The virtual printer's MQTT server gated all incoming
  command handling on `f"device/{self.serial}/request" in topic` — if the
  slicer's cached serial for the VP didn't exactly equal the VP's computed
  self.serial (model prefix + per-VP serial_suffix), every get_version,
  pushall, and project_file publish was silently dropped. Nothing was
  logged past the initial "MQTT publish to …" line, so the slicer never
  received a push_status or get_version response on its subscribed
  device/{serial}/report topic and hit its sync timeout. Responses were
  also unconditionally published on device/{self.serial}/report, so even
  when the inbound check happened to pass, replies targeted a topic the
  slicer wasn't listening on if its serial had drifted.

  Both directions are now serial-adaptive:

  - `_handle_publish` accepts any authenticated publish on a
    `device/*/request` topic and extracts the serial from the topic itself
    rather than comparing against self.serial.
  - A per-connection `_client_serials` dict tracks the serial the slicer
    actually uses, populated from the first SUBSCRIBE or PUBLISH seen on
    each connection and cleared on disconnect/stop.
  - `_send_status_report`, `_send_version_response`, `_send_print_response`
    now take an optional `serial` parameter (defaulting to self.serial)
    so every outgoing publish — including the periodic 1-second status
    push — targets the topic the slicer subscribed to.
  - The version response's embedded `module[].sn` fields now also carry
    the client's serial so the payload is internally consistent with the
    topic.
  - When the client's serial differs from self.serial an INFO log records
    the adaptation so it's visible in future support bundles.

  The working case (slicer's cached serial equals self.serial, as in my
  own H2D-1 Proxy setup) is bit-for-bit identical to the old behavior —
  the new check is strictly more permissive and only affects cases the
  old code silently dropped.

  Regression tests cover:
  - `_extract_serial_from_topic` valid/invalid topic shapes
  - mismatched-serial publish → handler runs, response topic and sn field
    both use the client's serial
  - non-`/request` topics → still rejected
  - pushall → status_report routed to the client's subscribed topic
  - `_client_serials` cleared on stop()
2026-04-10 12:13:54 +02:00
maziggy 7e7372c469 Fix SpoolBuddy update always pulling main instead of current branch
detect_current_branch() was reading .git/HEAD from settings.base_dir,
  which points at the data volume (DATA_DIR=/app/data in Docker) and
  never contains .git. The repo is at /app, so the lookup always failed
  and the code fell through to the GIT_BRANCH env-var → "main" fallback.
  The SpoolBuddy device was therefore checking out `main` regardless of
  which branch Bambuddy itself was running.

  The old subprocess-based implementation had the same bug but it was
  masked: the stock Docker image has no `git` binary, so `git rev-parse`
  raised FileNotFoundError, the except clause swallowed it, and the
  fallback kicked in. Swapping to filesystem reads exposed the wrong
  lookup path.

  Add a module-level _APP_DIR constant (parents[3] of the module file,
  same depth as config.py uses for its own _app_dir) and read `.git/HEAD`
  from there. A regression test plants a decoy .git in the data dir and
  asserts we still pick up the real one from the app root.

  Per your NO GIT WRITES rule, nothing is staged or committed.
2026-04-10 10:56:48 +02:00
maziggy 44cb26c7c3 Fix SpoolBuddy update Docker failure — set LOGNAME/USER/HOME in image
Follow-up to the asyncssh migration. asyncssh.connect() internally
  calls getpass.getuser() for ~/.ssh/config host matching, regardless
  of the explicit `username=` passed for the remote login. Under an
  arbitrary Docker PUID with no /etc/passwd entry, getpass.getuser()
  raises "No username set in the environment" (OSError in Python 3.13+,
  previously a bare KeyError).

  Fix: set LOGNAME=bambuddy, USER=bambuddy, HOME=/app in the Dockerfile.
  getpass.getuser() tries env vars before pwd.getpwuid(), so the lookup
  never touches the passwd database and works for any PUID the operator
  picks — no helper code, no image rebuild for different UIDs.

  Also pass config=[] to asyncssh.connect() so it does not try to load
  ~/.ssh/config (whose default path needs a resolvable home directory).

  An earlier draft of this fix added a Python helper that caught the
  KeyError and injected LOGNAME at module import. That was both more
  code than needed and broken on Python 3.13, which wraps the KeyError
  in an OSError the helper didn't catch — so the module import itself
  crashed, producing a 500 on /spoolbuddy/devices/{id}/update. Reverted
  in favour of the one-line ENV fix.
2026-04-10 10:46:06 +02:00
maziggy 60d034d40c Fix SpoolBuddy update Docker failure — asyncssh local-username lookup
Follow-up to the previous commit that swapped the `ssh`/`ssh-keygen`
  subprocesses for asyncssh. asyncssh.connect() internally calls
  getpass.getuser() to resolve the *local* username for ~/.ssh/config
  host matching, regardless of the explicit `username=` we pass for the
  remote login. Under an arbitrary Docker PUID with no /etc/passwd
  entry, getpass.getuser() tries LOGNAME/USER/LNAME/USERNAME (all unset
  in python:3.13-slim) and falls back to pwd.getpwuid(), which raises
  KeyError. asyncssh rewraps that as "Unknown local username: set one
  of LOGNAME, USER, LNAME, or USERNAME in the environment" — which
  surfaced in the UI as "ssh connection failed: no username set in the
  environment".

  Fix is two-part:

  - _ensure_local_username_env() runs at module import. If getpass
    .getuser() already works, or any of LOGNAME/USER/LNAME/USERNAME is
    set, it is a no-op. Otherwise it sets LOGNAME=bambuddy so asyncssh
    can proceed. Native installs are untouched.

  - asyncssh.connect() is now called with config=[] to skip the
    default ~/.ssh/config load, which relies on a resolvable home
    directory that may not exist under arbitrary Docker PUIDs.

  Three new unit tests cover the env-var fallback, including the case
  where the operator has set USER but the passwd lookup still fails.
2026-04-10 10:35:57 +02:00
maziggy a78a4bff2e Fix SpoolBuddy update still failing in Docker after keypair fix
Commit 67749565 eliminated ssh-keygen from the SpoolBuddy remote-update
  flow, but the update path still shelled out to the OpenSSH `ssh` client
  for every command. Like ssh-keygen, the `ssh` binary calls
  getpwuid(getuid()) during startup and aborts with "No user exists for
  uid <N>" when the container runs under an arbitrary PUID that isn't in
  /etc/passwd (python:3.13-slim only ships a root entry, so any
  `user: "1000:1000"` compose setup trips the same error).

  detect_current_branch() had a related problem: when the git repo is
  bind-mounted into the container, .git exists inside Docker, so the code
  tried to run `git rev-parse`. Git isn't in the image, so the subprocess
  silently fell back to the GIT_BRANCH env var — and if git ever were
  added, it could hit the same getpwuid trap.

  The entire update path is now subprocess-free:

  - _run_ssh_command uses asyncssh (pure-Python, built on the already
    installed cryptography library). Connection errors map to rc=255 to
    match `ssh`'s convention; asyncio.timeout handles the timeout path.
  - detect_current_branch reads .git/HEAD directly (handling git-worktree
    `gitdir:` pointer files too), keeping the same GIT_BRANCH → "main"
    fallback chain.
  - shutil and the inline `import subprocess` are gone from the module.

  Regression tests assert that neither keypair creation, branch
  detection, nor command execution spawns any subprocess. Native installs
  are unaffected.
2026-04-10 10:25:08 +02:00
maziggy 648f7d6ba8 P2S - enable AMS drying for firmware 01.02.00.00 and later 2026-04-09 15:42:34 +02:00
maziggy 6774956565 Fix SpoolBuddy update failing in Docker with "no user exists for uid"
The SpoolBuddy remote-update flow shelled out to `ssh-keygen` to create
  its update keypair on first use. Inside the Docker container the process
  runs under an arbitrary PUID that is not listed in /etc/passwd, so
  ssh-keygen aborted at the getpwuid() home-directory lookup with
  "no user exists for uid 1001" and the update button failed.

  Generate the ed25519 keypair in-process via the `cryptography` library
  (already a dependency) and serialize it in OpenSSH format. No subprocess,
  no /etc/passwd lookup. Native installs are unaffected.

  Added a regression test that asserts no subprocess is spawned during
  keypair creation so this can't come back.
2026-04-09 12:53:20 +02:00
maziggy 39a5840f67 Fix camera reconnect counter off-by-one and ffmpeg log flood (#925)
Two bugs surfaced while investigating camera reconnect behaviour in #925.

  The camera page briefly displayed "Reconnecting attempt 6 of 5" before
  giving up, because the attempt counter could be incremented to the
  maximum while the reconnect banner was still rendering. The displayed
  value is now clamped to the configured maximum.

  Every failed ffmpeg spawn logged the full ~20-line ffmpeg version,
  configuration, and lib* banner, producing hundreds of lines of noise
  per failed camera click (one reported click produced 555 log lines
  across 30 retries). A new _summarize_ffmpeg_stderr helper strips the
  banner and caps output at the last 10 meaningful lines, applied at
  all three stderr log sites (immediate-failure, stream-ended,
  read-timeout). Covered by unit tests for empty input, banner
  stripping, line cap, blank-line filtering, and banner-only input.

  The underlying "camera service stops accepting connections after
  prolonged uptime" behaviour in the X1C firmware is still under
  investigation — these two fixes are independent of that root cause.
2026-04-09 11:29:57 +02:00
maziggy 848f558105 LDAP: POSIX primary group support and default fallback group
Two related LDAP authentication changes.

  Fix: POSIX primary group membership was ignored. authenticate_ldap_user
  only searched for posixGroup entries via memberUid (supplementary
  groups). A user's primary group — referenced by the gidNumber attribute
  on the user object matching gidNumber on a posixGroup — was never
  resolved, so users whose role came from their primary group landed
  without the expected permissions. The authenticator now runs a second
  search for posixGroup entries whose gidNumber matches the user's
  primary gidNumber, then dedupes DNs case-insensitively before passing
  the list to resolve_group_mapping (LDAP DNs are case-insensitive by
  spec).

  New feature: ldap_default_group setting. Settings → Authentication →
  LDAP → Advanced has a new "Default group" selector. When an LDAP user
  authenticates but is not listed in any mapped LDAP group, they are
  assigned to this fallback group instead of being left with no groups
  (and therefore no permissions). A warning is logged each time the
  fallback is applied so admins can spot missing group assignments.
  Empty setting preserves the old behavior.

  Tests: added 4 mocked authenticate_ldap_user tests covering primary
  gidNumber lookup, dedupe of overlapping memberUid+primary gid matches,
  case-insensitive DN dedupe, and the guard when a user entry has no
  gidNumber attribute. Also extended the existing parse_ldap_config tests
  to cover the new default_group field.

  Backend: ldap_service.py (primary group + dedupe + default_group
  field), schemas/settings.py (schema field), api/routes/auth.py
  (fallback wiring in _provision_ldap_user / _sync_ldap_user).

  Frontend: LDAPSettings.tsx default-group dropdown in the Advanced
  collapsible, api/client.ts type field, new i18n keys in all 7 locales
  (defaultGroup, defaultGroupNone, defaultGroupHint).
2026-04-09 10:48:42 +02:00
maziggy d0b91ad4d5 Fix support bundle leaking virtual printer IP address
The debug support bundle included virtual_printer_remote_interface_ip
  unmasked in support-info.json. The setting key didn't match any
  sensitive-key filter substring. Added "_ip" to the filter set so IP
  address settings are excluded. Log file content was already redacted
  by the existing IPv4 regex.
2026-04-09 08:25:40 +02:00
maziggy 813d9dde38 Fix Spoolman location not cleared on auto-sync when spool removed from AMS (#921)
The on_ams_change auto-sync callback set locations for new spools but
  never called clear_location_for_removed_spools(), leaving stale locations
  that caused double-booked slots. Also pass synced_spool_ids in the
  single-printer sync route to match the sync-all endpoint behavior.
2026-04-09 08:16:54 +02:00
maziggy 100610a1ef Bumped version 2026-04-08 13:17:25 +02:00
maziggy b76d6210cf Add SpoolBuddy quick menu with power control and system commands (#893)
Swipe down from the top of the SpoolBuddy display to open a quick-access
  menu for toggling printer smart plugs and managing the device (restart
  daemon, restart browser, reboot, shutdown). All destructive actions
  require confirmation.

  Backend: new POST /spoolbuddy/devices/{id}/system/command endpoint
  queuing reboot/shutdown/restart_daemon/restart_browser commands.
  Daemon: handles commands via subprocess (sudo reboot, systemctl restart).
  Frontend: SpoolBuddyQuickMenu component, swipe-down gesture detection,
  i18n keys for all 7 locales.
2026-04-08 11:29:44 +02:00
maziggy b6599dd419 Add LDAP/Active Directory authentication (#794)
Users can authenticate against an LDAP/AD server with configurable
  server URL, bind DN, search base, and user filter. Supports StartTLS
  and LDAPS — plaintext is not allowed. Both Active Directory (memberOf)
  and POSIX groups (memberUid) are mapped to BamBuddy groups on each
  login. Auto-provisioning creates local accounts on first LDAP login.
  Local admin accounts remain as fallback when LDAP is unreachable.
  Password management is disabled for LDAP users.
2026-04-08 10:41:27 +02:00
maziggy 42b1653f52 Fix spool weight tracking when auto-archive disabled for queue/reprint prints (#839)
When auto_archive was disabled but the print was dispatched by BamBuddy
  (queue/reprint), on_print_start discarded the expected print entry and
  returned early. The archive was never promoted to _active_prints, so at
  completion archive_id and ams_mapping were both None — all tracking paths
  failed silently. Now detects expected prints before the auto-archive
  early-return and falls through to the normal promotion path. Also injects
  the stored ams_mapping into the usage tracker session for printers where
  MQTT request topic subscription fails (P1S, A1).
2026-04-08 08:17:42 +02:00
maziggy 4c052e04e1 Fix SpoolBuddy inventory not updating on spool changes (#905)
Spool CRUD endpoints (create, bulk create, update, delete, archive,
  restore) did not emit websocket events, so SpoolBuddy Dashboard and
  other tabs relying on event-driven cache invalidation never refreshed.

  All inventory mutation endpoints now broadcast an `inventory_changed`
  websocket event. Frontend handles it by invalidating `inventory-spools`.
2026-04-07 12:32:44 +02:00
maziggy 8c00b1b75f Fix multi-plug automation only working for first plug (#903)
When multiple smart plugs were assigned to the same printer, only the
  first plug's automation triggered. All automation paths (print start
  auto-on, print complete auto-off, queue auto-off, scheduler power-on)
  now iterate every plug linked to the printer. Also fix queue auto-off
  hardcoded to Tasmota instead of using the correct service for the plug
  type.
2026-04-07 09:44:48 +02:00
maziggy 2d9a56b3d0 Fix ghost jobs from SQLite lock on print completion (#897)
Queue status update (printing → completed) failed silently when SQLite
  was locked by another writer, leaving ghost jobs permanently stuck in
  printing status. Add run_with_retry() for SQLite lock retries and split
  runtime tracker into per-printer commits to reduce lock hold time.
2026-04-07 09:20:58 +02:00
maziggy 113bae4c63 Fix developer mode probe destabilizing MQTT on auto-reconnect (#887)
The dev mode probe (ams_filament_setting to ext slot) fired on every
  auto-reconnect, which destabilized some firmware MQTT brokers (A1/P1)
  causing a reconnect-probe-disconnect feedback loop. Now caches the
  probe result across reconnects and only probes once on first connect,
  with a 5s delay to let the session stabilize.
2026-04-07 09:00:37 +02:00
maziggy 4df0349310 Fix spool weight tracking when auto-archive is disabled (#839)
When auto-archive was off, archive_id was None at print completion so
  the entire 3MF tracking path was skipped. AMS remain% fallback also
  failed on printers reporting remain=-1. Now searches library files and
  previous archives by filename to locate the 3MF without an archive,
  and captures the AMS slot-to-tray mapping at print start so it's
  available at completion regardless of archive state.
2026-04-07 08:31:14 +02:00
maziggy f006472f79 Add auto-print G-code injection for queue items (#422)
Per-model start/end G-code snippets configurable in Settings (Workflow
  tab). Queue items get "Inject G-code" toggle — scheduler injects
  snippets into a temp 3MF copy before FTP upload. Supports Farmloop,
  SwapMod, AutoClear, Printflow 3D and similar bed-clearing systems.
  Original files are never modified.
2026-04-05 11:14:26 +02:00
maziggy aa9b60a265 Fix spool weight tracking for fallback archives, VT trays, and notifications (#839)
Usage tracking failed silently when FTP download failed (fallback archive
  with no 3MF), when printing from external spool holder (VT tray not
  iterated by AMS fallback), and notifications showed "Unknown" for time
  and filament. Now resolves 3MF from library/previous archives, tracks
  VT tray remain% deltas, enriches notifications with usage tracker
  results, and captures print time from MQTT for fallback archives.
2026-04-05 09:59:05 +02:00
maziggy 1645b51dad Add external folder subfolder preservation and fix file manager stale UI after delete
External folder scan now mirrors disk subfolder structure into the folder
  tree instead of flattening all files into root. Hidden directories are
  filtered, orphaned subfolders are cleaned up on rescan. Fixes #890.

  File manager delete endpoints (folder, file, bulk) now commit before
  returning the response — previously relied on post-response auto-commit,
  causing a race where the frontend refetch arrived before the commit.
2026-04-05 09:33:46 +02:00
maziggy edfd2e77c0 Revert "Add Load/Unload External filament buttons to printer controls"
This reverts commit e3cde14a0b.
2026-04-05 08:21:37 +02:00
NNeerr00 e3cde14a0b Add Load/Unload External filament buttons to printer controls
New dedicated MQTT methods, API endpoints, and UI buttons for
loading/unloading filament from the external spool holder without
requiring an AMS. Includes state guards to prevent load when filament
is already loaded and unload when nothing is loaded.
2026-04-05 00:04:26 +02:00
maziggy 2d5dd1b31f Fix AMS slot changes failing until manual reconnect (#887)
Add timeout and retry to the developer mode probe. After a keep-alive
  timeout, paho auto-reconnects but the session can be half-broken: the
  printer sends status but ignores commands. The probe had no recovery —
  one unanswered probe permanently blocked retries. Now times out after
  10s with one retry; two consecutive failures force-close the socket for
  a clean reconnect.
2026-04-04 14:08:29 +02:00
maziggy aeb61e58d9 Fix spool manager deducting double filament after print completion (#880)
Skip AMS remain% weight sync in on_ams_change while a print session is
  active. The MQTT FINISH message triggers both the AMS weight sync (SET
  from remain%) and the usage tracker (ADD from 3MF data) in the same
  event loop cycle, causing double deduction. The active-session check is
  snapshotted before any await to prevent a race with on_print_complete
  popping the session during interleaved execution.
2026-04-04 13:38:16 +02:00
maziggy 2a6df22075 Restrict temp file permissions for camera snapshots
Camera snapshot, test, and plate detection endpoints created temporary
  JPEG files with default 0644 permissions. Switch from NamedTemporaryFile
  to mkstemp with explicit 0600 permissions.
2026-04-04 13:18:53 +02:00
maziggy 4d09b3f669 Add HTTP security headers middleware
Set X-Content-Type-Options, X-Frame-Options, and Referrer-Policy on all
  responses. CSP omitted (React inline styles would require unsafe-inline,
  negating protection). HSTS omitted (LAN app commonly accessed over HTTP).
2026-04-04 13:08:37 +02:00
maziggy 70b12e1949 Fix API key empty printer_ids granting full access
An API key with printer_ids=[] was treated the same as null (global
  access) due to a falsy check. Now None means global access and []
  means no printer access. Added a startup migration to normalize any
  existing [] rows to NULL so they retain their intended global access.

  Also fixed the webhook /queue endpoint which used the same falsy
  check, allowing []-scoped keys to see all printers.
2026-04-04 13:01:54 +02:00
maziggy 5709282200 Add authentication to bug report debug logging endpoints
The bug report endpoints (start-logging, stop-logging, submit) had no
  authentication, allowing anyone on the network to enable debug logging,
  retrieve sanitized system logs, and trigger bug report submissions when
  auth was enabled. All three now require permission when auth is enabled:
  start-logging requires settings:update, stop-logging and submit require
  settings:read. Endpoints remain open when auth is disabled (default).
2026-04-04 12:54:31 +02:00
maziggy 7fa00ed475 Fix path traversal vulnerability in file upload endpoints
Archive upload endpoints used the client-supplied filename directly
  in file paths, allowing an authenticated attacker to write files
  outside the intended directory (e.g. ../../evil.3mf bypasses the
  .3mf extension check). Added _safe_filename() helper that normalizes
  backslashes and extracts the basename before constructing paths.
2026-04-04 12:41:01 +02:00
maziggy 039db1217d Add shortest-job-first queue scheduling with starvation guard (#879)
New SJF toggle badge on the queue page. When enabled, the scheduler
  picks shorter print jobs before longer ones instead of FIFO. A
  starvation guard flags jobs that get skipped once, moving them to
  the front on the next cycle so long jobs can't be postponed indefinitely.

  - Add print_time_seconds and been_jumped columns to PrintQueueItem
  - Cache print duration from 3MF metadata at queue item creation
  - SJF query: printer_id, target_model, been_jumped DESC, print_time_seconds ASC, position
  - Mark jumped items in-memory after each print start
  - Toggle badge on queue page header with live state indicator
  - Frontend auto-sorts to match scheduler order when SJF enabled
  - Settings schema, boolean parsing, and migration (SQLite + PostgreSQL)
  - i18n badge keys for all 7 locales
  - 10 integration tests for SJF ordering and starvation logic
  - Wiki, website, README, and changelog updated
2026-04-04 10:25:17 +02:00
maziggy 3381f73e0f Enable AMS drying support for H2S with firmware 01.02.00.00+ (#886)
Move H2S from the drying-unsupported blocklist to the firmware-gated
  list, requiring minimum firmware 01.02.00.00 (released end of March
  2026). Both remote AMS drying and queue auto-drying now work on H2S.
2026-04-04 09:26:00 +02:00
maziggy 0f0fac9b32 Show database engine and version on System Information page
Display the active database backend (SQLite or PostgreSQL) and its
  version in the Database section of the System Info page. SQLite queries
  sqlite_version(), PostgreSQL queries SELECT version() and
  pg_database_size(). Helps users verify which database is in use.
2026-04-04 09:05:35 +02:00
maziggy 610431d6b7 Add optional PostgreSQL database support
Bambuddy can now use an external PostgreSQL database via the
  DATABASE_URL environment variable. SQLite remains the default.
  Dialect-aware helpers handle upserts, PRAGMAs, FTS (FTS5 vs
  tsvector+GIN), backup/restore, and health checks. All migration
  blocks use savepoints to prevent Postgres transaction poisoning.
  Backups are always portable SQLite format regardless of backend.
  Cross-database restore imports SQLite backups into PostgreSQL
  with automatic boolean/datetime conversion, NOT NULL default
  filling, and FK constraint handling.
2026-04-03 11:33:29 +02:00
maziggy 76adf70fd4 Add separate power/energy URLs and multipliers for REST smart plugs (#472)
REST/Webhook smart plugs can now fetch power and energy data from
  individual URLs instead of requiring all values in a single status
  response. Each value falls back to the shared Status URL when no
  separate URL is set, preserving backward compatibility. Added power
  and energy multipliers for unit conversion (e.g. 0.001 for Wh→kWh).
2026-04-03 08:31:08 +02:00
maziggy d327c70006 Fix WebSocket crash on printers without fun field (#873)
Race condition in _update_state: dev mode probe released GIL via MQTT
  publish between raw_data overwrite and vt_tray list restoration, letting
  the event loop iterate over raw dict keys (strings) instead of spool
  dicts. Affects A1, P1, and X1Plus firmware that don't send the fun field.

  Fix: normalize vt_tray dict→list before raw_data assignment, restore
  preserved fields before any GIL-releasing work, add defensive guard in
  printer_state_to_dict.
2026-04-02 13:58:42 +02:00
maziggy e5a0382dba Fix test conftest missing 4 model imports causing slot_preset_mappings table error
The test conftest.py model import list was out of sync with database.py,
  missing slot_preset, project_bom, spool_k_profile, and spoolbuddy_device.
  Base.metadata.create_all() never created those tables in the test DB.
2026-04-02 13:29:44 +02:00
maziggy eec1e43d6d Fix AMS history cleanup crash from naive/aware datetime mismatch
The periodic cleanup of old AMS sensor history entries (every ~24h)
  failed with "can't compare offset-naive and offset-aware datetimes".
  The cutoff used datetime.now(timezone.utc) but recorded_at stores
  naive datetimes via SQLite's func.now(). Use utcnow() instead.
2026-04-02 11:14:06 +02:00
maziggy 34ccc7b6e7 Fix bed cooled notification never firing on some firmware (#872)
Replace polling-based bed cooldown monitor with event-driven approach.
  Some firmware stops sending bed_temper after print completion, causing
  the 30-min polling loop to time out on stale cached values. Now reacts
  instantly to bed_temper MQTT data via a new on_bed_temp_update callback.
2026-04-02 10:37:48 +02:00
maziggy cb0c4b5ffe Standardize webhook notification payloads with structured event data (#871)
Thread event_type and template variables through the webhook call chain so
  all generic webhook payloads include an "event" field and event-specific
  data (printer, filename, duration, etc.) as top-level JSON fields. Existing
  title/message/timestamp/source fields are unchanged. Slack format unaffected.
2026-04-02 10:13:35 +02:00
maziggy f4df4393be Add spool inventory and print archive backup to GitHub backup (#870)
GitHub backup can now optionally include spool inventory (with usage
  history) and print archive metadata as JSON. Both toggles are off by
  default. No binary files (gcode/3MF) are included.
2026-04-02 09:59:00 +02:00
maziggy ad96337587 Fix external spool ams_mapping2 using wrong ams_id on single-nozzle printers (#859)
Single-nozzle printers (X1C, P1S, A1) report tray_now=254 for external
  spool, but BambuStudio sends ams_id=255 (VIRTUAL_TRAY_MAIN_ID) in the
  print command's ams_mapping2 field. Bambuddy was passing 254 as-is,
  causing firmware to target AMS tray 0 instead of external spool —
  resulting in 07FF_8012 "Failed to get AMS mapping table" or prints stuck
  at heatbed heating when an AMS is connected but empty.

  Map external spool to ams_id=255 for all non-H2D printers. H2D
  dual-nozzle printers retain 254 (deputy) / 255 (main) distinction.
2026-04-02 09:27:35 +02:00
maziggy ae2a262585 Add developer mode probe for printers without fun field 2026-04-01 12:41:18 +02:00
maziggy 75fa935851 Fix filament color name and subtype inconsistencies (#857) 2026-04-01 12:20:12 +02:00
maziggy 3270179090 Add batch print quantity to print/schedule dialog (#342) 2026-04-01 11:55:30 +02:00
maziggy 9aa9fdc586 Add configurable default print options (#858) 2026-04-01 10:30:05 +02:00