3dd1ffb485 v0.2.2b1 (#602)
* 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

* Minor Spoolbuddy frontend improvements

* 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.

* Updated CI

* Fix naive-vs-aware datetime crash from 0.2.1 timezone migration

The timezone fix (ed36eaf) replaced datetime.utcnow() with
datetime.now(timezone.utc) across ~80 call sites, but SQLAlchemy's
SQLite DateTime columns strip tzinfo on read, returning naive datetimes.
Any Python-side comparison (subtraction, <, >) between an aware "now"
and a naive DB value raises TypeError.

Add `if value.tzinfo is None: value = value.replace(tzinfo=timezone.utc)`
guards at all 8 affected comparison sites:
- maintenance.py: last_performed_at subtraction (500 on /maintenance/overview)
- auth.py: API key expires_at check (2 locations)
- print_scheduler.py: scheduled_time comparison
- smart_plug_manager.py: auto_off_pending_since elapsed calc
- smart_plugs.py: power_alert_last_triggered cooldown
- main.py: last_runtime_update elapsed calc
- archives.py: timelapse completed_at fallback

* Fix naive-vs-aware datetime crash from 0.2.1 timezone migration

The timezone fix (ed36eaf) replaced datetime.utcnow() with
datetime.now(timezone.utc) across ~80 call sites, but SQLAlchemy's
SQLite DateTime columns strip tzinfo on read, returning naive datetimes.
Any Python-side comparison (subtraction, <, >) between an aware "now"
and a naive DB value raises TypeError.

Add `if value.tzinfo is None: value = value.replace(tzinfo=timezone.utc)`
guards at all 9 affected comparison sites:
- maintenance.py: last_performed_at subtraction (2 code paths — days-based
  and hours-based intervals both crashed on /maintenance/overview)
- auth.py: API key expires_at check (2 locations)
- print_scheduler.py: scheduled_time comparison
- smart_plug_manager.py: auto_off_pending_since elapsed calc
- smart_plugs.py: power_alert_last_triggered cooldown
- main.py: last_runtime_update elapsed calc
- archives.py: timelapse completed_at fallback

* Fix queue badge showing on printers without matching filament (#486)

The purple queue counter badge in the printer card header used the raw
unfiltered queue item count, so it appeared on ALL printers of the same
model when a job was scheduled for "any [model]" — even printers without
the matching filament color. The PrinterQueueWidget below it (which
shows "Clear Plate & Start") already filtered by filament type + color.

Apply the same filament compatibility filter (type check + color override
check) to the badge count so it only shows on printers that can actually
run the queued jobs.

* Fix Windows install syntax error from LF line endings (#544)

The start_bambuddy.bat launcher had Unix (LF) line endings. When a
user's git is configured with core.autocrlf=false or input, the file
is checked out with LF endings and cmd.exe fails with "the syntax of
the command is incorrect" before reaching the hash check.

Add .gitattributes forcing CRLF for *.bat files so they always get
correct line endings on checkout regardless of git config.

* Add SpoolBuddy tag detection modal and fix NFC reader silent failure

  Frontend: Replace inline SpoolInfoCard/UnknownTagCard with full-screen
  TagDetectedModal that auto-opens on NFC tag detection. Known spools show
  remaining weight, fill bar, and offer "Assign to AMS" (new sub-modal with
  printer selector + AMS slot grid) and "Sync Weight". Unknown tags offer
  "Add to Inventory" and "Link to Spool". Modal stays open on tag removal,
  won't re-open for dismissed tags, reopens on re-place after removal.

  Daemon: Fix PN5180 NFC reader silently stopping tag detection when the
  reader drifts into a stuck state. Add auto-recovery (full hardware reset
  after 10 consecutive errors), preventive RF cycling every 60s when idle,
  and periodic status logging. Heartbeat now reports actual nfc_ok/scale_ok
  from reader instances instead of hardcoded True.

* Add SpoolBuddy tag detection modal and fix NFC reader silent failure

  Frontend: Replace inline SpoolInfoCard/UnknownTagCard with full-screen
  TagDetectedModal that auto-opens on NFC tag detection. Known spools show
  remaining weight, fill bar, and offer "Assign to AMS" (new sub-modal with
  printer selector + AMS slot grid) and "Sync Weight". Unknown tags offer
  "Add to Inventory" and "Link to Spool". Modal stays open on tag removal,
  won't re-open for dismissed tags, reopens on re-place after removal.

  Daemon: Fix PN5180 NFC reader silently stopping tag detection. The reader
  drifts into a stuck state where activate_type_a() returns None without
  raising exceptions, making error-based recovery useless. Changed the
  preventive idle maintenance from a lightweight RF off/on cycle to a full
  hardware reset (RST pin toggle + RF re-init) every 60s — the same reset
  that a service restart performs. Also added error-based auto-recovery
  after 10 consecutive exceptions, promoted poll errors to WARNING, added
  periodic status logging, and fixed heartbeat to report actual nfc_ok/
  scale_ok instead of hardcoded True.

* Add SpoolBuddy tag detection modal and fix NFC reader polling

  Frontend: Replace inline SpoolInfoCard/UnknownTagCard with full-screen
  TagDetectedModal that auto-opens on NFC tag detection. Known spools show
  remaining weight, fill bar, and offer "Assign to AMS" (new sub-modal with
  printer selector + AMS slot grid) and "Sync Weight". Unknown tags offer
  "Add to Inventory" and "Link to Spool". Modal stays open on tag removal,
  won't re-open for dismissed tags, reopens on re-place after removal.

  Daemon: Fix PN5180 NFC reader failing to maintain tag detection. After a
  successful SELECT the card stays in ACTIVE state and ignores subsequent
  WUPA/REQA, causing immediate false "tag removed" events. Added a brief
  RF off/on cycle (13ms) before each poll to force cards back to IDLE
  state. Also added a preventive full hardware reset every 60s when idle
  to recover from deeper stuck states where activate_type_a() silently
  returns None without exceptions. Heartbeat now reports actual nfc_ok/
  scale_ok instead of hardcoded True.

* Add SpoolBuddy tag detection modal and fix NFC reader polling

  Frontend: Replace inline SpoolInfoCard/UnknownTagCard with full-screen
  TagDetectedModal that auto-opens on NFC tag detection. Known spools show
  remaining weight, fill bar, and offer "Assign to AMS" (new sub-modal with
  printer selector + AMS slot grid) and "Sync Weight". Unknown tags offer
  "Add to Inventory" and "Link to Spool". Modal stays open on tag removal,
  won't re-open for dismissed tags, reopens on re-place after removal.

  Daemon: Fix PN5180 NFC reader failing to maintain tag detection. After a
  successful SELECT the card stays in ACTIVE state and ignores subsequent
  WUPA/REQA. Added a conditional RF off/on cycle (13ms) before each poll,
  but only when a tag is present — resets card from ACTIVE to IDLE for
  re-selection. The cycle is skipped when idle to avoid degrading reader
  state with continuous RF toggling, which prevented new tags from being
  detected after removal. Also added a preventive full hardware reset every
  60s when idle to recover from deeper stuck states, error-based recovery
  after 10 consecutive exceptions, and accurate heartbeat reporting of
  NFC/scale health.

* Add SpoolBuddy tag detection modal and fix NFC reader polling

  Frontend: Replace inline SpoolInfoCard/UnknownTagCard with full-screen
  TagDetectedModal that auto-opens on NFC tag detection. Known spools show
  remaining weight, fill bar, and offer "Assign to AMS" (new sub-modal with
  printer selector + AMS slot grid) and "Sync Weight". Unknown tags offer
  "Add to Inventory" and "Link to Spool". Modal stays open on tag removal,
  won't re-open for dismissed tags, reopens on re-place after removal.

  Daemon: Fix PN5180 NFC reader failing to detect tags. Each
  activate_type_a() call returning None corrupts the PN5180 transceive
  state, silently preventing all subsequent tag detection. Fixed by
  performing a full hardware reset (RST pin toggle + RF re-init) before
  every idle poll (~240ms, ~1.8 Hz rate). When a tag is present, a light
  RF off/on cycle (13ms) resets the card from ACTIVE to IDLE state for
  continuous re-selection. Also added error-based recovery, periodic
  status logging, and accurate heartbeat NFC/scale health reporting.

* Fix SpoolBuddy scale tare & calibration not being applied

  The tare and calibrate buttons on the Settings page queued commands
  but never executed them due to three broken links:

  1. Daemon received tare command via heartbeat but never called
     scale.tare() — the ScaleReader was available in shared dict
     but unused
  2. No API endpoint for the daemon to report the new tare offset
     back to the backend DB, so tare results were lost
  3. Heartbeat updated config but never called
     scale.update_calibration(), so ScaleReader kept initial values

  Added set-tare endpoint + API client method, and fixed heartbeat
  loop to execute tare, persist the result, and propagate calibration
  changes to the ScaleReader instance.

* Fix SpoolBuddy scale tare & calibration not being applied

  The tare and calibrate buttons on the Settings page queued commands
  but never executed them due to three broken links:

  1. Daemon received tare command via heartbeat but never called
     scale.tare() — the ScaleReader was available in shared dict
     but unused
  2. No API endpoint for the daemon to report the new tare offset
     back to the backend DB, so tare results were lost
  3. Heartbeat updated config but never called
     scale.update_calibration(), so ScaleReader kept initial values

  Added set-tare endpoint + API client method, and fixed heartbeat
  loop to execute tare, persist the result, and propagate calibration
  changes to the ScaleReader instance. Skip calibration sync on the
  heartbeat cycle that delivers a tare command, since the response
  predates the tare and would overwrite it with stale values.

* Fix SpoolBuddy scale tare & calibration not being applied

  The tare and calibrate buttons on the Settings page queued commands
  but never executed them due to four broken links:

  1. Daemon received tare command via heartbeat but never called
     scale.tare() — the ScaleReader was available in shared dict
     but unused
  2. No API endpoint for the daemon to report the new tare offset
     back to the backend DB, so tare results were lost
  3. Heartbeat updated config but never called
     scale.update_calibration(), so ScaleReader kept initial values
  4. The heartbeat response delivering the tare command still had
     pre-tare values, immediately overwriting the new offset to zero

  Added set-tare endpoint + API client method, and fixed heartbeat
  loop to execute tare, persist the result, propagate calibration
  changes to the ScaleReader, and skip calibration sync on the
  heartbeat cycle that delivers a tare command.

  Also replaced the calibration weight input with a touch-friendly
  numpad since the RPi kiosk has no physical keyboard.

* Fix SpoolBuddy scale tare & calibration not being applied

  The tare and calibrate buttons on the Settings page queued commands
  but never executed them due to five broken links:

  1. Daemon received tare command but never called scale.tare()
  2. No endpoint to persist tare offset back to backend DB
  3. Heartbeat never called scale.update_calibration()
  4. Heartbeat response with stale values overwrote new tare to zero
  5. set-factor used DB tare_offset (stale/zero), producing wrong
     calibration factor — empty scale showed ~5000g

  Fixed daemon to execute tare, persist result, and propagate
  calibration. Calibration step now captures raw ADC at tare time
  and sends it with step 2, making factor computation self-contained.

  Replaced calibration weight input with compact touch numpad for
  the RPi kiosk's 1024x600 touchscreen (no physical keyboard).

* Fix A1 Mini "unknown" status from non-UTF-8 MQTT payload (#549)

  Some firmware versions send MQTT payloads with non-UTF-8 bytes.
  UnicodeDecodeError was uncaught, silently dropping the entire message
  and leaving printer status as "unknown" with 0°C temps and no AMS.

  Fall back to decode(errors="replace") to keep JSON parseable.

* Add H2C dual nozzle variant O1C2 model support (#489)

  The H2C dual nozzle variant reports model code O1C2 via MQTT, but only
  O1C was recognized. This caused the camera to use the wrong protocol
  (chamber image on port 6000 instead of RTSP on port 322), producing a
  reconnect loop. Added O1C2 to all model ID maps across 8 files.

* Updated commit message (now includes the test fix):

  Fix camera button permissions & ffmpeg process leak (#550)

  Camera button on printer card was clickable without camera:view
  permission. ffmpeg processes (~240MB each) accumulated after closing
  camera streams because: (1) stop endpoint called terminate() without
  wait()/kill(), (2) HTTP disconnect detection only ran between frames
  so was blocked when the generator was stuck on stdout read, and
  (3) no mechanism caught processes orphaned by generator abandonment
  or app restarts.

  - Add camera:view permission check + tooltip to camera button
  - Fix stop endpoint: terminate() → wait(2s) → kill() → wait()
  - Add background disconnect monitor (polls every 2s, kills ffmpeg
    directly on disconnect)
  - Add periodic /proc scan (every 60s) that SIGKILLs any ffmpeg
    with rtsps://bblp: not in an active stream
  - Add noCamera i18n key to all 6 locales
  - Fix camera API test mocks for async wait() and pid attribute

* Fix sidebar navigation not respecting user permissions

  Sidebar nav items (Archives, Queue, Stats, Profiles, Maintenance,
  Projects, Inventory, Files) were visible to all users regardless of
  role permissions — only Settings was gated. Now each item is hidden
  when the user lacks the corresponding read permission. Printers
  remains always visible as the home page.

  Also adds missing inventory:read|create|update|delete to the frontend
  Permission type (existed in backend but was absent from the frontend
  type definition).

* Uodated issue template

* Updated README

* Fix Windows install syntax error from multi-line for /f command (#544)

  The Python hash verification in start_bambuddy.bat used a multi-line
  `for /f "usebackq"` with a backtick-delimited command split across
  lines. Windows CMD cannot parse line breaks inside backtick-delimited
  for /f commands, causing "The syntax of the command is incorrect" at
  step 1/6. Removed the entire redundant verification block — the
  verify_sha256 subroutine already checks the archive against the
  pinned hash. The removed block also had a secondary bug: it always
  downloaded the amd64 checksum from python.org even on arm64 systems.

* Separate firmware and software sections in Updates settings card

  The Updates card on the Settings page mixed printer firmware and
  Bambuddy software update toggles with no visual distinction. Now
  groups them into labeled sections ("Printer Firmware" at top,
  "Bambuddy Software" below) with a divider between them.

* Fix support package: mask subnet IPs, detect host mode, parse top-level fun, add virtual printers

  Four support package improvements:

  1. Mask first two octets of subnet IPs in support info
     (192.168.1.0/24 → x.x.1.0/24) to avoid leaking private network
     addresses.

  2. Fix Docker network_mode_hint detection. The old heuristic
     (interface count > 2) always reported "bridge" on single-NIC
     hosts because get_network_interfaces() excludes Docker
     interfaces. Now checks for docker0/br-*/veth* visibility via
     socket.if_nameindex() — these are only visible in host mode.

  3. Parse MQTT "fun" field at top level of payload (not just inside
     "print" key). Some firmware versions send it there, which
     explains why developer_mode was null for most users.

  4. Add virtual_printers section to support info with mode, model,
     enabled/running status, and pending file count.

* Fix virtual printer config changes ignored on running instances

  sync_from_db() skipped VPs already in self._instances without checking
  if their config had changed. Mode, model, access code, bind IP, remote
  interface IP, and target printer changes were silently ignored until
  manual toggle off/on or full restart. Now detects config drift and
  restarts affected instances.

* Fix queue 500 error when cancelled print exists (#558)

  The MQTT completion handler stored "aborted" as the queue item status
  when a print was cancelled mid-print, but the response schema only
  allows "cancelled". Pydantic validation failed on the invalid status
  when listing all queue items, returning 500. Filtering by specific
  status excluded the bad row so those still worked.

  Normalise "aborted" → "cancelled" before storing. A startup fixup
  also converts any existing "aborted" rows in the database.

* Redesign SpoolBuddy dashboard: inline spool cards, full-screen AMS assign modal, filament ID normalization

  - 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

* Added delete tag button to inventory's edit spool modal

* Fix VP bind server rejecting TLS connections on port 3002 (#559)

  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.

* Fix SpoolBuddy scale calibration lost after reboot

  _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.

* fix(stats): calculate success rate from completed and failed prints only

Previously divided successful prints by total_prints which includes
cancelled/stopped prints, causing an incorrect percentage.

* feat(stats): add new widgets, shared MetricToggle, and timeframe filtering

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.

* test(stats): update StatsPage tests to match current widget structure

Enrich mock archives with full fields, fix failure analysis endpoint,
remove outdated Filament Types test, and add coverage for new widgets
(Records, Printer Stats sub-cards, Filament Trends sub-cards).

* refactor(stats): improve Color Distribution card layout and toggle

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.

* feat(stats): add /archives/slim endpoint and fix dashboard bugs

- 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

* feat(stats): add hourly heatmap for short timeframes and fix timezone bugs

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.

* Fix SpoolBuddy "Assign to AMS" slot showing empty fields in slicer

  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.

* Add SpoolBuddy backend + frontend test coverage

  21 backend integration tests for all 12 SpoolBuddy API endpoints
  (device register/re-register/list, heartbeat status/commands/404/
  offline broadcast, NFC tag match/unmatch/removal, scale reading/
  weight calculation/missing spool, calibration tare/set-tare/
  set-factor/zero-delta/get) and 20 frontend component tests for
  WeightDisplay, SpoolInfoCard, UnknownTagCard, and TagDetectedModal.

* Fix printer card losing print info when paused (#562)

  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".

* Fix num_weeks calculation to ensure at least one week

* Updated README

* Add daily beta build publish script

  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.

* Updated README

* Add daily beta build publish script

  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.

* Fix SpoolBuddy tag_type for linked spools + add inventory weight check column

  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.

* Fix SpoolBuddy AMS page fill levels, ext-R active bug, and layout

  - 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

* Updated Spoolbuddy install script

* Add on-screen virtual keyboard for SpoolBuddy kiosk UI

  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).

* Spoolbuddy frontend fixes

* Display Controls (brightness + blanking)

  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

* Spoolbuddy

  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.

* Redesign SpoolBuddy settings page, add language/time format support

  - 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

* Add printer status badges, remove SpoolBuddy inventory page

  - 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

* Add SpoolBuddy NFC tag writing with OpenTag3D format

  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.

* Fix K-profile greenlet error on auto-created RFID spools

  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.

* Fix spurious error notifications from print_error status codes

  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().

* Updated docker-publish-daily-beta.sh

* Added Simplified Chinese translation zh-CN (Issue #571)

* Add customizable low stock threshold, add low stock filter (#531)

* 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>

* Add i18n keys for printer card drag-drop print overlay

  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.

* [Feature]: Printer Page - Add a print button & Drop zone on the printer card (#569)

* 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>

* Post work PR #569

* Fix AMS slot modal infinite scroll loop on Windows

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 energy cost to archive card (#573)

* Add energy cost to archive card

* Replace Disc3 icon with Coins in ArchivesPage

---------

Co-authored-by: MartinNYHC <mz@v8w.de>

* Revert "Merge pull request #578 from aneopsy/fix-ams-slot"

This reverts commit d4aa77f502, reversing
changes made to 82178f8abd.

* Reverted commit d4aa77f502

* Remove obsolete slicer_binary_path setting from database

  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.

* Add Hungarian Forint (HUF) currency for filament cost tracking

  Closes #579 — adds HUF with symbol "Ft" to the supported currencies
  list so Hungarian users can track filament costs in their local currency.

* Fix AMS slot showing wrong material for "Support for" profiles

  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

* Improve FTP upload progress and widen print modal

  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".

* Fix archive card showing "Source" badge for sliced .3mf files

  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.

* Refactor date and currency utility functions

- Extract shared detectSystemDateFormat() helper to eliminate
  duplicated system locale detection in date.ts
- Derive SUPPORTED_CURRENCIES from CURRENCY_SYMBOLS map instead
  of maintaining a duplicate hardcoded list
- Consolidate redundant time-parsing regex paths into one
- Use applyTimeFormat() consistently in formatETA()
- Add exhaustive switch default for type safety

* Consolidate duplicate utility functions into shared modules

Extract repeated helper functions from components into shared utils
(colors.ts, date.ts, file.ts) to reduce duplication:
- isLightColor, parseFilamentColor, hexToColorName → utils/colors.ts
- formatMediaTime, formatDurationFromHours → utils/date.ts
- formatFileSize guard hardened for NaN/negative → utils/file.ts
- formatBytes (ToastContext), formatTime (Timelapse*) deduplicated

Functions with subtly different behavior are intentionally kept local
(formatFilament, formatStorageSize, formatBytes, formatUptime,
formatDateTime, formatDate, formatTimeAgo) to preserve exact UI output.

* Fix spurious 0300_0002 error notification via HMS array path (#583)

  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.

* Add SSDP model codes to firmware check mapping (#584)

  The firmware check only mapped display names (e.g., "H2D Pro") to
  API keys but not raw SSDP device codes (e.g., "O1E", "O2D"). If a
  printer's model was stored as the raw SSDP code, the firmware check
  either failed or matched the wrong model — H2D Pro matched against
  H2D firmware, showing a false update-available badge.

  Add all known SSDP device codes to MODEL_TO_API_KEY so firmware
  versions resolve correctly regardless of how the model was stored.

* Show ethernet indicator instead of WiFi signal for wired printers (#585)

  Parse home_flag bit 18 (0x00040000) from MQTT to detect ethernet
  connections. When set, the printer card shows a green "Ethernet"
  badge with a cable icon instead of WiFi signal strength in dBm.
  The printer info modal also displays "Ethernet" instead of WiFi
  signal details.

* Post work PR #581

* Add in-app bug reporting with relay, debug log collection, and privacy controls

  Floating bug report button submits issues via bambuddy.cool relay (no GitHub
  token needed locally). Collects 30s debug logs with printer push_all, sanitizes
  all sensitive data, uploads logs as files to GitHub. Screenshot upload/paste/drag
  with JPEG compression. Translated into all 7 languages. Includes 21 tests.

* [Fix]: AMS slot modal infinite scroll loop on Windows (#580)

* Fix AMS slot modal not scrolling to preselected filament

The scroll useEffect fired before the loading spinner was replaced
by the preset list, so the DOM element didn't exist yet. The ref
guard was then set, preventing any retry once buttons appeared.

- Move isLoading computation before the scroll effect and add it
  as a dependency so the effect re-runs when loading completes
- Gate scroll on !isLoading so it only runs when buttons exist
- Only mark scrolledToRef after the element is actually found
- Use requestAnimationFrame to scroll after browser layout
- Use block:'center' so the selected item is clearly visible

Preselect filament for printer-configured AMS slots

When an AMS slot was configured directly on the printer (no
savedPresetId), the modal didn't preselect the filament even though
it was in the list. Add a third fallback that matches trayInfoIdx
against the full filteredPresets list (cloud and builtin sources).

* Restore scrolledToRef lost during merge

The useRef import, scrolledToRef declaration, and its reset were
dropped during the 0.2.2b1 merge, causing a ReferenceError in the
scroll-to-preselected-filament effect.

* Fix preset scroll loop and wire up useEffect scroll logic

- Remove inline ref callbacks with scrollIntoView on both preset
  button lists (fullscreen + standard) to stop the infinite scroll
  loop on Windows
- Add data-preset-id attribute so the existing useEffect/querySelector
  scroll logic can actually find the elements
- Replace filteredPresets dependency with builtinFilaments in the
  selection useEffect to prevent search keystrokes from overriding
  manual preset selection

* Adjust scroll behavior in ConfigureAmsSlotModal

---------

Co-authored-by: MartinNYHC <mz@v8w.de>

* Housekeeping

---------

Co-authored-by: tridev <c.tripod@gmx.ch>
Co-authored-by: AneoPsy <paultevatheis@gmail.com>
Co-authored-by: Keybored <78739882+Keybored02@users.noreply.github.com>
2026-03-04 14:22:21 +01:00
2026-02-28 14:39:18 +01:00
2026-03-04 14:22:21 +01:00
2026-01-31 14:31:10 +01:00
2026-03-04 14:22:21 +01:00
2026-03-04 14:22:21 +01:00
2026-03-04 14:22:21 +01:00
2026-03-04 14:22:21 +01:00
2025-12-09 17:06:54 +00:00
2026-02-19 09:59:23 +01:00
2026-02-28 14:34:46 +01:00
2025-12-14 09:36:19 +01:00
2026-03-04 14:22:21 +01:00
2025-12-30 10:58:34 +01:00
2026-02-17 20:09:47 +00:00
2026-02-05 10:28:06 +01:00
2026-03-04 14:22:21 +01:00
2026-02-19 16:32:44 +01:00
2026-02-05 10:28:06 +01:00
2026-03-01 18:29:28 +01:00
2025-11-28 10:21:08 +01:00
2026-03-04 14:22:21 +01:00
2026-02-22 13:55:35 +01:00
2026-02-20 15:28:47 +01:00
2026-02-16 10:57:39 +01:00
2026-02-27 09:48:31 +01:00
2026-02-05 10:28:06 +01:00
2026-02-04 15:54:34 +01:00

Bambuddy Logo

Bambuddy

Self-hosted print archive and management system for Bambu Lab 3D printers

Release License Stars Issues Discord Ko-fi

Features • Screenshots • Quick Start • Documentation • Discord • Contributing


🌐 NEW: Remote Printing with Proxy Mode

Proxy Mode Architecture

Print from anywhere in the world — Bambuddy's new Proxy Mode acts as a secure relay between your slicer and printer:

  • 🔒 TLS-encrypted control channels — MQTT and FTP control fully encrypted
  • 🛡️ VPN recommended — Use Tailscale/WireGuard for full data encryption (details)
  • 🌍 No cloud dependency — Direct connection through your own Bambuddy server
  • 🔑 Uses printer's access code — No additional credentials needed
  • ⚡ Full-speed printing — FTP and MQTT protocols proxied transparently

Perfect for remote print farms, traveling makers, or accessing your home printer from work.

👉 Setup Guide →


Why Bambuddy?

  • Own your data — All print history stored locally, no cloud dependency
  • Works offline — Uses Developer Mode for direct printer control via local network
  • Full automation — Schedule prints, auto power-off, get notified when done
  • Multi-printer support — Manage your entire print farm from one interface

✨ Features

📦 Print Archive

  • Automatic 3MF archiving with metadata
  • 3D model preview (Three.js)
  • Duplicate detection & full-text search
  • Photo attachments & failure analysis
  • Timelapse editor (trim, speed, music) with automatic AVI-to-MP4 conversion for P1-series printers, manual upload & remove
  • Re-print to any connected printer with AMS mapping (auto-match or manual slot selection, multi-plate support, nozzle-aware matching for dual-nozzle H2D/H2D Pro)
  • Plate thumbnail browsing for multi-plate archives (hover to navigate between plates)
  • Archive comparison (side-by-side diff)
  • Tag management (rename/delete across all archives)
  • Print Log — Chronological table view of all print activity with columns for date/time, print name, printer, user, status, duration, and filament. Filterable by search, printer, user, status, and date range. Pagination with configurable page size. Clear button removes log entries without affecting archives.

📊 Monitoring & Control

  • Real-time printer status via WebSocket
  • Live camera streaming (MJPEG) & snapshots with multi-viewer support
  • Streaming overlay for OBS - Embeddable page with camera + status for live streaming (/overlay/:printerId), configurable FPS (?fps=30), status-only mode (?camera=false)
  • External camera support (MJPEG, RTSP, HTTP snapshot, USB/V4L2) with layer-based timelapse
  • Build plate empty detection - Auto-pause print if objects detected on plate (multi-reference calibration, ROI adjustment)
  • Fan status monitoring (part cooling, auxiliary, chamber)
  • Printer control (stop, pause, resume, chamber light)
  • Resizable printer cards (S/M/L/XL)
  • Skip objects during print
  • AMS slot RFID re-read
  • AMS slot configuration (model-filtered presets, K profiles, color picker, pre-population for configured slots)
  • Dual external spool support for H2D (Ext-L / Ext-R)
  • HMS error monitoring with history and clear errors
  • Print success rates & trends
  • Filament usage tracking
  • Cost analytics & failure analysis
  • CSV/Excel export

⏰ Scheduling & Automation

  • Background print dispatch — FTP uploads and print-start commands run in the background with real-time WebSocket progress toasts (per-job upload bars, status badges, cancel button)
  • Print queue with drag-and-drop
  • Multi-printer selection (send to multiple printers at once)
  • Model-based queue assignment (send to "any X1C" for load balancing) with location filtering
  • Filament override for model-based queue (swap filament colors/types before scheduling)
  • Filament validation (only assign to printers with required filaments)
  • Per-printer AMS mapping (individual slot configuration for print farms)
  • Scheduled prints (date/time)
  • Queue Only mode (stage without auto-start)
  • Clear plate confirmation between queued prints
  • Smart plug integration (Tasmota, Home Assistant, MQTT)
  • MQTT smart plugs: Subscribe to Zigbee2MQTT, Shelly, or any MQTT topic for energy monitoring
  • Energy consumption tracking (per-print kWh and cost)
  • HA energy sensor support (for plugs with separate power/energy sensors)
  • Auto power-on before print
  • Auto power-off after cooldown

📁 File Manager (Library)

  • Upload and organize sliced files (3MF, gcode, STL)
  • STL thumbnail generation - Auto-generate previews for STL files on upload or batch generate for existing files
  • ZIP file extraction with folder structure preservation
  • Option to create folder from ZIP filename
  • Folder structure with drag-and-drop
  • Rename files and folders via context menu
  • Print directly to any printer with full options
  • Add to queue without creating archive upfront
  • Plate selection for multi-plate 3MF files
  • Duplicate detection via file hash
  • Mobile-friendly with always-visible action buttons

📁 Projects

  • Group related prints (e.g., "Voron Build")
  • Track plates (print jobs) and parts separately
  • Auto-detect parts count from 3MF files
  • Color-coded project badges
  • Bulk assign archives via multi-select toolbar
  • Import/Export projects as ZIP (includes files) or JSON

🔔 Notifications

  • WhatsApp, Telegram, Discord
  • Email, Pushover, ntfy
  • Custom webhooks
  • Quiet hours & daily digest
  • Customizable message templates with per-filament usage details
  • Print finish photo URL in notifications
  • Filament usage and progress in failed/cancelled print notifications
  • HMS error alerts (AMS, nozzle, etc.)
  • Build plate detection alerts
  • Bed cooled alerts (configurable threshold)
  • Queue events (waiting, skipped, failed)

🧵 Spool Inventory

  • Built-in spool inventory with AMS slot assignment, usage tracking, and remaining weight management
  • Automatic filament consumption tracking: 3MF slicer estimates for all spools (primary), AMS remain% delta as fallback
  • Per-layer gcode accuracy for partial prints (failed/cancelled), with linear scaling fallback
  • Per-spool cost tracking — Set cost/kg on each spool; costs are automatically calculated at print completion and aggregated to archives. Print modal shows real-time cost preview. Configurable default cost and currency in Settings.
  • Bulk spool addition — Add multiple identical spools at once (quantity 1–100) with a single form submission. Quick Add mode for stock spools that only need material, color, and weight.
  • Spool catalog, color catalog, PA profile matching, and low-stock alerts

🔧 Integrations

  • Spoolman filament sync with per-filament usage tracking and fill level display
  • MQTT publishing for Home Assistant, Node-RED, etc.
  • Prometheus metrics - Export printer telemetry for Grafana dashboards
  • Bambu Cloud profile management
  • Local Profiles - Import OrcaSlicer presets (.orca_filament, .bbscfg, .bbsflmt, .zip, .json) without Bambu Cloud
  • K-profiles (pressure advance)
  • GitHub backup - Schedule automatic backups of cloud profiles, k profiles and settings to GitHub
  • External sidebar links
  • Webhooks & API keys
  • Interactive API browser with live testing

🖨️ Virtual Printer & Remote Printing

  • 🌐 Proxy Mode (NEW!) — Print remotely from anywhere via secure TLS relay
  • Emulates a Bambu Lab printer on your network
  • Send prints directly from Bambu Studio/Orca Slicer
  • Configurable printer model (X1C, P1S, A1, H2D, etc.)
  • Archive mode, Review mode, Queue mode, or Proxy mode
  • SSDP discovery (same LAN) or manual IP entry (VPN/remote)
  • Network interface override for multi-NIC/Docker/VPN setups
  • Secure TLS/MQTT/FTP communication

🛠️ Maintenance & Support

  • Maintenance scheduling & tracking
  • Interval reminders (hours/days)
  • Print time accuracy stats
  • File manager for printer storage
  • Firmware update helper with version badge (LAN-only printers)
  • Debug logging toggle with live indicator
  • Live application log viewer with filtering
  • Support bundle generator with comprehensive diagnostics (privacy-filtered)
  • In-app bug reporting — Submit bug reports directly from the UI with optional screenshot (upload, paste, or drag & drop), automatic diagnostic log collection (30s debug capture with printer push), and system info. Reports create GitHub issues via a secure relay. Privacy-first: all logs are sanitized and sensitive data (IPs, serials, credentials) is never included.

🔒 Optional Authentication

  • Enable/disable authentication any time
  • Group-based permissions (50+ granular permissions)
  • Default groups: Administrators, Operators, Viewers
  • JWT tokens with secure password hashing
  • Comprehensive API protection (200+ endpoints secured)
  • User management (create, edit, delete, groups)
  • User activity tracking (who uploaded archives, library files, queued prints, started prints)
  • Advanced Auth via Email — SMTP integration for automated user onboarding and self-service password resets
  • Admin creates users with email — system sends secure random password automatically
  • Users can reset their own password from the login screen (no admin needed)
  • Customizable email templates (welcome email, password reset)

Plus: Configurable slicer (Bambu Studio / OrcaSlicer) • Customizable themes (style, background, accent) • Mobile responsive • Keyboard shortcuts • Multi-language (EN/DE/JA/IT) • Auto updates • Database backup/restore • System info dashboard


🎬 Demo

Bambuddy Demo Video
Click to watch the demo on YouTube


📸 Screenshots

Click to expand screenshots

Printers
Real-time printer monitoring with AMS status

Archives
Print archive with 3D preview and project assignment

Reprint AMS Mapping
Re-print with AMS filament mapping preview

Timelapse Editor
Built-in timelapse editor with trim, speed, and music

Projects
Group related prints into projects

Project Detail
Project detail view with assigned archives

Project Detail Timeline
Project timeline and print history

Queue
Print scheduling and queue management

Schedule Print
Schedule prints for specific date and time

Statistics
Customizable statistics dashboard

Maintenance
Maintenance tracking per printer

Maintenance Settings
Configure maintenance types and intervals

Cloud Profiles
Bambu Cloud filament profiles

Cloud Profiles Edit
Edit filament preset settings

K-Profiles
Pressure advance (K-factor) profiles

K-Profiles Edit
Edit K-factor profile settings

Settings
General configuration and integrations

Smart Plugs
Smart plug control and energy monitoring

Notifications
Multi-provider notification system

API Keys
API keys and webhook endpoints

Virtual Printer Settings
Virtual printer configuration

Slicer Virtual Printer
Virtual printer appears in Bambu Studio/Orca Slicer

MQTT Debug Log
MQTT debug logging for troubleshooting

Quick Power Plug
Quick power plug control in sidebar


🚀 Quick Start

Requirements

  • Python 3.10+ (3.11/3.12 recommended)
  • Bambu Lab printer with Developer Mode enabled (see below)
  • "Store sent files on external storage" enabled in Bambu Studio/OrcaSlicer
  • Same local network as printer

Installation

Option A: Pre-built image (fastest)

mkdir bambuddy && cd bambuddy
curl -O https://raw.githubusercontent.com/maziggy/bambuddy/main/docker-compose.yml
docker compose up -d

Option B: Build from source

git clone https://github.com/maziggy/bambuddy.git
cd bambuddy
docker compose up -d --build

Open http://localhost:8000 in your browser.

Multi-architecture support: Pre-built images are available for linux/amd64 and linux/arm64 (Raspberry Pi 4/5).

macOS/Windows users: Docker Desktop doesn't support network_mode: host. Edit docker-compose.yml: comment out network_mode: host and uncomment the ports: section. Printer discovery won't work - add printers manually by IP.

Linux users: If you get "permission denied" errors, either prefix commands with sudo (e.g., sudo docker compose up -d) or add your user to the docker group.

Docker Configuration & Commands

Environment Variables:

Variable Default Description
TZ UTC Your timezone (e.g., America/New_York, Europe/Berlin)
PORT 8000 Port BamBuddy runs on (with host networking mode)
DEBUG false Enable debug logging
LOG_LEVEL INFO Log level: DEBUG, INFO, WARNING, ERROR

Data Persistence:

Volume Purpose
bambuddy.db SQLite database with all your print data
archive/ Archived 3MF files and thumbnails
logs/ Application logs

Updating:

# Pre-built image: just pull the latest
docker compose pull && docker compose up -d

# From source: rebuild after pulling changes
cd bambuddy && git pull && docker compose up -d --build

Daily Beta Builds:

Beta builds with the latest fixes are pushed regularly to the same beta version tag:

# Pull the current beta
docker pull ghcr.io/maziggy/bambuddy:0.2.2b1
# or from Docker Hub
docker pull maziggy/bambuddy:0.2.2b1

Use Watchtower to automatically update when new daily builds are pushed.

Note: Beta builds use version tags like 0.2.2b1 — they are never tagged as latest. Your stable installation won't auto-update to a beta unless you explicitly pull a beta tag.

Useful Commands:

# View logs
docker compose logs -f

# Stop/Start
docker compose down
docker compose up -d

# Shell access
docker compose exec bambuddy /bin/bash

Custom Port:

ports:
  - "3000:8000"  # Access on port 3000

Reverse Proxy (Nginx):

server {
    listen 443 ssl http2;
    server_name bambuddy.yourdomain.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass http://localhost:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 86400;
    }
}

Note: WebSocket support is required for real-time printer updates.

Network Mode Host (required for printer discovery and camera streaming):

services:
  bambuddy:
    build: .
    network_mode: host

Note: Docker's default bridge networking cannot receive SSDP multicast packets for automatic printer discovery. When using network_mode: host, Bambuddy auto-detects your network subnet and can discover printers via subnet scanning in the Add Printer dialog.

Windows (Portable Launcher)

The easiest way to run Bambuddy on Windows - no installation required:

git clone https://github.com/maziggy/bambuddy.git
cd bambuddy
start_bambuddy.bat

Double-click start_bambuddy.bat and it will:

  • Download Python and Node.js automatically (portable, no system changes)
  • Install dependencies and build the frontend
  • Open your browser to http://localhost:8000

Everything is stored in the .portable\ folder. Use start_bambuddy.bat reset to clean up.

Custom port: set PORT=9000 & start_bambuddy.bat

Manual Installation (Linux/macOS)

# Clone and setup
git clone https://github.com/maziggy/bambuddy.git
cd bambuddy
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

# Run
uvicorn backend.app.main:app --host 0.0.0.0 --port 8000

Open http://localhost:8000 and add your printer!

Need detailed instructions? See the Installation Guide

Enabling Developer Mode

Developer Mode allows third-party software like Bambuddy to control your printer over the local network.

  1. On printer: Settings → Network → LAN Only Mode → Enable
  2. Enable Developer Mode (appears after LAN Only Mode is enabled)
  3. Note the Access Code displayed
  4. Find IP address in network settings
  5. Find Serial Number in device info

Note: Developer Mode disables cloud features but provides full local control. Standard LAN Mode (without Developer Mode) only allows read-only monitoring.

Slicer Settings

In Bambu Studio or OrcaSlicer, enable "Store sent files on external storage" so that print files (3MF) are saved to the printer's SD card. Bambuddy needs these files to extract thumbnails and 3D model previews.

  1. Open Bambu Studio or OrcaSlicer
  2. Go to the Device tab for your printer
  3. In Print Options, enable Store Sent Files on External Storage

📚 Documentation

Full documentation available at wiki.bambuddy.cool:


🖨️ Supported Printers

Series Models
X1 X1, X1 Carbon, X1E
H2 H2D, H2D Pro, H2C, H2S
P1 P1P, P1S
P2 P2S
A1 A1, A1 Mini

🛠️ Tech Stack

Component Technology
Backend Python, FastAPI, SQLAlchemy
Frontend React, TypeScript, Tailwind CSS
Database SQLite
3D Viewer Three.js
Communication MQTT (TLS), FTPS

🤝 Contributing

Contributions welcome! Here's how to help:

  1. Test — Report issues with your printer model
  2. Translate — Add new languages
  3. Code — Submit PRs for bugs or features
  4. Document — Improve wiki and guides
# Development setup
git clone https://github.com/maziggy/bambuddy.git
cd bambuddy

# Backend
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
DEBUG=true uvicorn backend.app.main:app --reload

# Frontend (separate terminal)
cd frontend && npm install && npm run dev

See CONTRIBUTING.md for guidelines.


📄 License

MIT License — see LICENSE for details.


🙏 Acknowledgments

  • Bambu Lab for amazing printers
  • The reverse engineering community for protocol documentation
  • All testers and contributors

If you like Bambuddy and want to support it, you can buy Martin a coffee.


Made with ❤️ for the 3D printing community

Join our Discord • Report Bug • Request Feature • Documentation

S
Description
Your Bambu Lab. No Cloud. Your Rules. Self-hosted command center for Bambu Lab — from one A1 to an entire print farm.
Readme AGPL-3.0
865 MiB
Languages
Python 39.4%
TypeScript 34.1%
JavaScript 25.8%
Shell 0.5%
HTML 0.1%