The previous fix only filtered status codes (< 0x4000) from the
print_error field. Firmware can also send the same false positive
through the hms array in MQTT, which had no such filter. Apply the
same < 0x4000 check to the HMS parser so status/phase indicators
are skipped regardless of which MQTT field carries them.
The isSlicedFile() check only matched .gcode or .gcode.3mf extensions,
so archives from .3mf prints (the standard Bambu slicer output) showed
a "SOURCE" badge instead of "GCODE". Since .3mf can be either sliced or
a raw CAD export, now checks the archive's total_layers and
print_time_seconds metadata to distinguish them. Also passes the
original filename when creating archives from file manager prints.
FTP upload: reduce chunk size from 1MB to 64KB for smooth progress bar
updates (~1s intervals instead of 20+ second gaps). Skip voidresp() for
all printer models — H2D delays the 226 response by 30+ seconds after
data transfer, causing a hang at 100%. Add transfer speed and TLS
handshake timing to logs for diagnosing slow connections.
Print/Schedule modal: widen from max-w-lg (512px) to max-w-2xl (672px)
to accommodate long filament profile names like "PLA Support for PETG
PETG Basic @Bambu Lab H2D 0.4 nozzle".
Profiles like "PLA Support for PETG PETG Basic @Bambu Lab H2D" have
filament_type PETG, but both the frontend and backend name parsers
found "PLA" first (iterating material types in order). The MQTT command
sent tray_type=PLA and tray_info_idx=GFL99 (PLA generic), so the
slicer displayed PLA instead of PETG.
- Frontend parsePresetName(): detect "X Support for Y" pattern, extract
material after "Support for"
- Frontend ConfigureAmsSlotModal: prefer corrected parsed material over
stored localPreset.filament_type for tray_type, tray_info_idx, and
temperature fallback
- Backend _parse_material_from_name(): same "Support for" handling for
future profile imports
- Backend assign_spool: prioritize spool.material over lp.filament_type
for generic filament ID lookup
The slicer_binary_path key was left in the settings table from earlier
slicer integration research but is no longer referenced anywhere in the
codebase. Add a startup migration step that deletes orphaned settings
keys so they don't persist in backups.
Inline callback refs on preset buttons called scrollIntoView on every
re-render, creating a scroll → re-render → scrollIntoView loop on
Windows. Replace with a useEffect + data attribute approach that only
scrolls when the selected preset actually changes.
* Add printer card print flow with drag-drop and reusable LibraryUploadModal
Extract inline upload modal from FileManagerPage into a reusable
LibraryUploadModal component. Add a Print button and drag-drop zone
to printer cards that upload files to the library and open PrintModal
with the printer pre-selected. Only .gcode/.gcode.3mf files are
accepted for printing. PrintModal hides printer selector when a
printer is provided via initialSelectedPrinterIds prop.
* Add printer compatibility check for drag-drop and upload-to-print flows
Validates sliced_for_model from file metadata against the target printer model
before opening the print modal, preventing users from printing with incompatible files.
* Improve FileUploadModal: auto-close on success, inline errors, file validation, and i18n
- Close modal automatically after successful upload instead of showing summary
- Show printer compatibility errors inline in the modal instead of closing
- Add validateFile and accept props to restrict file types for print flow
- Add onFileUploaded error return to prevent modal close on incompatible files
- Internationalize all hardcoded error/warning strings across 6 locales
* Clean up incompatible files from library and fix print button i18n
- Delete uploaded file from library when printer compatibility check fails
in both drag-drop and modal upload flows
- Fix print button tooltip to use existing common.print i18n key
- Fix accept attribute to use .gcode,.3mf for proper browser support
* Add printer information modal accessible from card 3-dot menu
Replace inline IP/serial display at bottom of printer card with a
dedicated "Printer Information" modal opened via the card menu. The
modal shows model, status, state, IP address (copyable), serial number
(copyable), WiFi signal, firmware, developer mode, nozzle count, SD
card, auto-archive, total print hours, location, and added date, along
with the printer image. Includes full i18n support across all 6 locales
and accessibility attributes (role="dialog", aria-modal).
* Extract getPrinterImage and getWifiStrength into shared printer utils
---------
Co-authored-by: MartinNYHC <mz@v8w.de>
PR #569 introduces drag-drop printing on printer cards with
inline fallback strings for common.uploading, common.uploadFailed,
printers.dropToPrint, and printers.cannotPrint. Add these keys
to all 7 locale files so they render in the correct language.
* feat(queue): show spool grams left in filament slot mapping
* Bumped version
* Add SpoolBuddy AMS slot config, external slots, and dashboard redesign
- AMS page: external spool slots (Ext/Ext-L/Ext-R), click-to-configure
modal on all slots, temperature/humidity threshold-colored indicators,
nozzle L/R badges for dual-nozzle printers, compact AMS-HT layout
- Dashboard: two-column layout with device status + printers list (left)
and current spool card (right), state-colored scale/NFC icons, dashed
border card styling
- Daemon: suppress redundant scale reports (±2g threshold + stability
state change detection) to prevent weight display bouncing
- TopBar: auto-select online printers only, SpoolBuddy logo
* Fix SpoolBuddy daemon crash when read_tag module is missing
NFCReader.__init__ imported read_tag and instantiated PN5180() outside
the try/except block, so a missing module crashed the entire daemon.
Moved the import inside the existing try/except so the daemon gracefully
skips NFC polling — matching the scale reader's existing behavior.
* Fix SpoolBuddy daemon failing to import hardware drivers
The daemon imports read_tag and scale_diag as bare modules, but they
live in spoolbuddy/scripts/ which isn't on sys.path when systemd runs
the daemon. Added scripts/ to sys.path at startup, resolved relative
to the module file. Also moved the read_tag import inside NFCReader's
try/except (was crashing the daemon instead of skipping gracefully)
and demoted hardware-not-available messages from ERROR to INFO.
* Increase scale moving average window to reduce weight bouncing
5 samples at 100ms (500ms window) wasn't enough to smooth NAU7802 ADC
noise — the averaged value still varied by >2g between 1s report
intervals, and the stability state kept flipping, triggering a report
every cycle. Increased to 20 samples (2s window) so noise is smoothed
before reaching the reporting layer.
* Remove stability flipping as scale report trigger
When ADC noise kept the spread hovering around the 2g stability
threshold, the stable flag toggled every cycle, forcing a report with
a slightly different weight each time. Now only actual weight changes
of >=2g trigger reports. The stable flag is still included in each
report for consumers that need it.
* Fix formatting of option elements in FilamentMapping
* Make low stock threshold editable
* Add new filter for low spools
* Update bug report template to require additional fields
* Added toast for invalid imputs with locales, updated inputb field restrictions
* Minor Spoolbuddy frontend improvements
* Updated test_backend.sh
* Updated Spoolbuddy install script to strip down Raspbian
* Updated Spoolbuddy install script
* Add API key auth support to /auth/me for SpoolBuddy kiosk
When Bambuddy auth is enabled, the SpoolBuddy kiosk gets redirected to
the login page because ProtectedRoute requires a user from GET /auth/me,
which only handled JWT tokens. The kiosk daemon already has an API key
but couldn't use it to satisfy the frontend auth check.
- Backend: /auth/me now accepts API keys (Bearer bb_xxx or X-API-Key)
and returns a synthetic admin UserResponse with all permissions
- Frontend: AuthContext reads ?token= from URL on first load, stores in
localStorage, and strips from URL (prevents history/referrer leakage)
- Install script: kiosk URL now includes ?token=${API_KEY}
- Tests: 3 new integration tests (Bearer API key, X-API-Key header,
invalid key rejection)
* SpoolBuddy touch-friendly UI overhaul for 1024x600 kiosk display
Enlarge all interactive elements across 9 SpoolBuddy components to meet
44px minimum tap targets on the RPi touchscreen. Increase nav icons
(20→24px), labels (10→12px), bar heights, section headers, printer
buttons, spool visualizations, fill bars, and status indicators.
Compact the dashboard stats bar and remove the printers card. Add
fullScreen prop to ConfigureAmsSlotModal with two-column layout
(filament list left, K-profile + color right) to eliminate scrolling.
* Minor changes, CSS fixes
* Refactor usageFilter state to remove 'lowstock' option for clarity
* Move var saving to API, add test coverage
* fix: threshold validation and cleanup
* Change test input from '150' to '0'
---------
Co-authored-by: tridev <c.tripod@gmx.ch>
Co-authored-by: MartinNYHC <mz@v8w.de>
Some firmware sends non-zero print_error values (e.g. 0x03000002 →
0300_0002) during normal printing as status/phase indicators. The
parser treated any non-zero value as a real error, triggering
notifications. All known real errors have codes >= 0x4000 (fatal,
warning, prompt ranges). Now skips print_error values where the
low 16 bits are below 0x4000.
Also fix K-profile greenlet error on auto-created RFID spools by
eagerly setting k_profiles=[] after flush in create_spool_from_tray().
create_spool_from_tray() didn't eagerly load the k_profiles
relationship, so auto_assign_spool() triggered a lazy load in async
context when iterating spool.k_profiles. Set k_profiles=[] on new
spools since they can never have K-profiles yet.
Write NTAG213/215/216 tags for third-party spools via the SpoolBuddy
kiosk UI. New "Write" page with three workflows: existing spool, new
spool creation, and tag replacement. Backend encodes 133-byte OpenTag3D
NDEF payloads (material, color, brand, weight, temp). Daemon writes
page-by-page via PN5180 NTAG WRITE command with read-back verification.
Write commands flow through heartbeat polling with WebSocket status
updates. Includes 39 new tests and translations for all 6 languages.
- Add printer status badges to dashboard left column (green/gray
online/offline dots, compact wrapping pills, overflow-hidden)
- Remove SpoolBuddy inventory page — inventory management belongs
in the main Bambuddy frontend, not the kiosk
- Remove inventory nav item from bottom nav (now 3 tabs)
- Replace "Add to Inventory" navigate with quick-add modal that
creates a basic PLA spool entry via API, with a hint to use
the main Bambuddy UI for full spool details instead
- Redesign settings page with tabbed layout (Device, Display, Scale, Updates)
- Add screen blank timeout: blanks after touch inactivity, tap to wake
- Add CSS brightness filter for HDMI displays (no sysfs on HDMI)
- Add backend `language` field to app settings for server-side persistence
- Sync UI language from backend on kiosk load (separate Chromium instance)
- Top bar clock respects user's time format setting (system/12h/24h)
- Add SpoolBuddy settings translations for all 6 languages (en/de/fr/ja/it/pt-BR)
- Disable Chromium swipe-to-navigate in kiosk install script
- Add `video` group for DSI backlight access
1. Device tab — repo URL unlinked
Replaced the <a> tag with a <span> so the GitHub URL is displayed as plain text (no clickable link in kiosk mode).
2. Display controls — moved to frontend
Root cause: The daemon runs as a system user without Wayland access, and the kiosk uses an HDMI display (no sysfs backlight). Neither wlopm, vcgencmd, nor sysfs writes
can work in this setup.
Fix — frontend-controlled display in SpoolBuddyLayout.tsx:
- Brightness: CSS filter: brightness(X) applied to the layout root. Immediate visual dimming that works on any display type.
- Screen blanking: Full-screen black overlay (z-[9999]) shown after the configured inactivity timeout. Touch anywhere to wake.
- Activity tracking: Resets on pointerdown/keydown events AND on WebSocket-driven NFC/scale changes (weight update, tag scan).
- Queries device data every 15s to read the current brightness and blank timeout values.
Daemon display_control.py — Simplified:
- Removed vcgencmd/wlopm/subprocess usage entirely
- _blank/_unblank are now state-tracking no-ops (log only)
- DSI backlight brightness via sysfs is preserved as a bonus for DSI displays
- Updated docstring to explain the architecture
3. Scale tab — redesigned with step indicator
Idle state: Same as before — weight card with tare/calibrate buttons.
Calibration wizard: Two-column layout:
- Left column (~64px): Vertical step indicator with numbered circles, checkmark for completed steps, and a connecting line
- Right column: Live weight bar (with stable/settling indicator), step-specific content (instructions or numpad), and action buttons pinned to bottom via mt-auto
The numpad buttons are min-h-[52px] with text-lg — large enough for touch on 1024x600. The weight info card is hidden during the weight entry step to maximize vertical
space.
Root cause: The daemon used wlopm for screen blanking, but wlopm was never installed. Additionally, the daemon runs as the spoolbuddy system user which has no access to
the Wayland socket, so Wayland-based tools can't work.
Fix in display_control.py:
- Replaced wlopm --off/--on with vcgencmd display_power 0/1 — this is a Raspberry Pi firmware-level command that's pre-installed and works without Wayland socket access
- Added shutil.which("vcgencmd") check at init to avoid repeated failures on non-RPi hardware
- Added explicit PermissionError handling for brightness writes with a helpful message about the video group
Fix in install.sh:
- Added video group to the spoolbuddy service user's groups (was: gpio, spi, i2c → now: gpio, spi, i2c, video), which grants access to both vcgencmd and sysfs backlight
files
Scale Tab Numpad
Root cause: On the 1024x600 kiosk screen (~376px available content height), the weight info card + numpad + action buttons exceeded the space, causing tiny buttons and
overlapping.
Fix in SpoolBuddySettingsPage.tsx:
- Hide the weight info card during weight entry step (calStep !== 'weight'), reclaiming ~70px
- Compact inline weight reading in the step header (small dot + monospace text) so users can still see the live scale value
- Larger numpad buttons: min-h-[56px] with text-lg font size (was no min-height, text-sm)
- Added active:scale-95 for tactile touch feedback
- mt-auto on action buttons to push them to the bottom, preventing overlap
- Removed the wrapping card around the calibration flow to save vertical padding
The Raspberry Pi kiosk has no physical keyboard and system-level virtual
keyboards (squeekboard, wvkbd) don't auto-show/hide with labwc/Chromium.
Add a react-simple-keyboard QWERTY keyboard that auto-shows on input
focus, with dark theme, shift/caps/backspace, email keys (@, .), and a
two-phase close that prevents ghost-click passthrough to elements below.
Inputs with data-vkb="false" opt out (e.g. SpoolBuddySettingsPage numpad).
- Show inventory-based fill levels for non-BL spools in AMS slots by
fetching spool assignments and computing fill from weight_used/label_weight,
falling back to AMS remain when no assignment exists
- Fix ext-R slot falsely highlighted as active when idle: tray_now=255
(no tray loaded) matched ext-R's id=255 without a guard
- Improve single-slot card layout: use responsive grid aligned with AMS
card columns, fix vertical stretching with few AMS units
- Remove redundant L/R nozzle badges from ext slots (label already shows
Ext-L/Ext-R)
- Add 16 tests for ext active state logic and fill override fallback
SpoolBuddy's "Link to Spool" used the generic updateSpool API which only
set tag_uid, leaving tag_type and data_origin empty. Now uses linkTagToSpool
with tag_type='generic' and data_origin='nfc_link'.
Added a "Weight Check" inventory column (hidden by default) that compares
each spool's last scale measurement against calculated gross weight with
±50g tolerance. Shows green check for match, yellow warning + sync button
for mismatch. Backend stores last_scale_weight and last_weighed_at on each
spool when weight is synced via SpoolBuddy. Includes edge case handling
when scale weight < core weight. i18n keys added for all 6 locales.
Builds multi-arch Docker image from the current APP_VERSION in config.py
and pushes to GHCR + Docker Hub, overwriting the same beta tag. Creates
or updates a GitHub prerelease with changelog notes. Users re-pull the
tag or use Watchtower to get daily updates. Supports --parallel,
--ghcr-only, --dockerhub-only, and --skip-release flags. Documents
daily beta builds in README and DOCKERHUB.md.
Automates the full beta release workflow: version bump in config.py,
CHANGELOG date update, git commit + tag, multi-arch Docker build+push
to GHCR and Docker Hub, and GitHub prerelease creation with changelog
notes. Supports --parallel, --ghcr-only, --dockerhub-only, and
--skip-release flags. Documents daily beta builds in README and
DOCKERHUB.md.
The printer card only showed progress bar, print name, ETA, layers,
and cover image when gcode_state was RUNNING. A paused print (state
PAUSE) fell through to the idle display, hiding all print context.
Added PAUSE to the display conditions in both compact and expanded
view modes, and replaced the hardcoded "Printing" status label with
getStatusDisplay() so it correctly shows "Paused".
The assign_spool endpoint sent wrong MQTT field values for user presets,
causing the slicer's AMS slot detail card to show all fields empty.
Two bugs: (1) cloud API was called with the raw slicer_filament value
including its version suffix (e.g. PFUS9ac902733670a9_07), returning 404;
the silent fallback sent setting_id as tray_info_idx instead of the real
filament_id; (2) no SlotPresetMapping was saved after assignment.
Now strips version suffixes before cloud lookup, resolves the real
filament_id via cloud API (with local preset and generic fallbacks),
includes brand in tray_sub_brands, and saves slot preset mapping.
Print Activity now shows an hourly heatmap (hours x days) for timeframes
≤7 days and dynamically adjusts calendar months for longer ranges.
Adds hourly granularity to Filament Trends, persists timeframe selection,
fixes optional chaining in QuickStatsWidget, and fixes UTC/local timezone
mismatches in date key generation. Also hardens the /archives/slim limit
param and fixes empty query string handling in API client.
- Add lightweight GET /archives/slim endpoint with column-level SELECT
(13 fields vs 46), skipping duplicate detection for ~70-80% payload
reduction on the stats dashboard
- Add ArchiveSlim Pydantic schema and TypeScript type
- Switch StatsPage and FilamentTrends to use ArchiveSlim
- Fix critical bug in failure_analysis.py: use effective_days for week
count, separate non-date filters, build fresh week_filter per loop
- Fix busiestDay timezone bug: parse YYYY-MM-DD with split() + local
Date constructor instead of new Date() which creates UTC midnight
- Fix success streak ordering: sort by completed_at || created_at
instead of created_at alone
- Add 6 integration tests for /archives/slim endpoint
Use MetricToggle component with new exclude prop instead of inline
buttons, default to weight-first order, and redesign chart layout
with centered donut, total label, and 2-column legend grid.
Add Print Time of Day, Color Distribution, and Records widgets to the
Stats page. Extract shared MetricToggle component for consistent
weight/prints/time toggles. Add date range filtering to archives and
stats API endpoints. Reorganize widgets into parent cards (Printer Stats
and Filament Trends) and fix Dashboard layout persistence for new widgets.
_get_mac_id() used Path.iterdir() to find a network interface for the
device ID, but iteration order is non-deterministic. On different boots
it could pick eth0 (MAC …3100) or wlan0 (MAC …3102), producing a
different device_id each time. Since calibration is stored per device ID
in the backend DB, a new ID meant registering as an uncalibrated device.
Sort interfaces alphabetically so the same one is always selected.
BambuStudio uses TLS on port 3002 for certain printer models (e.g. A1
Mini / N1). The bind server only spoke plain TCP on both ports, so the
TLS ClientHello was rejected as "invalid frame" and the slicer could
never discover or connect to the virtual printer.
Port 3002 now uses TLS (reusing the VP's existing certificate), port
3000 remains plain TCP. Also updated proxy-mode to use TLSProxy for
the port 3002 bind proxy instead of raw TCPProxy.
- Replace TagDetectedModal with inline SpoolInfoCard/UnknownTagCard
in dashboard right panel (known spools show assign/sync/close,
unknown tags show add-to-inventory/link/close)
- Rewrite AssignToAmsModal as full-screen overlay reusing AmsUnitCard,
with AMS-HT and external slot support, single assignSpool API call
- Remove printer selector from assign modal (uses top bar selection)
- Extract filament_id <-> setting_id conversion to shared utility
(backend/app/utils/filament_ids.py), used by inventory + cloud routes
- Normalize slicer_filament in assign_spool to derive proper
tray_info_idx and setting_id for MQTT (was sending setting_id="")
- Rename SpoolBuddy top bar status label "Online" -> "Backend"
- Remove weightStable guard from sync weight button