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.
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.
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.
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.
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.
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.
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
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.
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).
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.
Admins can now filter the Statistics page by user via a new
stats:filter_by_user permission. A user dropdown appears in the stats
header showing all users plus "No User (System)" for prints without
attribution. The filter applies to all stats widgets, failure analysis,
and CSV/Excel exports. Backend validates the permission on all 4 stats
endpoints, returning 403 if the filter is used without authorization.
Camera streams, snapshots, thumbnails, timelapse videos, photos, QR
codes, and cover images served via <img>/<video> tags were previously
unauthenticated because browser media elements cannot send Authorization
headers. When auth is enabled, these endpoints are now protected by a
reusable stream token (?token=xxx) obtained from POST
/printers/camera/stream-token (requires CAMERA_VIEW permission).
Auto-off now has a "Keep Enabled" toggle that keeps it active between
prints instead of disabling after each use (one-shot). Useful for HA
accessories like BentoBox filters that should always power off after
prints. Default behavior (one-shot) is unchanged.
The daemon now collects CPU temp, core count, load average, memory/disk
usage, OS info, and system uptime every heartbeat using stdlib-only reads
from /proc and /sys. Stats are sent as a JSON blob in the heartbeat
payload, stored in a new system_stats TEXT column, and displayed in a
new "System" tab in SpoolBuddy Settings with color-coded usage bars.
The kiosk touchscreen has no way to hard-refresh, and the service worker
served stale cached JS after updates. SpoolBuddy pages now unregister
any existing SW and skip registration entirely. Regular desktop/mobile
users still get the SW. Restored kiosk restart in SSH update flow since
SW is no longer an obstacle.
Merged version, status, and update check into a single card. Buttons
are side by side when up to date. Reduced padding and font sizes.
Removed separate "complete" banner since status is now cleared on
re-registration. SSH Setup section is smaller and more compact.
The SSH update set status to "complete" after the daemon had already
restarted and re-registered, overwriting the cleared state so it stuck
forever. Removed the post-restart "complete" write — daemon
re-registration is now the completion signal, clearing any update status.
The OTA update feature added update_status and update_message to the
SpoolBuddyDevice model but no ALTER TABLE migration, causing
"no such column: spoolbuddy_devices.update_status" on existing databases.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
X1C and X1 virtual printers used legacy SSDP model codes
(3DPrinter-X1-Carbon, 3DPrinter-X1) that BambuStudio doesn't
recognize, causing "incompatible printer preset" errors when
sending prints. Changed to the correct codes (BL-P001, BL-P002)
that real printers report via SSDP.
Also fixed proxy mode auto-inherit storing printer display names
(e.g. "X1C") instead of SSDP codes, by adding a resolution layer
that maps display names to model codes.
DB migration auto-converts existing VPs on startup.
Per-printer camera rotation (0°/90°/180°/270°) for cameras mounted
in portrait or upside-down. CSS rotation for live views, Pillow
rotation for notification snapshots. Setting visible in external
camera config when enabled.
Printer File Manager Doesn't Auto-Refresh ([#704](https://github.com/maziggy/bambuddy/issues/704)) — The printer file manager (SD card browser) only fetched the file list once when opened. Files uploaded from BambuStudio/OrcaSlicer while the modal was open wouldn't appear until the user clicked the refresh button or reopened the modal. Now auto-refreshes every 30 seconds while open. Reported by @shadowjig.
Database Connection Pool Exhaustion Under Load ([#704](https://github.com/maziggy/bambuddy/issues/704)) — Background tasks (print scheduler FTP uploads, camera captures, notification sends, timelapse stitching) held database sessions open during slow network I/O, consuming connection pool slots for seconds at a time. With the default pool of 15 connections (size 5 + overflow 10), concurrent operations during print start/complete events could exhaust the pool, causing `QueuePool limit reached` errors and `greenlet_spawn` failures in RFID spool auto-assignment. Doubled the pool to 30 connections (size 10 + overflow 20). Reported by @shadowjig.
The migration that removes the UNIQUE constraint on smart_plugs.printer_id
used an exact substring match ("printer_id INTEGER UNIQUE") to detect the
constraint. Databases created with older SQLAlchemy versions may express
the constraint differently (quoted column names, table-level UNIQUE clause,
or separate UNIQUE indexes), causing the migration to silently skip.
Users hit "IntegrityError: UNIQUE constraint failed: smart_plugs.printer_id"
when assigning a second HA switch to a printer.
Replace the exact string match with regex pattern matching that handles
inline constraints, table-level UNIQUE(printer_id), quoted column names,
and standalone UNIQUE indexes.
Notify users when the first layer finishes printing so they can check
adhesion remotely. Triggers once per print when layer 2 begins
(layer_num >= 2, capped at <= 5 to handle reconnects). Includes a
camera snapshot attachment. Adds the on_first_layer_complete toggle
to all notification providers, with backend/frontend/i18n support
across all 7 locales.
Cloud credentials were stored globally — one Bambu Cloud account per
Bambuddy instance. When auth was enabled, any user logging into Cloud
overwrote everyone else's credentials. Credentials are now stored
per-user: each user gets their own independent Cloud login.
Also fixed cloud data endpoints (settings, fields, preset CRUD)
requiring settings:read/settings:update permissions instead of
cloud:auth — users who had "Cloud Auth" enabled but "Settings"
disabled couldn't load profiles after logging in.