2486 Commits
Author SHA1 Message Date
maziggy 686dce5af4 fix(gcode_viewer): close CodeQL XSS + useless-escape alerts on PR #1263
- slider-shim.js: HTML-attribute-escape opts.id before interpolation
    (only caller passes a constant, but defends against future taint)
  - prettygcode.js: drop useless \\? escape inside [...] character class
2026-05-11 13:29:55 +02:00
maziggy ac72aacbfa Tool: Bandit B108
Severity: Warning ×4
  Issue: "Probable insecure usage of temp file/directory" — /tmp/<filename> literals used as synthetic DB field values in two integration tests
  Status: Fixed
  ────────────────────────────────────────
  Tool: CodeQL Python / JS
  Severity: Pending
  Issue: Still running on the head SHA
  Status: —
  ────────────────────────────────────────
  Tool: Trivy container scan
  Severity: Pending
  Issue: Still running
  Status: —
  ────────────────────────────────────────
  Tool: Bandit (Python Security Analysis)
  Severity: Pass
  Issue: The separate Bandit run on the changes already passes
  Status: ✓
2026-05-11 13:15:30 +02:00
maziggy 0457b92988 Merge remote-tracking branch 'origin/main' into 0.2.4 2026-05-11 13:11:43 +02:00
maziggy 6062b691d5 Bumped version 2026-05-11 12:45:15 +02:00
maziggy 1c778e8a68 fix(security): bump pip to >=26.1 in Dockerfile (CVE-2026-6357)
The python:3.13-slim-trixie base image ships pip 26.0.1, which runs its
  self-update check after installing wheels — a malicious wheel that included
  a module name matching a deferred stdlib import (urllib, ssl, ...) could
  hijack the import inside the install step. GitHub code-scanning alert #778
  flagged this as medium-severity.

  Dockerfile now upgrades pip to >=26.1 immediately before the requirements.txt
  install, so the requirements install runs under the patched pip and the
  resulting dist-info metadata in the final image is the fixed version.
  No requirements.txt change — the floor is enforced at the image-build layer
  where the vulnerable copy actually lived.
