Commit Graph
64 Commits
Author SHA1 Message Date
maziggy ea4d5eb794 fix(ui): drying popover footer no longer clipped by iOS Safari URL bar (#1669)
100vh and window.innerHeight report the layout viewport on iOS Safari, so
  the popover's Start Drying button rendered behind the bottom toolbar overlay.
  Switch the popover maxHeight to 100dvh and default computePopoverPosition's
  viewportHeight to visualViewport.height (with innerHeight fallback). The
  flip-above decision and the body-scroll fallback now both run against the
  actually-visible area, so the footer button stays reachable on iPhone.
2026-06-07 08:08:44 +02:00
maziggy b8916ac3de fix(presets): match Bambu cloud @BBL A1M as A1 Mini (#1649)
Reporter on an A1 Mini saw the AMS slot Configure dropdown render no
  Bambu / Generic filament profiles, and saw the Profiles tab strip
  A1 Mini results when filtering by that model. Bambu rolled out a
  profile rename mid-2026: the @BBL <code> suffix on 106 cloud profiles
  shifted from the long display form to a terse model code -- e.g.
  "Bambu PLA Basic @BBL A1 Mini ..." is now
  "Bambu PLA Basic @BBL A1M ...". User-authored profiles still use the
  long form. Bambuddy's filters did a verbatim uppercase compare
  ("A1M" vs "A1 MINI"), so every renamed cloud profile silently
  disappeared from the picker.

  Centralize the alias check in slicerPrinterMatch.ts. New
  PRINTER_MODEL_SUFFIX_ALIASES table maps "A1 Mini" <-> "A1M"
  bidirectionally; exported matchesPrinterModelSuffix() does the
  case-insensitive compare with the alias fallback. Two consumer
  sites swap to the helper:

    * ConfigureAmsSlotModal.tsx (Orca cloud and Bambu cloud filter
      branches) -- the AMS slot picker, hit directly and reached from
      SpoolBuddy's AMS page via mapModelCode(printer?.model)
    * slicerPrinterMatch.ts:classifyByBambuName -- the SliceModal
      Process / Filament compatibility check

  Backend printer_models.py also gets a "Bambu Lab A1M" -> "A1 Mini"
  entry so server-side 3MF model normalization stays consistent if a
  3MF ever embeds the short form.

  Kept the alias table narrow on purpose. Wide-net aliasing (e.g.
  "X1" <-> "X1C") would silently collapse physically distinct
  printers. When Bambu introduces the next rename, it is one new row
  in the table -- /api/v1/cloud/settings is the place to grep, called
  out in the source comment.
2026-06-06 09:10:27 +02:00
maziggy 632334953c fix(inventory): support transparent / clear filament end-to-end (#1545)
Reporter wanted to select a transparent filament colour in the spool
  editor; CMW-ISS confirmed on v0.2.5b1 that AMS-detected transparent
  spools were silently labelled "Black" in the filament-mapping dropdown
  because the colour name resolver dropped the alpha byte and the underlying
  RGB 000000 HSL-bucketed to "Black". Spoolman already supported 8-digit
  hex; the built-in inventory didn't.

  Eight collapsing sites fixed together so transparent reaches the user
  intact:

  - frontend/src/utils/colors.ts: hexToColorName / getColorName /
    resolveSpoolColorName / isLightColor short-circuit to "Clear" for
    alpha=00 before HSL bucketing or catalog lookup
  - frontend/src/utils/amsHelpers.ts::normalizeColor preserves the alpha
    byte when alpha < FF (normalizeColorForCompare unchanged so type/colour
    matching is unaffected)
  - frontend/src/components/spool-form/constants.ts: new
    { name: 'Clear', hex: '00000000' } preset in QUICK_COLORS
  - frontend/src/components/spool-form/ColorSection.tsx: hex draft accepts
    0-8 chars, commits at 6 (+FF) or 8 verbatim; blur pads 7-char to 8;
    selectColor passes 6-char as +FF / 8-char verbatim; isSelected matches
    on full rgba; swatch buttons paint a checkerboard for alpha=00
  - backend/app/api/routes/printers.py::get_available_filaments preserves
    the full rgba on both AMS and vt_tray branches (6-char dedup key
    unchanged)
  - backend/app/services/spoolman.py::parse_ams_tray drops the silent
    00000000 -> F5E6D3FF cream rewrite — the swatch renderer paints a
    checkerboard underlay for alpha < FF already (added in #1154), so the
    rewrite was hidden technical debt that made every AMS-detected
    transparent spool land in inventory as cream
  - backend/app/services/spool_tag_matcher.py::create_spool_from_tray
    short-circuits the colour-catalog lookup for alpha=00 and stores
    color_name="Clear" directly — otherwise an RFID-tagged transparent
    Bambu spool would resolve against the #000000 catalog row (or "Black"
    via the HSL fallback) before the frontend's resolver ever saw it
  - Two shared helpers in utils/colors.ts — getSwatchStyle(rgba) (style
    object: checkerboard for alpha=00) and spoolColorString(rgba)
    (8-char hex string for SVG fill) — applied to every simple-swatch
    site that would otherwise have rendered Clear spools as solid black:
    LabelTemplatePickerModal, SpoolBuddyInventoryPage (SpoolCircle + dot),
    SpoolBuddyAmsPage (both branches), SpoolBuddyWriteTagPage (4 sites),
    ForecastPanel, AssignToAmsModal, AssignSpoolModal (both branches),
    InventorySpoolInfoCard, TagDetectedModal, SpoolInfoCard, LinkSpoolModal,
    and the FilamentSwatch tooltip title fallback

  Intentionally NOT changed: native <input type="color"> keeps 6-char hex
  (can't pick alpha; onChange still emits +FF, correct); Spoolman's
  _find_or_create_filament strips alpha (Spoolman catalog is 6-char only);
  print_scheduler colour matching strips alpha (auto-mapping treats Clear
  as Black for slot compatibility); label_renderer prints "#RRGGBB" on the
  physical label (printers can't print transparency, swatch fill via
  _color_from_hex still honours alpha).
2026-05-29 10:15:53 +02:00
maziggy 6591fc011f fix(slicer): filter process / filament presets by nozzle diameter too (#1325 follow-up #2)
After the @BBL name fallback landed, IndividualGhost1905 reported that
  an X2D 0.4 selection still mixed 0.2 / 0.6 / 0.8 nozzle process variants
  into the main dropdown. The fallback's two extractors —
  extractPrinterPresetModel and extractBblToken — both ended their regex
  with `\s+[\d.]+\s*nozzle\s*$` and discarded the match, reducing
  "Bambu Lab X2D 0.4 nozzle" and "0.40mm Strength @BBL X2D 0.8 nozzle"
  to the same "X2D" string. Match was model-only; nozzle ignored.

  Bambu's naming convention: 0.4 is the default and DROPS the suffix; 0.2
  / 0.6 / 0.8 carry an explicit "<size> nozzle" segment. So a process
  preset with no suffix is implicitly 0.4 — not "any nozzle".

  Have both extractors return { model, nozzle }, parsing the suffix out
  instead of stripping. classifyByBambuName then requires both model AND
  nozzle to compare equal; a null process nozzle counts as "0.4" per the
  convention above. Differing nozzles fall into the existing "Other
  printers" group — no new group label.

  The bundle path was already nozzle-correct: a .bbscfg is scoped to one
  printer-preset-name including its nozzle, and the bundle-side exact
  match is therefore nozzle-aware. Only the @BBL name fallback needed
  fixing. The `compatible_printers` tier is also unaffected (Bambu's
  bundled `compatible_printers` lists include the full printer-preset
  name with nozzle, so disambiguation already works).

  If the selected printer preset name has no parseable nozzle (non-Bambu
  / hand-typed), the matcher degrades to model-only. Bambu printer
  presets always carry one in practice; this is defensive.

  9 new tests cover the matrix:
    - 0.4 printer ↔ no-suffix process: match
    - 0.4 printer ↔ 0.6 / 0.8 process: mismatch
    - 0.6 printer ↔ 0.6 process: match
    - 0.6 printer ↔ no-suffix process (=0.4): mismatch
    - same rule applied to filament presets
    - explicit "0.4 nozzle" suffix on process still matches 0.4 printer
    - wrong-model still mismatches even when nozzles agree
    - no-nozzle printer name degrades to model-only match

  One existing test ("handles a trailing nozzle-size suffix on the @BBL
  tag") had asserted that a 0.6-nozzle process matched a 0.4 printer —
  the exact reporter complaint. Reframed: matching 0.4-suffix still
  matches, 0.6-suffix now mismatches.
2026-05-23 15:02:23 +02:00
maziggy 3058c5789b fix(slice): @BBL name fallback for users without slicer bundles (#1325 follow-up)
The first cut of #1325 swapped a stale hardcoded model table for
  bundle-based compatibility matching: a cloud / standard process or
  filament preset was classified by consulting the user's uploaded
  Slicer Bundles (.bbscfg). That works perfectly when bundles cover
  every printer in the user's cloud catalogue, and silently no-ops
  otherwise - every cloud preset resolves to 'unknown', nothing moves
  into "Other printers", and the dropdowns look identical to the
  pre-fix state. The reporter saw exactly this on a clean install
  with no bundles uploaded.

  Restored BambuStudio's `@BBL <token>` name convention as a third
  tier below the bundle path, but driven by the canonical backend
  PRINTER_MODEL_MAP - exposed via a new GET /slicer/printer-models
  route - rather than a manually-maintained frontend table. The
  matcher inverts the registry into short-code -> printer-fragment
  ("X1C" -> "X1 Carbon"), normalises whitespace + case so "A1 mini"
  and "A1 Mini" compare equal, and falls back to raw-token compare
  for models not yet in the registry (so a future "Q1" matches
  without a code change). Adding a new Bambu model still touches
  exactly the one backend file already listed in the Bambu Model
  Codes registry.

  Tests: 2 new in test_slicer_presets.py (route returns the full
  PRINTER_MODEL_MAP, route returns a copy not the live dict); 11
  new in slicerPrinterMatch.test.ts covering registry-driven X1C
  vs X1 Carbon, A1 vs A1 mini, H2D vs H2D Pro, P2S / H2C / H2S /
  X2D (which the original hardcoded list was missing), raw-token
  fallback, registry-not-loaded-yet degradation, and the
  precedence rules between compatible_printers / bundle / @BBL
  name. 38 slicer-presets + 36 slicerPrinterMatch + 34 SliceModal
  tests green; backend ruff clean; frontend build clean.
2026-05-23 12:52:04 +02:00
maziggy 6bc6a1d683 feat(virtual-printer): setup diagnostic + one-click slicer-certificate export
Two recurring virtual-printer support pains, both on the Virtual Printers
  settings page.

  Setup check: a stethoscope action on each VP card runs a pass/fail/warn/skip
  checklist — VP enabled, services running, bind interface still exists, access
  code set, target printer (proxy mode), and a live TCP probe of the FTP / MQTT
  / discovery ports on the bind IP. start_server swallows per-service bind
  errors, so a service object can exist while nothing is listening; probing the
  bind IP from outside is the only reliable signal and it catches the common
  "VP not visible in the slicer" bind-IP-conflict and stale-interface cases.

  Slicer certificate: virtual printers present a TLS cert signed by a shared CA
  the slicer must trust. Until now users had to docker exec in and cat
  bbl_ca.crt. A "Slicer certificate" row on the settings card now offers Copy
  and Download (bambuddy-virtual-printer-ca.crt) plus the SHA-256 fingerprint.
  GET /virtual-printers/ca-certificate returns only the public certificate; the
  CA private key never leaves the backend. The CA is generated on demand so the
  button works before the first VP is enabled.

  Backend:
  - services/virtual_printer/diagnostic.py — run_vp_diagnostic + port probes
  - schemas/virtual_printer.py — VPDiagnosticResult
  - CertificateService.get_ca_certificate_info() + manager helper
  - routes: GET /virtual-printers/ca-certificate, /{vp_id}/diagnostic

  Frontend:
  - VirtualPrinterDiagnosticModal.tsx; stethoscope button on VirtualPrinterCard
  - caCert row on VirtualPrinterList; utils/clipboard.ts (shared copy w/
    non-secure-context fallback + downloadTextFile), de-duplicating the
    existing FQDN-copy logic
  - vpDiagnostic.* + virtualPrinter.caCert.* across all 9 locales

  9 backend unit tests + 4 route integration tests + 6 frontend tests.
  Backend ruff clean, frontend build clean, i18n parity green.
2026-05-22 13:24:35 +02:00
maziggy e0247fc6a6 fix(slicer): filter process/filament presets by uploaded bundles, not preset names (#1325)
The process dropdown still mixed @BBL P2S presets into an X1C list:
  slicerPrinterMatch matched cloud/standard presets by parsing the
  @BBL <model> name suffix against a hardcoded model-code allow-list
  that was missing P2S, H2C and X2D — so those presets resolved to
  "unknown" and stayed in the main list instead of "Other printers".

  Drop both hardcoded model tables. Compatibility now comes from the
  user's uploaded Slicer Bundles: a bundle is scoped to one printer and
  lists the presets it ships, so a preset matches a printer exactly when
  some bundle for that printer contains it. New models are covered the
  moment their bundle is uploaded.
2026-05-22 08:01:42 +02:00
maziggy 7aad3fb395 fix(inventory): show group totals on collapsed grouped rows (issue #1368)
With "Group similar" enabled, the collapsed group row showed a single
  member's values — a group of five 1 kg spools displayed 1000 g instead
  of 5 kg (#1368).

  The group header now aggregates across all members: the table view's
  Label / Net / Gross / Used / Remaining columns and the grid card's
  weight figure show group totals. Identity columns (Material, Brand,
  Colour) and the Cost/kg rate stay per-spool-correct since they are the
  group key; per-spool-only fields with no meaningful total (dates,
  location, note, tag ID) keep the representative member's value. The
  expanded per-spool rows are unchanged.

  New aggregateGroupSpool helper with 4 unit tests; the table group
  component's header prop is renamed representative -> headerSpool.
  Frontend-only — all data was already in the spool list.
2026-05-21 14:00:43 +02:00
maziggy 7eba29624b feat(slicer): filter process & filament profiles by selected printer (issue #1325)
The Slice dialog listed every process / filament preset regardless of
  the chosen printer (#1325). Picking a printer profile now filters both
  dropdowns to compatible presets; presets resolving to a different Bambu
  model move into a trailing "Other printers" group.

  Matching uses the slicer's own compatible_printers list for imported
  (local) presets, and falls back to the "@BBL <model>" name suffix for
  cloud / standard presets where no compatibility metadata is available,
  so all three tiers are covered. Compatibility-unknown presets (custom
  or untagged) are never hidden. The pre-pick and printer-switch paths
  follow the same rule.

  - UnifiedPreset gains compatible_printers, exposed for the local tier
  - new frontend util slicerPrinterMatch.ts with the matching logic
  - slice.otherPrinters added across all 9 locales
2026-05-21 13:33:13 +02:00
maziggy fd620df3d6 feat(currency): add Belize Dollars (BZD) to currency dropdown (#1454)
Adds BZD with symbol BZ$ to the Settings cost-currency picker so
  users in Belize can track filament costs in their local currency
  without doing 2:1 USD mental conversions.
2026-05-20 11:35:04 +02:00
Seb 14919a80a5 Merge pull request #1440 from Person2099/fix/filament-override-ams-mapping-dispatch
fix: compute AMS mapping from force-colour overrides when 3MF reqs unavailable
2026-05-20 11:25:45 +02:00
maziggy 0f68039416 Fix: AMS drying popover no longer renders off the bottom of the viewport (#1447 part 1)
The flame-icon onClick on PrintersPage computed popover position as a
  fixed { top: rect.bottom + 4, left: Math.max(8, rect.right - 240) }
  with no viewport-overflow check. The flame icon sits at the bottom of
  the AMS info section on the printer card, so on most realistic viewports
  rect.bottom + 4 + popover_height (~320px) overruns viewport.height and
  the popover renders partially or entirely off-screen with the Start
  button unreachable. Reporter (kleinweby, P1S + AMS-HT) worked around it
  via DevTools to confirm the popover was actually there, just clipped.

  Extract a computePopoverPosition() helper in utils/popoverPosition.ts:
  - defaults to below + right-aligned to the trigger (preserves the
    original visual layout when there's room),
  - flips ABOVE the trigger when below would overflow AND above fits,
  - stays below in the degraded case (popover taller than the viewport) —
    at least the top is visible and the user can scroll inside the
    popover; flipping to a top-clipped position would lose the action
    buttons too,
  - clamps the left coordinate so a trigger near either viewport edge
    can't push the popover off-screen horizontally either.

  Both PrintersPage callsites (the compact AMS row at :3498 and the
  dual-nozzle layout at :4011) route through the helper.

  This is part 1 of #1447. The functional drying bug — printer receives
  the ams_filament_drying MQTT command, ACKs it, but never starts/stops
  drying on Bambuddy's request while the printer's own touchscreen works
  — is NOT addressed here. Diagnosing it needs the printer's actual
  response payload (whether result: "fail" and the specific reason code),
  which bambu_mqtt.py:918 currently doesn't log for ams_filament_drying.
  Punted to a follow-up where I'll extend the existing extrusion_cali_* /
  ams_filament_setting payload-logging path at :919-920 to cover
  ams_filament_drying too, ask the reporter to retry, and fix the
  command side based on what the printer actually returns.
2026-05-20 10:00:15 +02:00
maziggy fcee1a6f7e Fix: Print Activity heatmap buckets by local date, not UTC date (#1446)
PrintCalendar.tsx had three instances of the same UTC-shortcut anti-pattern:

  1. Bucketing input dates via `date.split('T')[0]` — gives the UTC day
     while the cell tooltip rendered local via `toLocaleDateString`. Same
     data, two renderers, only one was tz-correct. Reporter on CDT (UTC-5)
     saw evening prints jump to "tomorrow's" cell.

  2. Per-cell lookup key built via `day.toISOString().split('T')[0]`. The
     `day` Date objects produced by the calendar-generation loop are
     local-tz (constructed via `new Date()` + `setDate`), so `toISOString`
     shifted them back to UTC before the lookup — would have re-broken the
     join even after the bucketing fix.

  3. "Today" ring comparison used `new Date().toISOString().split('T')[0]`
     too — at 23:00 local the ring would have moved to UTC-tomorrow's cell.

  Fix adds a `localDateKey(input: string | Date): string` helper to
  utils/date.ts that wraps parseUTCDate and formats via the local-tz
  getters (`getFullYear` / `getMonth` / `getDate` with two-digit padding),
  returning a stable comparable YYYY-MM-DD. PrintCalendar.tsx uses it in
  all three spots so the buckets, the cell join, and the today ring share
  the same local-tz axis as the user's tooltip label.

  Backend stays UTC. Bucketing is a presentation concern and the browser
  already knows the user's tz.

  Stragglers flagged for follow-up: StatsPage.computeDateRange builds
  dateFrom/dateTo for backend stats queries using getUTC* getters, so a
  "this week" picked at 23:00 local on Sunday in CDT sends UTC-Monday-based
  ranges to the backend. Fixing it properly also needs the backend to
  filter on a tz-shifted UTC range, and Bambuddy has no user-tz setting
  model today. localDateKey is in place for reuse when that work lands.
2026-05-20 09:45:28 +02:00
maziggy 5c17cd4973 fix(inventory): restore Spoolman spool ID search + Unassign button (#1336)
Two regressions reported in #1336:

  1. spoolMatchesQuery did not include spool.id in the predicate, so
     typing a numeric Spoolman ID into the Assign Spool dialog or the
     Inventory page search returned no matches. Predicate now also
     tests String(spool.id).includes(q).

  2. The Unassign button in the spool edit modal was permanently
     disabled for Spoolman-mode spools. The modal only ever queried
     the legacy spool_assignments table (keyed by spool_id), but in
     Spoolman mode the assignment lives in spoolman_slot_assignments
     (keyed by spoolman_spool_id). Both the lookup query and the
     unassign mutation now branch on the spoolmanMode prop and call
     the Spoolman-flavored endpoints.
2026-05-14 14:19:12 +02:00
maziggy d08183278f fix(users): show password rules in form + match FE check to BE (#1303)
Create/Edit User modal previously had no hint about backend password
  complexity, and the FE pre-check was only length>=6 — so any weak
  password got bounced as a bare "HTTP 422" toast after the round-trip.

  - New checkPasswordComplexity util mirroring backend's validator order
  - Helper text under password inputs in both modals
  - Submit disabled until every rule passes
  - client.ts 422 parser falls back to JSON.stringify(detail) when the
    mapped array is empty, so a bare status code never masks the real
    Pydantic detail again
  - i18n keys added in all 8 locales (EN/DE fully translated, others
    per project's English-seed convention)
  - 7 unit tests pinning the validator contract, including the
    reporter's "12345678" input
2026-05-13 09:50:42 +02:00
maziggy 79d54a8d53 feat(archives): build-plate icon on cards + uniform printer/model line (#1253)
Show an OrcaSlicer-style bed icon in the archive card's printer-name row
  indicating which build plate the print was sliced for (Cool /
  Cool SuperTack / Engineering / High Temp / Textured PEI / Smooth PEI),
  with the full plate name in the hover tooltip. Closes the gap where
  users had to remember which plate matched a re-print or open the
  source 3MF in a slicer just to read the bed setting.

  Card row also unified: archives with a real Bambuddy-printer
  association used to render "H2D-1 GCODE ..." while slicer-only uploads
  rendered "Sliced for X1C GCODE ..." -- same line, two different shapes.
  Drop the "Sliced for " prefix so both render as a uniform
  "<name-or-model> [bed-icon] GCODE <hash>" row, scanning identically
  regardless of provenance.

  Backend: new bed_type column on print_archives (idempotent ALTER TABLE
  migration; SQLite + Postgres safe). Populated from curr_bed_type in
  Metadata/slice_info.config (per-plate, authoritative -- that's what
  got sent to the printer for the exported plate) with a fallback to
  project_settings.config for older 3MF shapes. Wired through both
  archive_to_response() (the hand-rolled dict converter that bypasses
  from_attributes -- easy to miss) and the /rescan endpoint, so old
  archives can be re-parsed via the existing per-archive Rescan button.

  Backfill script (scripts/backfill_archive_bed_type.py, --dry-run
  supported) re-opens every NULL archive's 3MF on disk to populate the
  column. Auto-loads .env from project root before importing backend
  modules (config.py reads DATABASE_URL from os.environ at import time,
  not from pydantic-settings at Settings() time) and prints the resolved
  DB URL with credentials redacted, so operators can confirm they're
  hitting the intended database -- Postgres or SQLite.

  Frontend: 6 OrcaSlicer-style PNGs ship in frontend/public/img/bed/ --
  under /img/ because that path is already statically mounted; a
  toplevel /bed-icons/ tried first hit the SPA catch-all and returned
  index.html as text/html. New utils/bedType.ts maps slicer strings
  case-insensitively, covering both Bambu Studio and OrcaSlicer naming
  variants for the same physical plate. Unmapped or NULL bed_type
  simply omits the icon, so cards stay clean for pre-feature archives.
2026-05-10 09:54:10 +02:00
MartinNYHC b30a283184 Feature/spoolman inventory UI (#1241)
feat(spoolman-inventory): squashed feature work for rebase onto dev

Squashed all commits from feature/spoolman-inventory-ui onto a single commit
to enable a clean rebase onto dev. Original per-commit history preserved at
backup tag backup/spoolman-inventory-ui-prerebase-20260507-105721.
2026-05-08 11:52:42 +02:00
MartinNYHC dac2a31192 Revert "feat(inventory): unified Spoolman inventory UI + AMS slot assignments…" (#1232)
This reverts commit 55d71498e9.
2026-05-07 11:30:31 +02:00
Sn0rrii 55d71498e9 feat(inventory): unified Spoolman inventory UI + AMS slot assignments + Storage Location + NFC write support + Spoolman Filament Catalog Picker (#1114)
feat(spoolman-inventory): squashed feature work for rebase onto dev

Squashed all commits from feature/spoolman-inventory-ui onto a single commit
to enable a clean rebase onto dev. Original per-commit history preserved at
backup tag backup/spoolman-inventory-ui-prerebase-20260507-105721.
2026-05-07 11:15:24 +02:00
Martha Augsburger 7baf1caf75 UI/multi color handling (#1205) 2026-05-05 09:34:16 +02:00
maziggy 76b997fc8a fix(slicer): encode file URL in protocol-handler scheme on Windows/Linux
"Open in Slicer" emitted `orcaslicer://open?file=<URL>` and
  `bambustudio://open?file=<URL>` by plain string concatenation, relying
  on a stale comment that claimed the browser preserves URLs in the query
  string. That ignores the slicer's own `url_decode()` on the received
  query (BS post_init → url_decode + split_str; OrcaSlicer Downloader
  regex + url_decode), so any already-percent-encoded character — most
  commonly `%20` from filenames with spaces — decoded to a literal space
  and the slicer's subsequent HTTP GET returned 0 bytes or 404.

  All three URL forms now use `encodeURIComponent()` (matching what the
  macOS `bambustudioopen://` branch was already doing, which is why the
  bug didn't surface on macOS). Corrected the file-level comment to
  document the actual invariant.

  Regression test in slicer.test.ts feeds the exact issue reproduction
  URL and asserts `%2520` appears in the generated href.
2026-04-23 10:03:44 +02:00
maziggy 1682b6956f fix(dispatch): clean up transient library upload from Direct-Print flow (#730)
The "Print" button on a printer card (and drag-drop-onto-card) used
  FileUploadModal to persist the file as a LibraryFile, then dispatched
  through POST /library/files/{id}/print. The LibraryFile row + disk file
  were left behind after every one-off print, polluting File Manager with
  entries the user never asked to save.

  FilePrintRequest.cleanup_library_after_dispatch (default False) opts
  into post-dispatch cleanup. When set, _run_print_library_file stages
  db.delete(lib_file) in the same transaction as archive_print so a
  mid-flight FTP / start_print failure rolls both back cleanly, commits
  together, then unlinks the library disk file + thumbnail after commit
  succeeds. External library files (is_external=True) are never touched.

  Only the Printers-page Direct-Print PrintModal sets the flag. Every
  other api.printLibraryFile caller (File Manager Print, Project Detail
  Print) leaves it unset — their entries are there by user intent.

  Also moves formatPrintName out of PrintersPage.tsx into a new
  utils/printName.ts module — fa1c46d9 (#881) exported it inline so its
  test could import it, tripping react-refresh/only-export-components.
2026-04-21 14:10:04 +02:00
maziggy 6fb814c5ea feat(printer): add X2D support — camera, dual-nozzle, K-profile, maintenance (#988)
The Bambu Lab X2D (launched April 2026, dual-nozzle, enclosed, hardened
  steel rod gantry, AMS 2 Pro compatible) identifies itself as internal
  model code N6 via SSDP/MQTT, and real serials begin with 20P9. None of
  these identifiers existed in Bambuddy's registries, so the camera
  service fell back to the chamber-image protocol on port 6000 (X2D
  doesn't speak it), firmware-check logged "Unknown printer model: N6",
  and the dual-nozzle K-profile paths — gated on the H2D serial prefix
  "094" — would have treated X2D as single-nozzle.

  Backend:
  - Register N6 → X2D across every registry (PRINTER_MODEL_ID_MAP,
    PRINTER_MODEL_MAP, STEEL_ROD_MODELS, ETHERNET_MODELS,
    CHAMBER_TEMP_SUPPORTED_MODELS, firmware-check API keys + wiki path,
    virtual-printer SSDP/product/serial tables, DB vp_model_fixes).
  - supports_rtsp(): match the X2 display-name prefix and the N6 internal
    code; camera now routes to RTSP on port 322.
  - Dual-nozzle serial prefix check in bambu_mqtt.delete_kprofile and
    kprofiles.set_kprofile broadened to ("094", "20P9") — X2D now takes
    the H2D-style cali_idx in-place edit path.
  - is_h2d model gate in bambu_mqtt.start_print extended with "X2D" so
    timelapse / bed_leveling / flow_cali / vibration_cali / layer_inspect
    are sent as integers and external-spool ams_id 254/255 routing is
    preserved (H2D-style deputy-nozzle addressing).

  X2D uses hardened steel rods like P2S — it is intentionally placed in
  STEEL_ROD_MODELS, not CARBON_ROD_MODELS. A regression-guard test pins
  the classification.

  Frontend:
  - mapModelCode in PrintersPage and SpoolBuddyAmsPage handle N6 and X2D.
  - Enclosure-door badge and airduct-mode whitelists include X2D.
  - MaintenancePage.getMaintenanceWikiUrl routes X2D to P2S wiki URLs for
    steel-rod lubrication, belt tension, cold-pull, and PTFE tube
    (exported to enable direct unit testing).

  Tests:
  - test_printer_models.py: TestX2DModel (10 assertions).
  - test_bambu_mqtt.py: X2D in start_print ams_mapping and is_h2d gate;
    TestDeleteKProfileDualNozzleDetection across H2D, X2D, P2S, X1C.
  - MaintenancePageWikiUrls.test.tsx: 15 assertions covering X2D, P2S
    regression, X1C/H2D/A1Mini regression, and model-name normalisation.

  Docs:
  - README: added X2 series to the supported printers table.
  - CHANGELOG: new entry under 0.2.3b4 Fixed.

  Credit to @krautech for the report and debug bundle, and to @legend813
  for PR #989 which seeded most of the registry changes — rod-type
  classification was corrected (steel, not carbon) and the dual-nozzle /
  K-profile / is_h2d gaps were added on top.
2026-04-16 10:40:32 +02:00
maziggy d74ab06072 feat(firmware): list all announced versions with usable/unavailable status, support rollback
Firmware update modal now shows every version from Bambu's wiki release
  history, each badged Usable/Unavailable/Installed. Selecting a usable row
  — newer or older than current — swaps the release notes and enables
  install for that version, so rollback no longer requires hand-flashing.

  Wiki scraper tightened to only read heading-anchor ids (h-XXXXXXXX-YYYYMMDD)
  instead of any XX.XX.XX.XX substring, eliminating false positives like an
  AMS firmware version mentioned in an H2D changelog being listed as H2D
  firmware.

  Refs #568
2026-04-14 11:10:24 +02:00
maziggy 99c193b535 refactor(colors): color_catalog is the single source of truth (#857)
The Printer tab AMS popup and spool auto-provisioner resolved color
  names from hardcoded tray_id_name tables with a suffix-code fallback —
  and suffix codes like "R1" are not globally unique across material
  families. A17-R1 (PLA Translucent Cherry Pink) fell through the
  fallback and resolved to "Scarlet Red" (A01-R1, PLA Matte), baking
  the wrong name into auto-created inventory spools.

  The fix removes the hardcoded tables entirely. Backend resolves color
  names via the existing color_catalog table by hex; frontend fetches a
  compact {hex: name} map once per session via a new
  GET /inventory/colors/map endpoint (auth-gated but not on
  inventory:read — read-only views need it too) and stores it in a
  ColorCatalogProvider context. A useSyncExternalStore hook cascades a
  re-render into pages mounted before the fetch completes so they
  refresh from HSL-fallback names once the catalog loads.

  Existing auto-provisioned spools keep their stored names; only new
  provisioning and live display benefit. Co-Authored-By is intentionally
  omitted here per project convention — set it via git config if needed.
2026-04-11 12:10:45 +02:00
maziggy 5a696f9fa3 ● Add prefer lowest remaining filament in auto-matching (#805)
When multiple AMS spools match the same type/color criteria, an optional
  setting now prefers the spool with the lowest remaining filament. This
  helps consume partial spools before starting new ones. Sorting applies
  to all matching paths: queue scheduler, print modal, and multi-printer
  mapping. Unknown remain values (-1) sort to end.
2026-03-31 14:14:15 +02:00
maziggy 94c6696469 Add Ukrainian Hryvnia to known currencies (#815) 2026-03-26 17:25:35 +01:00
maziggy d6e883652d Misc Spoolbuddy frontend fixes 2026-03-22 11:34:21 +01:00
maziggy 5a4a2260b1 Fix PA-CF/PA12-CF/PAHT-CF type matching and force color match click target (#688)
Bambu Lab firmware treats PA-CF, PA12-CF, and PAHT-CF as interchangeable,
  but the print scheduler and filament override UI used exact string matching.
  A 3MF requiring PA-CF wouldn't match PA12-CF in the AMS, leaving the
  override dropdown disabled and preventing scheduler assignment.

  Add a filament type equivalence system (both backend and frontend) so
  these PA variants are treated as compatible in scheduler assignment,
  AMS slot matching, force color match validation, and the override dropdown.

  Also fix the "Force color match" checkbox click target being too wide
  (entire row instead of just the checkbox/label) by changing flex to
  inline-flex on the label element.
2026-03-13 12:56:31 +01:00
maziggy ed99ee68f4 Shorten inventory location labels for compact display
AMS slots now show "B3" instead of "AMS-B Slot 3" and external spool
  holders show "Ext" instead of "External", keeping the table readable
  at narrow widths.
2026-03-11 09:57:38 +01:00
d7401a80df Feature: Force Color Match (#625)
* Added "Force Color Match" feature to filament override

* Update settings.py

* Update settings.py

* Label updates for warnings and checkbox case

* Initial plan

* Add noMatchingMaterial i18n translations to all 7 locale files

Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>

* Use {{material}} interpolation in noMatchingMaterial; populate from filament_overrides with getColorName

Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>

* Updates for PR#625 comments

* Added "Force Color Match" feature to filament override

* Update settings.py

* Update settings.py

* Label updates for warnings and checkbox case

* Initial plan

* Add noMatchingMaterial i18n translations to all 7 locale files

Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>

* Use {{material}} interpolation in noMatchingMaterial; populate from filament_overrides with getColorName

Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>

* Updates to PrintersPage and QueuePage

* Update QueuePage.tsx

* Rebase to 0.2.2b3

* Fix compile errors with FilamentOverride

* Updates for PR#625 comments

https://github.com/maziggy/bambuddy/pull/625#pullrequestreview-3908738261

* Fix force color match default, loadedFilamentTypes bug, remove dead translations

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
Co-authored-by: MartinNYHC <mz@v8w.de>
2026-03-09 11:54:02 +01:00
maziggy a6aec2aed8 Added ILS: '₪' (Israeli New Shekel) to the currency list 2026-03-07 13:02:06 +01:00
maziggy 60b17ea5d8 Added MYR (Malaysian Ringgit) with the symbol RM to the currency list 2026-03-07 08:59:51 +01:00
maziggy 1bc3da3fad Fix multi-printer filament mapping ignoring nozzle on dual-nozzle printers (#624)
The per-printer InlineMappingEditor showed filaments from both nozzles
  on H2D printers. The single-printer FilamentMapping dropdown was fixed
  in 29e9593 but the multi-printer code path was missed. Added nozzle_id
  filtering to both auto-match logic and dropdown options via shared
  autoMatchFilament() and filterFilamentsByNozzle() functions in
  amsHelpers.ts, with 10 regression tests.
2026-03-05 15:38:37 +01:00
AneoPsy abe40f1ed0 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.
2026-03-03 04:20:21 -10:00
AneoPsy 15521deb0a 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
2026-03-03 03:45:12 -10:00
maziggy 23b2d97170 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.
2026-03-03 12:10:29 +01:00
AneoPsyandMartinNYHC fa9d3779cc [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>
2026-03-03 10:26:30 +01:00
AneoPsy 792a86f1e5 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.
2026-03-01 03:11:28 -10:00
maziggy ed36eafbec Fix timestamps off by timezone offset in non-UTC containers (#504)
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.
2026-02-24 08:18:02 +01:00
MartinNYHC 1d636c9a16 Merge branch '0.2.1b' into refactor-all-pages 2026-02-18 16:54:55 +01:00
maziggy a903816b8a fix(slicer): token-based auth for Open in Slicer downloads (#433)
Slicer protocol handlers (bambustudio://, orcaslicer://) launch the
slicer app which fetches files via HTTP but cannot send auth headers.
The global auth middleware returned 401, causing "importing failed."

Add short-lived, single-use download tokens: the frontend fetches a
token via authenticated POST, then builds a /dl/{token}/{filename} URL
the slicer can access without auth headers. Tokens are validated
server-side (5-min expiry, single-use). Applies to archive files,
source 3MFs, and library files.

Also fix platform-specific URL formats to match slicer source code:
- macOS: bambustudioopen:// with encodeURIComponent
- Windows/Linux: bambustudio://open?file= (was wrongly using macOS scheme on Linux)
- OrcaSlicer: orcaslicer://open?file=
2026-02-18 16:37:35 +01:00
AneoPsy 00b02ee620 Fix conflict 2026-02-18 03:32:24 -10:00
MartinNYHC 445865bbde Remove empty line at the beginning of file 2026-02-18 13:56:36 +01:00
AneoPsy d9dd15dbff fix(ui): storage size formatting, overdue queue items, and i18n fixes
- Add formatStorageSize helper in FileManagerModal to display storage
  in GB/MB instead of raw bytes
- Show "Overdue" label for queue items whose scheduled time has passed
  by more than 1 minute
- Fix missing "in" prefix for English inDays translation
- Fix swapped justNow/now translations in Italian locale
- Fix whitespace formatting for waiting key across all locales
- Add trailing newline to date.ts and date.test.ts
2026-02-18 02:45:43 -10:00
AneoPsy b477fbf46d Refactors date and file formatting utilities
refactor(utils): consolidate date and file size formatting

Extracts formatting logic into dedicated utility functions to improve
code maintainability and consistency across components.

Changes:
- Consolidate date formatting functions (formatRelativeTime, etc.)
- Consolidate file size formatting into single formatFileSize utility
- Fix file size calculation errors
- Update UI components to use new utilities
- Change "ASAP" to "Waiting" for unscheduled queue items

Tests:
- Add comprehensive unit tests for date utilities
- Add unit tests for file size formatting
2026-02-17 01:15:33 -10:00
AneoPsy 7134007e6a fix(queue): optimize printer status polling and minor cleanup
- Only poll printer status for actively printing items
- Fix indentation in useQuery block
- Add trailing newline to utils/date.ts
2026-02-15 22:47:06 -10:00
AneoPsy 687a48e655 feat(queue): enhance currently printing item display
- Add progress bar, time remaining, ETA, and layer count to currently printing item
- Remove duplicate formatDuration & formatETA functions, now imported from utils/dates
- Remove formatTime (duplicate of formatDuration)
- Update formatETA to support optional i18n translation
- Update translation: common.tomorrow
2026-02-15 22:25:52 -10:00
maziggy a37dfaf7fb feat: dual external spool support, AMS slot model filtering & pre-population
Backend:
- Add dual external spool support for H2D (vt_tray as list: Ext-L/Ext-R)
- Add cloud filament ID map endpoint (/cloud/filament-id-map)
- Fix RFID spool data erased by periodic AMS updates (skip tag matcher
  for RFID-tagged trays)
- Fix AMS slot config overwrites RFID spool state
- Fix K-profile selection corrupts existing profiles on X1C/P1S
- Resolve K-profiles filament name via cloud filament ID map
- Update print scheduler and usage tracker for dual external spools

Frontend:
- Add printer model filtering to ConfigureAmsSlotModal (cloud/local/builtin
  presets filtered by @BBL model suffix and compatible_printers)
- Add pre-population for configured slots (preset, color, K-profile)
- Add K-Profiles view with accurate filament name resolution
- Internationalize all ConfigureAmsSlotModal strings (en/de/fr/it/ja — 21 keys)
- Add 5 new ConfigureAmsSlotModal tests (model filtering, pre-selection,
  color pre-population, i18n)
- Update PrintersPage for dual external spool rendering

Docs:
- Update CHANGELOG, README, website features, and wiki AMS docs
2026-02-15 08:04:01 +01:00
MartinNYHC 44df253c67 Merge branch '0.2.0b' into feature/inventory 2026-02-12 16:38:03 +01:00