If the printer drops or ignores the MQTT project_file command (same
half-broken-session shape as #887/#936), the queue item was permanently
stuck in "printing" at 100% because the scheduler optimistically flipped
the DB row right after the publish succeeded locally. A new watchdog
polls the printer state for up to 45s after dispatch; if there's no
transition, it reverts the item to "pending" and force-reconnects the
MQTT session so the scheduler can retry.
Follow-up to the earlier H2D SD card badge fix. The badge was still
flapping because Bambu firmwares send partial MQTT pushes carrying only
the legacy `sdcard` field (without home_flag), and the fallback path
re-engaged on every such push. Latch home_flag as the canonical source
once seen; reset the latch on reconnect so a firmware change still
re-learns.
When a project_file command was unacknowledged for 15s, Bambuddy
previously logged "printer may need restart" and left the broken MQTT
session in place — requiring the user to power-cycle the printer. The
existing half-broken-session recovery only ran via the developer-mode
probe path, which skips printers with a known developer_mode value.
Extract the existing force-reconnect logic into a reusable helper and
call it from _verify_print_response on dispatch timeout. The next
dispatch attempt then lands on a fresh MQTT session without a reboot.
New users repeatedly reported queued prints "not starting" because the
confirmation prompt was waiting on an ack they didn't know existed.
Flip the default in the settings schema and in the frontend fallbacks
so a missing/unset value reads as disabled. Existing installs keep
their saved preference.
Parse `sdcard` from `home_flag` bits 8-9 (HAS_SDCARD_NORMAL /
HAS_SDCARD_ABNORMAL) when available and fall back to a type-tolerant
truthy check on the top-level `sdcard` field. Firmware ships that
field inconsistently (bool, int `1`, or string `"HAS_SDCARD_NORMAL"`),
so the previous `is True` identity check flipped the badge to red on
every report that carried a non-bool value.
The strict CSP added in 0.2.3b4 blocked three things at once:
external sidebar-link iframes (no frame-src declared, so they fell
back to default-src 'self'), the inline service-worker registration
script in index.html, and the Google Fonts @import used for Inter.
- Add `frame-src 'self' https:` so user-configured HTTPS iframe
targets load; frame-ancestors 'none' still prevents Bambuddy
itself from being framed cross-origin.
- Move the inline SW-registration script into public/sw-register.js
so `script-src 'self'` covers it without 'unsafe-inline' or
per-build hashes.
- Allow fonts.googleapis.com in style-src and fonts.gstatic.com in
font-src so the Inter webfont loads.
Search field at the top of Settings now finds Sidebar Links,
Spoolman, Spool/Color Catalog, all four Failure Detection
sections, Email auth (Advanced + SMTP test), 2FA (TOTP, Email
OTP, Linked Accounts), SSO/OIDC, LDAP Server Config, and the
four Backup sub-cards (GitHub, History, Local, Scheduled).
Replaces the hardcoded searchIndex array in SettingsPage.tsx
with a module-level registry (frontend/src/lib/settingsSearch.ts).
Each settings card calls registerSettingsSearch(...) at module
scope, so adding a new card means adding one colocated line
instead of editing a distant central array. Anchor ids were
added to the corresponding Card elements in the affected
components so scrollIntoView lands on the right section.
Surface four Home Assistant-style controls on the Printers page card:
- SD Card badge in the top status row (green / red, icon-only).
- Enclosure Door badge in the top status row (green / yellow, icon-only).
Detection per printer family — X1/X1C/X1E read home_flag bit 23, all
others read top-level `stat` (hex string) bit 23 — so X1 firmware that
does not flip stat bit 23 stops false-triggering "open". WebSocket
status-change dedup key now includes door_open so toggling the door
alone publishes a push, no 30s REST-poll wait.
- Airduct Mode badge beside the speed control (cooling / heating)
for P2S/H2D/H2C/H2S; one-click dropdown calls the existing
set_airduct MQTT command via a new POST /printers/{id}/airduct-mode
route.
- Force Refresh entry in the kebab menu — calls the existing
/printers/{id}/refresh-status endpoint to request a pushall snapshot
without forcing a reconnect.
Tests: door-open parsing (X1 home_flag, non-X1 stat, ignore mismatched
source, invalid hex) and airduct route (validation, not-connected,
success, failure).
Adds a Failure Detection tab under Settings that wires Bambuddy to a
self-hosted Obico ml_api container — no cloud, no account, no WebSocket.
While a print is running, the detection service periodically hands the
printer's camera snapshot URL to the ML API and smooths scores over
time (30-frame warmup + EWM, alpha=2/13, short/long rolling means) so
one noisy frame can't trigger an action. When the smoothed score
crosses HIGH, the configured action fires exactly once per print:
notify, pause, or pause-and-cut-power (via linked smart plugs).
- Backend: new obico_detection + obico_smoothing + obico_actions
services, /obico/status and /obico/test-connection routes
(SETTINGS_READ / SETTINGS_UPDATE), six obico_* AppSettings fields
with validators for sensitivity/action/enabled_printers.
- Frontend: FailureDetectionSettings component (enable, ML URL + test,
sensitivity, action, poll interval, per-printer monitor list, live
status + detection history), new sidebar tab with service-active
bullet, toast on save.
- Tests: 17 detection unit tests + 15 smoothing unit tests + 4
frontend component tests.
- Docs: README bullet, CHANGELOG entry, wiki page under Analytics,
website features.html entry.
The security-headers middleware added in 0.2.3b4 set X-Frame-Options: DENY
on every response, which blocked the Spoolman page iframe when Spoolman
was served from the same host as Bambuddy via a reverse proxy. Relaxed
to SAMEORIGIN — same-origin embedding works again, cross-origin
clickjacking protection is preserved.
With Auto Off enabled and another job queued, the smart plug cut power when a
print finished and immediately re-powered the printer because the scheduler
saw pending items. The printer booted fresh into IDLE and the next job
auto-dispatched, bypassing the "Clear Plate & Start Next" confirmation.
Root cause: the plate-clear gate lived only in PrinterManager._plate_cleared
(in-memory set) and _is_printer_idle treated IDLE as unconditionally idle. On
power cycle the in-memory flag was lost and the IDLE-on-boot state skipped
the gate entirely.
Fix:
- Replace the in-memory flag with an awaiting_plate_clear column on the
printers table, rehydrated into the PrinterManager at startup.
- Set the flag in on_print_complete for completed/failed prints (not user
cancellations); clear it on ack and on scheduler dispatch.
- _is_printer_idle now short-circuits to not-idle whenever require_plate_clear
is on and the flag is set, regardless of the currently reported state —
so the gate holds through power cycles, Bambuddy restarts, and the printer
booting back into IDLE.
- /printers/{id}/clear-plate no longer requires the printer to report
FINISH/FAILED; it accepts the ack whenever the flag is raised.
- Frontend widgets (PrinterQueueWidget, Layout, BulkPrinterToolbar) gate on
the flag rather than reported state.
Tests: added regression tests for IDLE+awaiting=True (the #961 case) and
full DB round-trip tests for the persistence layer.
* Add filament_vendor field to UnlinkedSpool model and populate from API response
* Add filament_vendor field to UnlinkedSpool interface
* Enhance LinkSpoolModal to include filament_vendor in search and display
Display now powers on via wlopm when the daemon detects an NFC tag or
a significant weight change (>=50g, i.e. spool placed/removed) while
the screen is blanked. Minor scale fluctuations no longer wake the
display or prevent blanking. The daemon only re-blanks screens it woke
itself — touch-based wake/blank stays with swayidle so the two don't
conflict. Daemon discovers the Wayland session from the shared runtime
dir since it runs as a systemd service outside the compositor.
Display now powers on via wlopm when the daemon detects an NFC tag or
a significant weight change (>=50g, i.e. spool placed/removed) while
the screen is blanked. Minor scale fluctuations no longer wake the
display or prevent blanking. The daemon only re-blanks screens it woke
itself — touch-based wake/blank stays with swayidle so the two don't
conflict. Daemon discovers the Wayland session from the shared runtime
dir since it runs as a systemd service outside the compositor.
Display now powers on via wlopm when the daemon detects an NFC tag or
weight change while the screen is blanked. The daemon only re-blanks
screens it woke itself — touch-based wake/blank stays with swayidle so
the two don't conflict. Daemon discovers the Wayland session from the
shared runtime dir since it runs as a systemd service outside the
compositor.
Display now powers on via wlopm when the daemon detects an NFC tag or
weight change while the screen is blanked. Daemon discovers the Wayland
session from the shared runtime dir and coexists with swayidle which
continues to handle touch-based wake independently.
NozzleRackCard computed its rack base via min(present_ids), which breaks
when the lowest-ID slot is the one currently mounted to a hotend — the
firmware omits that ID from device.nozzle.info entirely, so min() picks
the next slot up and every remaining nozzle renders one position too
far left, with the "empty" placeholder pushed off the right end.
Use the fixed H2C rack base of 16 (matching
test_h2c_nozzle_rack_populated_with_8_entries in the backend) so the
empty slot stays anchored to its physical position regardless of which
nozzle is currently mounted.
Adds a frontend regression test covering ID 16 missing.
Follow-up to the SpoolBuddy LCD power-off fix. Field-testing on a
Raspberry Pi OS Bookworm kiosk showed the watchdog appearing absent
from `ps ax | grep spool` — actually it had already `exec`'d into
`swayidle`, but with no logging there was no way to confirm that
without manually re-running the script.
- spoolbuddy-idle.sh now redirects stdout+stderr to
~/.cache/spoolbuddy-idle.log and prints WAYLAND_DISPLAY,
XDG_RUNTIME_DIR, PATH, the resolved timeout, and the final
`swayidle` command line on every start.
- Auto-detect WAYLAND_DISPLAY by scanning $XDG_RUNTIME_DIR for a
wayland-* socket (10s retry loop) so the script survives the race
where labwc launches autostart before exporting its env.
- Default XDG_RUNTIME_DIR to /run/user/$(id -u) if unset.
The SpoolBuddy kiosk's "screen blank timeout" setting only painted a
black CSS overlay over the browser window — the HDMI panel's backlight
stayed on indefinitely, wasting power and risking burn-in on
OLED/LED panels.
Move blanking down to the OS layer:
- install.sh now installs swayidle + wlopm + jq and rewrites labwc's
autostart to launch a new spoolbuddy-idle.sh watchdog instead of the
old `wlr-randr --on` keep-alive loop.
- The watchdog sources /opt/bambuddy/spoolbuddy/.env, derives device_id
from the first non-loopback MAC (same algorithm as daemon/config.py),
fetches the configured blank_timeout from the backend once on boot,
and execs `swayidle -w timeout $T 'wlopm --off HDMI-A-1' resume
'wlopm --on HDMI-A-1'`. Touch/keypress wakes via labwc's input event
path. timeout=0 skips swayidle entirely so existing installs that
never picked a timeout keep their current always-on behavior.
- New GET /api/v1/spoolbuddy/devices/{id}/display endpoint returns the
current brightness + blank_timeout. Gated on INVENTORY_UPDATE (same
level the daemon heartbeat key already uses) so existing SpoolBuddy
API keys work without extra permissions.
- SpoolBuddyLayout drops blanked state, the blank timer, activity
listeners, resetActivity, and the CSS overlay. Runtime updates to
the timeout take effect on next kiosk/browser restart; default for
newly-enabled blanking is 300 seconds.
The prior fix (9f643724) added a list branch to _strip_container but only
walked one level deep. SQLAlchemy's insertmanyvalues feature can pass
parameters as nested containers (e.g. a list of tuples, or a tuple inside
a list) depending on the dialect path, so the inner tz-aware datetimes
still reached asyncpg and the hourly snapshot loop kept failing with:
asyncpg.DataError: invalid input for query argument $2: ...
(can't subtract offset-naive and offset-aware datetimes)
Replaced the two-helper design with a single recursive _strip() that
walks dict/list/tuple at any depth. One top-level call now handles every
parameter shape SQLAlchemy may use, regardless of executemany or the
insertmanyvalues batching path.
The hourly smart plug energy snapshot loop introduced with #941 crashed
every cycle on PostgreSQL installs with:
asyncpg.DataError: invalid input for query argument $2:
datetime.datetime(..., tzinfo=datetime.timezone.utc)
(can't subtract offset-naive and offset-aware datetimes)
The engine has a `before_cursor_execute` hook that strips tzinfo from
aware datetimes before they reach asyncpg (all schema datetime columns
are TIMESTAMP WITHOUT TIME ZONE). The hook's `_strip_container` handled
dict and tuple parameter containers but fell through `list` unchanged.
When SQLAlchemy's insertmanyvalues feature batches two or more rows into
a single INSERT ... SELECT FROM (VALUES ...) statement, it passes the
positional params as a flat `list`, so the tz-aware datetimes survived
the hook and reached asyncpg.
Added a list branch that mirrors the tuple one. No schema change needed
— the column stays naive UTC like the rest of the codebase. SQLite was
never affected.
The Printer tab AMS popup and spool auto-provisioner resolved color
names from hardcoded tray_id_name tables with a suffix-code fallback —
and suffix codes like "R1" are not globally unique across material
families. A17-R1 (PLA Translucent Cherry Pink) fell through the
fallback and resolved to "Scarlet Red" (A01-R1, PLA Matte), baking
the wrong name into auto-created inventory spools.
The fix removes the hardcoded tables entirely. Backend resolves color
names via the existing color_catalog table by hex; frontend fetches a
compact {hex: name} map once per session via a new
GET /inventory/colors/map endpoint (auth-gated but not on
inventory:read — read-only views need it too) and stores it in a
ColorCatalogProvider context. A useSyncExternalStore hook cascades a
re-render into pages mounted before the fetch completes so they
refresh from HSL-fallback names once the catalog loads.
Existing auto-provisioned spools keep their stored names; only new
provisioning and live display benefit. Co-Authored-By is intentionally
omitted here per project convention — set it via git config if needed.
LDAP auto-provisioning hit a NOT NULL constraint error on upgraded SQLite
installs because the existing migration only ran on PostgreSQL. The SQLite
branch now patches sqlite_master via writable_schema and bumps schema_version
so the change takes effect without a restart. Fresh installs were unaffected.
The Statistics page reported "Gesamt" (All Time) kWh correctly but showed
zero for Today/Week/Month in total-consumption mode. Two bugs drove it:
1. The starting plug counter was kept in an in-memory dict
`_print_energy_start` that was lost on any backend restart mid-print, so
the per-print `energy_kwh` delta silently never got computed. The stats
endpoint's fallback path `SUM(PrintArchive.energy_kwh)` therefore summed
to zero for users running in total mode.
2. Total-consumption mode has no per-print delta by design — it includes
idle/preheat/standby — so the fallback to archive rows was the wrong
strategy even when the data existed.
Fix, in two parts:
- Persist `energy_start_kwh` on the archive row and read it back from a
fresh session at print end. Deletes `_print_energy_start` and its 5
call sites, replacing them with a single `_record_energy_start()` helper.
Per-print tracking is now restart-resilient regardless of tracking mode.
- Add hourly `smart_plug_energy_snapshots` table + `_snapshot_loop()` in
SmartPlugManager. Rewrote the `/archives/stats` energy branch as
`_sum_snapshot_deltas()` which computes per-plug
`max(0, last-in-range - baseline)` where baseline is the latest snapshot
at or before the range start, falling back to the earliest-ever snapshot
and signalling `energy_data_warming_up` when no pre-range baseline
exists (fresh upgrade). MQTT plugs are skipped from snapshots since they
only report "today" and have no lifetime counter.
Frontend: QuickStatsWidget renders an AlertTriangle next to Energy Used /
Energy Cost with a tooltip when `energy_data_warming_up` is true, so the
"low values right after upgrading" situation is explained in-product.
Fully localised across 7 UI languages.
Tests: new backend unit tests cover the snapshot delta arithmetic
(baseline/endpoint, counter reset clamp, multi-plug, warming-up fallback,
endpoint windowing), per-print restart resilience via expunge_all, and the
snapshot task lifecycle (start idempotent, stop cancels). Frontend tests
assert the warning icon appears only when the flag is set and only on the
energy tiles.
Docs: updated `CHANGELOG.md`, `README.md`, wiki `features/energy.md`,
wiki `features/statistics.md`, and website `features.html` with the new
behaviour and warming-up explanation.
Previously, if a SpoolBuddy daemon crashed during registration it could
end up registered twice. The kiosk UI silently used only the first
device and there was no UI path to remove the orphan — administrators
had to delete the row directly in the database.
Adds a new Settings → SpoolBuddy tab that lists every registered device
with live connection status, system details (firmware, IP, CPU temp,
memory, disk, OS, daemon + system uptime), hardware health flags, and
an Unregister action gated by a confirm modal. A yellow banner appears
whenever more than one device is registered to flag likely crash-
duplicates. Backend adds DELETE /spoolbuddy/devices/{device_id} gated
by inventory:delete and broadcasts spoolbuddy_unregistered over WS so
other tabs refresh immediately.
The tab header shows a device-count pill and a green/gray status bullet
reflecting whether at least one registered device is online. An online
device that is accidentally unregistered re-registers itself on its
next heartbeat. Localized in English, German, and Japanese. The kiosk
layout still uses devices[0] — once the orphan is unregistered, the
remaining device naturally becomes [0].
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)
* 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.