Queue endpoints now return filament_type, filament_color, layer_height,
nozzle_diameter, and sliced_for_model from the archive or library file.
Previously this data was only available via the archive API.
Virtual printer "print_queue" mode created queue items with no printer
assignment. Now sets target_model from the VP's SSDP model code (e.g.
P1S, X1C) for "Any Printer" scheduling, or uses target_printer_id if
configured.
Settings page auto-save onSuccess overwrote localSettings with the
server response, discarding characters typed during the save request.
Removed the stale state overwrite so in-progress input is preserved.
When scheduling a print to "any printer" with a filament color override,
the scheduler picked a printer with the correct filament type but wrong
color. _find_idle_printer_for_model() used color matching only for
ranking candidates, not filtering — a printer with 0 color matches was
still selected if it had the right types.
Now requires at least 1 color match when filament overrides specify
colors. Printers with 0 matches are skipped and reported in the
"waiting for filament" reason instead of being treated as candidates.
Filenames containing / \ ? or # (e.g. "Abzweigdose/Verteilerdose 70mm")
broke the slicer protocol handler. BambuStudio/OrcaSlicer url_decode()
the entire protocol URL before downloading, which decoded %2F back to /,
creating extra path segments that returned 404. Sanitize these characters
to _ in slicer download URLs — the filename is cosmetic (backend resolves
files by archive ID).
Three independent code paths wrote to archive.cost with conflicting
strategies, causing the same model to produce different prices on each
reprint (e.g. £0.77, £1.54, £2.03).
- Remove add_reprint_cost (Path 3) — redundant cost accumulation that
double-counted on top of usage tracker
- Fix usage tracker (Path 2) — compute cost from current session's
results only, not a SUM of all historical SpoolUsageHistory rows
- Remove _reprint_archives tracking set from main.py
- Update test mocks to match simplified query pattern
archive.cost now always reflects the cost of a single print.
All backend timestamps used datetime.now() (server local time) or the
deprecated datetime.utcnow(). The frontend's parseUTCDate() assumes
timestamps without timezone indicators are UTC and appends 'Z', so
stored timestamps were off by the timezone offset when the container's
timezone wasn't UTC.
Backend: replaced datetime.now() and datetime.utcnow() with
datetime.now(timezone.utc) across 16 files (~80 call sites) for all
database fields and DB comparisons. Cosmetic timestamps (filenames,
user-facing local time formatting) intentionally left as local time.
Frontend: replaced 13 new Date(backendTimestamp) calls with
parseUTCDate() across 8 files to correctly interpret UTC timestamps.
The sidebar hid Settings based on hardcoded role === 'user' instead of
the settings:read permission, and login set user state directly from the
response instead of re-fetching full auth status with permissions.
The "Power off printer when done" checkbox in the print modal and the
auto power off tri-state toggle in the bulk edit modal were accessible
to all users regardless of permissions. Users without printers:control
can now no longer enable auto power off — controls are disabled and
visually dimmed.
The bed cooldown monitor was defined at the end of on_print_complete,
after an early return that exits when no archive is found. Prints
started from BambuStudio or the printer's touchscreen have no archive,
so the function returned before the bed cooldown task was ever created.
Moved the bed cooldown block (function def + task creation) to before
the archive_id early-return so it fires for all completed prints.
Also hardened the temperature dict check from truthiness to isinstance.
Three related fixes in PrintersPage.tsx:
1. External spool (vt_tray) fill level was missing the AMS remain
fallback — only checked Spoolman/inventory, ignoring valid AMS data.
Now uses the same fallback chain as regular and AMS-HT slots.
2. When fill was unknown (null), slot visual showed a full-width gray
bar ("full") while the hover card showed "—" ("empty"). Removed the
misleading gray fallback bar from all three slot types so both views
consistently show "unknown".
3. Fill source priority always preferred AMS remain over Spoolman and
inventory, even when those sources were more accurate (e.g. spools
with usage tracking or migrated from Spoolman). Reversed priority to
Spoolman → Inventory → AMS remain. Fixed fillSource label to reflect
the actual data source (was always 'ams' even when using fallback).
constructor before the try/except, so the FileNotFoundError was unhandled and crashed the entire daemon via asyncio.gather().
Now when the scale hardware isn't available (no /dev/i2c-0), it logs "Scale init failed" and sets self._ok = False, so scale_poll_loop will skip polling with "Scale not
available, skipping scale polling" — same graceful degradation the NFC reader already has.
External spool assignments (ams_id=255) were silently deleted on every
AMS change because _find_tray_in_ams_data only searches AMS units, not
vt_tray. Now looks up external assignments in the printer's vt_tray data
instead, and preserves assignments when vt_tray hasn't arrived yet.
On H2C/H2D dual-nozzle printers, tray_now=254 is sent generically for both
external spools. The direct ID comparison always matched Ext-L (id=254),
never Ext-R (id=255). Now uses active_extruder to disambiguate: extruder
1 (left) → Ext-L, extruder 0 (right) → Ext-R.
Previously the Inventory sidebar item was hidden when Spoolman was
enabled. Now it always shows — when Spoolman is active, /inventory
embeds the Spoolman web UI in the content area via iframe; when
disabled, it renders the internal inventory page as before.
When a printer has pending queue items and is in FINISH/FAILED state
awaiting plate clearing, a yellow dot now appears on the Printers
sidebar icon. Uses useQueries to check statuses of printers with
pending queue items from the existing WebSocket-warmed React Query
cache — no new backend endpoints or additional polling needed.
H2D firmware reports stg_cur=74 during print preparation, which was
not in the STAGE_NAMES lookup table (sourced from BambuStudio, which
only goes up to 66). This caused "Unknown stage (74)" to display on
the printer card. Also adds stage 77 ("Preparing AMS") which exists
in BambuStudio source but was missing from the lookup.
When scheduling a print to "any printer" (model-based assignment),
users can now override the 3MF's original filament choices per slot.
The override dropdown shows compatible filaments loaded across all
printers of the selected model, filtered by type and nozzle (H2D).
The scheduler matches against overridden type/color instead of the
original 3MF values, preferring printers with exact color matches.
Backend:
- New GET /printers/available-filaments endpoint (aggregates loaded
filaments across printers of a model, includes extruder_id for
dual-nozzle nozzle-aware filtering)
- New filament_overrides JSON column on print_queue table
- Scheduler applies overrides before AMS mapping, clears tray_info_idx
on overridden slots so color matching takes priority
- Printer selection prefers printers with most override color matches
Frontend:
- New FilamentOverride component with per-slot override dropdowns
- Type-filtered options (PLA slots only show PLA)
- Nozzle-aware filtering (H2D: only shows filaments on correct extruder)
- Integrated into PrintModal for model-based queue mode
Tests:
- 11 backend unit tests (color match counting, override application)
- 12 frontend tests (rendering, type/nozzle filtering, interactions)
i18n: All 6 locales (en, de, fr, it, ja, pt-BR)
SpoolBuddy turns a Raspberry Pi 4B with a PN5180 NFC reader and
NAU7802 scale into a filament management station that integrates
with Bambuddy via REST API and WebSocket.
Backend: SpoolBuddyDevice model, 10 REST endpoints (/spoolbuddy/*),
6 WebSocket broadcast types, background offline-detection watchdog.
RPi daemon: asyncio service with concurrent NFC polling (300ms,
MIFARE Classic + Bambu HKDF key derivation), scale reading (10 SPS,
5-sample moving average, stability detection), and 10s heartbeat
with exponential backoff reconnect.
Frontend: kiosk-optimized 1024x600 UI at /spoolbuddy with Dashboard
(live weight + NFC spool detection), AMS Overview, Inventory,
Printers, and Settings pages. useSpoolBuddyState reducer hook
driven by WebSocket CustomEvents.
i18n: all 6 locales (en, de, fr, it, ja, pt-BR).
Group toggle in inventory toolbar collapses identical unused/unassigned
spools into expandable rows/cards with count badges. Grouping key uses
material, subtype, brand, color, and label weight. Brand and subtype
fields now appear in quick-add mode as optional (no asterisk, no
validation). Quantity field restricted to quick-add mode only.
When the 3MF source file wasn't downloaded from the printer (e.g. FTP
failure on P2S), archive.file_path was null. The finish photo capture
silently skipped because it derived the save directory from file_path.
Now falls back to archive/{id}/ directory and logs a warning.
The rename endpoint updated `filename` but not `file_metadata.print_name`,
which the UI uses as the primary display name. Since print_name is extracted
from the 3MF at upload, it always took precedence over the renamed filename.
Now also updates print_name in file metadata when present.
The MQTT "fun" field is an integer in the JSON payload, but the
parser used int(value, 16) which requires a string. This raised
TypeError on every message, silently swallowed by the except clause,
so developer_mode was never set.