2026-05-11 12:36:31 +02:00
maziggy a25177097d Post work PR #1255 2026-05-11 12:29:53 +02:00
BurntOutHylian 7afb303ffd feat(#1239): Update Gitea and Forgejo due to API changes from initial cut (#1255)
feat(#1239): first cut at Gitea backups silently failing after 1st run
feat(#1239): Added Token Scope for Forgejo edge case. Also included: test coverage for fixes
2026-05-11 12:27:41 +02:00
maziggy 90c3efece9 Housekeeping 2026-05-11 10:43:43 +02:00
maziggy dfd9fcedf4 fix(printer-card): confirm before HA entity toggle on printer card (#1260)
Smart plugs with "Show on Printer Card" enabled appear as a chip in the
  HA-entities row below the main plug controls. One click cut power to the
  printer instantly — including mid-print — while the main Off button right
  next to it already routes through a ConfirmModal. The HA-row chip was
  added later and skipped the same gating.

  Branch on entity type: script.* entities keep firing instantly (fire-once
  triggers, not power switches — confirming each click would be annoying),
  but switch/light/anything-else entities now open a ConfirmModal first.
  Reuses the same variant="danger" + running-print warning copy as the
  existing power-off confirmation when status.state === 'RUNNING'.
2026-05-11 08:26:51 +02:00
maziggy fed8f1f74f Added install_docker for Windows 11 2026-05-10 15:22:10 +02:00
maziggy 90b26e806b Updated README 2026-05-10 13:59:07 +02:00
maziggy 28c68feaeb Updated README 2026-05-10 13:58:48 +02:00
maziggy ba3dc613d1 Changed app defaults 2026-05-10 11:15:47 +02:00
maziggy 080176c6e5 fix(filament-mapping): X2D/H2D dual-nozzle without AMS lost
external-spool extruder routing (#1257)

  X2D with 0 AMS units and two external spools (Ext-L feeding left
  extruder, Ext-R feeding right) showed "Required filament type not
  found in printer" even when the matching filament was physically
  loaded. Cause: useFilamentMapping derived dual-nozzle status from
  ams_extruder_map being non-empty -- that map is populated from AMS
  info bits, so dual-nozzle printers without AMS got an empty map
  and hasDualNozzle=false. External spools then fell through to
  extruderId=undefined, and the nozzle-aware filter rejected every
  candidate because undefined !== 0/1.

  Prefer the hardware-reported printerStatus.nozzles array length as
  the dual-nozzle signal -- populated regardless of AMS configuration
  -- and keep the ams_extruder_map branch as fallback for older
  firmware that might not surface nozzles. Affects all dual-nozzle
  printers running without AMS: X2D, H2D, X2 Pro.

  Regression test pins both layers the bug straddled --
  buildLoadedFilaments extruderId assignment per external spool, and
  computeAmsMapping picking the correct external for a per-nozzle
  requirement -- so a future change that re-breaks either fails CI.
2026-05-10 10:15:07 +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
maziggy 18af751acd Updated README 2026-05-09 17:44:44 +02:00
maziggy 47dd4dd016 Updated README 2026-05-09 17:44:20 +02:00
maziggy 77bf53b7f1 fix(gcode-viewer): add in-app back button (was browser-back only)
Opening the GCode Viewer from a File Manager card or Archive card mounts
  GCodeViewerPage as a full-height iframe inside the Layout shell. The page
  rendered nothing but the iframe, so once the third-party viewer's UI took
  over the content area there was no in-app affordance to return to the
  originating list - only the browser's back button.

  Add a thin bar above the iframe with an ArrowLeft button. The label adapts
  to the entry point - "Back to Print Archives" when the URL carries
  ?archive=, "Back to File Manager" when it carries ?library_file=, generic
  "Back" otherwise. Click prefers navigate(-1) so the user lands back in
  their original list with scroll position and filters preserved; falls
  back to /archives or /files when the page was opened in a fresh tab and
  there's no SPA history to return to.

  New gcodeViewer.{back, backToArchives, backToFiles} i18n namespace added
  to all 8 locales with native translations.
2026-05-09 12:41:13 +02:00
maziggy 83a83ed724 feat(labels): add 40x30 mm template, hex colour code, bolder brand (issue #809 follow-up)
Three enhancements requested by @oliboehm after the V1 label-printing
  ship in #809:

  - New box_40x30 single-label template (common DK/Brother roll size,
    good for filament-bag and storage-bin labels). Routes through the
    existing roomy layout since height >= 20 mm.

  - Colour hex code (#RRGGBB, alpha-stripped, uppercase) rendered on
    every label - useful when several near-identical material/colour
    spools sit next to each other and the swatch alone isn't enough to
    tell them apart. Skipped silently when rgba is None or malformed.

  - Brand line bumped to Helvetica-Bold (was regular) and a couple of
    points larger on both layouts so it reads cleanly at arm's length.

  Wired through the SpoolLabelTemplate union, the modal's
  TEMPLATE_OPTIONS, and the inventory.labels.templates.box40x30 i18n
  key in all 8 locales (native translations for de/fr/it/ja/pt-BR/
  zh-CN/zh-TW). Modal regression test widened from 4 to 5 template
  buttons. Three new renderer tests pin the hex-code render, the
  hex-code skip on invalid rgba, and the bold-brand font reference.
2026-05-09 10:10:20 +02:00
maziggy a193145522 fix(archives): hide truncated "Re..." / "Sc..." button labels on narrow card widths (issue #1249)
The archive card's action row crams 6 buttons into one line: 2 labelled
  (Reprint + Schedule, or Slice when un-sliced) plus 4 icon-only utilities.
  The labelled buttons used `flex-1` to share whatever the icon buttons
  left over, with the label gated on `hidden sm:inline truncate`.

  Tailwind viewport breakpoints can't see the card width. The grid grows
  columns alongside viewport (md:2 lg:3 xl:4), so cards stay ~320-380 px
  wide regardless of breakpoint, and the labelled buttons end up with
  ~30 px of space — enough to render "Re..." / "Sc..." and not much else.

  Bump the label breakpoint sm: -> xl: so labels appear only at
  viewport >= 1280px where the cards actually have room. Below that,
  the buttons render icon-only and the existing title= attribute serves
  as the hover tooltip.
2026-05-09 09:53:25 +02:00
maziggy 04096620a9 Post work PR #1246 2026-05-09 09:44:50 +02:00
Miguel Ángel López Vicente 6a130c09d7 [Feature] Copy filament (#1246) 2026-05-09 09:40:21 +02:00
maziggy 30fe88a334 fix(inventory): show all per-printer/per-nozzle variants in spool form's Slicer Preset dropdown (issue #1248)
Two defects in buildFilamentOptions, surfaced together:

  1. The function was precedence-based — cloud presets short-circuited
     the local-presets branch, silently hiding any imported Local Profile
     while the user was logged into Bambu Cloud. The wiki documents the
     dropdown as "merged and deduplicated" across cloud + local + built-in.

  2. Cloud default presets and local presets were being collapsed by base
     name (everything after "@" stripped), so all P1S/X1C/A1 variants of
     "Bambu PLA Basic" rendered as a single row. The spool form is
     printer-agnostic by design, so the right semantic is to show every
     variant individually — the union across all printers — not collapse
     them. AMS Slot is per-printer (it filters), the spool form is
     union-of-all (it doesn't).

  Rewrote the merge to push each cloud setting_id and each LocalPreset row
  as its own FilamentOption with the full @printer suffix preserved in
  displayName. Built-in dedup against cloud setting_id is kept (mirrors
  ConfigureAmsSlotModal.tsx). Wired api.getBuiltinFilaments() into both
  callers. slicer_filament persistence is unchanged so existing spools
  keep slicing correctly.
2026-05-09 08:43:51 +02:00
maziggy f5ecc61cda fix(spoolbuddy): lower /update permission to INVENTORY_UPDATE so kiosk's own Settings -> Update button works
The kiosk's Settings -> Update Daemon button returned "API keys cannot
  be used for administrative operations" because POST /spoolbuddy/devices/
  {id}/update was gated on Permission.SETTINGS_UPDATE, and SETTINGS_UPDATE
  is in the _APIKEY_DENIED_PERMISSIONS deny-list introduced by PR #1241.
  Every kiosk-side request tripped the deny-list before the API key's
  scope set (Read / Print Queue / Control / Legacy) was even consulted.

  Same root cause as the four QuickMenu System buttons fixed in 0.2.4b3
  (Restart Daemon / Restart Browser / Reboot / Shutdown). Missed /update
  in that audit on the reasoning "replaces the daemon binary, different
  threat surface" — but that's wrong: restart_daemon already replaces
  the running daemon process, so daemon-replacement is not a step up in
  blast radius. The SSH update is also strictly scoped to the one device
  the operator physically controls (git fetch + pip install + systemctl
  restart on that host) — same threat profile as the system commands
  already running on INVENTORY_UPDATE.

  Lower /spoolbuddy/devices/{id}/update from SETTINGS_UPDATE to
  INVENTORY_UPDATE so it aligns with the rest of the kiosk-scoped routes
  (calibration/tare, display, cancel-write, system/command,
  system/command-result, update-status). The main Bambuddy in-app updater
  at POST /api/v1/updates/apply keeps SETTINGS_UPDATE — that one runs on
  the Bambuddy host and is correctly fenced behind the deny-list.
2026-05-08 14:28:41 +02:00
maziggy 3f58fc74b4 fix(http): RFC 6266-encode Content-Disposition so non-ASCII filenames don't crash response (issue #1245)
Reported by @1000Delta. The printer file download (and three sibling
  endpoints) raised UnicodeEncodeError: 'latin-1' codec can't encode
  characters... on any filename outside U+0000..U+00FF (Chinese,
  Japanese, Arabic, accented Latin), because the route pushed `filename`
  straight into Content-Disposition: attachment; filename="...".
  Starlette/uvicorn encodes response headers as latin-1, so the assignment
  crashed at write-time.

  New backend/app/utils/http.py::build_content_disposition emits both an
  ASCII-stripped legacy filename="..." fallback and an RFC 5987
  filename*=UTF-8''<percent-encoded> parameter. Every modern browser
  prefers the *= form, so the original Unicode filename round-trips
  through Save-As intact.

  Same shape was latent in three siblings and fixed in the same PR
  (no deferred follow-ups): archive QR endpoint (archive.print_name
  from 3MF metadata), project ZIP export (project.name — the existing
  isalnum() sanitiser passes non-ASCII through), and the PDF label
  streamer (latent today, callers ASCII-only but the helper hardens it).
2026-05-08 14:06:40 +02:00
maziggy 91fa9cd432 Housekeeping 2026-05-08 13:33:04 +02:00
maziggy 829bc2dd6a Bumped version 2026-05-08 12:49:12 +02:00
maziggy a7b3e01b86 chore(deps): bump python-multipart floor 0.0.26→0.0.27 (CVE-2026-42561) 2026-05-08 12:45:17 +02:00
maziggy ef7fd4fa5c fix(spoolbuddy): respect Spoolman mode end-to-end + multiple cache/UX/permission fixes
Seven intertwined SpoolBuddy + Spoolman bugs from feature/spoolman-inventory-ui
  testing, fixed as one batch since they all live on the same path:

  1. /spoolbuddy/nfc/tag-scanned always tried local DB first and only
     consulted Spoolman as a fallback on local-DB miss. A stale local
     row silently won over the authoritative Spoolman record. Now gates
     on _get_spoolman_client_or_none() so the route uses Spoolman
     exclusively when enabled, local exclusively otherwise.

  2. Dashboard "Assign to AMS" button was a no-op when the matched
     spool wasn't yet in the cached spools query (newly created in
     Spoolman, or unarchived after page load). The card rendered via
     `displayedSpool ?? sbState.matchedSpool` fallback but the modal's
     stricter guard silently failed to mount. New effectiveModalSpool
     synthesises an InventorySpool-shaped object from the WebSocket-
     delivered MatchedSpool (9-field subset, sufficient for the modal
     since it only needs `id` to route the assign API).

  3. AMS-page slot picker explicitly returned null for the
     assign/unassign branch when a slot had a SpoolmanSlotAssignment
     but no tag-linked spool — only Configure stayed visible. Now
     resolves the assignment via spoolmanSlotAssignmentsAll +
     spoolmanInventorySpoolsCache, renders a "Assigned spool" info
     card, and exposes an Unassign button wired to a new
     unassignSpoolmanSlotMutation (DELETE
     /spoolman/inventory/slot-assignments/<id>).

  4. LinkSpoolModal showed "Unknown color" for every Spoolman spool
     because Spoolman doesn't standardise color_name — most installs
     only populate color_hex and filament.name (which often carries
     the colour, e.g. "PLA Basic Red"). _map_spoolman_spool now falls
     back to the filament's subtype (filament name minus material
     prefix) when color_name is empty, so spools are visually
     distinguishable. The NFC write-tag warning specifically checks
     the raw filament.color_name (not the mapped value) so the
     "tag encodes empty color name" warning still fires on installs
     that genuinely lack the field.

  5. Writing a tag for spool B didn't clear the same tag from spool A,
     so a single NFC UID could map to two spools at once and
     find_spool_by_tag returned whichever came first in the cached
     list. nfc_write_result now searches Spoolman for any other spool
     currently bound to the target UID and clears its extra.tag
     (best-effort: cleanup failure logs a warning but doesn't block
     the write, since the chip is already written).

  6. The kiosk display held stale spoolmanSlotAssignments cache
     permanently because a long-running browser window has no
     focus/remount triggers to fire a refetch. Adds
     refetchInterval: 3_000 so the kiosk picks up changes from another
     client (Bambuddy main UI, direct Spoolman edit) within seconds.

  7. Kiosk QuickMenu System buttons (Restart Daemon / Restart Browser /
     Reboot / Shutdown) all 403'd silently. /system/command was gated
     on Permission.SETTINGS_UPDATE (T-Gap 2 from a prior security
     audit) but every other kiosk-scoped device route uses
     INVENTORY_UPDATE; the kiosk operator's session has the latter,
     not the former. Lowered to INVENTORY_UPDATE so operators can
     recover the kiosk from the kiosk. Risk is bounded — only the 4
     named commands are accepted (no RCE), reboot/shutdown require
     physical-access recovery anyway, the same operator already
     controls printers + weighs spools. /update keeps SETTINGS_UPDATE
     because it can replace the daemon binary.
2026-05-08 12:09:34 +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
maziggy ceffcfaef6 fix(vp): overlay storage indicators on cached push so slicer pre-flight passes for P1S/A1 targets (issue #1228)
Slicer "Send to printer" worked on 0.2.3.2 with a queue-mode VP and
  started failing on 0.2.4b3 with BambuStudio's generic "storage needs
  to be inserted before send to printer" error. Multiple users
  reported it across P1S, P2S, Docker bridge, macvlan, and host
  networking. @rtadams89's debug-level support archive showed the
  smoking gun: slicer establishes MQTT TLS, gets pushall +
  get_version, then never opens an FTP connection — pre-flight
  rejects before any data transfer.

  The 0.2.3.2 synthetic stub baked in three SD/storage indicators
  that BambuStudio's "Send" pre-flight reads: home_flag with bit 8
  (HAS_SDCARD_NORMAL, 0x100), sdcard=True, and a storage:{free,total}
  block. The 0.2.4b3 cached-as-base slicer-mirror (7dea33d0) passes
  the live target's push_status through with only an IP rewrite — if
  the real firmware doesn't report those fields (P1S/A1 with no SD
  card, older field shapes, confirmed on P1S firmware 01.10.00.00),
  the slicer sees "no storage" and aborts. H2D and X1C reproductions
  worked because those firmwares do report the indicators.

  In _send_status_report's cached-as-base branch, after copying the
  cache and applying the existing protocol/upload-state overrides:

  - home_flag |= 0x100 (preserves any other bits the real printer set)
  - sdcard = True (force-set even when real says False)
  - storage = setdefault(...) (only fills in if missing — real values
    pass through unchanged when the printer reports them)

  For VP usage the slicer uploads via FTPS to Bambuddy's filesystem
  at /app/data/virtual_printer/uploads/<vpid>/; the printer's actual
  SD card is irrelevant on that path, so forcing "storage available"
  is correct for the queue / immediate / review modes the
  cached-as-base path covers.
2026-05-08 09:19:09 +02:00
maziggy 554f5172f0 Post work PR #1219 2026-05-08 09:06:18 +02:00
Sn0rrii 90743cfa39 feat(encryption): MFA at-rest encryption auto-bootstrap with status UI (#1219) (#1231)
chore(i18n): extend parity gate to all locales with strict/info tiers

  Previously the script only inspected en/zh-CN/zh-TW, leaving de/fr/it/ja/pt-BR
  drift invisible. Now locales are auto-discovered from src/i18n/locales/, and a
  STRICT list (de, zh-CN, zh-TW — currently in parity) gates CI while the rest
  report informationally until their drift is caught up. ja notably has 27 real
  placeholder bugs worth fixing before promotion to strict.
2026-05-08 09:01:51 +02:00
maziggy bb03a2b373 fix(frontend): revert base: '' so deep SPA routes load their assets on initial navigation (issue #1221)
PR #1195 (d6a31393) set Vite's base: '' to emit relative asset URLs
  in the built index.html, intended as a partial improvement for
  path-prefixed reverse proxies. The relative URLs broke every deep
  SPA route on initial load: popup windows, direct URL paste, page
  refresh on /camera/<id>, /projects/<id>, /groups/<id>/edit,
  /external/<id>, /files/trash, and the SpoolBuddy kiosk paths.

  Browser resolves ./assets/index-XXX.js against the document URL.
  For /camera/<id>, that gives /camera/assets/index-XXX.js — the SPA
  catch-all returns index.html (text/html) for that path, and modern
  browsers refuse to execute HTML as a JS module under
  X-Content-Type-Options: nosniff. Hence the empty-popup symptom
  reported on #1221 across P1S / P2S / X1 / Docker / git / Chrome /
  Firefox / Brave / Safari, plus the quieter "blank page on refresh"
  on every other deep route.

  Reverts the two PR #1195 lines: removes base: '' from
  vite.config.ts (Vite default '/' restored, emitting absolute asset
  URLs /assets/..., /manifest.json, /sw-register.js) and reverts
  register('sw.js') to register('/sw.js') in public/sw-register.js.

  PR #1195's class of bug — path-prefixed reverse proxy users serving
  Bambuddy at a subpath — was already explicitly closed as wontfix in
  that thread because supporting it requires subpath-aware
  bootstrapping (API_BASE, React Router basename, PWA manifest scope,
  SW scope) for every user forever. The supported alternative for that
  audience stays as documented in the #1195 closing comment: NPM
  (Nginx Proxy Manager) addon + Cloudflare Tunnel at a real domain
  with HTTPS, then HA Webpage panel embedding via
  TRUSTED_FRAME_ORIGINS — that path doesn't depend on base: '' at all.

  The trade-off is intentional: revert reaches every user impacted by
  deep-route initial-load bugs (much larger population than
  path-prefixed proxy users), in exchange for an already-wontfixed
  subpath-proxy regression that has a working alternative.
2026-05-08 08:39:00 +02:00
maziggy 4c0a12b95e fix(label-picker): pack templates into a 2x2 grid so all 4 plus Cancel fit on tight viewports (issue #1230)
The earlier `min-h-0` fix on the spool list (61314cf2) made the
  shrinkable child shrinkable, but on @elit3ge's 838px viewport the
  four stacked templates (~310px) plus footer still blew past
  max-h-[90vh] once Brave's browser chrome ate into vh, and
  overflow-hidden on the modal clipped Avery 5160 mid-row with the
  Cancel button entirely below the clipped bottom edge — no scroll
  path. The screenshot showed the spool list at ~5 visible rows with
  its own scrollbar still active, confirming the templates section's
  natural height was the dominant problem, not the spool list.

  Templates now render as a responsive grid (grid-cols-1
  sm:grid-cols-2 gap-2) so the four buttons pack into a 2x2 grid
  above the sm breakpoint, trimming ~150px of vertical. Per-cell
  padding tightens to p-2.5, labels/hints get text-sm + truncate,
  and the full strings are reachable via title="<label> — <hint>"
  on each button. Footer drops py-3 to py-2 for a few extra pixels.
  The min-h-0 on the spool list is kept as a belt-and-braces shrink
  for any viewport tighter still. Mobile (<sm) keeps the stacked
  layout — no regression there.
2026-05-08 07:27:35 +02:00
maziggy 20fa8fbfdc fix(configure-ams-slot): expand long filament profile names inline on hover (issue #1237)
Long preset names like "SUNLU PETG GLOW IN THE DARK GEN2 @Bambu Lab
  H2C 0.4 nozzle" were visually clipped in the Configure AMS Slot
  modal's preset picker. With several near-identical entries differing
  only in nozzle size, users had to open browser dev tools to tell
  them apart.

  A `title={preset.name}` alone was too slow visually — browsers wait
  500-1000ms before rendering native tooltips. The row now un-truncates
  inline on hover via group-hover:whitespace-normal + break-all, so the
  full name appears the moment the cursor enters the row. `truncate`
  stays as the default to keep the list compact when scanning.

  The native `title={preset.name}` is also kept as a belt-and-braces
  fallback for assistive tech and touch devices where :hover doesn't
  fire. Both desktop and mobile layouts updated.

  Test: new ConfigureAmsSlotModal.test.tsx regression that pins the
  truncate / group-hover:whitespace-normal / group-hover:break-all
  classes on the span, the title attribute, and the `group` class on
  the parent button — so a future refactor that drops any of those
  fails CI.
2026-05-08 07:15:23 +02:00
maziggy 233808956b ● fix(backup): Gitea wraps GitCommit in Commit schema — extract tree SHA from both shapes (issue #1224 follow-up)
Subsequent backups against Gitea 1.24+ failed with the opaque
  "Backup failed: 'tree'" message after the initial-backup fix landed in
  7ee89b56. Root cause: Gitea's GET /repos/{owner}/{repo}/git/commits/{sha}
  returns the wrapped Commit schema where the tree lives at
  data["commit"]["tree"]["sha"], whereas GitHub's same-named Git Database
  endpoint returns the unwrapped GitCommit schema with tree at the top
  level. The bare commit_response.json()["tree"]["sha"] lookup at
  gitea.py:109 raised KeyError: 'tree' and the broad except in push_files
  surfaced it as the opaque "Backup failed: 'tree'" string — masking the
  real shape mismatch.

  Adds a _commit_tree_sha() helper that tries the flat shape first
  (GitHub-compatible / older Gitea) and falls back to the wrapped shape
  (Gitea 1.24+, Forgejo). Returns None on truly malformed responses;
  push_files maps that to a clear "Failed to extract tree SHA from commit
  response" instead of leaking a KeyError repr. Keeps the existing-files
  diff working on both shapes so subsequent backups don't re-upload every
  blob — preferred over the .get()-and-skip approach which would have
  required also dropping base_tree from the tree POST and re-uploading
  unchanged files on every backup.
2026-05-08 07:00:22 +02:00
maziggy 61314cf20b fix(label-picker): allow spool list to shrink so all 4 templates and Cancel stay visible (issue #1230)
The Print Labels modal used a flex column with overflow-hidden on the
  outer container, the spool list as the flex-1 shrinkable child, and the
  templates + footer as fixed siblings below it. The spool list had
  min-h-[160px], which combined with the implicit min-height: auto on
  flex items meant it could not yield space when the modal was tight —
  templates and the Cancel button overflowed the modal's max-h-[90vh] and
  got clipped. Reproducible on Windows 11 + Brave at 1080p with browser
  chrome / DPI scaling reducing the effective viewport.

  Switching to min-h-0 both removes the explicit floor and overrides
  min-height: auto so flex shrinking actually works; the spool list now
  yields height to keep all four templates and the Cancel button visible
  on constrained viewports. Larger viewports behave identically since
  flex-1 still grows to fill.

  Adds a regression test that asserts all four template names + the
  Cancel button render in the DOM and pins the structural fix by
  checking the spool list scroller has min-h-0 with no min-h-[…] literal.
2026-05-07 11:53:22 +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
maziggy ded161626a Post work PR #1203 2026-05-07 10:48:49 +02:00
Ed 3c0c7a8ddc [FEAT] Printer page header update (#1203) 2026-05-07 10:43:11 +02:00
maziggy 972e635233 fix(spool-tag-matcher): filter catalog lookup by material variant, not hex alone (issue #1227)
Three Bambu Lab catalog rows share #FFFFFF — Jade White (PLA Basic),
  Ivory White (PLA Matte), White (PLA Silk). The catalog lookup in
  create_spool_from_tray filtered by manufacturer + hex only with no
  ORDER BY, so SQLite returned rows in rowid order and the first-inserted
  entry (Jade White) won every RFID-driven spool creation regardless of
  the actual material the AMS reported. Inserting an Ivory White PLA
  Matte roll always produced a spool named "Jade White".

  Same class of bug bites any other shared-hex pair across PLA Basic /
  Matte / Silk; the whites were just the most visible.

  Fix: add a material filter using tray_sub_brands (the printer-reported
  material variant — "PLA Matte" / "PLA Basic" / "PLA Silk"), which
  matches the catalog's `material` column directly. Use the raw
  tray_sub_brands value (captured before the gradient/dual/tri-color
  subtype upgrade) because the catalog stores "PLA Basic" for gradient
  rolls too — the upgraded subtype lives on the spool, not the catalog.

  Also add ORDER BY id to the query so the fallback path (empty
  tray_sub_brands — third-party spools / OpenTag tags) is deterministic
  across SQLite + PostgreSQL instead of DB-implementation-defined.

  Tests: 4 new in test_spool_tag_matcher.py — Ivory White PLA Matte
  resolves to Ivory not Jade (the regression pin), PLA Silk White
  resolves to White, Jade White PLA Basic still works with all three
  #FFFFFF entries seeded, and the empty-sub_brands fallback stays
  deterministic via the new ORDER BY.

  Existing spools already mis-named in the database don't auto-correct
  on next AMS read — the matcher only fires on new RFID-driven creation.
  Affected users need a manual rename in Inventory after upgrading.
2026-05-07 10:31:27 +02:00
maziggy 7ee89b561b fix(backup): Gitea/Forgejo handle list-shaped ref response and empty-repo bootstrap (issue #1224 and #1225)
Two interacting bugs in the Gitea/Forgejo backend, both inherited from
  GitHubBackend because PR #1160 assumed Gitea's Git Data API was fully
  GitHub-compatible. It isn't, on two specific points:

  1. List-shaped ref response. Gitea/Forgejo's
     GET /api/v1/repos/{owner}/{repo}/git/refs/heads/{branch} returns a
     GET /api/v1/repos/{owner}/{repo}/git/refs/heads/{branch} returns a
     list of matching refs even when only one matches; GitHub returns a
     single object. The inherited push paths did
     ref_response.json()["object"]["sha"] and crashed with
     "list indices must be integers or slices, not str" against any
     populated Gitea repo.

  2. Empty-repo writes refused. GitHub accepts blob/tree/commit POSTs
     against a brand-new empty repo and creates the initial commit
     implicitly. Gitea refuses every blob POST with 404 until the repo
     has at least one commit, so _create_initial_commit silently failed:
     blobs returned 404, tree_items stayed empty, the tree POST then
     also 404'd ("Failed to create tree").

  Fix lives entirely in GiteaBackend — github.py is untouched so the
  proven GitHub path takes zero risk. GiteaBackend now overrides
  push_files, _create_branch_and_push, and _create_initial_commit:

  - _ref_sha() helper accepts both list and dict shapes; called at the
    two SHA extraction sites in push_files and _create_branch_and_push.
  - _create_initial_commit posts to Gitea's Contents API
    (POST /api/v1/repos/{owner}/{repo}/contents with a files array plus
    branch + new_branch) which seeds the initial commit + branch in
    one transaction and is documented to work on empty repos.

  ForgejoBackend extends GiteaBackend with no overrides and inherits
  both fixes; tests pin that.
2026-05-07 10:20:35 +02:00
maziggy c6e6c4cdd9 fix(usage-tracker): split filament weight when AMS auto-falls-back mid-print (issue 957)
When one spool ran out and the AMS transparently switched to a sibling
  slot of the same material, the usage tracker credited the originally-
  mapped spool with the full 3MF estimate AND added the fallback spool's
  remain%-delta on top — so a 78g print could record as 138g across two
  spools, leaving the empty spool's recorded weight beyond its label.

  Two interacting bugs:

  1. bambu_mqtt.py: the tray-change recorder gated on
     `state in ("RUNNING", "PAUSE")`, but P2S firmware briefly transitions
     out of RUNNING during the AMS swap (into LOADING etc.), so the
     literal-string gate missed the switch entirely and tray_change_log
     stayed empty. Re-key on the print-lifecycle flags
     (_was_running and not _completion_triggered) so any tray change
     between print start and completion is captured regardless of the
     momentary gcode_state.

  2. usage_tracker.py: the splitting branch was gated on
     `not slot_to_tray`, so the splitting code only ran for prints where
     the slicer mapping hadn't been captured — i.e. never on the actual
     fallback case (slot_to_tray is populated by every print_cmd). Drop
     the gate: when tray_change_log has > 1 entries, splitting takes
     over and per-segment per-layer gcode usage replaces the stale
     mapping. Path 2 (AMS remain%-delta) then naturally skips both trays
     because they're already in handled_trays after splitting,
     eliminating the double-credit.
2026-05-06 14:42:38 +02:00
maziggy a3e09891d1 fix(docker): copy gcode_viewer assets into the production image (issue #1218)
The embedded GCode viewer's static assets (gcode_viewer/) were never
  copied into the production Docker image, so /gcode-viewer/ returned a
  bare FastAPI 404 ({"detail":"Not Found"}) and 3D Preview broke for every
  Docker user since the viewer landed in 0.2.4b1. The Vite production
  build doesn't stage the directory either — the dev server serves it via
  a configureServer middleware that's dev-only.

  Dockerfile now copies gcode_viewer/ alongside the React build output.

  Defence in depth: main.py logs an ERROR at startup when
  _gcode_viewer_dir/index.html is missing so future packaging gaps surface
  in docker logs and the support bundle instead of as silent runtime 404s.

  The existing integration test accepted 404 unconditionally
  (assert response.status_code in (200, 404)) so CI never caught the
  missing files. Add test_gcode_viewer_index_served_when_assets_present
  which skips when the directory is intentionally absent (unit-test envs)
  but asserts 200 + non-empty HTML body when the assets do exist on disk —
  so a broken COPY fails CI loudly rather than shipping a broken image.
2026-05-06 14:23:39 +02:00
maziggy f87749d683 Updated CHANGELOG 2026-05-06 11:44:14 +02:00
maziggy c68bd53255 Updated README.md 2026-05-06 10:32:09 +02:00
maziggy a50958e426 feat(slice-modal): Bundle tier for picking presets from imported .bbscfg
Closes the loop on the bundle work: users who imported a Printer
  Preset Bundle via Settings → Slicer Bundles can now pick it in the
  SliceModal and slice through the bundle dispatch path the backend
  already supports.

  UX:
  - New "Slicer bundle" picker at the top of the modal, rendered only
    when at least one bundle is imported (GET /slicer/bundles non-empty)
  - Selecting a bundle replaces cloud/local/standard preset dropdowns
    with bundle-scoped pickers (process + per-slot filament names from
    the bundle). Printer is implicit (each .bbscfg has exactly one).
  - Submit routes through SliceRequest.bundle so the backend skips
    PresetRef resolution and asks the sidecar to materialise the JSON
    triplet from the stored bundle by name.
  - "None" leaves the modal on the original preset triplet path.

  Frontend types: SliceBundleSpec + bundle?: SliceBundleSpec on SliceRequest.
2026-05-06 09:55:14 +02:00
maziggy 7e1105dcb6 feat(slicer): bundle dispatch path for library slice route
When SliceRequest.bundle is set, the dispatch picks the per-category
  JSON triplet from a sidecar-stored .bbscfg by name instead of
  resolving cloud/local/standard PresetRefs. Mirrors the bundle-aware
  preview slice (committed earlier) so live slices match the same
  profile triplet the modal previewed against.

  Schema:
  - SliceBundleSpec: bundle_id + printer_name + process_name +
    filament_names (min-length-1 list, plate-slot order)
  - SliceRequest.bundle: optional, validator skips preset-required
    check when set so bundle-only requests validate

  Dispatch:
  - _run_slicer_with_fallback branches on request.bundle
  - Skips resolve_preset_ref, calls slice_with_bundle
  - 3MF + bundle CLI 5xx still falls back to embedded-settings slice
    (used_embedded_settings=True surfaces in the response)
  - Sidecar 404 (unknown bundle / preset name) maps to 400
2026-05-06 09:42:15 +02:00