From 2bbe7b9da2d3d41b363c3e6b38c797c5603b6513 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:52:04 +0000 Subject: [PATCH 1/8] Bump markdown-it Bumps the npm_and_yarn group with 1 update in the /frontend directory: [markdown-it](https://github.com/markdown-it/markdown-it). Updates `markdown-it` from 14.1.0 to 14.1.1 - [Changelog](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md) - [Commits](https://github.com/markdown-it/markdown-it/compare/14.1.0...14.1.1) --- updated-dependencies: - dependency-name: markdown-it dependency-version: 14.1.1 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e7d76a189..bdd89d9b9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6046,9 +6046,9 @@ } }, "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1", From 3ce98899fbdf7f78066697eddd1b41a5a81e4e6d Mon Sep 17 00:00:00 2001 From: Mori Naoyuki Date: Sun, 15 Feb 2026 09:35:14 +0900 Subject: [PATCH 2/8] The script stopped launching after moving the folder, so I fixed it. --- install/start_bambuddy.bat | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/install/start_bambuddy.bat b/install/start_bambuddy.bat index 2f5b8c02c..d0d9cbe04 100644 --- a/install/start_bambuddy.bat +++ b/install/start_bambuddy.bat @@ -20,7 +20,14 @@ REM start_bambuddy.bat reset Clean all & fresh start REM set PORT=9000 & start_bambuddy.bat Change port REM ============================================ -set "ROOT=%~dp0" +REM If launched from the "install" folder, go up one level; otherwise use current directory. +for %%I in ("%CD%") do set "CURRENT_DIR_NAME=%%~nxI" +if /I "%CURRENT_DIR_NAME%"=="install" ( + set "ROOT=%CD%\.." + cd .. +) else ( + set "ROOT=%CD%" +) if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" set "PORTABLE=%ROOT%\.portable" From bb8852791515fa15f59f37dd6ebd78837e208c76 Mon Sep 17 00:00:00 2001 From: Mori Naoyuki Date: Sun, 15 Feb 2026 10:04:23 +0900 Subject: [PATCH 3/8] Changed to use %~dp (for improved security) --- install/start_bambuddy.bat | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/install/start_bambuddy.bat b/install/start_bambuddy.bat index d0d9cbe04..55aad8986 100644 --- a/install/start_bambuddy.bat +++ b/install/start_bambuddy.bat @@ -20,15 +20,17 @@ REM start_bambuddy.bat reset Clean all & fresh start REM set PORT=9000 & start_bambuddy.bat Change port REM ============================================ -REM If launched from the "install" folder, go up one level; otherwise use current directory. -for %%I in ("%CD%") do set "CURRENT_DIR_NAME=%%~nxI" -if /I "%CURRENT_DIR_NAME%"=="install" ( - set "ROOT=%CD%\.." - cd .. +REM Resolve ROOT based on the script location (more reliable than %CD%). +set "SCRIPT_DIR=%~dp0" +if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%" +for %%I in ("%SCRIPT_DIR%") do set "SCRIPT_DIR_NAME=%%~nxI" +if /I "%SCRIPT_DIR_NAME%"=="install" ( + set "ROOT=%SCRIPT_DIR%\.." ) else ( - set "ROOT=%CD%" + set "ROOT=%SCRIPT_DIR%" ) if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" +cd /d "%ROOT%" set "PORTABLE=%ROOT%\.portable" set "PYTHON_DIR=%PORTABLE%\python" From a37dfaf7fb68cb232ff8e33aba58654c25ea98fa Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 15 Feb 2026 08:04:01 +0100 Subject: [PATCH 4/8] feat: dual external spool support, AMS slot model filtering & pre-population MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 6 + README.md | 3 +- backend/app/api/routes/cloud.py | 61 + backend/app/api/routes/inventory.py | 52 +- backend/app/api/routes/kprofiles.py | 3 + backend/app/api/routes/printers.py | 268 +- backend/app/api/routes/support.py | 2 +- backend/app/main.py | 5 +- backend/app/schemas/printer.py | 2 +- backend/app/services/bambu_mqtt.py | 208 +- backend/app/services/print_scheduler.py | 42 +- backend/app/services/printer_manager.py | 61 +- backend/app/services/spool_tag_matcher.py | 68 +- backend/app/services/spoolman_tracking.py | 17 +- backend/app/services/usage_tracker.py | 13 +- .../unit/services/test_printer_manager.py | 27 +- .../unit/services/test_spoolman_tracking.py | 4 +- .../tests/unit/test_scheduler_ams_mapping.py | 8 +- .../components/AddPrinterDiscovery.test.tsx | 1 + .../components/ConfigureAmsSlotModal.test.tsx | 77 + .../__tests__/components/PrintModal.test.tsx | 2 +- .../hooks/useFilamentMapping.test.ts | 6 +- .../src/__tests__/pages/PrintersPage.test.tsx | 1 + frontend/src/api/client.ts | 6 +- frontend/src/components/AssignSpoolModal.tsx | 8 +- .../src/components/ConfigureAmsSlotModal.tsx | 219 +- frontend/src/components/KProfilesView.tsx | 71 +- frontend/src/hooks/useFilamentMapping.ts | 36 +- frontend/src/i18n/locales/de.ts | 24 + frontend/src/i18n/locales/en.ts | 24 + frontend/src/i18n/locales/fr.ts | 24 + frontend/src/i18n/locales/it.ts | 37 + frontend/src/i18n/locales/ja.ts | 24 + frontend/src/pages/PrintersPage.tsx | 396 +- frontend/src/utils/amsHelpers.ts | 2 +- static/assets/index-BOd5pCVD.js | 8684 ----------------- static/assets/index-CS3Lw7Ok.js | 8684 +++++++++++++++++ static/assets/index-D--TAtCz.css | 1 + static/assets/index-DZOdnZuT.css | 1 - static/index.html | 4 +- 40 files changed, 10007 insertions(+), 9175 deletions(-) delete mode 100644 static/assets/index-BOd5pCVD.js create mode 100644 static/assets/index-CS3Lw7Ok.js create mode 100644 static/assets/index-D--TAtCz.css delete mode 100644 static/assets/index-DZOdnZuT.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d8a6c6a8..177709b6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ All notable changes to Bambuddy will be documented in this file. - **Notification Templates — Filament Usage Variables** ([#336](https://github.com/maziggy/bambuddy/issues/336)) — `print_complete`, `print_failed`, and `print_stopped` notification events now expose `{filament_grams}` (total grams, scaled by progress for partial prints), `{filament_details}` (per-filament breakdown with AMS slot info, e.g. "AMS-A T1 PLA: 12.4g | AMS-A T3 PETG: 2.8g"), and `{progress}` (completion percentage for failed/stopped prints). The `{filament_details}` variable includes the AMS unit and tray position for each filament used, with "Ext" shown for external spool holders. Falls back to type-only format (e.g. "PLA: 10.0g") when usage tracking data is unavailable. Webhook payloads include `filament_used`, `filament_details`, and `progress` fields. Per-slot filament data is stored in archive `extra_data` for downstream use. - **Printer Status Summary Bar — Next Available & Availability Count** ([#354](https://github.com/maziggy/bambuddy/issues/354)) — The status bar on the Printers page now shows an availability count ("X available") alongside the printing/offline counts, and a "Next available" indicator showing which printing printer will finish soonest — with printer name, mini progress bar, completion percentage, and remaining time. Useful for print farms to quickly identify the next free printer. Updates in real-time via WebSocket. Translated in all 4 locales (en, de, ja, it). - **Nozzle-Aware AMS Filament Mapping for Dual-Nozzle Printers** ([#318](https://github.com/maziggy/bambuddy/issues/318)) — On dual-nozzle printers (H2D, H2D Pro), each AMS unit is physically connected to either the left or right nozzle. Bambuddy now reads nozzle assignments from the 3MF file (`filament_nozzle_map` + `physical_extruder_map` in `project_settings.config`) and constrains filament matching to only AMS trays connected to the correct nozzle via `ams_extruder_map`. Applies to the print scheduler, reprint modal, queue modal, and multi-printer selection. Falls back gracefully to unfiltered matching when no trays exist on the target nozzle. The filament mapping UI shows L/R nozzle badges for dual-nozzle prints. Translated in all 4 locales (en, de, ja, it). +- **Dual External Spool Support for H2D** — H2-series printers with two external spool holders (Ext-L and Ext-R) are now fully supported. The external spool section renders as a grid with both slots, each showing filament type, color, fill level, and hover card details. Previously only a single external spool was displayed. Applies to the printer card, filament mapping, print scheduler, usage tracking, and inventory assignment. The `vt_tray` field is now an array across the entire stack (MQTT, API, WebSocket, frontend). +- **AMS Slot Configuration — Model Filtering & Pre-Population** — The Configure AMS Slot modal now filters filament presets by the connected printer model. Only presets matching the printer (e.g., "@BBL X1C" presets for X1C printers) and generic presets without a model suffix are shown. Local presets are filtered by their `compatible_printers` field. When re-configuring an already-configured slot, the modal pre-selects the saved preset, pre-populates the color, and auto-selects the active K-profile. The preset list auto-scrolls to the selected item. All modal strings are now fully translated in 5 locales (en, de, fr, it, ja). +- **K-Profiles View — Accurate Filament Name Resolution** — K-profile filament names are now resolved from builtin filament tables and user cloud presets (via new `/cloud/filament-id-map` endpoint) instead of showing raw IDs like "GFU99" or "P4d64437". Falls back to extracting names from the profile name field. ### Fixed - **Bulk Archive Delete Leaves Orphaned Database Records** — When bulk-deleting archives, the files were removed from disk before the database commit. If concurrent SQLite writes caused a lock timeout, the commit failed and rolled back — leaving database records pointing to deleted files (broken thumbnails, 404 errors). Fixed by deleting the database record first and only removing files after a successful commit. @@ -18,6 +21,9 @@ All notable changes to Bambuddy will be documented in this file. - **AMS Slot Configuration Overwritten on Startup** — Bambuddy was resetting AMS slot filament presets on every startup and reconnection. The `on_ams_change` callback unconditionally unlinked Bambu Lab spool assignments on each MQTT push-all response, then re-assigned them by sending `ams_filament_setting` without a `setting_id`, which cleared the printer's filament preset. Now compares spool RFID identifiers (`tray_uuid` / `tag_uid`) before unlinking — if the same spool is still in the slot, the assignment is preserved and no `ams_filament_setting` command is sent. - **Bambu Lab Spool Detection False Positives** — The `is_bambu_lab_spool()` function (backend) and `isBambuLabSpool()` (frontend) incorrectly identified third-party spools as Bambu Lab spools when they used Bambu generic filament presets (e.g., "Generic PLA"). The `tray_info_idx` field (e.g., "GFA00") identifies the filament *type*, not the spool manufacturer — third-party spools using Bambu presets also have GF-prefixed values. Removed `tray_info_idx` from detection logic; now uses only hardware RFID identifiers (`tray_uuid` and `tag_uid`) which are physically embedded in genuine Bambu Lab spools. - **FTP Disconnect Raises EOFError When Server Dies** — `BambuFTPClient.disconnect()` only caught `OSError` and `ftplib.Error`, but `quit()` raises `EOFError` when the server has closed the connection mid-session. `EOFError` is not a subclass of either, so it propagated to callers. Now caught alongside the other exception types for clean best-effort disconnect. +- **RFID Spool Data Erased by Periodic AMS Updates** — Periodic MQTT push-all responses cleared `tag_uid` and `tray_uuid` fields because they were included in the "always update" list. These fields are now preserved during updates and only cleared when a spool is physically removed (slot clearing detected by empty `tray_type`). This fixes the AMS "eye" icon disappearing for RFID spools after startup. +- **AMS Slot Configuration Overwrites RFID Spool State** — Configuring an AMS slot for an RFID-detected Bambu Lab spool sent `ams_set_filament_setting`, which replaced the firmware's RFID-managed filament config with a manual one — causing the slicer's "eye" icon to change to a "pen" icon. Now detects RFID spools and skips the filament setting command, only sending K-profile selection. +- **K-Profile Selection Corrupts Existing Profiles on X1C/P1S** — The `extrusion_cali_sel` command included a `setting_id` field that BambuStudio never sends, causing firmware to mislink calibration data. The `extrusion_cali_set` command was sent unconditionally, overwriting existing profile metadata. Now `setting_id` is removed from selection commands, and `extrusion_cali_set` is only sent when no existing profile is selected (`cali_idx < 0`). ### Improved - **SQLite WAL Mode for Database Reliability** — Database now uses Write-Ahead Logging (WAL) mode with a 5-second busy timeout, reducing "database is locked" errors under concurrent access. WAL mode allows simultaneous reads during writes, improving responsiveness for multi-printer setups. Automatically enabled on startup. diff --git a/README.md b/README.md index 5c07cd1bd..1c139a6f7 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,8 @@ Perfect for remote print farms, traveling makers, or accessing your home printer - Resizable printer cards (S/M/L/XL) - Skip objects during print - AMS slot RFID re-read -- AMS slot configuration (custom presets, K profiles, color picker) +- 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 - Print success rates & trends - Filament usage tracking diff --git a/backend/app/api/routes/cloud.py b/backend/app/api/routes/cloud.py index 898b731f2..439dafba0 100644 --- a/backend/app/api/routes/cloud.py +++ b/backend/app/api/routes/cloud.py @@ -889,6 +889,67 @@ async def get_builtin_filaments( return [{"filament_id": fid, "name": name} for fid, name in _BUILTIN_FILAMENT_NAMES.items()] +# Cache for filament_id → name mapping (resolved from cloud preset details) +_filament_id_name_cache: dict[str, str] = {} +_filament_id_name_cache_time: float = 0 + + +@router.get("/filament-id-map") +async def get_filament_id_map( + db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.FILAMENTS_READ), +): + """ + Get filament_id → name mapping for user cloud presets. + + K-profiles store a filament_id (e.g., "P4d64437") which is different from + the cloud preset setting_id (e.g., "PFUS9ac902733670a9"). This endpoint + fetches details for all custom presets and returns the mapping. + Cached for 5 minutes. + """ + import time + + global _filament_id_name_cache, _filament_id_name_cache_time + + if _filament_id_name_cache and time.time() - _filament_id_name_cache_time < FILAMENT_CACHE_TTL: + return _filament_id_name_cache + + token, _ = await get_stored_token(db) + if not token: + return _filament_id_name_cache or {} + + cloud = get_cloud_service() + cloud.set_token(token) + if not cloud.is_authenticated: + return _filament_id_name_cache or {} + + try: + data = await cloud.get_slicer_settings() + custom_presets = data.get("filament", {}).get("private", []) + + result: dict[str, str] = {} + for preset in custom_presets: + setting_id = preset.get("setting_id", "") + if not setting_id: + continue + try: + detail = await cloud.get_setting_detail(setting_id) + fid = detail.get("filament_id", "") + name = detail.get("name", "") + if fid and name: + # Strip printer/nozzle suffix: "Devil Design PLA Basic @Bambu Lab H2D 0.4 nozzle" → "Devil Design PLA Basic" + clean_name = name.split(" @")[0].strip() if " @" in name else name + result[fid] = clean_name + except Exception: + pass + + _filament_id_name_cache = result + _filament_id_name_cache_time = time.time() + return result + except Exception: + return _filament_id_name_cache or {} + + @router.get("/fields/{preset_type}") async def get_preset_fields( preset_type: Literal["filament", "print", "process", "printer"], diff --git a/backend/app/api/routes/inventory.py b/backend/app/api/routes/inventory.py index 813a57f12..22f90bd64 100644 --- a/backend/app/api/routes/inventory.py +++ b/backend/app/api/routes/inventory.py @@ -645,18 +645,32 @@ async def assign_spool( fingerprint_type = None state = printer_manager.get_status(data.printer_id) if state and state.raw_data: - ams_data = state.raw_data.get("ams", {}) - ams_list = ( - ams_data.get("ams", []) if isinstance(ams_data, dict) else ams_data if isinstance(ams_data, list) else [] - ) - tray = _find_tray_in_ams_data( - ams_list, - data.ams_id, - data.tray_id, - ) - if tray: - fingerprint_color = tray.get("tray_color", "") - fingerprint_type = tray.get("tray_type", "") + if data.ams_id == 255: + # External slot: look up tray from vt_tray by global ID + vt_tray = state.raw_data.get("vt_tray") or [] + ext_id = data.tray_id + 254 # 0→254, 1→255 + for vt in vt_tray: + if isinstance(vt, dict) and int(vt.get("id", 254)) == ext_id: + fingerprint_color = vt.get("tray_color", "") + fingerprint_type = vt.get("tray_type", "") + break + else: + ams_data = state.raw_data.get("ams", {}) + ams_list = ( + ams_data.get("ams", []) + if isinstance(ams_data, dict) + else ams_data + if isinstance(ams_data, list) + else [] + ) + tray = _find_tray_in_ams_data( + ams_list, + data.ams_id, + data.tray_id, + ) + if tray: + fingerprint_color = tray.get("tray_color", "") + fingerprint_type = tray.get("tray_type", "") # 3. Upsert assignment (replace if same printer+ams+tray) existing = await db.execute( @@ -715,16 +729,27 @@ async def assign_spool( setting_id=setting_id, ) - # b. Look up K-profile for this spool + printer + nozzle + # b. Look up K-profile for this spool + printer + nozzle + extruder nozzle_diameter = "0.4" if state and state.nozzles: nd = state.nozzles[0].nozzle_diameter if nd: nozzle_diameter = nd + # Determine slot's extruder from ams_extruder_map + slot_extruder = None + if state and state.ams_extruder_map: + if data.ams_id == 255: + # External slots: ext-L (tray 0) → extruder 1, ext-R (tray 1) → extruder 0 + slot_extruder = 1 - data.tray_id # 0→1, 1→0 + else: + slot_extruder = state.ams_extruder_map.get(str(data.ams_id)) + matching_kp = None for kp in spool.k_profiles: if kp.printer_id == data.printer_id and kp.nozzle_diameter == nozzle_diameter: + if slot_extruder is not None and kp.extruder_id is not None and kp.extruder_id != slot_extruder: + continue matching_kp = kp break @@ -735,7 +760,6 @@ async def assign_spool( cali_idx=matching_kp.cali_idx, filament_id=tray_info_idx, nozzle_diameter=nozzle_diameter, - setting_id=matching_kp.setting_id, ) configured = True diff --git a/backend/app/api/routes/kprofiles.py b/backend/app/api/routes/kprofiles.py index 4b009bb60..0fbe45e31 100644 --- a/backend/app/api/routes/kprofiles.py +++ b/backend/app/api/routes/kprofiles.py @@ -278,6 +278,9 @@ async def delete_kprofile( if not success: raise HTTPException(500, "Failed to send K-profile delete command") + # Wait for printer to process the delete before frontend refetches + await asyncio.sleep(0.5) + return {"success": True, "message": "K-profile deleted successfully"} diff --git a/backend/app/api/routes/printers.py b/backend/app/api/routes/printers.py index b2d50bca9..cde182c02 100644 --- a/backend/app/api/routes/printers.py +++ b/backend/app/api/routes/printers.py @@ -237,7 +237,7 @@ async def get_printer_status( # Parse AMS data from raw_data ams_units = [] - vt_tray = None + vt_tray = [] ams_exists = False raw_data = state.raw_data or {} @@ -319,38 +319,41 @@ async def get_printer_status( ) ) - # Virtual tray (external spool holder) - comes from vt_tray in raw_data + # Virtual tray (external spool holder) - comes from vt_tray in raw_data (list) if "vt_tray" in raw_data: - vt_data = raw_data["vt_tray"] - # Filter out empty/invalid tag values for vt_tray - vt_tag_uid = vt_data.get("tag_uid", "") - if vt_tag_uid in ("", "0000000000000000"): - vt_tag_uid = None - vt_tray_uuid = vt_data.get("tray_uuid", "") - if vt_tray_uuid in ("", "00000000000000000000000000000000"): - vt_tray_uuid = None + for vt_data in raw_data["vt_tray"]: + # Filter out empty/invalid tag values for vt_tray + vt_tag_uid = vt_data.get("tag_uid", "") + if vt_tag_uid in ("", "0000000000000000"): + vt_tag_uid = None + vt_tray_uuid = vt_data.get("tray_uuid", "") + if vt_tray_uuid in ("", "00000000000000000000000000000000"): + vt_tray_uuid = None - # Get K value: first try tray's k field, then lookup from K-profiles - vt_k_value = vt_data.get("k") - vt_cali_idx = vt_data.get("cali_idx") - if vt_k_value is None and vt_cali_idx is not None and vt_cali_idx in kprofile_map: - vt_k_value = kprofile_map[vt_cali_idx] + # Get K value: first try tray's k field, then lookup from K-profiles + vt_k_value = vt_data.get("k") + vt_cali_idx = vt_data.get("cali_idx") + if vt_k_value is None and vt_cali_idx is not None and vt_cali_idx in kprofile_map: + vt_k_value = kprofile_map[vt_cali_idx] - vt_tray = AMSTray( - id=254, # Virtual tray ID - tray_color=vt_data.get("tray_color"), - tray_type=vt_data.get("tray_type"), - tray_sub_brands=vt_data.get("tray_sub_brands"), - tray_id_name=vt_data.get("tray_id_name"), - tray_info_idx=vt_data.get("tray_info_idx"), - remain=vt_data.get("remain", 0), - k=vt_k_value, - cali_idx=vt_cali_idx, - tag_uid=vt_tag_uid, - tray_uuid=vt_tray_uuid, - nozzle_temp_min=vt_data.get("nozzle_temp_min"), - nozzle_temp_max=vt_data.get("nozzle_temp_max"), - ) + tray_id = int(vt_data.get("id", 254)) + vt_tray.append( + AMSTray( + id=tray_id, + tray_color=vt_data.get("tray_color"), + tray_type=vt_data.get("tray_type"), + tray_sub_brands=vt_data.get("tray_sub_brands"), + tray_id_name=vt_data.get("tray_id_name"), + tray_info_idx=vt_data.get("tray_info_idx"), + remain=vt_data.get("remain", 0), + k=vt_k_value, + cali_idx=vt_cali_idx, + tag_uid=vt_tag_uid, + tray_uuid=vt_tray_uuid, + nozzle_temp_min=vt_data.get("nozzle_temp_min"), + nozzle_temp_max=vt_data.get("nozzle_temp_max"), + ) + ) # Convert nozzle info to response format nozzles = [ @@ -1637,40 +1640,72 @@ async def configure_ams_slot( if not client: raise HTTPException(status_code=400, detail="Printer not connected") - # Send the filament setting command (type, color, temp) - success = client.ams_set_filament_setting( - ams_id=ams_id, - tray_id=tray_id, - tray_info_idx=tray_info_idx, - tray_type=tray_type, - tray_sub_brands=tray_sub_brands, - tray_color=tray_color, - nozzle_temp_min=nozzle_temp_min, - nozzle_temp_max=nozzle_temp_max, - setting_id=setting_id, - ) + # Detect RFID spool before sending commands + is_rfid_spool = False + state = printer_manager.get_status(printer_id) + if state and state.raw_data: + from backend.app.api.routes.inventory import _find_tray_in_ams_data + from backend.app.services.spool_tag_matcher import is_valid_tag - if not success: - raise HTTPException(status_code=500, detail="Failed to send filament configuration command") + ams_data = state.raw_data.get("ams", {}) + ams_list = ( + ams_data.get("ams", []) if isinstance(ams_data, dict) else ams_data if isinstance(ams_data, list) else [] + ) + current_tray = _find_tray_in_ams_data(ams_list, ams_id, tray_id) + if current_tray: + is_rfid_spool = is_valid_tag( + current_tray.get("tag_uid", ""), + current_tray.get("tray_uuid", ""), + ) - # Send the calibration/K-profile commands - # Use the K profile's filament_id if provided, otherwise use tray_info_idx + # Send filament setting + K-profile commands filament_id_for_kprofile = kprofile_filament_id if kprofile_filament_id else tray_info_idx + if is_rfid_spool: + # RFID spool: skip ams_set_filament_setting to preserve RFID state (eye icon). + # The firmware already has filament config from the RFID tag. + logger.info("[configure_ams_slot] RFID spool detected — skipping ams_set_filament_setting") + else: + # Non-RFID spool: send filament setting (type, color, temp) + # When a K-profile is selected, use the K-profile's filament_id as + # tray_info_idx so BambuStudio queries the right PA history table. + # But always use the PRESET's setting_id (not the K-profile's) — + # BambuStudio uses setting_id to identify the filament preset and + # overriding it with the K-profile's setting_id confuses the slicer. + effective_tray_info_idx = filament_id_for_kprofile if cali_idx >= 0 else tray_info_idx + success = client.ams_set_filament_setting( + ams_id=ams_id, + tray_id=tray_id, + tray_info_idx=effective_tray_info_idx, + tray_type=tray_type, + tray_sub_brands=tray_sub_brands, + tray_color=tray_color, + nozzle_temp_min=nozzle_temp_min, + nozzle_temp_max=nozzle_temp_max, + setting_id=setting_id, + ) + + if not success: + raise HTTPException(status_code=500, detail="Failed to send filament configuration command") + # Method 1: Select existing calibration profile by cali_idx - # IMPORTANT: Only pass setting_id if the K profile itself has one (from kprofile_setting_id) - # Do NOT use the preset's setting_id as fallback - it breaks the K profile linking in the slicer + # Do NOT include setting_id — BambuStudio never sends it in extrusion_cali_sel, + # and including it causes the firmware to mislink the profile on X1C/P1S. client.extrusion_cali_sel( ams_id=ams_id, tray_id=tray_id, cali_idx=cali_idx, filament_id=filament_id_for_kprofile, nozzle_diameter=nozzle_diameter, - setting_id=kprofile_setting_id if kprofile_setting_id else None, ) - # Method 2: Also directly set the K value if provided (for better compatibility) - if k_value > 0: + # Method 2: Only send extrusion_cali_set when NO existing profile was selected + # (cali_idx == -1). When cali_idx >= 0, extrusion_cali_sel already selected the + # correct profile. Sending extrusion_cali_set with the same cali_idx would MODIFY + # the existing profile's metadata (extruder_id, nozzle_id, name, setting_id), + # corrupting it — e.g., overwriting a High Flow extruder 1 profile with + # hardcoded extruder_id=0 and nozzle_id=HS00. + if k_value > 0 and cali_idx < 0: # Calculate global tray ID for extrusion_cali_set if ams_id <= 3: global_tray_id = ams_id * 4 + tray_id @@ -1682,11 +1717,12 @@ async def configure_ams_slot( client.extrusion_cali_set( tray_id=global_tray_id, k_value=k_value, - n_coef=0.0, nozzle_diameter=nozzle_diameter, - bed_temp=60, nozzle_temp=nozzle_temp_max, - max_volumetric_speed=20.0, + filament_id=filament_id_for_kprofile, + setting_id=kprofile_setting_id or "", + name=tray_sub_brands or "", + cali_idx=cali_idx, ) # Request fresh status push from printer so frontend gets updated data via WebSocket @@ -2109,9 +2145,133 @@ async def refresh_ams_slot( if not success: raise HTTPException(400, message) + # Apply PA profile after delay (RFID re-read takes a few seconds) + asyncio.create_task(_apply_pa_after_refresh(printer_id, ams_id, slot_id)) + return {"success": True, "message": message} +async def _apply_pa_after_refresh(printer_id: int, ams_id: int, slot_id: int): + """Apply PA profile after RFID re-read completes. + + Waits for the printer to finish processing the RFID data, then selects + the K-profile via extrusion_cali_sel. Does NOT re-send ams_set_filament_setting + because that would overwrite the RFID-provided filament data. + """ + await asyncio.sleep(5) + try: + from backend.app.api.routes.inventory import _find_tray_in_ams_data + from backend.app.core.database import async_session + from backend.app.models.spool import Spool + from backend.app.models.spool_assignment import SpoolAssignment as SA + from backend.app.services.spool_tag_matcher import is_bambu_tag + + client = printer_manager.get_client(printer_id) + if not client: + return + + state = printer_manager.get_status(printer_id) + if not state or not state.raw_data: + return + + # Find current tray data (should have RFID data by now) + ams_data = state.raw_data.get("ams", {}) + ams_list = ( + ams_data.get("ams", []) if isinstance(ams_data, dict) else ams_data if isinstance(ams_data, list) else [] + ) + tray = _find_tray_in_ams_data(ams_list, ams_id, slot_id) + if not tray or not tray.get("tray_type"): + logger.debug("PA re-apply: no tray data for AMS%d-T%d", ams_id, slot_id) + return + + tag_uid = tray.get("tag_uid", "") + tray_uuid = tray.get("tray_uuid", "") + tray_info_idx = tray.get("tray_info_idx", "") + if not is_bambu_tag(tag_uid, tray_uuid, tray_info_idx): + return + + async with async_session() as db: + from sqlalchemy import select as sa_select + from sqlalchemy.orm import selectinload + + result = await db.execute( + sa_select(SA) + .options(selectinload(SA.spool).selectinload(Spool.k_profiles)) + .where(SA.printer_id == printer_id, SA.ams_id == ams_id, SA.tray_id == slot_id) + ) + assignment = result.scalar_one_or_none() + if not assignment or not assignment.spool or not assignment.spool.k_profiles: + return + + spool = assignment.spool + nozzle_diameter = "0.4" + if state.nozzles: + nd = state.nozzles[0].nozzle_diameter + if nd: + nozzle_diameter = nd + + # Determine slot's extruder from ams_extruder_map + slot_extruder = None + if state.ams_extruder_map: + if ams_id == 255: + # External slots: ext-L (tray 0) → extruder 1, ext-R (tray 1) → extruder 0 + slot_extruder = 1 - slot_id # 0→1, 1→0 + else: + slot_extruder = state.ams_extruder_map.get(str(ams_id)) + + matching_kp = None + for kp in spool.k_profiles: + if kp.printer_id == printer_id and kp.nozzle_diameter == nozzle_diameter: + if slot_extruder is not None and kp.extruder_id is not None and kp.extruder_id != slot_extruder: + continue + matching_kp = kp + break + + if not matching_kp or matching_kp.cali_idx is None: + return + + # The filament_id in extrusion_cali_sel must match the filament preset + # under which the K-profile was calibrated. Use spool.slicer_filament + # (the preset assigned in inventory), falling back to tray's RFID value. + kp_filament_id = spool.slicer_filament or tray_info_idx + + logger.info( + "PA re-apply AMS%d-T%d: cali_idx=%d, filament_id=%s", + ams_id, + slot_id, + matching_kp.cali_idx, + kp_filament_id, + ) + + # 1. Select K-profile + # NOTE: Do NOT send ams_set_filament_setting here — it tells the firmware + # "this is a manual config" which destroys the RFID-detected spool state + # (changes eye icon to pen icon in slicer). + client.extrusion_cali_sel( + ams_id=ams_id, + tray_id=slot_id, + cali_idx=matching_kp.cali_idx, + filament_id=kp_filament_id, + nozzle_diameter=nozzle_diameter, + ) + + # NOTE: Do NOT send extrusion_cali_set here. extrusion_cali_sel already + # selected the correct profile by cali_idx. Sending extrusion_cali_set with + # the same cali_idx would MODIFY the existing profile's metadata (extruder_id, + # nozzle_id, name), corrupting it. + + logger.info( + "Applied PA profile cali_idx=%d k=%.3f to printer %d AMS%d-T%d", + matching_kp.cali_idx, + matching_kp.k_value or 0, + printer_id, + ams_id, + slot_id, + ) + except Exception as e: + logger.warning("Failed to apply PA profile after RFID re-read: %s", e) + + @router.get("/{printer_id}/runtime-debug") async def get_runtime_debug( printer_id: int, diff --git a/backend/app/api/routes/support.py b/backend/app/api/routes/support.py index 48a73141a..c86dd4378 100644 --- a/backend/app/api/routes/support.py +++ b/backend/app/api/routes/support.py @@ -473,7 +473,7 @@ async def _collect_support_info() -> dict: for unit in ams_units: trays = unit.get("tray", []) ams_tray_count += len([t for t in trays if t.get("tray_type")]) - has_vt_tray = state.raw_data.get("vt_tray") is not None + has_vt_tray = bool(state.raw_data.get("vt_tray")) info["printers"].append( { diff --git a/backend/app/main.py b/backend/app/main.py index 751b9a816..94400e70c 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -335,12 +335,14 @@ async def on_printer_status_change(printer_id: int, state: PrinterState): bed_target = round(temps.get("bed_target", 0)) nozzle_target = round(temps.get("nozzle_target", 0)) + # Include tray_now and vt_tray hash so external spool changes trigger broadcasts + vt_tray_key = hash(str(state.raw_data.get("vt_tray", []))) if state.raw_data else 0 status_key = ( f"{state.connected}:{state.state}:{state.progress}:{state.layer_num}:" f"{nozzle_temp}:{bed_temp}:{nozzle_2_temp}:{chamber_temp}:" f"{state.stg_cur}:{bed_target}:{nozzle_target}:" f"{state.cooling_fan_speed}:{state.big_fan1_speed}:{state.big_fan2_speed}:" - f"{state.chamber_light}:{state.active_extruder}" + f"{state.chamber_light}:{state.active_extruder}:{state.tray_now}:{vt_tray_key}" ) # MQTT relay - publish status (before dedup check - always publish to MQTT) @@ -707,6 +709,7 @@ async def on_ams_change(printer_id: int, ams_data: list): spool, printer_manager, db, + tray_info_idx=tray_info_idx, ) await db.commit() await ws_manager.broadcast( diff --git a/backend/app/schemas/printer.py b/backend/app/schemas/printer.py index 5c0864837..34e4cedc4 100644 --- a/backend/app/schemas/printer.py +++ b/backend/app/schemas/printer.py @@ -199,7 +199,7 @@ class PrinterStatus(BaseModel): hms_errors: list[HMSErrorResponse] = [] ams: list[AMSUnit] = [] ams_exists: bool = False - vt_tray: AMSTray | None = None # Virtual tray / external spool + vt_tray: list[AMSTray] = [] # Virtual tray / external spool(s) sdcard: bool = False # SD card inserted store_to_sdcard: bool = False # Store sent files on SD card timelapse: bool = False # Timelapse recording active diff --git a/backend/app/services/bambu_mqtt.py b/backend/app/services/bambu_mqtt.py index b8e5e774d..519e0766e 100644 --- a/backend/app/services/bambu_mqtt.py +++ b/backend/app/services/bambu_mqtt.py @@ -128,7 +128,7 @@ class PrinterState: chamber_light: bool = False # Active extruder for dual nozzle (0=right, 1=left) - from device.extruder.info[X].hnow active_extruder: int = 0 - # Currently loaded tray (global ID): 254 = external spool, 255 = no filament + # Currently loaded tray (global ID): 254/255 = external spools, 255 = no filament on legacy printers tray_now: int = 255 # Pending load target - used to track what tray we're loading for H2D disambiguation pending_tray_target: int | None = None @@ -451,14 +451,33 @@ class BambuMQTTClient: except Exception as e: logger.error("[%s] Error handling AMS data from print: %s", self.serial_number, e) + # Handle vir_slot (H2-series external spool data) — list of external trays + # Process vir_slot FIRST so it takes priority over vt_tray + if "vir_slot" in print_data: + vir_slot = print_data["vir_slot"] + if isinstance(vir_slot, list) and vir_slot: + # Fix: single-nozzle printers (X1C, P1S, A1) report their single + # external slot with id=255 in vir_slot, but tray_now=254 when active. + # Remap id=255→254 for single-slot printers so active detection works. + # Dual-nozzle (H2D) has 2 slots: id=254 (Ext-L) and id=255 (Ext-R). + if len(vir_slot) == 1 and str(vir_slot[0].get("id", "")) == "255": + vir_slot[0]["id"] = "254" + self.state.raw_data["vt_tray"] = vir_slot + # Handle vt_tray (virtual tray / external spool) data - if "vt_tray" in print_data: + # Only use vt_tray if vir_slot is NOT in this message AND we don't already + # have vir_slot data (H2-series sends vt_tray as a single active spool dict + # which would overwrite the correct multi-slot vir_slot data) + if "vt_tray" in print_data and "vir_slot" not in print_data: vt_tray = print_data["vt_tray"] - self.state.raw_data["vt_tray"] = vt_tray - # Log vt_tray to investigate per-extruder data for H2D - if not hasattr(self, "_vt_tray_logged") or not self._vt_tray_logged: - logger.info("[%s] vt_tray data: %s", self.serial_number, vt_tray) - self._vt_tray_logged = True + existing = self.state.raw_data.get("vt_tray") + # Don't let a single-spool vt_tray dict overwrite multi-slot vir_slot data + if isinstance(vt_tray, dict) and isinstance(existing, list) and len(existing) > 1: + pass # Keep the vir_slot data + else: + if isinstance(vt_tray, dict): + vt_tray = [vt_tray] + self.state.raw_data["vt_tray"] = vt_tray # Parse ams_status directly from print data (NOT from print.ams) # ams_status is a combined value: lower 8 bits = sub status, bits 8-15 = main status @@ -486,7 +505,10 @@ class BambuMQTTClient: # Check for K-profile response (extrusion_cali) if "command" in print_data: - logger.debug("[%s] Received command response: %s", self.serial_number, print_data.get("command")) + cmd = print_data.get("command") + logger.debug("[%s] Received command response: %s", self.serial_number, cmd) + if cmd in ("extrusion_cali_sel", "extrusion_cali_set", "extrusion_cali_del", "ams_filament_setting"): + logger.info("[%s] %s response: %s", self.serial_number, cmd, print_data) if "command" in print_data and print_data.get("command") == "extrusion_cali_get": self._handle_kprofile_response(print_data) @@ -941,13 +963,20 @@ class BambuMQTTClient: if tray_id is not None and tray_id in existing_trays: # Merge: start with existing, update with new non-empty values merged_tray = existing_trays[tray_id].copy() + # Detect slot-clearing updates (spool removal): + # When tray_type is explicitly empty, clear everything + # including RFID data (tag_uid/tray_uuid). + slot_clearing = new_tray.get("tray_type") == "" for key, value in new_tray.items(): # Fields that should always be updated (even with empty/zero values): # - remain, k, id, cali_idx: status indicators where 0 is valid - # - tray_type, tray_sub_brands, tag_uid, tray_uuid, tray_info_idx, - # tray_color, tray_id_name: slot content indicators that must - # be cleared when a spool is removed (fixes #147 - old AMS - # empty slot) + # - tray_type, tray_sub_brands, tray_info_idx, tray_color, + # tray_id_name: slot content indicators that must be cleared + # when a spool is removed (fixes #147 - old AMS empty slot) + # NOTE: tag_uid and tray_uuid are NOT in always_update_fields. + # They are only cleared during spool removal (slot_clearing=True). + # Periodic AMS updates often include empty RFID fields which + # would overwrite valid data from the initial pushall. always_update_fields = ( "remain", "k", @@ -955,17 +984,20 @@ class BambuMQTTClient: "cali_idx", "tray_type", "tray_sub_brands", - "tag_uid", - "tray_uuid", "tray_info_idx", "tray_color", "tray_id_name", ) - if key in always_update_fields or value not in ( - None, - "", - "0000000000000000", - "00000000000000000000000000000000", + if ( + key in always_update_fields + or slot_clearing + or value + not in ( + None, + "", + "0000000000000000", + "00000000000000000000000000000000", + ) ): merged_tray[key] = value merged_trays.append(merged_tray) @@ -2424,7 +2456,7 @@ class BambuMQTTClient: def _handle_kprofile_response(self, data: dict): """Handle K-profile response from printer.""" response_nozzle = data.get("nozzle_diameter") - _response_seq_id = data.get("sequence_id", "?") + response_seq_id = data.get("sequence_id", "?") filaments = data.get("filaments", []) expected_nozzle = getattr(self, "_expected_kprofile_nozzle", None) has_pending_request = self._pending_kprofile_response is not None @@ -2432,7 +2464,8 @@ class BambuMQTTClient: # Log all incoming responses when we have a pending request (for debugging) if has_pending_request: logger.info( - f"[{self.serial_number}] K-profile response: nozzle={response_nozzle}, {len(filaments)} profiles, expected={expected_nozzle}" + f"[{self.serial_number}] K-profile response: nozzle={response_nozzle}, " + f"seq_id={response_seq_id}, {len(filaments)} profiles, expected={expected_nozzle}" ) # If we have a pending request, only accept responses with matching nozzle_diameter @@ -3305,6 +3338,7 @@ class BambuMQTTClient: command = {"print": {"command": "ams_get_rfid", "ams_id": ams_id, "slot_id": tray_id, "sequence_id": "0"}} self._client.publish(self.topic_publish, json.dumps(command), qos=1) logger.info("[%s] Triggering RFID re-read: AMS %s, slot %s", self.serial_number, ams_id, tray_id) + return True, f"Refreshing AMS {ams_id} tray {tray_id}" def ams_set_filament_setting( @@ -3341,18 +3375,33 @@ class BambuMQTTClient: logger.warning("[%s] Cannot set AMS filament setting: not connected", self.serial_number) return False - # Calculate slot_id based on AMS type - if ams_id <= 3: + # Calculate mqtt IDs based on AMS type + if ams_id == 255: + vt_tray = self.state.raw_data.get("vt_tray", []) if self.state.raw_data else [] + if len(vt_tray) > 1: + # Dual external slots (H2D): each ext slot is its own virtual AMS unit + # (254=ext-L / slot 0, 255=ext-R / slot 1) + mqtt_ams_id = 254 + tray_id + else: + # Single external slot (X1C, P1S, A1): always ams_id=255 + mqtt_ams_id = 255 + mqtt_tray_id = 0 + slot_id = 0 + elif ams_id <= 3: + mqtt_ams_id = ams_id + mqtt_tray_id = tray_id slot_id = tray_id else: - # AMS-HT or external: slot_id = 0 + # AMS-HT: single tray per unit + mqtt_ams_id = ams_id + mqtt_tray_id = tray_id slot_id = 0 command = { "print": { "command": "ams_filament_setting", - "ams_id": ams_id, - "tray_id": tray_id, + "ams_id": mqtt_ams_id, + "tray_id": mqtt_tray_id, "slot_id": slot_id, "tray_info_idx": tray_info_idx, "tray_type": tray_type, @@ -3390,17 +3439,32 @@ class BambuMQTTClient: logger.warning("[%s] Cannot reset AMS slot: not connected", self.serial_number) return False - # Calculate slot_id based on AMS type - if ams_id <= 3: + # Calculate mqtt IDs based on AMS type + if ams_id == 255: + vt_tray = self.state.raw_data.get("vt_tray", []) if self.state.raw_data else [] + if len(vt_tray) > 1: + # Dual external slots (H2D): each ext slot is its own virtual AMS unit + mqtt_ams_id = 254 + tray_id + else: + # Single external slot (X1C, P1S, A1): always ams_id=255 + mqtt_ams_id = 255 + mqtt_tray_id = 0 + slot_id = 0 + elif ams_id <= 3: + mqtt_ams_id = ams_id + mqtt_tray_id = tray_id slot_id = tray_id else: + # AMS-HT: single tray per unit + mqtt_ams_id = ams_id + mqtt_tray_id = tray_id slot_id = 0 command = { "print": { "command": "ams_filament_setting", - "ams_id": ams_id, - "tray_id": tray_id, + "ams_id": mqtt_ams_id, + "tray_id": mqtt_tray_id, "slot_id": slot_id, "tray_info_idx": "", "tray_type": "", @@ -3425,20 +3489,21 @@ class BambuMQTTClient: cali_idx: int, filament_id: str, nozzle_diameter: str = "0.4", - setting_id: str | None = None, ) -> bool: """Set calibration profile (K value) for an AMS slot. This command selects a K profile from the printer's calibration list. Use cali_idx=-1 to use the default K value (0.020). + Note: Do NOT send setting_id in this command — BambuStudio never includes + it, and adding it causes the firmware to mislink the profile on X1C/P1S. + Args: ams_id: AMS unit ID (0-3 for regular AMS, 128-135 for HT AMS) tray_id: Tray ID within the AMS (0-3) cali_idx: Calibration profile index (-1 for default) filament_id: Filament preset ID (same as tray_info_idx) nozzle_diameter: Nozzle diameter string (e.g., "0.4") - setting_id: Full setting ID with version (e.g., "GFSL05_07") - optional Returns: True if command was sent, False otherwise @@ -3447,13 +3512,34 @@ class BambuMQTTClient: logger.warning("[%s] Cannot set calibration: not connected", self.serial_number) return False - # Calculate slot_id based on AMS type - # tray_id in the command should be the local tray index (0-3) - if ams_id <= 3: + # Calculate mqtt IDs based on AMS type. + # IMPORTANT: extrusion_cali_sel uses GLOBAL tray_id (unlike ams_filament_setting + # which uses LOCAL). BambuStudio confirms: tray_id = ams_id * 4 + slot. + if ams_id == 255: + # External spool: extrusion_cali_sel uses GLOBAL tray_id (unlike + # ams_filament_setting which uses LOCAL tray_id=0). + vt_tray = self.state.raw_data.get("vt_tray", []) if self.state.raw_data else [] + if len(vt_tray) > 1: + # Dual external slots (H2D): each ext slot is its own virtual AMS unit + # Confirmed from BambuStudio logs: ext-R sends ams_id=255, tray_id=255 + mqtt_ams_id = 254 + tray_id + mqtt_tray_id = 254 + tray_id + else: + # Single external slot (X1C, P1S, A1): global tray_id=254 + mqtt_ams_id = 254 + mqtt_tray_id = 254 + slot_id = 0 + elif ams_id <= 3: + mqtt_ams_id = ams_id + mqtt_tray_id = ams_id * 4 + tray_id slot_id = tray_id elif ams_id >= 128 and ams_id <= 135: + mqtt_ams_id = ams_id + mqtt_tray_id = tray_id slot_id = 0 else: + mqtt_ams_id = ams_id + mqtt_tray_id = tray_id slot_id = 0 command = { @@ -3462,20 +3548,16 @@ class BambuMQTTClient: "cali_idx": cali_idx, "filament_id": filament_id, "nozzle_diameter": nozzle_diameter, - "ams_id": ams_id, - "tray_id": tray_id, # Local tray index (0-3), not global + "ams_id": mqtt_ams_id, + "tray_id": mqtt_tray_id, "slot_id": slot_id, "sequence_id": "0", } } - # Include setting_id if provided (helps slicer show correct K profile) - if setting_id: - command["print"]["setting_id"] = setting_id - command_json = json.dumps(command) logger.info( - f"[{self.serial_number}] Publishing extrusion_cali_sel: AMS {ams_id}, tray {tray_id}, cali_idx={cali_idx}, setting_id={setting_id}" + f"[{self.serial_number}] Publishing extrusion_cali_sel: AMS {ams_id}, tray {tray_id}, cali_idx={cali_idx}" ) logger.debug("[%s] extrusion_cali_sel command: %s", self.serial_number, command_json) self._client.publish(self.topic_publish, command_json, qos=1) @@ -3485,25 +3567,26 @@ class BambuMQTTClient: self, tray_id: int, k_value: float, - n_coef: float = 0.0, nozzle_diameter: str = "0.4", - bed_temp: int = 60, nozzle_temp: int = 220, - max_volumetric_speed: float = 20.0, + filament_id: str = "", + setting_id: str = "", + name: str = "", + cali_idx: int = -1, ) -> bool: """Directly set K value (pressure advance) for a tray. - This command sets the K value directly without selecting from stored profiles. - Use this when you want to apply a specific K value to a tray. + Uses the filaments array format required by current firmware. Args: tray_id: Global tray ID (ams_id * 4 + slot) k_value: Pressure advance K value (e.g., 0.020) - n_coef: N coefficient (usually 0.0 for manual, 1.4 for auto-calibration) nozzle_diameter: Nozzle diameter string (e.g., "0.4") - bed_temp: Bed temperature for calibration reference nozzle_temp: Nozzle temperature for calibration reference - max_volumetric_speed: Max volumetric speed for calibration reference + filament_id: Filament preset ID (e.g., "GFA02") + setting_id: Setting ID (e.g., "GFSA02_07") + name: Profile display name + cali_idx: Calibration index (-1 for new) Returns: True if command was sent, False otherwise @@ -3512,17 +3595,28 @@ class BambuMQTTClient: logger.warning("[%s] Cannot set K value: not connected", self.serial_number) return False + nozzle_id = f"HS00-{nozzle_diameter}" + + filament_entry = { + "ams_id": 0, + "cali_idx": cali_idx, + "extruder_id": 0, + "filament_id": filament_id, + "k_value": f"{k_value:.6f}", + "n_coef": "1.400000", + "name": name, + "nozzle_diameter": nozzle_diameter, + "nozzle_id": nozzle_id, + "setting_id": setting_id, + "tray_id": tray_id, + } + command = { "print": { "command": "extrusion_cali_set", - "tray_id": tray_id, - "k_value": k_value, - "n_coef": n_coef, + "filaments": [filament_entry], "nozzle_diameter": nozzle_diameter, - "bed_temp": bed_temp, - "nozzle_temp": nozzle_temp, - "max_volumetric_speed": max_volumetric_speed, - "sequence_id": "0", + "sequence_id": str(self._sequence_id), } } diff --git a/backend/app/services/print_scheduler.py b/backend/app/services/print_scheduler.py index 7c28ebdcb..3a1e3aec5 100644 --- a/backend/app/services/print_scheduler.py +++ b/backend/app/services/print_scheduler.py @@ -333,10 +333,9 @@ class PrintScheduler: if tray_type: loaded_types.add(tray_type.upper()) - # Check external spool (virtual tray, stored in raw_data["vt_tray"]) - vt_tray = status.raw_data.get("vt_tray") - if vt_tray: - vt_type = vt_tray.get("tray_type") + # Check external spool(s) (virtual tray, stored in raw_data["vt_tray"] as list) + for vt in status.raw_data.get("vt_tray") or []: + vt_type = vt.get("tray_type") if vt_type: loaded_types.add(vt_type.upper()) @@ -538,23 +537,24 @@ class PrintScheduler: } ) - # Check external spool (vt_tray) - vt_tray = status.raw_data.get("vt_tray") - if vt_tray and vt_tray.get("tray_type"): - color = self._normalize_color(vt_tray.get("tray_color", "")) - filaments.append( - { - "type": vt_tray["tray_type"], - "color": color, - "tray_info_idx": vt_tray.get("tray_info_idx", ""), - "ams_id": -1, - "tray_id": 0, - "is_ht": False, - "is_external": True, - "global_tray_id": 254, - "extruder_id": 0 if ams_extruder_map else None, - } - ) + # Check external spool(s) (vt_tray is a list) + for idx, vt in enumerate(status.raw_data.get("vt_tray") or []): + if vt.get("tray_type"): + color = self._normalize_color(vt.get("tray_color", "")) + tray_id = int(vt.get("id", 254)) + filaments.append( + { + "type": vt["tray_type"], + "color": color, + "tray_info_idx": vt.get("tray_info_idx", ""), + "ams_id": -1, + "tray_id": idx, + "is_ht": False, + "is_external": True, + "global_tray_id": tray_id, + "extruder_id": (tray_id - 254) if ams_extruder_map else None, + } + ) return filaments diff --git a/backend/app/services/printer_manager.py b/backend/app/services/printer_manager.py index 9c3a5d00f..809cdc757 100644 --- a/backend/app/services/printer_manager.py +++ b/backend/app/services/printer_manager.py @@ -502,7 +502,7 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None, mo """ # Parse AMS data from raw_data ams_units = [] - vt_tray = None + vt_tray = [] raw_data = state.raw_data or {} # Build K-profile lookup map: cali_idx -> k_value @@ -578,37 +578,40 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None, mo } ) - # Parse virtual tray (external spool) + # Parse virtual tray (external spool) — now a list if "vt_tray" in raw_data: - vt_data = raw_data["vt_tray"] - vt_tag_uid = vt_data.get("tag_uid") - if vt_tag_uid in ("", "0000000000000000"): - vt_tag_uid = None - vt_tray_uuid = vt_data.get("tray_uuid") - if vt_tray_uuid in ("", "00000000000000000000000000000000"): - vt_tray_uuid = None + for vt_data in raw_data["vt_tray"]: + vt_tag_uid = vt_data.get("tag_uid") + if vt_tag_uid in ("", "0000000000000000"): + vt_tag_uid = None + vt_tray_uuid = vt_data.get("tray_uuid") + if vt_tray_uuid in ("", "00000000000000000000000000000000"): + vt_tray_uuid = None - # Get K value for vt_tray - vt_k_value = vt_data.get("k") - vt_cali_idx = vt_data.get("cali_idx") - if vt_k_value is None and vt_cali_idx is not None and vt_cali_idx in kprofile_map: - vt_k_value = kprofile_map[vt_cali_idx] + # Get K value for vt_tray + vt_k_value = vt_data.get("k") + vt_cali_idx = vt_data.get("cali_idx") + if vt_k_value is None and vt_cali_idx is not None and vt_cali_idx in kprofile_map: + vt_k_value = kprofile_map[vt_cali_idx] - vt_tray = { - "id": 254, - "tray_color": vt_data.get("tray_color"), - "tray_type": vt_data.get("tray_type"), - "tray_sub_brands": vt_data.get("tray_sub_brands"), - "tray_id_name": vt_data.get("tray_id_name"), - "tray_info_idx": vt_data.get("tray_info_idx"), - "remain": vt_data.get("remain", 0), - "k": vt_k_value, - "cali_idx": vt_cali_idx, - "tag_uid": vt_tag_uid, - "tray_uuid": vt_tray_uuid, - "nozzle_temp_min": vt_data.get("nozzle_temp_min"), - "nozzle_temp_max": vt_data.get("nozzle_temp_max"), - } + tray_id = int(vt_data.get("id", 254)) + vt_tray.append( + { + "id": tray_id, + "tray_color": vt_data.get("tray_color"), + "tray_type": vt_data.get("tray_type"), + "tray_sub_brands": vt_data.get("tray_sub_brands"), + "tray_id_name": vt_data.get("tray_id_name"), + "tray_info_idx": vt_data.get("tray_info_idx"), + "remain": vt_data.get("remain", 0), + "k": vt_k_value, + "cali_idx": vt_cali_idx, + "tag_uid": vt_tag_uid, + "tray_uuid": vt_tray_uuid, + "nozzle_temp_min": vt_data.get("nozzle_temp_min"), + "nozzle_temp_max": vt_data.get("nozzle_temp_max"), + } + ) # Get ams_extruder_map from raw_data (populated by MQTT handler from AMS info field) ams_extruder_map = raw_data.get("ams_extruder_map", {}) diff --git a/backend/app/services/spool_tag_matcher.py b/backend/app/services/spool_tag_matcher.py index e8ba3013c..812f1a4d2 100644 --- a/backend/app/services/spool_tag_matcher.py +++ b/backend/app/services/spool_tag_matcher.py @@ -196,16 +196,19 @@ async def auto_assign_spool( spool: Spool, printer_manager, db: AsyncSession, + tray_info_idx: str = "", ) -> SpoolAssignment: """Create a SpoolAssignment and auto-configure the AMS slot via MQTT. - Reuses the same MQTT configuration logic as the manual assign endpoint. + For BL spools (RFID-detected), only K-profile commands are sent. + ams_set_filament_setting is NOT sent because the firmware already has + filament configuration from the RFID tag, and sending it would destroy + the RFID-detected state (eye → pen icon in BambuStudio). """ - from backend.app.api.routes.inventory import MATERIAL_TEMPS - # Get current tray state for fingerprint fingerprint_color = None fingerprint_type = None + tray = None state = printer_manager.get_status(printer_id) if state and state.raw_data: from backend.app.api.routes.inventory import _find_tray_in_ams_data @@ -246,34 +249,14 @@ async def auto_assign_spool( db.add(assignment) await db.flush() - # Auto-configure AMS slot via MQTT + # Apply K-profile via MQTT (if available) + # NOTE: Do NOT send ams_set_filament_setting here. This function is only + # called for BL spools (RFID-detected). The firmware already has the filament + # configuration from the RFID tag. Sending ams_set_filament_setting would + # destroy the RFID-detected state (eye → pen icon in BambuStudio/OrcaSlicer). try: client = printer_manager.get_client(printer_id) if client: - tray_type = spool.material - tray_sub_brands = f"{spool.material} {spool.subtype}" if spool.subtype else spool.material - tray_color = spool.rgba or "FFFFFFFF" - tray_info_idx = spool.slicer_filament or "" - setting_id = "" - - temp_min, temp_max = MATERIAL_TEMPS.get(spool.material.upper(), (200, 240)) - if spool.nozzle_temp_min is not None: - temp_min = spool.nozzle_temp_min - if spool.nozzle_temp_max is not None: - temp_max = spool.nozzle_temp_max - - client.ams_set_filament_setting( - ams_id=ams_id, - tray_id=tray_id, - tray_info_idx=tray_info_idx, - tray_type=tray_type, - tray_sub_brands=tray_sub_brands, - tray_color=tray_color, - nozzle_temp_min=temp_min, - nozzle_temp_max=temp_max, - setting_id=setting_id, - ) - # Apply K-profile if available nozzle_diameter = "0.4" if state and state.nozzles: @@ -288,23 +271,40 @@ async def auto_assign_spool( break if matching_kp and matching_kp.cali_idx is not None: + # The filament_id in extrusion_cali_sel must match the filament preset + # under which the K-profile was calibrated. Use spool.slicer_filament + # (the preset assigned in inventory), falling back to tray's RFID value. + cali_filament_id = spool.slicer_filament or tray_info_idx or "" client.extrusion_cali_sel( ams_id=ams_id, tray_id=tray_id, cali_idx=matching_kp.cali_idx, - filament_id=tray_info_idx, + filament_id=cali_filament_id, nozzle_diameter=nozzle_diameter, - setting_id=matching_kp.setting_id, + ) + + # NOTE: Do NOT send extrusion_cali_set here. extrusion_cali_sel already + # selected the correct profile by cali_idx. Sending extrusion_cali_set + # with the same cali_idx would MODIFY the existing profile's metadata + # (extruder_id, nozzle_id, name), corrupting it. + + logger.info( + "Applied K-profile cali_idx=%d for spool %d on printer %d AMS%d-T%d", + matching_kp.cali_idx, + spool.id, + printer_id, + ams_id, + tray_id, ) logger.info( - "Auto-configured AMS slot ams=%d tray=%d for spool %d on printer %d (RFID match)", - ams_id, - tray_id, + "Auto-assigned spool %d to printer %d AMS%d-T%d (RFID match)", spool.id, printer_id, + ams_id, + tray_id, ) except Exception as e: - logger.warning("MQTT auto-configure failed for spool %d (RFID match): %s", spool.id, e) + logger.warning("K-profile apply failed for spool %d (RFID match): %s", spool.id, e) return assignment diff --git a/backend/app/services/spoolman_tracking.py b/backend/app/services/spoolman_tracking.py index b02d24a1a..9128dfed6 100644 --- a/backend/app/services/spoolman_tracking.py +++ b/backend/app/services/spoolman_tracking.py @@ -66,14 +66,15 @@ def build_ams_tray_lookup(raw_data: dict) -> dict[int, dict]: "tray_type": tray.get("tray_type", ""), } - # External spool (global_tray_id = 254) - vt_tray = raw_data.get("vt_tray") - if vt_tray and vt_tray.get("tray_type"): - lookup[254] = { - "tray_uuid": vt_tray.get("tray_uuid", ""), - "tag_uid": vt_tray.get("tag_uid", ""), - "tray_type": vt_tray.get("tray_type", ""), - } + # External spool(s) (vt_tray is a list, global_tray_id from each entry's "id") + for vt in raw_data.get("vt_tray") or []: + if vt.get("tray_type"): + tray_id = int(vt.get("id", 254)) + lookup[tray_id] = { + "tray_uuid": vt.get("tray_uuid", ""), + "tag_uid": vt.get("tag_uid", ""), + "tray_type": vt.get("tray_type", ""), + } return lookup diff --git a/backend/app/services/usage_tracker.py b/backend/app/services/usage_tracker.py index 8c24fe9f1..67cfd7e31 100644 --- a/backend/app/services/usage_tracker.py +++ b/backend/app/services/usage_tracker.py @@ -263,8 +263,13 @@ async def _track_from_3mf( tray_now_override: int | None = None if not slot_to_tray and len(nonzero_slots) == 1: state = printer_manager.get_status(printer_id) - if state and state.tray_now < 255: + if state and 0 <= state.tray_now <= 254: tray_now_override = state.tray_now + elif state and state.tray_now == 255: + # 255 = "no filament" on legacy printers, but valid 2nd external spool on H2-series + vt_tray = state.raw_data.get("vt_tray") or [] + if any(int(vt.get("id", 0)) == 255 for vt in vt_tray if isinstance(vt, dict)): + tray_now_override = state.tray_now # Scale factor for partial prints (failed/aborted) if status == "completed": @@ -322,7 +327,11 @@ async def _track_from_3mf( if isinstance(mapped, int) and mapped >= 0: global_tray_id = mapped - if global_tray_id >= 128: + if global_tray_id >= 254: + # External spool: ams_id=255 (sentinel), tray_id=slot index (0 or 1) + ams_id = 255 + tray_id = global_tray_id - 254 + elif global_tray_id >= 128: ams_id = global_tray_id tray_id = 0 else: diff --git a/backend/tests/unit/services/test_printer_manager.py b/backend/tests/unit/services/test_printer_manager.py index 51e434207..e3d401419 100644 --- a/backend/tests/unit/services/test_printer_manager.py +++ b/backend/tests/unit/services/test_printer_manager.py @@ -734,23 +734,26 @@ class TestPrinterStateToDict: assert result["ams"][0]["tray"][0]["tag_uid"] is None def test_vt_tray_parsing(self, mock_state): - """Verify virtual tray is parsed correctly.""" + """Verify virtual tray is parsed correctly as a list.""" mock_state.raw_data = { - "vt_tray": { - "tray_color": "00FF00", - "tray_type": "PETG", - "tray_sub_brands": "Generic", - "remain": 60, - "tag_uid": "VT123", - } + "vt_tray": [ + { + "tray_color": "00FF00", + "tray_type": "PETG", + "tray_sub_brands": "Generic", + "remain": 60, + "tag_uid": "VT123", + } + ] } result = printer_state_to_dict(mock_state) - assert result["vt_tray"] is not None - assert result["vt_tray"]["id"] == 254 - assert result["vt_tray"]["tray_color"] == "00FF00" - assert result["vt_tray"]["tray_type"] == "PETG" + assert isinstance(result["vt_tray"], list) + assert len(result["vt_tray"]) == 1 + assert result["vt_tray"][0]["id"] == 254 + assert result["vt_tray"][0]["tray_color"] == "00FF00" + assert result["vt_tray"][0]["tray_type"] == "PETG" def test_hms_errors_conversion(self, mock_state): """Verify HMS errors are converted correctly.""" diff --git a/backend/tests/unit/services/test_spoolman_tracking.py b/backend/tests/unit/services/test_spoolman_tracking.py index 0a854d79d..cf41b50d2 100644 --- a/backend/tests/unit/services/test_spoolman_tracking.py +++ b/backend/tests/unit/services/test_spoolman_tracking.py @@ -99,14 +99,14 @@ class TestBuildAmsTrayLookup: def test_external_spool(self): raw = { "ams": [], - "vt_tray": {"tray_uuid": "EXT", "tag_uid": "X", "tray_type": "TPU"}, + "vt_tray": [{"tray_uuid": "EXT", "tag_uid": "X", "tray_type": "TPU"}], } lookup = build_ams_tray_lookup(raw) assert 254 in lookup assert lookup[254]["tray_type"] == "TPU" def test_empty_external_spool_skipped(self): - raw = {"ams": [], "vt_tray": {"tray_type": ""}} + raw = {"ams": [], "vt_tray": [{"tray_type": ""}]} lookup = build_ams_tray_lookup(raw) assert 254 not in lookup diff --git a/backend/tests/unit/test_scheduler_ams_mapping.py b/backend/tests/unit/test_scheduler_ams_mapping.py index 47fff82f7..f2ecd9320 100644 --- a/backend/tests/unit/test_scheduler_ams_mapping.py +++ b/backend/tests/unit/test_scheduler_ams_mapping.py @@ -140,7 +140,7 @@ class TestBuildLoadedFilaments: """Should include external spool.""" class MockStatus: - raw_data = {"vt_tray": {"tray_type": "TPU", "tray_color": "0000FF"}} + raw_data = {"vt_tray": [{"tray_type": "TPU", "tray_color": "0000FF"}]} result = scheduler._build_loaded_filaments(MockStatus()) assert len(result) == 1 @@ -466,7 +466,7 @@ class TestBuildLoadedFilamentsTrayInfoIdx: """Should extract tray_info_idx from external spool.""" class MockStatus: - raw_data = {"vt_tray": {"tray_type": "TPU", "tray_color": "0000FF", "tray_info_idx": "P4d64437"}} + raw_data = {"vt_tray": [{"tray_type": "TPU", "tray_color": "0000FF", "tray_info_idx": "P4d64437"}]} result = scheduler._build_loaded_filaments(MockStatus()) assert len(result) == 1 @@ -624,7 +624,7 @@ class TestNozzleAwareMapping: class MockStatus: raw_data = { - "vt_tray": {"tray_type": "TPU", "tray_color": "0000FF"}, + "vt_tray": [{"tray_type": "TPU", "tray_color": "0000FF"}], "ams_extruder_map": {"0": 0}, } @@ -637,7 +637,7 @@ class TestNozzleAwareMapping: """External spool extruder_id should be None without ams_extruder_map.""" class MockStatus: - raw_data = {"vt_tray": {"tray_type": "TPU", "tray_color": "0000FF"}} + raw_data = {"vt_tray": [{"tray_type": "TPU", "tray_color": "0000FF"}]} result = scheduler._build_loaded_filaments(MockStatus()) assert len(result) == 1 diff --git a/frontend/src/__tests__/components/AddPrinterDiscovery.test.tsx b/frontend/src/__tests__/components/AddPrinterDiscovery.test.tsx index 7ad6c60e8..02b202290 100644 --- a/frontend/src/__tests__/components/AddPrinterDiscovery.test.tsx +++ b/frontend/src/__tests__/components/AddPrinterDiscovery.test.tsx @@ -38,6 +38,7 @@ const mockPrinterStatus = { remaining_time: 0, filename: null, wifi_signal: -50, + vt_tray: [], }; describe('AddPrinterModal Discovery', () => { diff --git a/frontend/src/__tests__/components/ConfigureAmsSlotModal.test.tsx b/frontend/src/__tests__/components/ConfigureAmsSlotModal.test.tsx index a22d56673..2de3692bf 100644 --- a/frontend/src/__tests__/components/ConfigureAmsSlotModal.test.tsx +++ b/frontend/src/__tests__/components/ConfigureAmsSlotModal.test.tsx @@ -18,6 +18,11 @@ vi.mock('../../api/client', () => ({ saveSlotPreset: vi.fn(), getSettings: vi.fn().mockResolvedValue({}), updateSettings: vi.fn().mockResolvedValue({}), + getLocalPresets: vi.fn(), + getBuiltinFilaments: vi.fn(), + searchColors: vi.fn(), + getColorCatalog: vi.fn(), + resetAmsSlot: vi.fn(), }, })); @@ -69,10 +74,17 @@ const defaultProps = { describe('ConfigureAmsSlotModal', () => { beforeEach(() => { vi.clearAllMocks(); + // Mock scrollIntoView which is not available in jsdom + Element.prototype.scrollIntoView = vi.fn(); (api.getCloudSettings as ReturnType).mockResolvedValue(mockCloudSettings); (api.getKProfiles as ReturnType).mockResolvedValue(mockKProfiles); (api.configureAmsSlot as ReturnType).mockResolvedValue({ success: true }); (api.saveSlotPreset as ReturnType).mockResolvedValue({ success: true }); + (api.getLocalPresets as ReturnType).mockResolvedValue({ filament: [] }); + (api.getBuiltinFilaments as ReturnType).mockResolvedValue([]); + (api.searchColors as ReturnType).mockResolvedValue([]); + (api.getColorCatalog as ReturnType).mockResolvedValue([]); + (api.resetAmsSlot as ReturnType).mockResolvedValue({ success: true, message: 'ok' }); }); it('renders nothing visible when closed', () => { @@ -204,4 +216,69 @@ describe('ConfigureAmsSlotModal', () => { const configureButton = screen.getByRole('button', { name: /Configure Slot/i }); expect(configureButton).toBeInTheDocument(); }); + + it('filters presets by printer model', async () => { + // Render with printerModel="H2D" + render(); + // Wait for presets to load - the H2D preset should be visible + await waitFor(() => { + expect(screen.getByText(/Overture Matte PLA/)).toBeInTheDocument(); + }); + // The X1C preset should NOT be visible (filtered out by model) + expect(screen.queryByText(/Bambu PLA Basic @BBL X1C/)).not.toBeInTheDocument(); + }); + + it('shows current preset even when it does not match model filter', async () => { + // Render with printerModel="H2D" but savedPresetId pointing to the X1C preset + const slotInfo = { + ...defaultProps.slotInfo, + savedPresetId: 'GFSL05_09', // X1C preset + }; + render(); + await waitFor(() => { + // Both should be visible - H2D matches model, X1C is saved preset + // Use the full preset name to match the list item (not the "Filtering for" label) + expect(screen.getByText('Bambu PLA Basic @BBL X1C')).toBeInTheDocument(); + expect(screen.getByText(/Overture Matte PLA/)).toBeInTheDocument(); + }); + }); + + it('pre-selects saved preset when opening configured slot', async () => { + const slotInfo = { + ...defaultProps.slotInfo, + savedPresetId: 'GFSL05_09', + }; + render(); + await waitFor(() => { + // The saved preset should have the selected style (green border) + // Use the full preset name to avoid matching the "Filtering for" label + const presetButton = screen.getByText('Bambu PLA Basic @BBL X1C').closest('button'); + expect(presetButton).toHaveClass('bg-bambu-green/20'); + }); + }); + + it('pre-populates color from trayColor', async () => { + const slotInfo = { + ...defaultProps.slotInfo, + trayColor: 'FF0000FF', // Red with alpha + }; + render(); + await waitFor(() => { + expect(screen.getByTitle('White')).toBeInTheDocument(); + }); + // The hex display should show the pre-populated color + expect(screen.getByText('Hex: #FF0000', { exact: false })).toBeInTheDocument(); + }); + + it('uses translated text for modal elements', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('Configure AMS Slot')).toBeInTheDocument(); + expect(screen.getByText('Filament Profile')).toBeInTheDocument(); + }); + // Check footer buttons + expect(screen.getByRole('button', { name: /Configure Slot/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Cancel/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Reset Slot/i })).toBeInTheDocument(); + }); }); diff --git a/frontend/src/__tests__/components/PrintModal.test.tsx b/frontend/src/__tests__/components/PrintModal.test.tsx index 8db3431fb..2e829039c 100644 --- a/frontend/src/__tests__/components/PrintModal.test.tsx +++ b/frontend/src/__tests__/components/PrintModal.test.tsx @@ -67,7 +67,7 @@ describe('PrintModal', () => { return HttpResponse.json({ filaments: [] }); }), http.get('/api/v1/printers/:id/status', () => { - return HttpResponse.json({ connected: true, state: 'IDLE', ams: [], vt_tray: null }); + return HttpResponse.json({ connected: true, state: 'IDLE', ams: [], vt_tray: [] }); }), http.post('/api/v1/archives/:id/reprint', () => { return HttpResponse.json({ success: true }); diff --git a/frontend/src/__tests__/hooks/useFilamentMapping.test.ts b/frontend/src/__tests__/hooks/useFilamentMapping.test.ts index 68e997593..93dfa20c1 100644 --- a/frontend/src/__tests__/hooks/useFilamentMapping.test.ts +++ b/frontend/src/__tests__/hooks/useFilamentMapping.test.ts @@ -13,7 +13,7 @@ import { import type { PrinterStatus } from '../../api/client'; // Helper to create a minimal printer status with AMS data -function createPrinterStatus(ams: PrinterStatus['ams'], vt_tray?: PrinterStatus['vt_tray']): PrinterStatus { +function createPrinterStatus(ams: PrinterStatus['ams'], vt_tray: PrinterStatus['vt_tray'] = []): PrinterStatus { return { ams, vt_tray, @@ -89,7 +89,7 @@ describe('buildLoadedFilaments', () => { it('extracts external spool with tray_info_idx', () => { const status = createPrinterStatus( [], - { tray_type: 'TPU', tray_color: '0000FF', tray_info_idx: 'EXT001' } + [{ tray_type: 'TPU', tray_color: '0000FF', tray_info_idx: 'EXT001' }] ); const result = buildLoadedFilaments(status); @@ -339,7 +339,7 @@ describe('computeAmsMapping', () => { }; const status = createPrinterStatus( [], - { tray_type: 'TPU', tray_color: '0000FF', tray_info_idx: 'EXT001' } + [{ tray_type: 'TPU', tray_color: '0000FF', tray_info_idx: 'EXT001' }] ); const result = computeAmsMapping(reqs, status); diff --git a/frontend/src/__tests__/pages/PrintersPage.test.tsx b/frontend/src/__tests__/pages/PrintersPage.test.tsx index 8bf74b711..c39dac597 100644 --- a/frontend/src/__tests__/pages/PrintersPage.test.tsx +++ b/frontend/src/__tests__/pages/PrintersPage.test.tsx @@ -56,6 +56,7 @@ const mockPrinterStatus = { remaining_time: 0, filename: null, wifi_signal: -50, + vt_tray: [], }; describe('PrintersPage', () => { diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 4e7bbbc13..6be62e627 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -204,7 +204,7 @@ export interface PrinterStatus { hms_errors: HMSError[]; ams: AMSUnit[]; ams_exists: boolean; - vt_tray: AMSTray | null; // Virtual tray / external spool + vt_tray: AMSTray[]; // Virtual tray / external spool(s) sdcard: boolean; // SD card inserted store_to_sdcard: boolean; // Store sent files on SD card timelapse: boolean; // Timelapse recording active @@ -3028,6 +3028,8 @@ export const api = { request(`/cloud/settings?version=${version}`), getBuiltinFilaments: () => request('/cloud/builtin-filaments'), + getFilamentIdMap: () => + request>('/cloud/filament-id-map'), getCloudSettingDetail: (settingId: string) => request(`/cloud/settings/${settingId}`), createCloudSetting: (data: SlicerSettingCreate) => @@ -3425,6 +3427,8 @@ export const api = { request<{ status: string }>('/inventory/colors/reset', { method: 'POST' }), lookupColor: (manufacturer: string, colorName: string, material?: string) => request(`/inventory/colors/lookup?manufacturer=${encodeURIComponent(manufacturer)}&color_name=${encodeURIComponent(colorName)}${material ? `&material=${encodeURIComponent(material)}` : ''}`), + searchColors: (manufacturer?: string, material?: string) => + request(`/inventory/colors/search?${manufacturer ? `manufacturer=${encodeURIComponent(manufacturer)}` : ''}${manufacturer && material ? '&' : ''}${material ? `material=${encodeURIComponent(material)}` : ''}`), linkTagToSpool: (spoolId: number, data: { tag_uid?: string; tray_uuid?: string; tag_type?: string; data_origin?: string }) => request(`/inventory/spools/${spoolId}/link-tag`, { method: 'PATCH', diff --git a/frontend/src/components/AssignSpoolModal.tsx b/frontend/src/components/AssignSpoolModal.tsx index 6ec9b0ca2..10011af81 100644 --- a/frontend/src/components/AssignSpoolModal.tsx +++ b/frontend/src/components/AssignSpoolModal.tsx @@ -62,15 +62,17 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr if (!isOpen) return null; - // Filter out Bambu Lab spools (identified by RFID tag_uid or tray_uuid) - // and spools already assigned to other slots + // Filter out spools already assigned to other slots const assignedSpoolIds = new Set( (assignments || []) .filter(a => !(a.printer_id === printerId && a.ams_id === amsId && a.tray_id === trayId)) .map(a => a.spool_id) ); + // External slots (amsId 254 or 255) have no RFID reader, so show all spools. + // AMS slots only show manual spools (no tag_uid or tray_uuid). + const isExternalSlot = amsId === 254 || amsId === 255; const manualSpools = spools?.filter((spool: InventorySpool) => - !spool.tag_uid && !spool.tray_uuid && !assignedSpoolIds.has(spool.id) + !assignedSpoolIds.has(spool.id) && (isExternalSlot || (!spool.tag_uid && !spool.tray_uuid)) ); const filteredSpools = manualSpools?.filter((spool: InventorySpool) => { diff --git a/frontend/src/components/ConfigureAmsSlotModal.tsx b/frontend/src/components/ConfigureAmsSlotModal.tsx index 9cdc7248a..75497ffa4 100644 --- a/frontend/src/components/ConfigureAmsSlotModal.tsx +++ b/frontend/src/components/ConfigureAmsSlotModal.tsx @@ -14,6 +14,9 @@ interface SlotInfo { trayColor?: string; traySubBrands?: string; trayInfoIdx?: string; + extruderId?: number; + caliIdx?: number | null; + savedPresetId?: string; } // Get proper AMS label (handles HT AMS with ID 128+) @@ -72,6 +75,7 @@ interface ConfigureAmsSlotModalProps { printerId: number; slotInfo: SlotInfo; nozzleDiameter?: string; + printerModel?: string; onSuccess?: () => void; } @@ -209,12 +213,23 @@ function colorNameToHex(name: string): string | null { return COLOR_NAME_MAP[normalized] || null; } +// Extract printer model from preset name suffix "@BBL X1C 0.4 nozzle" → "X1C" +function extractPresetModel(name: string): string | null { + const atIdx = name.indexOf('@'); + if (atIdx < 0) return null; + const suffix = name.slice(atIdx + 1).trim(); + const bblMatch = suffix.match(/^BBL\s+(.+?)(?:\s+[\d.]+\s*nozzle)?$/i); + if (bblMatch) return bblMatch[1].trim(); + return null; +} + export function ConfigureAmsSlotModal({ isOpen, onClose, printerId, slotInfo, nozzleDiameter = '0.4', + printerModel, onSuccess, }: ConfigureAmsSlotModalProps) { const { t } = useTranslation(); @@ -256,6 +271,14 @@ export function ConfigureAmsSlotModal({ enabled: isOpen && !!printerId, }); + // Fetch color catalog + const { data: colorCatalog } = useQuery({ + queryKey: ['colorCatalog'], + queryFn: () => api.getColorCatalog(), + enabled: isOpen, + staleTime: Infinity, + }); + // Configure slot mutation const configureMutation = useMutation({ mutationFn: async () => { @@ -435,22 +458,44 @@ export function ConfigureAmsSlotModal({ // Collect IDs already covered by cloud and local to avoid duplicates in fallback const coveredIds = new Set(); + // Currently-configured preset should always be shown (bypass model filter) + const savedId = slotInfo.savedPresetId; + const trayIdx = slotInfo.trayInfoIdx; + // 1. Cloud presets if (cloudSettings?.filament) { for (const cp of cloudSettings.filament) { coveredIds.add(cp.setting_id); - if (!query || cp.name.toLowerCase().includes(query)) { - items.push({ id: cp.setting_id, name: cp.name, source: 'cloud', isUser: isUserPreset(cp.setting_id) }); + // Keep preset if it matches the slot's saved mapping or current tray_info_idx + const isCurrentPreset = savedId === cp.setting_id + || (trayIdx && (cp.setting_id === trayIdx || convertToTrayInfoIdx(cp.setting_id) === trayIdx)); + if (!isCurrentPreset && query && !cp.name.toLowerCase().includes(query)) continue; + // Filter by printer model if set (skip for current preset) + if (!isCurrentPreset && printerModel) { + const presetModel = extractPresetModel(cp.name); + if (presetModel && presetModel.toUpperCase() !== printerModel.toUpperCase()) continue; } + items.push({ id: cp.setting_id, name: cp.name, source: 'cloud', isUser: isUserPreset(cp.setting_id) }); } } // 2. Local presets if (localPresets?.filament) { for (const lp of localPresets.filament) { - if (!query || lp.name.toLowerCase().includes(query)) { - items.push({ id: `local_${lp.id}`, name: lp.name, source: 'local', isUser: false }); + const localId = `local_${lp.id}`; + const isSaved = savedId === localId; + if (!isSaved && query && !lp.name.toLowerCase().includes(query)) continue; + // Filter by compatible_printers if set (skip for saved preset) + if (!isSaved && printerModel && lp.compatible_printers) { + const compatModels = lp.compatible_printers.split(';').map(p => { + // Extract model from "BBL X1C" → "X1C" + const trimmed = p.trim(); + const bblMatch = trimmed.match(/^BBL\s+(.+)/i); + return bblMatch ? bblMatch[1].trim().toUpperCase() : trimmed.toUpperCase(); + }).filter(Boolean); + if (compatModels.length > 0 && !compatModels.includes(printerModel.toUpperCase())) continue; } + items.push({ id: localId, name: lp.name, source: 'local', isUser: false }); } } @@ -478,7 +523,7 @@ export function ConfigureAmsSlotModal({ if (!a.isUser && b.isUser) return 1; return a.name.localeCompare(b.name); }); - }, [cloudSettings?.filament, localPresets?.filament, builtinFilaments, searchQuery]); + }, [cloudSettings?.filament, localPresets?.filament, builtinFilaments, searchQuery, printerModel, slotInfo.savedPresetId, slotInfo.trayInfoIdx]); // Get full preset name for K profile filtering (brand + material, without printer suffix) const selectedPresetInfo = useMemo(() => { @@ -518,6 +563,41 @@ export function ConfigureAmsSlotModal({ // For backwards compatibility with the label const selectedMaterial = selectedPresetInfo?.fullName || ''; + // Filter color catalog entries matching the selected preset's brand + material + const catalogColors = useMemo(() => { + if (!colorCatalog || !selectedPresetInfo) return []; + + const { fullName, brand } = selectedPresetInfo; + + // Try to find colors matching the full preset name (e.g., "PLA Metal") + // The catalog uses the variant as part of the material field (e.g., material="PLA Metal") + // Extract the full material+variant from the preset name + const materialVariant = fullName.replace(/^(Bambu\s*(Lab)?|eSUN|Polymaker|Overture|Sunlu|Hatchbox)\s*/i, '').trim(); + + return colorCatalog.filter(entry => { + const entryMaterial = (entry.material || '').toUpperCase(); + const entryManufacturer = entry.manufacturer.toUpperCase(); + + // Match material: try full material+variant first, then just material type + const materialMatch = entryMaterial === materialVariant.toUpperCase() + || entryMaterial.includes(materialVariant.toUpperCase()) + || materialVariant.toUpperCase().includes(entryMaterial); + + if (!materialMatch) return false; + + // If brand is present, also match manufacturer + if (brand) { + const upperBrand = brand.toUpperCase(); + // Fuzzy match: "Bambu" matches "Bambu Lab", etc. + if (!entryManufacturer.includes(upperBrand) && !upperBrand.includes(entryManufacturer)) { + return false; + } + } + + return true; + }); + }, [colorCatalog, selectedPresetInfo]); + const matchingKProfiles = useMemo(() => { if (!kprofilesData?.profiles || !selectedPresetInfo) return []; @@ -575,34 +655,52 @@ export function ConfigureAmsSlotModal({ }); // Deduplicate profiles with same name and k_value (multi-nozzle printers have duplicates) - // Prefer extruder_id=1 (High Flow) profiles as they're more commonly used on H2D + // Prefer the profile matching the slot's extruder (e.g. ext-R uses extruder 0, ext-L uses extruder 1) const seen = new Map(); for (const profile of filtered) { const key = `${profile.name}|${profile.k_value}`; const existing = seen.get(key); if (!existing) { seen.set(key, profile); - } else if (profile.extruder_id === 1 && existing.extruder_id === 0) { - // Replace extruder_id=0 profile with extruder_id=1 (High Flow) profile + } else if (slotInfo.extruderId !== undefined && profile.extruder_id === slotInfo.extruderId && existing.extruder_id !== slotInfo.extruderId) { + // Replace with profile matching slot's extruder seen.set(key, profile); } } return Array.from(seen.values()); - }, [kprofilesData?.profiles, selectedPresetInfo]); + }, [kprofilesData?.profiles, selectedPresetInfo, slotInfo.extruderId]); // Pre-select current profile when modal opens, reset when closes useEffect(() => { - if (isOpen && cloudSettings?.filament) { - // Try to pre-select current profile based on trayInfoIdx - if (slotInfo.trayInfoIdx) { - const currentPreset = cloudSettings.filament.find( + if (isOpen) { + // Pre-populate from saved preset mapping (most reliable) + if (slotInfo.savedPresetId) { + setSelectedPresetId(slotInfo.savedPresetId); + } else if (slotInfo.trayInfoIdx && cloudSettings?.filament) { + // Fallback: try to match by tray_info_idx in cloud presets + // First try exact match on setting_id + let currentPreset = cloudSettings.filament.find( p => p.setting_id === slotInfo.trayInfoIdx ); + // Then try matching by converting setting_id → filament_id format + if (!currentPreset) { + currentPreset = cloudSettings.filament.find( + p => convertToTrayInfoIdx(p.setting_id) === slotInfo.trayInfoIdx + ); + } if (currentPreset) { setSelectedPresetId(currentPreset.setting_id); } } - } else if (!isOpen) { + + // Pre-populate color from current slot + if (slotInfo.trayColor) { + const hex = slotInfo.trayColor.slice(0, 6); + if (hex && hex !== '000000') { + setColorHex(hex); + } + } + } else { // Reset when modal closes setSelectedPresetId(''); setSelectedKProfile(null); @@ -611,17 +709,25 @@ export function ConfigureAmsSlotModal({ setSearchQuery(''); setShowSuccess(false); } - }, [isOpen, cloudSettings?.filament, slotInfo.trayInfoIdx]); + }, [isOpen, slotInfo.savedPresetId, slotInfo.trayInfoIdx, slotInfo.trayColor, cloudSettings?.filament]); // Auto-select best matching K profile when preset changes useEffect(() => { if (matchingKProfiles.length > 0) { - // Auto-select first matching profile + // Prefer the currently-active K-profile (by cali_idx) if available + if (slotInfo.caliIdx != null && slotInfo.caliIdx > 0) { + const active = matchingKProfiles.find(p => p.slot_id === slotInfo.caliIdx); + if (active) { + setSelectedKProfile(active); + return; + } + } + // Fallback: first matching profile setSelectedKProfile(matchingKProfiles[0]); } else { setSelectedKProfile(null); } - }, [selectedPresetId, matchingKProfiles]); + }, [selectedPresetId, matchingKProfiles, slotInfo.caliIdx]); // Escape key handler const handleKeyDown = useCallback((e: KeyboardEvent) => { @@ -659,7 +765,7 @@ export function ConfigureAmsSlotModal({
-

Configure AMS Slot

+

{t('configureAmsSlot.title')}

@@ -684,7 +790,7 @@ export function ConfigureAmsSlotModal({ {/* Slot info */}
-

Configuring slot:

+

{t('configureAmsSlot.configuringSlot')}

{slotInfo.trayColor && ( )} - {getAmsLabel(slotInfo.amsId, slotInfo.trayCount)} Slot {slotInfo.trayId + 1} + {t('configureAmsSlot.slotLabel', { ams: getAmsLabel(slotInfo.amsId, slotInfo.trayCount), slot: slotInfo.trayId + 1 })} {slotInfo.traySubBrands && ( ({slotInfo.traySubBrands}) @@ -710,7 +816,7 @@ export function ConfigureAmsSlotModal({ {/* Filament Profile Select */}
{(cloudSettings?.filament?.length === 0 && !localPresets?.filament?.length) - ? 'No presets available. Login to Bambu Cloud or import local profiles.' - : 'No matching presets found.'} + ? t('configureAmsSlot.noPresetsAvailable') + : t('configureAmsSlot.noMatchingPresets')}

) : ( filteredPresets.map((preset) => (
@@ -813,8 +922,40 @@ export function ConfigureAmsSlotModal({ {/* Optional: Custom color */}
+ {/* Catalog colors matching selected preset */} + {catalogColors.length > 0 && ( +
+

+ {t('configureAmsSlot.presetColors', { name: selectedPresetInfo?.fullName })} +

+
+ {catalogColors.map((entry) => ( + + ))} +
+
+ )} {/* Quick color buttons */}
{QUICK_COLORS_BASIC.map((color) => ( @@ -836,7 +977,7 @@ export function ConfigureAmsSlotModal({ @@ -902,13 +1043,13 @@ export function ConfigureAmsSlotModal({ className="px-2 py-1 text-xs text-bambu-gray hover:text-white bg-bambu-dark-tertiary rounded" title={t('configureAmsSlot.clearCustomColor')} > - Clear + {t('configureAmsSlot.clear')} )}
{colorHex && (

- Hex: #{colorHex} + {t('configureAmsSlot.hexLabel', { hex: colorHex })}

)}
@@ -928,19 +1069,19 @@ export function ConfigureAmsSlotModal({ {resetMutation.isPending ? ( <> - Resetting... + {t('configureAmsSlot.resetting')} ) : ( <> - Reset Slot + {t('configureAmsSlot.resetSlot')} )} {/* Cancel and Configure buttons on the right */}
diff --git a/frontend/src/components/KProfilesView.tsx b/frontend/src/components/KProfilesView.tsx index c7ed1f22a..7d5c15a9b 100644 --- a/frontend/src/components/KProfilesView.tsx +++ b/frontend/src/components/KProfilesView.tsx @@ -150,6 +150,7 @@ interface KProfileModalProps { printerId: number; nozzleDiameter: string; existingProfiles?: KProfile[]; // Existing profiles for filament selection + builtinFilaments?: { filament_id: string; name: string }[]; // Filament ID → name lookup isDualNozzle?: boolean; // Whether this is a dual-nozzle printer initialNote?: string; // Initial note value for the profile initialNoteKey?: string | null; // Key the note was stored under (for clearing) @@ -164,6 +165,7 @@ function KProfileModal({ printerId, nozzleDiameter, existingProfiles = [], + builtinFilaments = [], isDualNozzle = false, initialNote = '', initialNoteKey = null, @@ -197,12 +199,21 @@ function KProfileModal({ const [note, setNote] = useState(initialNote); // Extract unique filaments from existing K-profiles on the printer - // These have valid filament_ids that the printer recognizes + // Use builtin filament table for accurate name resolution (filament_id → name) + // Falls back to extracting from profile name for custom/unknown presets const knownFilaments = React.useMemo(() => { + // Build lookup map from builtin filament names (includes cloud presets from parent) + const builtinMap = new Map(); + for (const bf of builtinFilaments) { + builtinMap.set(bf.filament_id, bf.name); + } + const filamentMap = new Map(); for (const p of existingProfiles) { if (p.filament_id && !filamentMap.has(p.filament_id)) { - const filamentName = extractFilamentName(p.name || ''); + // Prefer builtin name (accurate), fall back to extracting from profile name + const builtinName = builtinMap.get(p.filament_id); + const filamentName = builtinName || extractFilamentName(p.name || ''); filamentMap.set(p.filament_id, { id: p.filament_id, name: filamentName || p.filament_id, @@ -212,7 +223,7 @@ function KProfileModal({ return Array.from(filamentMap.values()).sort((a, b) => a.name.localeCompare(b.name) ); - }, [existingProfiles]); + }, [existingProfiles, builtinFilaments]); const saveMutation = useMutation({ mutationFn: (data: KProfileCreate) => { @@ -769,6 +780,20 @@ export function KProfilesView() { staleTime: 60000, // Cache for 1 minute }); + // Fetch builtin filament names for accurate filament_id → name resolution + const { data: builtinFilaments } = useQuery({ + queryKey: ['builtinFilaments'], + queryFn: () => api.getBuiltinFilaments(), + staleTime: 300000, // Cache for 5 minutes (static data) + }); + + // Fetch filament_id → name mapping for user cloud presets (P* IDs) + const { data: filamentIdMap } = useQuery({ + queryKey: ['filamentIdMap'], + queryFn: () => api.getFilamentIdMap(), + staleTime: 300000, // Cache for 5 minutes + }); + // Fetch K-profile notes (stored locally) const { data: notesData, @@ -807,6 +832,39 @@ export function KProfilesView() { // Get connected printers for display const connectedPrinters = printers?.filter((p) => p.is_active) || []; + // Build filament lookup for name resolution (builtin + user cloud presets) + const builtinFilamentMap = React.useMemo(() => { + const map = new Map(); + if (builtinFilaments) { + for (const bf of builtinFilaments) { + map.set(bf.filament_id, bf.name); + } + } + // Also add user cloud presets (P* filament_ids resolved from cloud details) + if (filamentIdMap) { + for (const [fid, name] of Object.entries(filamentIdMap)) { + if (!map.has(fid)) { + map.set(fid, name); + } + } + } + return map; + }, [builtinFilaments, filamentIdMap]); + + // Enriched builtin filaments array (builtin + cloud presets merged) + // Pass this to modals so they have the full filament name lookup + const enrichedBuiltinFilaments = React.useMemo(() => { + return Array.from(builtinFilamentMap.entries()).map(([fid, name]) => ({ + filament_id: fid, + name, + })); + }, [builtinFilamentMap]); + + // Resolve filament name: builtin table first, then extract from profile name + const resolveFilamentName = React.useCallback((profile: KProfile) => { + return builtinFilamentMap.get(profile.filament_id) || extractFilamentName(profile.name); + }, [builtinFilamentMap]); + // Filter and sort profiles // Note: nozzle diameter filtering is done server-side via MQTT request const filteredProfiles = React.useMemo(() => { @@ -841,13 +899,13 @@ export function KProfilesView() { case 'k_value': return parseFloat(a.k_value) - parseFloat(b.k_value); case 'filament': - return extractFilamentName(a.name).localeCompare(extractFilamentName(b.name)); + return resolveFilamentName(a).localeCompare(resolveFilamentName(b)); case 'name': default: return a.name.localeCompare(b.name); } }); - }, [kprofiles?.profiles, searchQuery, extruderFilter, flowTypeFilter, sortOption]); + }, [kprofiles?.profiles, searchQuery, extruderFilter, flowTypeFilter, sortOption, resolveFilamentName]); // Check if selected printer is dual-nozzle (auto-detected from MQTT temperature data) const selectedPrinterData = printers?.find((p) => p.id === selectedPrinter); @@ -1394,6 +1452,7 @@ export function KProfilesView() { printerId={selectedPrinter} nozzleDiameter={nozzleDiameter} existingProfiles={allProfiles?.profiles || kprofiles?.profiles} + builtinFilaments={enrichedBuiltinFilaments} isDualNozzle={isDualNozzle} initialNote={note} initialNoteKey={key} @@ -1418,6 +1477,7 @@ export function KProfilesView() { printerId={selectedPrinter} nozzleDiameter={nozzleDiameter} existingProfiles={allProfiles?.profiles || kprofiles?.profiles} + builtinFilaments={enrichedBuiltinFilaments} isDualNozzle={isDualNozzle} onSaveNote={handleSaveNote} hasPermission={hasPermission} @@ -1438,6 +1498,7 @@ export function KProfilesView() { printerId={selectedPrinter} nozzleDiameter={nozzleDiameter} existingProfiles={allProfiles?.profiles || kprofiles?.profiles} + builtinFilaments={enrichedBuiltinFilaments} isDualNozzle={isDualNozzle} onSaveNote={handleSaveNote} hasPermission={hasPermission} diff --git a/frontend/src/hooks/useFilamentMapping.ts b/frontend/src/hooks/useFilamentMapping.ts index bb79c80c3..a23f9e5b0 100644 --- a/frontend/src/hooks/useFilamentMapping.ts +++ b/frontend/src/hooks/useFilamentMapping.ts @@ -41,22 +41,26 @@ export function buildLoadedFilaments(printerStatus: PrinterStatus | undefined): }); }); - // Add external spool if loaded - if (printerStatus?.vt_tray?.tray_type) { - const color = normalizeColor(printerStatus.vt_tray.tray_color); - filaments.push({ - type: printerStatus.vt_tray.tray_type, - color, - colorName: getColorName(color), - amsId: -1, - trayId: 0, - isHt: false, - isExternal: true, - label: 'External', - globalTrayId: 254, - trayInfoIdx: printerStatus.vt_tray.tray_info_idx || '', - extruderId: hasDualNozzle ? 0 : undefined, - }); + // Add external spool(s) if loaded + for (const extTray of printerStatus?.vt_tray ?? []) { + if (extTray.tray_type) { + const color = normalizeColor(extTray.tray_color); + const trayId = extTray.id ?? 254; + const hasDualExternal = (printerStatus?.vt_tray?.length ?? 0) > 1; + filaments.push({ + type: extTray.tray_type, + color, + colorName: getColorName(color), + amsId: -1, + trayId: trayId - 254, + isHt: false, + isExternal: true, + label: hasDualExternal ? (trayId === 254 ? 'Ext-L' : 'Ext-R') : 'External', + globalTrayId: trayId, + trayInfoIdx: extTray.tray_info_idx || '', + extruderId: hasDualNozzle ? (trayId - 254) : undefined, + }); + } } return filaments; diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 52bf72e7e..c166b4347 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -171,6 +171,8 @@ export default { // Printer card readyToPrint: 'Druckbereit', external: 'Extern', + extL: 'Ext-L', + extR: 'Ext-R', deleteArchives: 'Druckarchive löschen', noLabel: 'Keine Bezeichnung', printPreview: 'Druckvorschau', @@ -3204,15 +3206,37 @@ export default { // Configure AMS Slot Modal configureAmsSlot: { + title: 'AMS-Slot konfigurieren', + slotConfigured: 'Slot konfiguriert!', + configuringSlot: 'Slot wird konfiguriert:', + slotLabel: '{{ams}} Slot {{slot}}', searchPresets: 'Voreinstellungen suchen...', colorPlaceholder: 'Farbname oder Hex (z.B. braun, FF8800)', clearCustomColor: 'Benutzerdefinierte Farbe löschen', noCloudPresets: 'Keine Cloud-Voreinstellungen. Melden Sie sich bei Bambu Cloud an, um zu synchronisieren.', + noPresetsAvailable: 'Keine Voreinstellungen verfügbar. Melden Sie sich bei Bambu Cloud an oder importieren Sie lokale Profile.', noMatchingPresets: 'Keine passenden Voreinstellungen gefunden.', custom: 'Benutzerdefiniert', builtin: 'Integriert', settingsSentToPrinter: 'Einstellungen an Drucker gesendet', filamentProfile: 'Filamentprofil', + kProfileLabel: 'K-Profil (Pressure Advance)', + filteringFor: 'Filtern nach: {{material}}', + noKProfile: 'Kein K-Profil (Standard 0.020 verwenden)', + noMatchingKProfiles: 'Keine passenden K-Profile gefunden. Standard K=0.020 wird verwendet.', + selectFilamentFirst: 'Zuerst ein Filamentprofil auswählen', + kFromCalibration: 'K={{value}} aus Druckerkalibrierung', + customColorLabel: 'Benutzerdefinierte Farbe (optional)', + presetColors: '{{name}} Farben:', + showLessColors: 'Weniger Farben anzeigen', + showMoreColors: 'Mehr Farben anzeigen', + clear: 'Löschen', + hexLabel: 'Hex: #{{hex}}', + resetting: 'Wird zurückgesetzt...', + resetSlot: 'Slot zurücksetzen', + cancel: 'Abbrechen', + configuring: 'Wird konfiguriert...', + configureSlot: 'Slot konfigurieren', }, // GitHub Backup Settings diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 008348aae..b1e1d9bbf 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -171,6 +171,8 @@ export default { // Printer card readyToPrint: 'Ready to print', external: 'External', + extL: 'Ext-L', + extR: 'Ext-R', deleteArchives: 'Delete print archives', noLabel: 'No label', printPreview: 'Print preview', @@ -3209,15 +3211,37 @@ export default { // Configure AMS Slot Modal configureAmsSlot: { + title: 'Configure AMS Slot', + slotConfigured: 'Slot Configured!', + configuringSlot: 'Configuring slot:', + slotLabel: '{{ams}} Slot {{slot}}', searchPresets: 'Search presets...', colorPlaceholder: 'Color name or hex (e.g., brown, FF8800)', clearCustomColor: 'Clear custom color', noCloudPresets: 'No cloud presets. Login to Bambu Cloud to sync.', + noPresetsAvailable: 'No presets available. Login to Bambu Cloud or import local profiles.', noMatchingPresets: 'No matching presets found.', custom: 'Custom', builtin: 'Built-in', settingsSentToPrinter: 'Settings sent to printer', filamentProfile: 'Filament Profile', + kProfileLabel: 'K Profile (Pressure Advance)', + filteringFor: 'Filtering for: {{material}}', + noKProfile: 'No K profile (use default 0.020)', + noMatchingKProfiles: 'No matching K profiles found. Default K=0.020 will be used.', + selectFilamentFirst: 'Select a filament profile first', + kFromCalibration: 'K={{value}} from printer calibration', + customColorLabel: 'Custom Color (optional)', + presetColors: '{{name}} colors:', + showLessColors: 'Show less colors', + showMoreColors: 'Show more colors', + clear: 'Clear', + hexLabel: 'Hex: #{{hex}}', + resetting: 'Resetting...', + resetSlot: 'Reset Slot', + cancel: 'Cancel', + configuring: 'Configuring...', + configureSlot: 'Configure Slot', }, // GitHub Backup Settings diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index 96acda383..766ce4c2b 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -171,6 +171,8 @@ export default { // Printer card readyToPrint: 'Prête à imprimer', external: 'Externe', + extL: 'Ext-L', + extR: 'Ext-R', deleteArchives: 'Supprimer les archives d\'impression', noLabel: 'Pas d\'étiquette', printPreview: 'Aperçu avant impression', @@ -3205,15 +3207,37 @@ export default { // Configure AMS Slot Modal configureAmsSlot: { + title: 'Configurer le slot AMS', + slotConfigured: 'Slot configuré !', + configuringSlot: 'Configuration du slot :', + slotLabel: '{{ams}} Slot {{slot}}', searchPresets: 'Chercher presets...', colorPlaceholder: 'Nom couleur ou hex (ex: brown, FF8800)', clearCustomColor: 'Effacer couleur perso', noCloudPresets: 'Profils Cloud absents. Connectez-vous.', + noPresetsAvailable: 'Aucun preset disponible. Connectez-vous à Bambu Cloud ou importez des profils locaux.', noMatchingPresets: 'Aucun profil trouvé.', custom: 'Perso', builtin: 'Inclus', settingsSentToPrinter: 'Réglages envoyés', filamentProfile: 'Profil Filament', + kProfileLabel: 'Profil K (Pressure Advance)', + filteringFor: 'Filtrage pour : {{material}}', + noKProfile: 'Pas de profil K (utiliser défaut 0.020)', + noMatchingKProfiles: 'Aucun profil K trouvé. K=0.020 par défaut sera utilisé.', + selectFilamentFirst: 'Sélectionnez d\'abord un profil filament', + kFromCalibration: 'K={{value}} de la calibration imprimante', + customColorLabel: 'Couleur personnalisée (optionnel)', + presetColors: 'Couleurs {{name}} :', + showLessColors: 'Moins de couleurs', + showMoreColors: 'Plus de couleurs', + clear: 'Effacer', + hexLabel: 'Hex : #{{hex}}', + resetting: 'Réinitialisation...', + resetSlot: 'Réinitialiser le slot', + cancel: 'Annuler', + configuring: 'Configuration...', + configureSlot: 'Configurer le slot', }, // GitHub Backup Settings diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index f80d4b4e2..4b3b5d16e 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -168,6 +168,8 @@ export default { // Printer card readyToPrint: 'Pronta a stampare', external: 'Esterna', + extL: 'Ext-L', + extR: 'Ext-R', deleteArchives: 'Elimina archivi stampa', noLabel: 'Nessuna etichetta', printPreview: 'Anteprima stampa', @@ -2732,4 +2734,39 @@ export default { replaceCarbonFilter: 'Sostituisci filtro a carbone attivo', lubricateLeftNozzleRail: 'Lubrifica guida ugello sinistro (serie H2)', }, + + // Configure AMS Slot Modal + configureAmsSlot: { + title: 'Configura Slot AMS', + slotConfigured: 'Slot configurato!', + configuringSlot: 'Configurazione slot:', + slotLabel: '{{ams}} Slot {{slot}}', + searchPresets: 'Cerca preset...', + colorPlaceholder: 'Nome colore o hex (es. marrone, FF8800)', + clearCustomColor: 'Cancella colore personalizzato', + noCloudPresets: 'Nessun preset cloud. Accedi a Bambu Cloud per sincronizzare.', + noPresetsAvailable: 'Nessun preset disponibile. Accedi a Bambu Cloud o importa profili locali.', + noMatchingPresets: 'Nessun preset corrispondente trovato.', + custom: 'Personalizzato', + builtin: 'Integrato', + settingsSentToPrinter: 'Impostazioni inviate alla stampante', + filamentProfile: 'Profilo filamento', + kProfileLabel: 'Profilo K (Pressure Advance)', + filteringFor: 'Filtrando per: {{material}}', + noKProfile: 'Nessun profilo K (usa predefinito 0.020)', + noMatchingKProfiles: 'Nessun profilo K corrispondente. Verrà usato K=0.020 predefinito.', + selectFilamentFirst: 'Seleziona prima un profilo filamento', + kFromCalibration: 'K={{value}} dalla calibrazione stampante', + customColorLabel: 'Colore personalizzato (opzionale)', + presetColors: 'Colori {{name}}:', + showLessColors: 'Mostra meno colori', + showMoreColors: 'Mostra più colori', + clear: 'Cancella', + hexLabel: 'Hex: #{{hex}}', + resetting: 'Ripristino...', + resetSlot: 'Ripristina slot', + cancel: 'Annulla', + configuring: 'Configurazione...', + configureSlot: 'Configura slot', + }, }; diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index c3d26fa22..8adb9d74f 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -173,6 +173,8 @@ export default { noPrintersConfigured: 'プリンターが設定されていません', readyToPrint: '印刷可能', external: '外部', + extL: 'Ext-L', + extR: 'Ext-R', deleteArchives: '印刷アーカイブを削除', willBeSkipped: 'スキップされます', name: '名前', @@ -2920,15 +2922,37 @@ export default { // Configure AMS Slot Modal configureAmsSlot: { + title: 'AMSスロットの設定', + slotConfigured: 'スロットを設定しました!', + configuringSlot: 'スロットを設定中:', + slotLabel: '{{ams}} スロット {{slot}}', searchPresets: 'プリセットを検索...', colorPlaceholder: '色名またはHex(例: 茶色、FF8800)', clearCustomColor: 'カスタム色をクリア', noCloudPresets: 'クラウドプリセットがありません。Bambu Cloudにログインして同期してください。', + noPresetsAvailable: 'プリセットがありません。Bambu Cloudにログインするか、ローカルプロファイルをインポートしてください。', noMatchingPresets: '一致するプリセットが見つかりません。', custom: 'カスタム', builtin: '内蔵', settingsSentToPrinter: '設定をプリンターに送信しました', filamentProfile: 'フィラメントプロファイル', + kProfileLabel: 'Kプロファイル(Pressure Advance)', + filteringFor: 'フィルター中: {{material}}', + noKProfile: 'Kプロファイルなし(デフォルト0.020を使用)', + noMatchingKProfiles: '一致するKプロファイルが見つかりません。デフォルトK=0.020が使用されます。', + selectFilamentFirst: 'まずフィラメントプロファイルを選択してください', + kFromCalibration: 'K={{value}}(プリンターキャリブレーションから)', + customColorLabel: 'カスタム色(オプション)', + presetColors: '{{name}}の色:', + showLessColors: '色を減らす', + showMoreColors: '色をもっと表示', + clear: 'クリア', + hexLabel: 'Hex: #{{hex}}', + resetting: 'リセット中...', + resetSlot: 'スロットをリセット', + cancel: 'キャンセル', + configuring: '設定中...', + configureSlot: 'スロットを設定', }, // Email Settings diff --git a/frontend/src/pages/PrintersPage.tsx b/frontend/src/pages/PrintersPage.tsx index 55b38d188..0e39749e9 100644 --- a/frontend/src/pages/PrintersPage.tsx +++ b/frontend/src/pages/PrintersPage.tsx @@ -1384,6 +1384,44 @@ function getStatusDisplay(state: string | null | undefined, stg_cur_name: string } } +// Map SSDP model codes to display names +function mapModelCode(ssdpModel: string | null): string { + if (!ssdpModel) return ''; + const modelMap: Record = { + // H2 Series + 'O1D': 'H2D', + 'O1E': 'H2D Pro', + 'O2D': 'H2D Pro', + 'O1C': 'H2C', + 'O1S': 'H2S', + // X1 Series + 'BL-P001': 'X1C', + 'BL-P002': 'X1', + 'BL-P003': 'X1E', + // P Series + 'C11': 'P1S', + 'C12': 'P1P', + 'C13': 'P2S', + // A1 Series + 'N2S': 'A1', + 'N1': 'A1 Mini', + // Direct matches + 'X1C': 'X1C', + 'X1': 'X1', + 'X1E': 'X1E', + 'P1S': 'P1S', + 'P1P': 'P1P', + 'P2S': 'P2S', + 'A1': 'A1', + 'A1 Mini': 'A1 Mini', + 'H2D': 'H2D', + 'H2D Pro': 'H2D Pro', + 'H2C': 'H2C', + 'H2S': 'H2S', + }; + return modelMap[ssdpModel] || ssdpModel; +} + function PrinterCard({ printer, hideIfDisconnected, @@ -1469,6 +1507,9 @@ function PrinterCard({ trayColor?: string; traySubBrands?: string; trayInfoIdx?: string; + extruderId?: number; + caliIdx?: number | null; + savedPresetId?: string; } | null>(null); const [showFirmwareModal, setShowFirmwareModal] = useState(false); const [plateCheckResult, setPlateCheckResult] = useState<{ @@ -1516,8 +1557,8 @@ function PrinterCard({ } } } - if (status?.vt_tray?.tray_info_idx) { - ids.add(status.vt_tray.tray_info_idx); + for (const vt of status?.vt_tray ?? []) { + if (vt.tray_info_idx) ids.add(vt.tray_info_idx); } if (status?.nozzle_rack) { for (const slot of status.nozzle_rack) { @@ -1582,18 +1623,20 @@ function PrinterCard({ }, [status?.ams]); const amsData = (status?.ams && status.ams.length > 0) ? status.ams : cachedAmsData.current; - // Cache tray_now to prevent flickering when 255 (unloaded) or undefined values come in - // Only update cache when we get a valid tray ID (0-253 or 254 for external) - const cachedTrayNow = useRef(255); + // Cache tray_now to prevent flickering when undefined values come in + // Valid tray IDs: 0-253 for AMS, 254 for external spool + // tray_now=255 means "no tray loaded" (Bambu protocol sentinel) — never active + const cachedTrayNow = useRef(undefined); const currentTrayNow = status?.tray_now; - // Update cache synchronously during render if we have a valid value + // Update cache: 255 means "no tray" so clear cache; valid values get cached if (currentTrayNow !== undefined && currentTrayNow !== 255) { cachedTrayNow.current = currentTrayNow; + } else if (currentTrayNow === 255) { + cachedTrayNow.current = undefined; } - // Use cached value if current is 255/undefined but we had a valid value before - const effectiveTrayNow = (currentTrayNow === undefined || currentTrayNow === 255) - ? cachedTrayNow.current - : currentTrayNow; + const effectiveTrayNow = (currentTrayNow !== undefined && currentTrayNow !== 255) + ? currentTrayNow + : cachedTrayNow.current; // Fetch smart plug for this printer const { data: smartPlug } = useQuery({ @@ -2658,7 +2701,7 @@ function PrinterCard({ })()} {/* AMS Units - 2-Column Grid Layout */} - {amsData && amsData.length > 0 && viewMode === 'expanded' && (() => { + {(amsData?.length > 0 || status.vt_tray.length > 0) && viewMode === 'expanded' && (() => { // Separate regular AMS (4-tray) from HT AMS (1-tray) const regularAms = amsData.filter(ams => ams.tray.length > 1); const htAms = amsData.filter(ams => ams.tray.length === 1); @@ -2915,6 +2958,9 @@ function PrinterCard({ trayColor: tray?.tray_color || undefined, traySubBrands: tray?.tray_sub_brands || undefined, trayInfoIdx: tray?.tray_info_idx || undefined, + extruderId: mappedExtruderId, + caliIdx: tray?.cali_idx, + savedPresetId: slotPreset?.preset_id, }), }} > @@ -2928,6 +2974,7 @@ function PrinterCard({ amsId: ams.id, trayId: slotIdx, trayCount: ams.tray.length, + extruderId: mappedExtruderId, }), }} > @@ -2945,7 +2992,7 @@ function PrinterCard({ )} {/* Row 3: HT AMS + External spools (same style as regular AMS, 4 across) */} - {(htAms.length > 0 || (status.vt_tray && status.vt_tray.tray_type)) && ( + {(htAms.length > 0 || status.vt_tray.length > 0) && (
{/* HT AMS units - name/badge top, slot left, stats right */} {htAms.map((ams) => { @@ -3149,6 +3196,9 @@ function PrinterCard({ trayColor: tray?.tray_color || undefined, traySubBrands: tray?.tray_sub_brands || undefined, trayInfoIdx: tray?.tray_info_idx || undefined, + extruderId: mappedExtruderId, + caliIdx: tray?.cali_idx, + savedPresetId: slotPreset?.preset_id, }), }} > @@ -3162,6 +3212,7 @@ function PrinterCard({ amsId: ams.id, trayId: htSlotId, trayCount: ams.tray.length, + extruderId: mappedExtruderId, }), }} > @@ -3204,138 +3255,162 @@ function PrinterCard({
); })} - {/* External spool - name top, slot below (no stats) */} - {status.vt_tray && status.vt_tray.tray_type && (() => { - const extTray = status.vt_tray; - // Check if external spool is active (tray_now = 254) - const isExtActive = effectiveTrayNow === 254; - // Get cloud preset info if available - const extCloudInfo = extTray.tray_info_idx ? filamentInfo?.[extTray.tray_info_idx] : null; - // Get saved slot preset mapping (external spool uses amsId=255, trayId=0) - const extSlotPreset = slotPresets?.[255 * 4 + 0]; - - // Fill level fallback chain: Spoolman → Inventory spool - const extTrayTag = extTray.tray_uuid?.toUpperCase(); - const extLinkedSpool = extTrayTag ? linkedSpools?.[extTrayTag] : undefined; - const extSpoolmanFill = getSpoolmanFillLevel(extLinkedSpool); - const extInventoryAssignment = onGetAssignment?.(printer.id, 255, 0); - const extInventoryFill = (() => { - const sp = extInventoryAssignment?.spool; - if (sp && sp.label_weight > 0 && sp.weight_used > 0) { - return Math.round(Math.max(0, sp.label_weight - sp.weight_used) / sp.label_weight * 100); - } - return null; - })(); - const extEffectiveFill = extSpoolmanFill ?? extInventoryFill ?? null; - - // Build filament data for hover card - const extFilamentData = { - vendor: (isBambuLabSpool(extTray) ? 'Bambu Lab' : 'Generic') as 'Bambu Lab' | 'Generic', - profile: extCloudInfo?.name || extSlotPreset?.preset_name || extTray.tray_sub_brands || extTray.tray_type || 'Unknown', - colorName: getBambuColorName(extTray.tray_id_name) || hexToBasicColorName(extTray.tray_color), - colorHex: extTray.tray_color || null, - kFactor: formatKValue(extTray.k), - fillLevel: extEffectiveFill, - trayUuid: extTray.tray_uuid || null, - tagUid: extTray.tag_uid || null, - fillSource: extSpoolmanFill !== null ? 'spoolman' as const - : extInventoryFill !== null ? 'inventory' as const - : undefined, - }; - - const extSlotContent = ( -
-
-
- {extTray.tray_type || 'Spool'} -
- {/* Fill bar - use Spoolman data if available */} -
- {extSpoolmanFill !== null ? ( -
- ) : ( -
- )} -
+ {/* External spool(s) - grouped in one card like regular AMS */} + {status.vt_tray.length > 0 && ( +
+
+ {t('printers.external')}
- ); +
1 ? 'grid-cols-2' : 'grid-cols-1'} gap-1.5`}> + {[...status.vt_tray].sort((a, b) => (a.id ?? 254) - (b.id ?? 254)).map((extTray) => { + const extTrayId = extTray.id ?? 254; + const isExtActive = effectiveTrayNow === extTrayId; + const slotTrayId = extTrayId - 254; // 0 or 1 + const extLabel = isDualNozzle + ? (extTrayId === 254 ? t('printers.extL') : t('printers.extR')) + : ''; + const extCloudInfo = extTray.tray_info_idx ? filamentInfo?.[extTray.tray_info_idx] : null; + const extSlotPreset = slotPresets?.[255 * 4 + slotTrayId]; - return ( -
- {/* Row 1: Label */} -
- {t('printers.external')} -
- {/* Row 2: Slot (full width since no stats) */} - { - setLinkSpoolModal({ - tagUid: extFilamentData.tagUid || '', - trayUuid: uuid, - printerId: printer.id, - amsId: 255, - trayId: 0, - }); - } : undefined, - }} - inventory={(() => { - const assignment = onGetAssignment?.(printer.id, 255, 0); - return { - assignedSpool: assignment?.spool ? { - id: assignment.spool.id, - material: assignment.spool.material, - brand: assignment.spool.brand, - color_name: assignment.spool.color_name, - } : null, - onAssignSpool: extFilamentData.vendor !== 'Bambu Lab' ? () => setAssignSpoolModal({ - printerId: printer.id, - amsId: 255, - trayId: 0, - trayInfo: { - type: extFilamentData.profile, - color: extFilamentData.colorHex || '', - location: 'External Spool', - }, - }) : undefined, - onUnassignSpool: assignment && extFilamentData.vendor !== 'Bambu Lab' ? () => onUnassignSpool?.(printer.id, 255, 0) : undefined, - }; - })()} - configureSlot={{ - enabled: hasPermission('printers:control'), - onConfigure: () => setConfigureSlotModal({ - amsId: 255, // External spool indicator - trayId: 0, - trayCount: 1, // External = single slot - trayType: extTray.tray_type || undefined, - trayColor: extTray.tray_color || undefined, - traySubBrands: extTray.tray_sub_brands || undefined, - trayInfoIdx: extTray.tray_info_idx || undefined, - }), - }} - > - {extSlotContent} - + const extTrayTag = extTray.tray_uuid?.toUpperCase(); + const extLinkedSpool = extTrayTag ? linkedSpools?.[extTrayTag] : undefined; + const extSpoolmanFill = getSpoolmanFillLevel(extLinkedSpool); + const extInventoryAssignment = onGetAssignment?.(printer.id, 255, slotTrayId); + const extInventoryFill = (() => { + const sp = extInventoryAssignment?.spool; + if (sp && sp.label_weight > 0 && sp.weight_used > 0) { + return Math.round(Math.max(0, sp.label_weight - sp.weight_used) / sp.label_weight * 100); + } + return null; + })(); + const extEffectiveFill = extSpoolmanFill ?? extInventoryFill ?? null; + + const extFilamentData = { + vendor: (isBambuLabSpool(extTray) ? 'Bambu Lab' : 'Generic') as 'Bambu Lab' | 'Generic', + profile: extCloudInfo?.name || extSlotPreset?.preset_name || extTray.tray_sub_brands || extTray.tray_type || 'Unknown', + colorName: getBambuColorName(extTray.tray_id_name) || hexToBasicColorName(extTray.tray_color), + colorHex: extTray.tray_color || null, + kFactor: formatKValue(extTray.k), + fillLevel: extEffectiveFill, + trayUuid: extTray.tray_uuid || null, + tagUid: extTray.tag_uid || null, + fillSource: extSpoolmanFill !== null ? 'spoolman' as const + : extInventoryFill !== null ? 'inventory' as const + : undefined, + }; + + const isEmpty = !extTray.tray_type; + const extSlotContent = ( +
+
+
+ {extTray.tray_type || '—'} +
+
+ {extEffectiveFill !== null && extEffectiveFill >= 0 && !isEmpty ? ( +
+ ) : !isEmpty ? ( +
+ ) : null} +
+ {extLabel &&
{extLabel}
} +
+ ); + + return ( +
+ {!isEmpty ? ( + { + setLinkSpoolModal({ + tagUid: extFilamentData.tagUid || '', + trayUuid: uuid, + printerId: printer.id, + amsId: 255, + trayId: slotTrayId, + }); + } : undefined, + }} + inventory={(() => { + const assignment = onGetAssignment?.(printer.id, 255, slotTrayId); + return { + assignedSpool: assignment?.spool ? { + id: assignment.spool.id, + material: assignment.spool.material, + brand: assignment.spool.brand, + color_name: assignment.spool.color_name, + } : null, + onAssignSpool: () => setAssignSpoolModal({ + printerId: printer.id, + amsId: 255, + trayId: slotTrayId, + trayInfo: { + type: extFilamentData.profile, + color: extFilamentData.colorHex || '', + location: extLabel || t('printers.external'), + }, + }), + onUnassignSpool: assignment ? () => onUnassignSpool?.(printer.id, 255, slotTrayId) : undefined, + }; + })()} + configureSlot={{ + enabled: hasPermission('printers:control'), + onConfigure: () => setConfigureSlotModal({ + amsId: 255, + trayId: slotTrayId, + trayCount: 1, + trayType: extTray.tray_type || undefined, + trayColor: extTray.tray_color || undefined, + traySubBrands: extTray.tray_sub_brands || undefined, + trayInfoIdx: extTray.tray_info_idx || undefined, + extruderId: isDualNozzle ? (extTrayId === 254 ? 1 : 0) : undefined, + caliIdx: extTray.cali_idx, + savedPresetId: extSlotPreset?.preset_id, + }), + }} + > + {extSlotContent} + + ) : ( + setConfigureSlotModal({ + amsId: 255, + trayId: slotTrayId, + trayCount: 1, + extruderId: isDualNozzle ? (extTrayId === 254 ? 1 : 0) : undefined, + }), + }} + > + {extSlotContent} + + )} +
+ ); + })}
- ); - })()} +
+ )}
)}
@@ -3974,6 +4049,7 @@ function PrinterCard({ onClose={() => setConfigureSlotModal(null)} printerId={printer.id} slotInfo={configureSlotModal} + printerModel={mapModelCode(printer.model) || undefined} onSuccess={() => { // Refresh slot presets to show updated profile name queryClient.invalidateQueries({ queryKey: ['slotPresets', printer.id] }); @@ -4128,43 +4204,7 @@ function AddPrinterModal({ } }; - // Map SSDP model codes to dropdown values - const mapModelCode = (ssdpModel: string | null): string => { - if (!ssdpModel) return ''; - const modelMap: Record = { - // H2 Series - 'O1D': 'H2D', - 'O1E': 'H2D Pro', // Some devices report O1E - 'O2D': 'H2D Pro', // Some devices report O2D - 'O1C': 'H2C', - 'O1S': 'H2S', - // X1 Series - 'BL-P001': 'X1C', - 'BL-P002': 'X1', - 'BL-P003': 'X1E', - // P Series - 'C11': 'P1S', - 'C12': 'P1P', - 'C13': 'P2S', - // A1 Series - 'N2S': 'A1', - 'N1': 'A1 Mini', - // Direct matches - 'X1C': 'X1C', - 'X1': 'X1', - 'X1E': 'X1E', - 'P1S': 'P1S', - 'P1P': 'P1P', - 'P2S': 'P2S', - 'A1': 'A1', - 'A1 Mini': 'A1 Mini', - 'H2D': 'H2D', - 'H2D Pro': 'H2D Pro', - 'H2C': 'H2C', - 'H2S': 'H2S', - }; - return modelMap[ssdpModel] || ssdpModel; - }; + // Reuse module-level mapModelCode const selectPrinter = (printer: DiscoveredPrinter) => { // Don't pre-fill serial if it's a placeholder (unknown-*) - user needs to enter actual serial diff --git a/frontend/src/utils/amsHelpers.ts b/frontend/src/utils/amsHelpers.ts index 5e4dbe946..e1af13e06 100644 --- a/frontend/src/utils/amsHelpers.ts +++ b/frontend/src/utils/amsHelpers.ts @@ -88,7 +88,7 @@ export function getGlobalTrayId( trayId: number, isExternal: boolean ): number { - if (isExternal) return 254; + if (isExternal) return 254 + trayId; // AMS-HT units have IDs starting at 128 with a single tray — use ID directly if (amsId >= 128) return amsId; return amsId * 4 + trayId; diff --git a/static/assets/index-BOd5pCVD.js b/static/assets/index-BOd5pCVD.js deleted file mode 100644 index 062cc45fd..000000000 --- a/static/assets/index-BOd5pCVD.js +++ /dev/null @@ -1,8684 +0,0 @@ -function pse(t,e){for(var n=0;nr[i]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function r(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();var qw=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function wl(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var uT={exports:{}},zx={};var gU;function gse(){if(gU)return zx;gU=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function n(r,i,s){var o=null;if(s!==void 0&&(o=""+s),i.key!==void 0&&(o=""+i.key),"key"in i){s={};for(var l in i)l!=="key"&&(s[l]=i[l])}else s=i;return i=s.ref,{$$typeof:t,type:r,key:o,ref:i!==void 0?i:null,props:s}}return zx.Fragment=e,zx.jsx=n,zx.jsxs=n,zx}var bU;function bse(){return bU||(bU=1,uT.exports=gse()),uT.exports}var a=bse(),dT={exports:{}},Nn={};var xU;function xse(){if(xU)return Nn;xU=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),s=Symbol.for("react.consumer"),o=Symbol.for("react.context"),l=Symbol.for("react.forward_ref"),c=Symbol.for("react.suspense"),u=Symbol.for("react.memo"),d=Symbol.for("react.lazy"),h=Symbol.for("react.activity"),p=Symbol.iterator;function f(V){return V===null||typeof V!="object"?null:(V=p&&V[p]||V["@@iterator"],typeof V=="function"?V:null)}var x={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},v=Object.assign,b={};function y(V,W,ie){this.props=V,this.context=W,this.refs=b,this.updater=ie||x}y.prototype.isReactComponent={},y.prototype.setState=function(V,W){if(typeof V!="object"&&typeof V!="function"&&V!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,V,W,"setState")},y.prototype.forceUpdate=function(V){this.updater.enqueueForceUpdate(this,V,"forceUpdate")};function S(){}S.prototype=y.prototype;function N(V,W,ie){this.props=V,this.context=W,this.refs=b,this.updater=ie||x}var A=N.prototype=new S;A.constructor=N,v(A,y.prototype),A.isPureReactComponent=!0;var k=Array.isArray;function T(){}var C={H:null,A:null,T:null,S:null},j=Object.prototype.hasOwnProperty;function E(V,W,ie){var Z=ie.ref;return{$$typeof:t,type:V,key:W,ref:Z!==void 0?Z:null,props:ie}}function P(V,W){return E(V.type,W,V.props)}function U(V){return typeof V=="object"&&V!==null&&V.$$typeof===t}function L(V){var W={"=":"=0",":":"=2"};return"$"+V.replace(/[=:]/g,function(ie){return W[ie]})}var H=/\/+/g;function D(V,W){return typeof V=="object"&&V!==null&&V.key!=null?L(""+V.key):W.toString(36)}function $(V){switch(V.status){case"fulfilled":return V.value;case"rejected":throw V.reason;default:switch(typeof V.status=="string"?V.then(T,T):(V.status="pending",V.then(function(W){V.status==="pending"&&(V.status="fulfilled",V.value=W)},function(W){V.status==="pending"&&(V.status="rejected",V.reason=W)})),V.status){case"fulfilled":return V.value;case"rejected":throw V.reason}}throw V}function q(V,W,ie,Z,fe){var he=typeof V;(he==="undefined"||he==="boolean")&&(V=null);var ye=!1;if(V===null)ye=!0;else switch(he){case"bigint":case"string":case"number":ye=!0;break;case"object":switch(V.$$typeof){case t:case e:ye=!0;break;case d:return ye=V._init,q(ye(V._payload),W,ie,Z,fe)}}if(ye)return fe=fe(V),ye=Z===""?"."+D(V,0):Z,k(fe)?(ie="",ye!=null&&(ie=ye.replace(H,"$&/")+"/"),q(fe,W,ie,"",function(le){return le})):fe!=null&&(U(fe)&&(fe=P(fe,ie+(fe.key==null||V&&V.key===fe.key?"":(""+fe.key).replace(H,"$&/")+"/")+ye)),W.push(fe)),1;ye=0;var Fe=Z===""?".":Z+":";if(k(V))for(var ue=0;ue>>1,se=q[Q];if(0>>1;Qi(ie,X))Zi(fe,ie)?(q[Q]=fe,q[Z]=X,Q=Z):(q[Q]=ie,q[W]=X,Q=W);else if(Zi(fe,X))q[Q]=fe,q[Z]=X,Q=Z;else break e}}return I}function i(q,I){var X=q.sortIndex-I.sortIndex;return X!==0?X:q.id-I.id}if(t.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var s=performance;t.unstable_now=function(){return s.now()}}else{var o=Date,l=o.now();t.unstable_now=function(){return o.now()-l}}var c=[],u=[],d=1,h=null,p=3,f=!1,x=!1,v=!1,b=!1,y=typeof setTimeout=="function"?setTimeout:null,S=typeof clearTimeout=="function"?clearTimeout:null,N=typeof setImmediate<"u"?setImmediate:null;function A(q){for(var I=n(u);I!==null;){if(I.callback===null)r(u);else if(I.startTime<=q)r(u),I.sortIndex=I.expirationTime,e(c,I);else break;I=n(u)}}function k(q){if(v=!1,A(q),!x)if(n(c)!==null)x=!0,T||(T=!0,L());else{var I=n(u);I!==null&&$(k,I.startTime-q)}}var T=!1,C=-1,j=5,E=-1;function P(){return b?!0:!(t.unstable_now()-Eq&&P());){var Q=h.callback;if(typeof Q=="function"){h.callback=null,p=h.priorityLevel;var se=Q(h.expirationTime<=q);if(q=t.unstable_now(),typeof se=="function"){h.callback=se,A(q),I=!0;break t}h===n(c)&&r(c),A(q)}else r(c);h=n(c)}if(h!==null)I=!0;else{var V=n(u);V!==null&&$(k,V.startTime-q),I=!1}}break e}finally{h=null,p=X,f=!1}I=void 0}}finally{I?L():T=!1}}}var L;if(typeof N=="function")L=function(){N(U)};else if(typeof MessageChannel<"u"){var H=new MessageChannel,D=H.port2;H.port1.onmessage=U,L=function(){D.postMessage(null)}}else L=function(){y(U,0)};function $(q,I){C=y(function(){q(t.unstable_now())},I)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(q){q.callback=null},t.unstable_forceFrameRate=function(q){0>q||125Q?(q.sortIndex=X,e(u,q),n(c)===null&&q===n(u)&&(v?(S(C),C=-1):v=!0,$(k,X-Q))):(q.sortIndex=se,e(c,q),x||f||(x=!0,T||(T=!0,L()))),q},t.unstable_shouldYield=P,t.unstable_wrapCallback=function(q){var I=p;return function(){var X=p;p=I;try{return q.apply(this,arguments)}finally{p=X}}}})(fT)),fT}var wU;function wse(){return wU||(wU=1,mT.exports=vse()),mT.exports}var pT={exports:{}},ns={};var _U;function _se(){if(_U)return ns;_U=1;var t=Mb();function e(c){var u="https://react.dev/errors/"+c;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(e){console.error(e)}}return t(),pT.exports=_se(),pT.exports}var NU;function Sse(){if(NU)return Ux;NU=1;var t=wse(),e=Mb(),n=rW();function r(m){var g="https://react.dev/errors/"+m;if(1se||(m.current=Q[se],Q[se]=null,se--)}function ie(m,g){se++,Q[se]=m.current,m.current=g}var Z=V(null),fe=V(null),he=V(null),ye=V(null);function Fe(m,g){switch(ie(he,g),ie(fe,m),ie(Z,null),g.nodeType){case 9:case 11:m=(m=g.documentElement)&&(m=m.namespaceURI)?zz(m):0;break;default:if(m=g.tagName,g=g.namespaceURI)g=zz(g),m=Uz(g,m);else switch(m){case"svg":m=1;break;case"math":m=2;break;default:m=0}}W(Z),ie(Z,m)}function ue(){W(Z),W(fe),W(he)}function le(m){m.memoizedState!==null&&ie(ye,m);var g=Z.current,_=Uz(g,m.type);g!==_&&(ie(fe,m),ie(Z,_))}function Te(m){fe.current===m&&(W(Z),W(fe)),ye.current===m&&(W(ye),Fx._currentValue=X)}var re,Ie;function G(m){if(re===void 0)try{throw Error()}catch(_){var g=_.stack.trim().match(/\n( *(at )?)/);re=g&&g[1]||"",Ie=-1<_.stack.indexOf(` - at`)?" ()":-1<_.stack.indexOf("@")?"@unknown:0:0":""}return` -`+re+m+Ie}var Y=!1;function ce(m,g){if(!m||Y)return"";Y=!0;var _=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var M={DetermineComponentFrameRoot:function(){try{if(g){var kt=function(){throw Error()};if(Object.defineProperty(kt.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(kt,[])}catch(xt){var mt=xt}Reflect.construct(m,[],kt)}else{try{kt.call()}catch(xt){mt=xt}m.call(kt.prototype)}}else{try{throw Error()}catch(xt){mt=xt}(kt=m())&&typeof kt.catch=="function"&&kt.catch(function(){})}}catch(xt){if(xt&&mt&&typeof xt.stack=="string")return[xt.stack,mt.stack]}return[null,null]}};M.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var R=Object.getOwnPropertyDescriptor(M.DetermineComponentFrameRoot,"name");R&&R.configurable&&Object.defineProperty(M.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var O=M.DetermineComponentFrameRoot(),ne=O[0],Ne=O[1];if(ne&&Ne){var Be=ne.split(` -`),ut=Ne.split(` -`);for(R=M=0;MR||Be[M]!==ut[R]){var vt=` -`+Be[M].replace(" at new "," at ");return m.displayName&&vt.includes("")&&(vt=vt.replace("",m.displayName)),vt}while(1<=M&&0<=R);break}}}finally{Y=!1,Error.prepareStackTrace=_}return(_=m?m.displayName||m.name:"")?G(_):""}function B(m,g){switch(m.tag){case 26:case 27:case 5:return G(m.type);case 16:return G("Lazy");case 13:return m.child!==g&&g!==null?G("Suspense Fallback"):G("Suspense");case 19:return G("SuspenseList");case 0:case 15:return ce(m.type,!1);case 11:return ce(m.type.render,!1);case 1:return ce(m.type,!0);case 31:return G("Activity");default:return""}}function J(m){try{var g="",_=null;do g+=B(m,_),_=m,m=m.return;while(m);return g}catch(M){return` -Error generating stack: `+M.message+` -`+M.stack}}var te=Object.prototype.hasOwnProperty,de=t.unstable_scheduleCallback,me=t.unstable_cancelCallback,ee=t.unstable_shouldYield,z=t.unstable_requestPaint,F=t.unstable_now,K=t.unstable_getCurrentPriorityLevel,ve=t.unstable_ImmediatePriority,Se=t.unstable_UserBlockingPriority,Ce=t.unstable_NormalPriority,He=t.unstable_LowPriority,we=t.unstable_IdlePriority,$e=t.log,Qe=t.unstable_setDisableYieldValue,be=null,xe=null;function _e(m){if(typeof $e=="function"&&Qe(m),xe&&typeof xe.setStrictMode=="function")try{xe.setStrictMode(be,m)}catch{}}var Ue=Math.clz32?Math.clz32:Me,je=Math.log,Ve=Math.LN2;function Me(m){return m>>>=0,m===0?32:31-(je(m)/Ve|0)|0}var Ze=256,Pe=262144,Xe=4194304;function Re(m){var g=m&42;if(g!==0)return g;switch(m&-m){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return m&261888;case 262144:case 524288:case 1048576:case 2097152:return m&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return m&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return m}}function Le(m,g,_){var M=m.pendingLanes;if(M===0)return 0;var R=0,O=m.suspendedLanes,ne=m.pingedLanes;m=m.warmLanes;var Ne=M&134217727;return Ne!==0?(M=Ne&~O,M!==0?R=Re(M):(ne&=Ne,ne!==0?R=Re(ne):_||(_=Ne&~m,_!==0&&(R=Re(_))))):(Ne=M&~O,Ne!==0?R=Re(Ne):ne!==0?R=Re(ne):_||(_=M&~m,_!==0&&(R=Re(_)))),R===0?0:g!==0&&g!==R&&(g&O)===0&&(O=R&-R,_=g&-g,O>=_||O===32&&(_&4194048)!==0)?g:R}function ke(m,g){return(m.pendingLanes&~(m.suspendedLanes&~m.pingedLanes)&g)===0}function Je(m,g){switch(m){case 1:case 2:case 4:case 8:case 64:return g+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return g+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function yt(){var m=Xe;return Xe<<=1,(Xe&62914560)===0&&(Xe=4194304),m}function At(m){for(var g=[],_=0;31>_;_++)g.push(m);return g}function an(m,g){m.pendingLanes|=g,g!==268435456&&(m.suspendedLanes=0,m.pingedLanes=0,m.warmLanes=0)}function Pt(m,g,_,M,R,O){var ne=m.pendingLanes;m.pendingLanes=_,m.suspendedLanes=0,m.pingedLanes=0,m.warmLanes=0,m.expiredLanes&=_,m.entangledLanes&=_,m.errorRecoveryDisabledLanes&=_,m.shellSuspendCounter=0;var Ne=m.entanglements,Be=m.expirationTimes,ut=m.hiddenUpdates;for(_=ne&~_;0<_;){var vt=31-Ue(_),kt=1<"u")return null;try{return m.activeElement||m.body}catch{return m.body}}var Vr=/[\n"\\]/g;function Pn(m){return m.replace(Vr,function(g){return"\\"+g.charCodeAt(0).toString(16)+" "})}function _n(m,g,_,M,R,O,ne,Ne){m.name="",ne!=null&&typeof ne!="function"&&typeof ne!="symbol"&&typeof ne!="boolean"?m.type=ne:m.removeAttribute("type"),g!=null?ne==="number"?(g===0&&m.value===""||m.value!=g)&&(m.value=""+Mn(g)):m.value!==""+Mn(g)&&(m.value=""+Mn(g)):ne!=="submit"&&ne!=="reset"||m.removeAttribute("value"),g!=null?fi(m,ne,Mn(g)):_!=null?fi(m,ne,Mn(_)):M!=null&&m.removeAttribute("value"),R==null&&O!=null&&(m.defaultChecked=!!O),R!=null&&(m.checked=R&&typeof R!="function"&&typeof R!="symbol"),Ne!=null&&typeof Ne!="function"&&typeof Ne!="symbol"&&typeof Ne!="boolean"?m.name=""+Mn(Ne):m.removeAttribute("name")}function Hn(m,g,_,M,R,O,ne,Ne){if(O!=null&&typeof O!="function"&&typeof O!="symbol"&&typeof O!="boolean"&&(m.type=O),g!=null||_!=null){if(!(O!=="submit"&&O!=="reset"||g!=null)){fn(m);return}_=_!=null?""+Mn(_):"",g=g!=null?""+Mn(g):_,Ne||g===m.value||(m.value=g),m.defaultValue=g}M=M??R,M=typeof M!="function"&&typeof M!="symbol"&&!!M,m.checked=Ne?m.checked:!!M,m.defaultChecked=!!M,ne!=null&&typeof ne!="function"&&typeof ne!="symbol"&&typeof ne!="boolean"&&(m.name=ne),fn(m)}function fi(m,g,_){g==="number"&&$r(m.ownerDocument)===m||m.defaultValue===""+_||(m.defaultValue=""+_)}function hr(m,g,_,M){if(m=m.options,g){g={};for(var R=0;R<_.length;R++)g["$"+_[R]]=!0;for(_=0;_"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),kl=!1;if(Sa)try{var Cl={};Object.defineProperty(Cl,"passive",{get:function(){kl=!0}}),window.addEventListener("test",Cl,Cl),window.removeEventListener("test",Cl,Cl)}catch{kl=!1}var Ia=null,Si=null,rc=null;function Gr(){if(rc)return rc;var m,g=Si,_=g.length,M,R="value"in Ia?Ia.value:Ia.textContent,O=R.length;for(m=0;m<_&&g[m]===R[m];m++);var ne=_-m;for(M=1;M<=ne&&g[_-M]===R[O-M];M++);return rc=R.slice(m,1=nx),q4=" ",W4=!1;function K4(m,g){switch(m){case"keyup":return Oie.indexOf(g.keyCode)!==-1;case"keydown":return g.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function X4(m){return m=m.detail,typeof m=="object"&&"data"in m?m.data:null}var Zf=!1;function zie(m,g){switch(m){case"compositionend":return X4(g);case"keypress":return g.which!==32?null:(W4=!0,q4);case"textInput":return m=g.data,m===q4&&W4?null:m;default:return null}}function Uie(m,g){if(Zf)return m==="compositionend"||!gM&&K4(m,g)?(m=Gr(),rc=Si=Ia=null,Zf=!1,m):null;switch(m){case"paste":return null;case"keypress":if(!(g.ctrlKey||g.altKey||g.metaKey)||g.ctrlKey&&g.altKey){if(g.char&&1=g)return{node:_,offset:g-m};m=M}e:{for(;_;){if(_.nextSibling){_=_.nextSibling;break e}_=_.parentNode}_=void 0}_=rI(_)}}function aI(m,g){return m&&g?m===g?!0:m&&m.nodeType===3?!1:g&&g.nodeType===3?aI(m,g.parentNode):"contains"in m?m.contains(g):m.compareDocumentPosition?!!(m.compareDocumentPosition(g)&16):!1:!1}function sI(m){m=m!=null&&m.ownerDocument!=null&&m.ownerDocument.defaultView!=null?m.ownerDocument.defaultView:window;for(var g=$r(m.document);g instanceof m.HTMLIFrameElement;){try{var _=typeof g.contentWindow.location.href=="string"}catch{_=!1}if(_)m=g.contentWindow;else break;g=$r(m.document)}return g}function yM(m){var g=m&&m.nodeName&&m.nodeName.toLowerCase();return g&&(g==="input"&&(m.type==="text"||m.type==="search"||m.type==="tel"||m.type==="url"||m.type==="password")||g==="textarea"||m.contentEditable==="true")}var Kie=Sa&&"documentMode"in document&&11>=document.documentMode,Jf=null,vM=null,sx=null,wM=!1;function oI(m,g,_){var M=_.window===_?_.document:_.nodeType===9?_:_.ownerDocument;wM||Jf==null||Jf!==$r(M)||(M=Jf,"selectionStart"in M&&yM(M)?M={start:M.selectionStart,end:M.selectionEnd}:(M=(M.ownerDocument&&M.ownerDocument.defaultView||window).getSelection(),M={anchorNode:M.anchorNode,anchorOffset:M.anchorOffset,focusNode:M.focusNode,focusOffset:M.focusOffset}),sx&&ax(sx,M)||(sx=M,M=Pw(vM,"onSelect"),0>=ne,R-=ne,ac=1<<32-Ue(g)+R|_<Tn?(Qn=Yt,Yt=null):Qn=Yt.sibling;var lr=mt(tt,Yt,ct[Tn],St);if(lr===null){Yt===null&&(Yt=Qn);break}m&&Yt&&lr.alternate===null&&g(tt,Yt),qe=O(lr,qe,Tn),or===null?nn=lr:or.sibling=lr,or=lr,Yt=Qn}if(Tn===ct.length)return _(tt,Yt),er&&qc(tt,Tn),nn;if(Yt===null){for(;TnTn?(Qn=Yt,Yt=null):Qn=Yt.sibling;var Id=mt(tt,Yt,lr.value,St);if(Id===null){Yt===null&&(Yt=Qn);break}m&&Yt&&Id.alternate===null&&g(tt,Yt),qe=O(Id,qe,Tn),or===null?nn=Id:or.sibling=Id,or=Id,Yt=Qn}if(lr.done)return _(tt,Yt),er&&qc(tt,Tn),nn;if(Yt===null){for(;!lr.done;Tn++,lr=ct.next())lr=kt(tt,lr.value,St),lr!==null&&(qe=O(lr,qe,Tn),or===null?nn=lr:or.sibling=lr,or=lr);return er&&qc(tt,Tn),nn}for(Yt=M(Yt);!lr.done;Tn++,lr=ct.next())lr=xt(Yt,tt,Tn,lr.value,St),lr!==null&&(m&&lr.alternate!==null&&Yt.delete(lr.key===null?Tn:lr.key),qe=O(lr,qe,Tn),or===null?nn=lr:or.sibling=lr,or=lr);return m&&Yt.forEach(function(fse){return g(tt,fse)}),er&&qc(tt,Tn),nn}function Mr(tt,qe,ct,St){if(typeof ct=="object"&&ct!==null&&ct.type===v&&ct.key===null&&(ct=ct.props.children),typeof ct=="object"&&ct!==null){switch(ct.$$typeof){case f:e:{for(var nn=ct.key;qe!==null;){if(qe.key===nn){if(nn=ct.type,nn===v){if(qe.tag===7){_(tt,qe.sibling),St=R(qe,ct.props.children),St.return=tt,tt=St;break e}}else if(qe.elementType===nn||typeof nn=="object"&&nn!==null&&nn.$$typeof===j&&pm(nn)===qe.type){_(tt,qe.sibling),St=R(qe,ct.props),hx(St,ct),St.return=tt,tt=St;break e}_(tt,qe);break}else g(tt,qe);qe=qe.sibling}ct.type===v?(St=um(ct.props.children,tt.mode,St,ct.key),St.return=tt,tt=St):(St=Kv(ct.type,ct.key,ct.props,null,tt.mode,St),hx(St,ct),St.return=tt,tt=St)}return ne(tt);case x:e:{for(nn=ct.key;qe!==null;){if(qe.key===nn)if(qe.tag===4&&qe.stateNode.containerInfo===ct.containerInfo&&qe.stateNode.implementation===ct.implementation){_(tt,qe.sibling),St=R(qe,ct.children||[]),St.return=tt,tt=St;break e}else{_(tt,qe);break}else g(tt,qe);qe=qe.sibling}St=AM(ct,tt.mode,St),St.return=tt,tt=St}return ne(tt);case j:return ct=pm(ct),Mr(tt,qe,ct,St)}if($(ct))return Wt(tt,qe,ct,St);if(L(ct)){if(nn=L(ct),typeof nn!="function")throw Error(r(150));return ct=nn.call(ct),dn(tt,qe,ct,St)}if(typeof ct.then=="function")return Mr(tt,qe,tw(ct),St);if(ct.$$typeof===N)return Mr(tt,qe,Qv(tt,ct),St);nw(tt,ct)}return typeof ct=="string"&&ct!==""||typeof ct=="number"||typeof ct=="bigint"?(ct=""+ct,qe!==null&&qe.tag===6?(_(tt,qe.sibling),St=R(qe,ct),St.return=tt,tt=St):(_(tt,qe),St=MM(ct,tt.mode,St),St.return=tt,tt=St),ne(tt)):_(tt,qe)}return function(tt,qe,ct,St){try{dx=0;var nn=Mr(tt,qe,ct,St);return up=null,nn}catch(Yt){if(Yt===cp||Yt===Jv)throw Yt;var or=lo(29,Yt,null,tt.mode);return or.lanes=St,or.return=tt,or}finally{}}}var bm=EI(!0),PI=EI(!1),wd=!1;function UM(m){m.updateQueue={baseState:m.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function BM(m,g){m=m.updateQueue,g.updateQueue===m&&(g.updateQueue={baseState:m.baseState,firstBaseUpdate:m.firstBaseUpdate,lastBaseUpdate:m.lastBaseUpdate,shared:m.shared,callbacks:null})}function _d(m){return{lane:m,tag:0,payload:null,callback:null,next:null}}function Sd(m,g,_){var M=m.updateQueue;if(M===null)return null;if(M=M.shared,(mr&2)!==0){var R=M.pending;return R===null?g.next=g:(g.next=R.next,R.next=g),M.pending=g,g=Wv(m),fI(m,null,_),g}return qv(m,M,g,_),Wv(m)}function mx(m,g,_){if(g=g.updateQueue,g!==null&&(g=g.shared,(_&4194048)!==0)){var M=g.lanes;M&=m.pendingLanes,_|=M,g.lanes=_,gt(m,_)}}function HM(m,g){var _=m.updateQueue,M=m.alternate;if(M!==null&&(M=M.updateQueue,_===M)){var R=null,O=null;if(_=_.firstBaseUpdate,_!==null){do{var ne={lane:_.lane,tag:_.tag,payload:_.payload,callback:null,next:null};O===null?R=O=ne:O=O.next=ne,_=_.next}while(_!==null);O===null?R=O=g:O=O.next=g}else R=O=g;_={baseState:M.baseState,firstBaseUpdate:R,lastBaseUpdate:O,shared:M.shared,callbacks:M.callbacks},m.updateQueue=_;return}m=_.lastBaseUpdate,m===null?_.firstBaseUpdate=g:m.next=g,_.lastBaseUpdate=g}var $M=!1;function fx(){if($M){var m=lp;if(m!==null)throw m}}function px(m,g,_,M){$M=!1;var R=m.updateQueue;wd=!1;var O=R.firstBaseUpdate,ne=R.lastBaseUpdate,Ne=R.shared.pending;if(Ne!==null){R.shared.pending=null;var Be=Ne,ut=Be.next;Be.next=null,ne===null?O=ut:ne.next=ut,ne=Be;var vt=m.alternate;vt!==null&&(vt=vt.updateQueue,Ne=vt.lastBaseUpdate,Ne!==ne&&(Ne===null?vt.firstBaseUpdate=ut:Ne.next=ut,vt.lastBaseUpdate=Be))}if(O!==null){var kt=R.baseState;ne=0,vt=ut=Be=null,Ne=O;do{var mt=Ne.lane&-536870913,xt=mt!==Ne.lane;if(xt?(Yn&mt)===mt:(M&mt)===mt){mt!==0&&mt===op&&($M=!0),vt!==null&&(vt=vt.next={lane:0,tag:Ne.tag,payload:Ne.payload,callback:null,next:null});e:{var Wt=m,dn=Ne;mt=g;var Mr=_;switch(dn.tag){case 1:if(Wt=dn.payload,typeof Wt=="function"){kt=Wt.call(Mr,kt,mt);break e}kt=Wt;break e;case 3:Wt.flags=Wt.flags&-65537|128;case 0:if(Wt=dn.payload,mt=typeof Wt=="function"?Wt.call(Mr,kt,mt):Wt,mt==null)break e;kt=h({},kt,mt);break e;case 2:wd=!0}}mt=Ne.callback,mt!==null&&(m.flags|=64,xt&&(m.flags|=8192),xt=R.callbacks,xt===null?R.callbacks=[mt]:xt.push(mt))}else xt={lane:mt,tag:Ne.tag,payload:Ne.payload,callback:Ne.callback,next:null},vt===null?(ut=vt=xt,Be=kt):vt=vt.next=xt,ne|=mt;if(Ne=Ne.next,Ne===null){if(Ne=R.shared.pending,Ne===null)break;xt=Ne,Ne=xt.next,xt.next=null,R.lastBaseUpdate=xt,R.shared.pending=null}}while(!0);vt===null&&(Be=kt),R.baseState=Be,R.firstBaseUpdate=ut,R.lastBaseUpdate=vt,O===null&&(R.shared.lanes=0),Ad|=ne,m.lanes=ne,m.memoizedState=kt}}function jI(m,g){if(typeof m!="function")throw Error(r(191,m));m.call(g)}function DI(m,g){var _=m.callbacks;if(_!==null)for(m.callbacks=null,m=0;m<_.length;m++)jI(_[m],g)}var dp=V(null),rw=V(0);function RI(m,g){m=ru,ie(rw,m),ie(dp,g),ru=m|g.baseLanes}function VM(){ie(rw,ru),ie(dp,dp.current)}function GM(){ru=rw.current,W(dp),W(rw)}var co=V(null),$o=null;function Nd(m){var g=m.alternate;ie(Fi,Fi.current&1),ie(co,m),$o===null&&(g===null||dp.current!==null||g.memoizedState!==null)&&($o=m)}function qM(m){ie(Fi,Fi.current),ie(co,m),$o===null&&($o=m)}function FI(m){m.tag===22?(ie(Fi,Fi.current),ie(co,m),$o===null&&($o=m)):kd()}function kd(){ie(Fi,Fi.current),ie(co,co.current)}function uo(m){W(co),$o===m&&($o=null),W(Fi)}var Fi=V(0);function iw(m){for(var g=m;g!==null;){if(g.tag===13){var _=g.memoizedState;if(_!==null&&(_=_.dehydrated,_===null||ZA(_)||JA(_)))return g}else if(g.tag===19&&(g.memoizedProps.revealOrder==="forwards"||g.memoizedProps.revealOrder==="backwards"||g.memoizedProps.revealOrder==="unstable_legacy-backwards"||g.memoizedProps.revealOrder==="together")){if((g.flags&128)!==0)return g}else if(g.child!==null){g.child.return=g,g=g.child;continue}if(g===m)break;for(;g.sibling===null;){if(g.return===null||g.return===m)return null;g=g.return}g.sibling.return=g.return,g=g.sibling}return null}var Xc=0,An=null,kr=null,Vi=null,aw=!1,hp=!1,xm=!1,sw=0,gx=0,mp=null,iae=0;function ki(){throw Error(r(321))}function WM(m,g){if(g===null)return!1;for(var _=0;_O?O:8;var ne=q.T,Ne={};q.T=Ne,lA(m,!1,g,_);try{var Be=R(),ut=q.S;if(ut!==null&&ut(Ne,Be),Be!==null&&typeof Be=="object"&&typeof Be.then=="function"){var vt=rae(Be,M);xx(m,g,vt,fo(m))}else xx(m,g,M,fo(m))}catch(kt){xx(m,g,{then:function(){},status:"rejected",reason:kt},fo())}finally{I.p=O,ne!==null&&Ne.types!==null&&(ne.types=Ne.types),q.T=ne}}function cae(){}function sA(m,g,_,M){if(m.tag!==5)throw Error(r(476));var R=d5(m).queue;u5(m,R,g,X,_===null?cae:function(){return h5(m),_(M)})}function d5(m){var g=m.memoizedState;if(g!==null)return g;g={memoizedState:X,baseState:X,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Yc,lastRenderedState:X},next:null};var _={};return g.next={memoizedState:_,baseState:_,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Yc,lastRenderedState:_},next:null},m.memoizedState=g,m=m.alternate,m!==null&&(m.memoizedState=g),g}function h5(m){var g=d5(m);g.next===null&&(g=m.alternate.memoizedState),xx(m,g.next.queue,{},fo())}function oA(){return Ua(Fx)}function m5(){return Li().memoizedState}function f5(){return Li().memoizedState}function uae(m){for(var g=m.return;g!==null;){switch(g.tag){case 24:case 3:var _=fo();m=_d(_);var M=Sd(g,m,_);M!==null&&(Hs(M,g,_),mx(M,g,_)),g={cache:LM()},m.payload=g;return}g=g.return}}function dae(m,g,_){var M=fo();_={lane:M,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null},hw(m)?g5(g,_):(_=kM(m,g,_,M),_!==null&&(Hs(_,m,M),b5(_,g,M)))}function p5(m,g,_){var M=fo();xx(m,g,_,M)}function xx(m,g,_,M){var R={lane:M,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null};if(hw(m))g5(g,R);else{var O=m.alternate;if(m.lanes===0&&(O===null||O.lanes===0)&&(O=g.lastRenderedReducer,O!==null))try{var ne=g.lastRenderedState,Ne=O(ne,_);if(R.hasEagerState=!0,R.eagerState=Ne,oo(Ne,ne))return qv(m,g,R,0),Or===null&&Gv(),!1}catch{}finally{}if(_=kM(m,g,R,M),_!==null)return Hs(_,m,M),b5(_,g,M),!0}return!1}function lA(m,g,_,M){if(M={lane:2,revertLane:UA(),gesture:null,action:M,hasEagerState:!1,eagerState:null,next:null},hw(m)){if(g)throw Error(r(479))}else g=kM(m,_,M,2),g!==null&&Hs(g,m,2)}function hw(m){var g=m.alternate;return m===An||g!==null&&g===An}function g5(m,g){hp=aw=!0;var _=m.pending;_===null?g.next=g:(g.next=_.next,_.next=g),m.pending=g}function b5(m,g,_){if((_&4194048)!==0){var M=g.lanes;M&=m.pendingLanes,_|=M,g.lanes=_,gt(m,_)}}var yx={readContext:Ua,use:lw,useCallback:ki,useContext:ki,useEffect:ki,useImperativeHandle:ki,useLayoutEffect:ki,useInsertionEffect:ki,useMemo:ki,useReducer:ki,useRef:ki,useState:ki,useDebugValue:ki,useDeferredValue:ki,useTransition:ki,useSyncExternalStore:ki,useId:ki,useHostTransitionStatus:ki,useFormState:ki,useActionState:ki,useOptimistic:ki,useMemoCache:ki,useCacheRefresh:ki};yx.useEffectEvent=ki;var x5={readContext:Ua,use:lw,useCallback:function(m,g){return ws().memoizedState=[m,g===void 0?null:g],m},useContext:Ua,useEffect:t5,useImperativeHandle:function(m,g,_){_=_!=null?_.concat([m]):null,uw(4194308,4,a5.bind(null,g,m),_)},useLayoutEffect:function(m,g){return uw(4194308,4,m,g)},useInsertionEffect:function(m,g){uw(4,2,m,g)},useMemo:function(m,g){var _=ws();g=g===void 0?null:g;var M=m();if(xm){_e(!0);try{m()}finally{_e(!1)}}return _.memoizedState=[M,g],M},useReducer:function(m,g,_){var M=ws();if(_!==void 0){var R=_(g);if(xm){_e(!0);try{_(g)}finally{_e(!1)}}}else R=g;return M.memoizedState=M.baseState=R,m={pending:null,lanes:0,dispatch:null,lastRenderedReducer:m,lastRenderedState:R},M.queue=m,m=m.dispatch=dae.bind(null,An,m),[M.memoizedState,m]},useRef:function(m){var g=ws();return m={current:m},g.memoizedState=m},useState:function(m){m=tA(m);var g=m.queue,_=p5.bind(null,An,g);return g.dispatch=_,[m.memoizedState,_]},useDebugValue:iA,useDeferredValue:function(m,g){var _=ws();return aA(_,m,g)},useTransition:function(){var m=tA(!1);return m=u5.bind(null,An,m.queue,!0,!1),ws().memoizedState=m,[!1,m]},useSyncExternalStore:function(m,g,_){var M=An,R=ws();if(er){if(_===void 0)throw Error(r(407));_=_()}else{if(_=g(),Or===null)throw Error(r(349));(Yn&127)!==0||zI(M,g,_)}R.memoizedState=_;var O={value:_,getSnapshot:g};return R.queue=O,t5(BI.bind(null,M,O,m),[m]),M.flags|=2048,fp(9,{destroy:void 0},UI.bind(null,M,O,_,g),null),_},useId:function(){var m=ws(),g=Or.identifierPrefix;if(er){var _=sc,M=ac;_=(M&~(1<<32-Ue(M)-1)).toString(32)+_,g="_"+g+"R_"+_,_=sw++,0<_&&(g+="H"+_.toString(32)),g+="_"}else _=iae++,g="_"+g+"r_"+_.toString(32)+"_";return m.memoizedState=g},useHostTransitionStatus:oA,useFormState:YI,useActionState:YI,useOptimistic:function(m){var g=ws();g.memoizedState=g.baseState=m;var _={pending:null,lanes:0,dispatch:null,lastRenderedReducer:null,lastRenderedState:null};return g.queue=_,g=lA.bind(null,An,!0,_),_.dispatch=g,[m,g]},useMemoCache:ZM,useCacheRefresh:function(){return ws().memoizedState=uae.bind(null,An)},useEffectEvent:function(m){var g=ws(),_={impl:m};return g.memoizedState=_,function(){if((mr&2)!==0)throw Error(r(440));return _.impl.apply(void 0,arguments)}}},cA={readContext:Ua,use:lw,useCallback:o5,useContext:Ua,useEffect:rA,useImperativeHandle:s5,useInsertionEffect:r5,useLayoutEffect:i5,useMemo:l5,useReducer:cw,useRef:e5,useState:function(){return cw(Yc)},useDebugValue:iA,useDeferredValue:function(m,g){var _=Li();return c5(_,kr.memoizedState,m,g)},useTransition:function(){var m=cw(Yc)[0],g=Li().memoizedState;return[typeof m=="boolean"?m:bx(m),g]},useSyncExternalStore:II,useId:m5,useHostTransitionStatus:oA,useFormState:QI,useActionState:QI,useOptimistic:function(m,g){var _=Li();return VI(_,kr,m,g)},useMemoCache:ZM,useCacheRefresh:f5};cA.useEffectEvent=n5;var y5={readContext:Ua,use:lw,useCallback:o5,useContext:Ua,useEffect:rA,useImperativeHandle:s5,useInsertionEffect:r5,useLayoutEffect:i5,useMemo:l5,useReducer:eA,useRef:e5,useState:function(){return eA(Yc)},useDebugValue:iA,useDeferredValue:function(m,g){var _=Li();return kr===null?aA(_,m,g):c5(_,kr.memoizedState,m,g)},useTransition:function(){var m=eA(Yc)[0],g=Li().memoizedState;return[typeof m=="boolean"?m:bx(m),g]},useSyncExternalStore:II,useId:m5,useHostTransitionStatus:oA,useFormState:JI,useActionState:JI,useOptimistic:function(m,g){var _=Li();return kr!==null?VI(_,kr,m,g):(_.baseState=m,[m,_.queue.dispatch])},useMemoCache:ZM,useCacheRefresh:f5};y5.useEffectEvent=n5;function uA(m,g,_,M){g=m.memoizedState,_=_(M,g),_=_==null?g:h({},g,_),m.memoizedState=_,m.lanes===0&&(m.updateQueue.baseState=_)}var dA={enqueueSetState:function(m,g,_){m=m._reactInternals;var M=fo(),R=_d(M);R.payload=g,_!=null&&(R.callback=_),g=Sd(m,R,M),g!==null&&(Hs(g,m,M),mx(g,m,M))},enqueueReplaceState:function(m,g,_){m=m._reactInternals;var M=fo(),R=_d(M);R.tag=1,R.payload=g,_!=null&&(R.callback=_),g=Sd(m,R,M),g!==null&&(Hs(g,m,M),mx(g,m,M))},enqueueForceUpdate:function(m,g){m=m._reactInternals;var _=fo(),M=_d(_);M.tag=2,g!=null&&(M.callback=g),g=Sd(m,M,_),g!==null&&(Hs(g,m,_),mx(g,m,_))}};function v5(m,g,_,M,R,O,ne){return m=m.stateNode,typeof m.shouldComponentUpdate=="function"?m.shouldComponentUpdate(M,O,ne):g.prototype&&g.prototype.isPureReactComponent?!ax(_,M)||!ax(R,O):!0}function w5(m,g,_,M){m=g.state,typeof g.componentWillReceiveProps=="function"&&g.componentWillReceiveProps(_,M),typeof g.UNSAFE_componentWillReceiveProps=="function"&&g.UNSAFE_componentWillReceiveProps(_,M),g.state!==m&&dA.enqueueReplaceState(g,g.state,null)}function ym(m,g){var _=g;if("ref"in g){_={};for(var M in g)M!=="ref"&&(_[M]=g[M])}if(m=m.defaultProps){_===g&&(_=h({},_));for(var R in m)_[R]===void 0&&(_[R]=m[R])}return _}function _5(m){Vv(m)}function S5(m){console.error(m)}function N5(m){Vv(m)}function mw(m,g){try{var _=m.onUncaughtError;_(g.value,{componentStack:g.stack})}catch(M){setTimeout(function(){throw M})}}function k5(m,g,_){try{var M=m.onCaughtError;M(_.value,{componentStack:_.stack,errorBoundary:g.tag===1?g.stateNode:null})}catch(R){setTimeout(function(){throw R})}}function hA(m,g,_){return _=_d(_),_.tag=3,_.payload={element:null},_.callback=function(){mw(m,g)},_}function C5(m){return m=_d(m),m.tag=3,m}function M5(m,g,_,M){var R=_.type.getDerivedStateFromError;if(typeof R=="function"){var O=M.value;m.payload=function(){return R(O)},m.callback=function(){k5(g,_,M)}}var ne=_.stateNode;ne!==null&&typeof ne.componentDidCatch=="function"&&(m.callback=function(){k5(g,_,M),typeof R!="function"&&(Td===null?Td=new Set([this]):Td.add(this));var Ne=M.stack;this.componentDidCatch(M.value,{componentStack:Ne!==null?Ne:""})})}function hae(m,g,_,M,R){if(_.flags|=32768,M!==null&&typeof M=="object"&&typeof M.then=="function"){if(g=_.alternate,g!==null&&sp(g,_,R,!0),_=co.current,_!==null){switch(_.tag){case 31:case 13:return $o===null?kw():_.alternate===null&&Ci===0&&(Ci=3),_.flags&=-257,_.flags|=65536,_.lanes=R,M===ew?_.flags|=16384:(g=_.updateQueue,g===null?_.updateQueue=new Set([M]):g.add(M),OA(m,M,R)),!1;case 22:return _.flags|=65536,M===ew?_.flags|=16384:(g=_.updateQueue,g===null?(g={transitions:null,markerInstances:null,retryQueue:new Set([M])},_.updateQueue=g):(_=g.retryQueue,_===null?g.retryQueue=new Set([M]):_.add(M)),OA(m,M,R)),!1}throw Error(r(435,_.tag))}return OA(m,M,R),kw(),!1}if(er)return g=co.current,g!==null?((g.flags&65536)===0&&(g.flags|=256),g.flags|=65536,g.lanes=R,M!==PM&&(m=Error(r(422),{cause:M}),lx(zo(m,_)))):(M!==PM&&(g=Error(r(423),{cause:M}),lx(zo(g,_))),m=m.current.alternate,m.flags|=65536,R&=-R,m.lanes|=R,M=zo(M,_),R=hA(m.stateNode,M,R),HM(m,R),Ci!==4&&(Ci=2)),!1;var O=Error(r(520),{cause:M});if(O=zo(O,_),Mx===null?Mx=[O]:Mx.push(O),Ci!==4&&(Ci=2),g===null)return!0;M=zo(M,_),_=g;do{switch(_.tag){case 3:return _.flags|=65536,m=R&-R,_.lanes|=m,m=hA(_.stateNode,M,m),HM(_,m),!1;case 1:if(g=_.type,O=_.stateNode,(_.flags&128)===0&&(typeof g.getDerivedStateFromError=="function"||O!==null&&typeof O.componentDidCatch=="function"&&(Td===null||!Td.has(O))))return _.flags|=65536,R&=-R,_.lanes|=R,R=C5(R),M5(R,m,_,M),HM(_,R),!1}_=_.return}while(_!==null);return!1}var mA=Error(r(461)),Gi=!1;function Ba(m,g,_,M){g.child=m===null?PI(g,null,_,M):bm(g,m.child,_,M)}function A5(m,g,_,M,R){_=_.render;var O=g.ref;if("ref"in M){var ne={};for(var Ne in M)Ne!=="ref"&&(ne[Ne]=M[Ne])}else ne=M;return mm(g),M=KM(m,g,_,ne,O,R),Ne=XM(),m!==null&&!Gi?(YM(m,g,R),Qc(m,g,R)):(er&&Ne&&TM(g),g.flags|=1,Ba(m,g,M,R),g.child)}function T5(m,g,_,M,R){if(m===null){var O=_.type;return typeof O=="function"&&!CM(O)&&O.defaultProps===void 0&&_.compare===null?(g.tag=15,g.type=O,E5(m,g,O,M,R)):(m=Kv(_.type,null,M,g,g.mode,R),m.ref=g.ref,m.return=g,g.child=m)}if(O=m.child,!wA(m,R)){var ne=O.memoizedProps;if(_=_.compare,_=_!==null?_:ax,_(ne,M)&&m.ref===g.ref)return Qc(m,g,R)}return g.flags|=1,m=Gc(O,M),m.ref=g.ref,m.return=g,g.child=m}function E5(m,g,_,M,R){if(m!==null){var O=m.memoizedProps;if(ax(O,M)&&m.ref===g.ref)if(Gi=!1,g.pendingProps=M=O,wA(m,R))(m.flags&131072)!==0&&(Gi=!0);else return g.lanes=m.lanes,Qc(m,g,R)}return fA(m,g,_,M,R)}function P5(m,g,_,M){var R=M.children,O=m!==null?m.memoizedState:null;if(m===null&&g.stateNode===null&&(g.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),M.mode==="hidden"){if((g.flags&128)!==0){if(O=O!==null?O.baseLanes|_:_,m!==null){for(M=g.child=m.child,R=0;M!==null;)R=R|M.lanes|M.childLanes,M=M.sibling;M=R&~O}else M=0,g.child=null;return j5(m,g,O,_,M)}if((_&536870912)!==0)g.memoizedState={baseLanes:0,cachePool:null},m!==null&&Zv(g,O!==null?O.cachePool:null),O!==null?RI(g,O):VM(),FI(g);else return M=g.lanes=536870912,j5(m,g,O!==null?O.baseLanes|_:_,_,M)}else O!==null?(Zv(g,O.cachePool),RI(g,O),kd(),g.memoizedState=null):(m!==null&&Zv(g,null),VM(),kd());return Ba(m,g,R,_),g.child}function vx(m,g){return m!==null&&m.tag===22||g.stateNode!==null||(g.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),g.sibling}function j5(m,g,_,M,R){var O=IM();return O=O===null?null:{parent:$i._currentValue,pool:O},g.memoizedState={baseLanes:_,cachePool:O},m!==null&&Zv(g,null),VM(),FI(g),m!==null&&sp(m,g,M,!0),g.childLanes=R,null}function fw(m,g){return g=gw({mode:g.mode,children:g.children},m.mode),g.ref=m.ref,m.child=g,g.return=m,g}function D5(m,g,_){return bm(g,m.child,null,_),m=fw(g,g.pendingProps),m.flags|=2,uo(g),g.memoizedState=null,m}function mae(m,g,_){var M=g.pendingProps,R=(g.flags&128)!==0;if(g.flags&=-129,m===null){if(er){if(M.mode==="hidden")return m=fw(g,M),g.lanes=536870912,vx(null,m);if(qM(g),(m=Zr)?(m=Gz(m,Ho),m=m!==null&&m.data==="&"?m:null,m!==null&&(g.memoizedState={dehydrated:m,treeContext:bd!==null?{id:ac,overflow:sc}:null,retryLane:536870912,hydrationErrors:null},_=gI(m),_.return=g,g.child=_,za=g,Zr=null)):m=null,m===null)throw yd(g);return g.lanes=536870912,null}return fw(g,M)}var O=m.memoizedState;if(O!==null){var ne=O.dehydrated;if(qM(g),R)if(g.flags&256)g.flags&=-257,g=D5(m,g,_);else if(g.memoizedState!==null)g.child=m.child,g.flags|=128,g=null;else throw Error(r(558));else if(Gi||sp(m,g,_,!1),R=(_&m.childLanes)!==0,Gi||R){if(M=Or,M!==null&&(ne=Qt(M,_),ne!==0&&ne!==O.retryLane))throw O.retryLane=ne,cm(m,ne),Hs(M,m,ne),mA;kw(),g=D5(m,g,_)}else m=O.treeContext,Zr=Vo(ne.nextSibling),za=g,er=!0,xd=null,Ho=!1,m!==null&&yI(g,m),g=fw(g,M),g.flags|=4096;return g}return m=Gc(m.child,{mode:M.mode,children:M.children}),m.ref=g.ref,g.child=m,m.return=g,m}function pw(m,g){var _=g.ref;if(_===null)m!==null&&m.ref!==null&&(g.flags|=4194816);else{if(typeof _!="function"&&typeof _!="object")throw Error(r(284));(m===null||m.ref!==_)&&(g.flags|=4194816)}}function fA(m,g,_,M,R){return mm(g),_=KM(m,g,_,M,void 0,R),M=XM(),m!==null&&!Gi?(YM(m,g,R),Qc(m,g,R)):(er&&M&&TM(g),g.flags|=1,Ba(m,g,_,R),g.child)}function R5(m,g,_,M,R,O){return mm(g),g.updateQueue=null,_=OI(g,M,_,R),LI(m),M=XM(),m!==null&&!Gi?(YM(m,g,O),Qc(m,g,O)):(er&&M&&TM(g),g.flags|=1,Ba(m,g,_,O),g.child)}function F5(m,g,_,M,R){if(mm(g),g.stateNode===null){var O=np,ne=_.contextType;typeof ne=="object"&&ne!==null&&(O=Ua(ne)),O=new _(M,O),g.memoizedState=O.state!==null&&O.state!==void 0?O.state:null,O.updater=dA,g.stateNode=O,O._reactInternals=g,O=g.stateNode,O.props=M,O.state=g.memoizedState,O.refs={},UM(g),ne=_.contextType,O.context=typeof ne=="object"&&ne!==null?Ua(ne):np,O.state=g.memoizedState,ne=_.getDerivedStateFromProps,typeof ne=="function"&&(uA(g,_,ne,M),O.state=g.memoizedState),typeof _.getDerivedStateFromProps=="function"||typeof O.getSnapshotBeforeUpdate=="function"||typeof O.UNSAFE_componentWillMount!="function"&&typeof O.componentWillMount!="function"||(ne=O.state,typeof O.componentWillMount=="function"&&O.componentWillMount(),typeof O.UNSAFE_componentWillMount=="function"&&O.UNSAFE_componentWillMount(),ne!==O.state&&dA.enqueueReplaceState(O,O.state,null),px(g,M,O,R),fx(),O.state=g.memoizedState),typeof O.componentDidMount=="function"&&(g.flags|=4194308),M=!0}else if(m===null){O=g.stateNode;var Ne=g.memoizedProps,Be=ym(_,Ne);O.props=Be;var ut=O.context,vt=_.contextType;ne=np,typeof vt=="object"&&vt!==null&&(ne=Ua(vt));var kt=_.getDerivedStateFromProps;vt=typeof kt=="function"||typeof O.getSnapshotBeforeUpdate=="function",Ne=g.pendingProps!==Ne,vt||typeof O.UNSAFE_componentWillReceiveProps!="function"&&typeof O.componentWillReceiveProps!="function"||(Ne||ut!==ne)&&w5(g,O,M,ne),wd=!1;var mt=g.memoizedState;O.state=mt,px(g,M,O,R),fx(),ut=g.memoizedState,Ne||mt!==ut||wd?(typeof kt=="function"&&(uA(g,_,kt,M),ut=g.memoizedState),(Be=wd||v5(g,_,Be,M,mt,ut,ne))?(vt||typeof O.UNSAFE_componentWillMount!="function"&&typeof O.componentWillMount!="function"||(typeof O.componentWillMount=="function"&&O.componentWillMount(),typeof O.UNSAFE_componentWillMount=="function"&&O.UNSAFE_componentWillMount()),typeof O.componentDidMount=="function"&&(g.flags|=4194308)):(typeof O.componentDidMount=="function"&&(g.flags|=4194308),g.memoizedProps=M,g.memoizedState=ut),O.props=M,O.state=ut,O.context=ne,M=Be):(typeof O.componentDidMount=="function"&&(g.flags|=4194308),M=!1)}else{O=g.stateNode,BM(m,g),ne=g.memoizedProps,vt=ym(_,ne),O.props=vt,kt=g.pendingProps,mt=O.context,ut=_.contextType,Be=np,typeof ut=="object"&&ut!==null&&(Be=Ua(ut)),Ne=_.getDerivedStateFromProps,(ut=typeof Ne=="function"||typeof O.getSnapshotBeforeUpdate=="function")||typeof O.UNSAFE_componentWillReceiveProps!="function"&&typeof O.componentWillReceiveProps!="function"||(ne!==kt||mt!==Be)&&w5(g,O,M,Be),wd=!1,mt=g.memoizedState,O.state=mt,px(g,M,O,R),fx();var xt=g.memoizedState;ne!==kt||mt!==xt||wd||m!==null&&m.dependencies!==null&&Yv(m.dependencies)?(typeof Ne=="function"&&(uA(g,_,Ne,M),xt=g.memoizedState),(vt=wd||v5(g,_,vt,M,mt,xt,Be)||m!==null&&m.dependencies!==null&&Yv(m.dependencies))?(ut||typeof O.UNSAFE_componentWillUpdate!="function"&&typeof O.componentWillUpdate!="function"||(typeof O.componentWillUpdate=="function"&&O.componentWillUpdate(M,xt,Be),typeof O.UNSAFE_componentWillUpdate=="function"&&O.UNSAFE_componentWillUpdate(M,xt,Be)),typeof O.componentDidUpdate=="function"&&(g.flags|=4),typeof O.getSnapshotBeforeUpdate=="function"&&(g.flags|=1024)):(typeof O.componentDidUpdate!="function"||ne===m.memoizedProps&&mt===m.memoizedState||(g.flags|=4),typeof O.getSnapshotBeforeUpdate!="function"||ne===m.memoizedProps&&mt===m.memoizedState||(g.flags|=1024),g.memoizedProps=M,g.memoizedState=xt),O.props=M,O.state=xt,O.context=Be,M=vt):(typeof O.componentDidUpdate!="function"||ne===m.memoizedProps&&mt===m.memoizedState||(g.flags|=4),typeof O.getSnapshotBeforeUpdate!="function"||ne===m.memoizedProps&&mt===m.memoizedState||(g.flags|=1024),M=!1)}return O=M,pw(m,g),M=(g.flags&128)!==0,O||M?(O=g.stateNode,_=M&&typeof _.getDerivedStateFromError!="function"?null:O.render(),g.flags|=1,m!==null&&M?(g.child=bm(g,m.child,null,R),g.child=bm(g,null,_,R)):Ba(m,g,_,R),g.memoizedState=O.state,m=g.child):m=Qc(m,g,R),m}function L5(m,g,_,M){return dm(),g.flags|=256,Ba(m,g,_,M),g.child}var pA={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function gA(m){return{baseLanes:m,cachePool:kI()}}function bA(m,g,_){return m=m!==null?m.childLanes&~_:0,g&&(m|=mo),m}function O5(m,g,_){var M=g.pendingProps,R=!1,O=(g.flags&128)!==0,ne;if((ne=O)||(ne=m!==null&&m.memoizedState===null?!1:(Fi.current&2)!==0),ne&&(R=!0,g.flags&=-129),ne=(g.flags&32)!==0,g.flags&=-33,m===null){if(er){if(R?Nd(g):kd(),(m=Zr)?(m=Gz(m,Ho),m=m!==null&&m.data!=="&"?m:null,m!==null&&(g.memoizedState={dehydrated:m,treeContext:bd!==null?{id:ac,overflow:sc}:null,retryLane:536870912,hydrationErrors:null},_=gI(m),_.return=g,g.child=_,za=g,Zr=null)):m=null,m===null)throw yd(g);return JA(m)?g.lanes=32:g.lanes=536870912,null}var Ne=M.children;return M=M.fallback,R?(kd(),R=g.mode,Ne=gw({mode:"hidden",children:Ne},R),M=um(M,R,_,null),Ne.return=g,M.return=g,Ne.sibling=M,g.child=Ne,M=g.child,M.memoizedState=gA(_),M.childLanes=bA(m,ne,_),g.memoizedState=pA,vx(null,M)):(Nd(g),xA(g,Ne))}var Be=m.memoizedState;if(Be!==null&&(Ne=Be.dehydrated,Ne!==null)){if(O)g.flags&256?(Nd(g),g.flags&=-257,g=yA(m,g,_)):g.memoizedState!==null?(kd(),g.child=m.child,g.flags|=128,g=null):(kd(),Ne=M.fallback,R=g.mode,M=gw({mode:"visible",children:M.children},R),Ne=um(Ne,R,_,null),Ne.flags|=2,M.return=g,Ne.return=g,M.sibling=Ne,g.child=M,bm(g,m.child,null,_),M=g.child,M.memoizedState=gA(_),M.childLanes=bA(m,ne,_),g.memoizedState=pA,g=vx(null,M));else if(Nd(g),JA(Ne)){if(ne=Ne.nextSibling&&Ne.nextSibling.dataset,ne)var ut=ne.dgst;ne=ut,M=Error(r(419)),M.stack="",M.digest=ne,lx({value:M,source:null,stack:null}),g=yA(m,g,_)}else if(Gi||sp(m,g,_,!1),ne=(_&m.childLanes)!==0,Gi||ne){if(ne=Or,ne!==null&&(M=Qt(ne,_),M!==0&&M!==Be.retryLane))throw Be.retryLane=M,cm(m,M),Hs(ne,m,M),mA;ZA(Ne)||kw(),g=yA(m,g,_)}else ZA(Ne)?(g.flags|=192,g.child=m.child,g=null):(m=Be.treeContext,Zr=Vo(Ne.nextSibling),za=g,er=!0,xd=null,Ho=!1,m!==null&&yI(g,m),g=xA(g,M.children),g.flags|=4096);return g}return R?(kd(),Ne=M.fallback,R=g.mode,Be=m.child,ut=Be.sibling,M=Gc(Be,{mode:"hidden",children:M.children}),M.subtreeFlags=Be.subtreeFlags&65011712,ut!==null?Ne=Gc(ut,Ne):(Ne=um(Ne,R,_,null),Ne.flags|=2),Ne.return=g,M.return=g,M.sibling=Ne,g.child=M,vx(null,M),M=g.child,Ne=m.child.memoizedState,Ne===null?Ne=gA(_):(R=Ne.cachePool,R!==null?(Be=$i._currentValue,R=R.parent!==Be?{parent:Be,pool:Be}:R):R=kI(),Ne={baseLanes:Ne.baseLanes|_,cachePool:R}),M.memoizedState=Ne,M.childLanes=bA(m,ne,_),g.memoizedState=pA,vx(m.child,M)):(Nd(g),_=m.child,m=_.sibling,_=Gc(_,{mode:"visible",children:M.children}),_.return=g,_.sibling=null,m!==null&&(ne=g.deletions,ne===null?(g.deletions=[m],g.flags|=16):ne.push(m)),g.child=_,g.memoizedState=null,_)}function xA(m,g){return g=gw({mode:"visible",children:g},m.mode),g.return=m,m.child=g}function gw(m,g){return m=lo(22,m,null,g),m.lanes=0,m}function yA(m,g,_){return bm(g,m.child,null,_),m=xA(g,g.pendingProps.children),m.flags|=2,g.memoizedState=null,m}function I5(m,g,_){m.lanes|=g;var M=m.alternate;M!==null&&(M.lanes|=g),RM(m.return,g,_)}function vA(m,g,_,M,R,O){var ne=m.memoizedState;ne===null?m.memoizedState={isBackwards:g,rendering:null,renderingStartTime:0,last:M,tail:_,tailMode:R,treeForkCount:O}:(ne.isBackwards=g,ne.rendering=null,ne.renderingStartTime=0,ne.last=M,ne.tail=_,ne.tailMode=R,ne.treeForkCount=O)}function z5(m,g,_){var M=g.pendingProps,R=M.revealOrder,O=M.tail;M=M.children;var ne=Fi.current,Ne=(ne&2)!==0;if(Ne?(ne=ne&1|2,g.flags|=128):ne&=1,ie(Fi,ne),Ba(m,g,M,_),M=er?ox:0,!Ne&&m!==null&&(m.flags&128)!==0)e:for(m=g.child;m!==null;){if(m.tag===13)m.memoizedState!==null&&I5(m,_,g);else if(m.tag===19)I5(m,_,g);else if(m.child!==null){m.child.return=m,m=m.child;continue}if(m===g)break e;for(;m.sibling===null;){if(m.return===null||m.return===g)break e;m=m.return}m.sibling.return=m.return,m=m.sibling}switch(R){case"forwards":for(_=g.child,R=null;_!==null;)m=_.alternate,m!==null&&iw(m)===null&&(R=_),_=_.sibling;_=R,_===null?(R=g.child,g.child=null):(R=_.sibling,_.sibling=null),vA(g,!1,R,_,O,M);break;case"backwards":case"unstable_legacy-backwards":for(_=null,R=g.child,g.child=null;R!==null;){if(m=R.alternate,m!==null&&iw(m)===null){g.child=R;break}m=R.sibling,R.sibling=_,_=R,R=m}vA(g,!0,_,null,O,M);break;case"together":vA(g,!1,null,null,void 0,M);break;default:g.memoizedState=null}return g.child}function Qc(m,g,_){if(m!==null&&(g.dependencies=m.dependencies),Ad|=g.lanes,(_&g.childLanes)===0)if(m!==null){if(sp(m,g,_,!1),(_&g.childLanes)===0)return null}else return null;if(m!==null&&g.child!==m.child)throw Error(r(153));if(g.child!==null){for(m=g.child,_=Gc(m,m.pendingProps),g.child=_,_.return=g;m.sibling!==null;)m=m.sibling,_=_.sibling=Gc(m,m.pendingProps),_.return=g;_.sibling=null}return g.child}function wA(m,g){return(m.lanes&g)!==0?!0:(m=m.dependencies,!!(m!==null&&Yv(m)))}function fae(m,g,_){switch(g.tag){case 3:Fe(g,g.stateNode.containerInfo),vd(g,$i,m.memoizedState.cache),dm();break;case 27:case 5:le(g);break;case 4:Fe(g,g.stateNode.containerInfo);break;case 10:vd(g,g.type,g.memoizedProps.value);break;case 31:if(g.memoizedState!==null)return g.flags|=128,qM(g),null;break;case 13:var M=g.memoizedState;if(M!==null)return M.dehydrated!==null?(Nd(g),g.flags|=128,null):(_&g.child.childLanes)!==0?O5(m,g,_):(Nd(g),m=Qc(m,g,_),m!==null?m.sibling:null);Nd(g);break;case 19:var R=(m.flags&128)!==0;if(M=(_&g.childLanes)!==0,M||(sp(m,g,_,!1),M=(_&g.childLanes)!==0),R){if(M)return z5(m,g,_);g.flags|=128}if(R=g.memoizedState,R!==null&&(R.rendering=null,R.tail=null,R.lastEffect=null),ie(Fi,Fi.current),M)break;return null;case 22:return g.lanes=0,P5(m,g,_,g.pendingProps);case 24:vd(g,$i,m.memoizedState.cache)}return Qc(m,g,_)}function U5(m,g,_){if(m!==null)if(m.memoizedProps!==g.pendingProps)Gi=!0;else{if(!wA(m,_)&&(g.flags&128)===0)return Gi=!1,fae(m,g,_);Gi=(m.flags&131072)!==0}else Gi=!1,er&&(g.flags&1048576)!==0&&xI(g,ox,g.index);switch(g.lanes=0,g.tag){case 16:e:{var M=g.pendingProps;if(m=pm(g.elementType),g.type=m,typeof m=="function")CM(m)?(M=ym(m,M),g.tag=1,g=F5(null,g,m,M,_)):(g.tag=0,g=fA(null,g,m,M,_));else{if(m!=null){var R=m.$$typeof;if(R===A){g.tag=11,g=A5(null,g,m,M,_);break e}else if(R===C){g.tag=14,g=T5(null,g,m,M,_);break e}}throw g=D(m)||m,Error(r(306,g,""))}}return g;case 0:return fA(m,g,g.type,g.pendingProps,_);case 1:return M=g.type,R=ym(M,g.pendingProps),F5(m,g,M,R,_);case 3:e:{if(Fe(g,g.stateNode.containerInfo),m===null)throw Error(r(387));M=g.pendingProps;var O=g.memoizedState;R=O.element,BM(m,g),px(g,M,null,_);var ne=g.memoizedState;if(M=ne.cache,vd(g,$i,M),M!==O.cache&&FM(g,[$i],_,!0),fx(),M=ne.element,O.isDehydrated)if(O={element:M,isDehydrated:!1,cache:ne.cache},g.updateQueue.baseState=O,g.memoizedState=O,g.flags&256){g=L5(m,g,M,_);break e}else if(M!==R){R=zo(Error(r(424)),g),lx(R),g=L5(m,g,M,_);break e}else{switch(m=g.stateNode.containerInfo,m.nodeType){case 9:m=m.body;break;default:m=m.nodeName==="HTML"?m.ownerDocument.body:m}for(Zr=Vo(m.firstChild),za=g,er=!0,xd=null,Ho=!0,_=PI(g,null,M,_),g.child=_;_;)_.flags=_.flags&-3|4096,_=_.sibling}else{if(dm(),M===R){g=Qc(m,g,_);break e}Ba(m,g,M,_)}g=g.child}return g;case 26:return pw(m,g),m===null?(_=Qz(g.type,null,g.pendingProps,null))?g.memoizedState=_:er||(_=g.type,m=g.pendingProps,M=jw(he.current).createElement(_),M[Xt]=g,M[sr]=m,Ha(M,_,m),Ge(M),g.stateNode=M):g.memoizedState=Qz(g.type,m.memoizedProps,g.pendingProps,m.memoizedState),null;case 27:return le(g),m===null&&er&&(M=g.stateNode=Kz(g.type,g.pendingProps,he.current),za=g,Ho=!0,R=Zr,Dd(g.type)?(eT=R,Zr=Vo(M.firstChild)):Zr=R),Ba(m,g,g.pendingProps.children,_),pw(m,g),m===null&&(g.flags|=4194304),g.child;case 5:return m===null&&er&&((R=M=Zr)&&(M=Vae(M,g.type,g.pendingProps,Ho),M!==null?(g.stateNode=M,za=g,Zr=Vo(M.firstChild),Ho=!1,R=!0):R=!1),R||yd(g)),le(g),R=g.type,O=g.pendingProps,ne=m!==null?m.memoizedProps:null,M=O.children,XA(R,O)?M=null:ne!==null&&XA(R,ne)&&(g.flags|=32),g.memoizedState!==null&&(R=KM(m,g,aae,null,null,_),Fx._currentValue=R),pw(m,g),Ba(m,g,M,_),g.child;case 6:return m===null&&er&&((m=_=Zr)&&(_=Gae(_,g.pendingProps,Ho),_!==null?(g.stateNode=_,za=g,Zr=null,m=!0):m=!1),m||yd(g)),null;case 13:return O5(m,g,_);case 4:return Fe(g,g.stateNode.containerInfo),M=g.pendingProps,m===null?g.child=bm(g,null,M,_):Ba(m,g,M,_),g.child;case 11:return A5(m,g,g.type,g.pendingProps,_);case 7:return Ba(m,g,g.pendingProps,_),g.child;case 8:return Ba(m,g,g.pendingProps.children,_),g.child;case 12:return Ba(m,g,g.pendingProps.children,_),g.child;case 10:return M=g.pendingProps,vd(g,g.type,M.value),Ba(m,g,M.children,_),g.child;case 9:return R=g.type._context,M=g.pendingProps.children,mm(g),R=Ua(R),M=M(R),g.flags|=1,Ba(m,g,M,_),g.child;case 14:return T5(m,g,g.type,g.pendingProps,_);case 15:return E5(m,g,g.type,g.pendingProps,_);case 19:return z5(m,g,_);case 31:return mae(m,g,_);case 22:return P5(m,g,_,g.pendingProps);case 24:return mm(g),M=Ua($i),m===null?(R=IM(),R===null&&(R=Or,O=LM(),R.pooledCache=O,O.refCount++,O!==null&&(R.pooledCacheLanes|=_),R=O),g.memoizedState={parent:M,cache:R},UM(g),vd(g,$i,R)):((m.lanes&_)!==0&&(BM(m,g),px(g,null,null,_),fx()),R=m.memoizedState,O=g.memoizedState,R.parent!==M?(R={parent:M,cache:M},g.memoizedState=R,g.lanes===0&&(g.memoizedState=g.updateQueue.baseState=R),vd(g,$i,M)):(M=O.cache,vd(g,$i,M),M!==R.cache&&FM(g,[$i],_,!0))),Ba(m,g,g.pendingProps.children,_),g.child;case 29:throw g.pendingProps}throw Error(r(156,g.tag))}function Zc(m){m.flags|=4}function _A(m,g,_,M,R){if((g=(m.mode&32)!==0)&&(g=!1),g){if(m.flags|=16777216,(R&335544128)===R)if(m.stateNode.complete)m.flags|=8192;else if(mz())m.flags|=8192;else throw gm=ew,zM}else m.flags&=-16777217}function B5(m,g){if(g.type!=="stylesheet"||(g.state.loading&4)!==0)m.flags&=-16777217;else if(m.flags|=16777216,!nU(g))if(mz())m.flags|=8192;else throw gm=ew,zM}function bw(m,g){g!==null&&(m.flags|=4),m.flags&16384&&(g=m.tag!==22?yt():536870912,m.lanes|=g,xp|=g)}function wx(m,g){if(!er)switch(m.tailMode){case"hidden":g=m.tail;for(var _=null;g!==null;)g.alternate!==null&&(_=g),g=g.sibling;_===null?m.tail=null:_.sibling=null;break;case"collapsed":_=m.tail;for(var M=null;_!==null;)_.alternate!==null&&(M=_),_=_.sibling;M===null?g||m.tail===null?m.tail=null:m.tail.sibling=null:M.sibling=null}}function Jr(m){var g=m.alternate!==null&&m.alternate.child===m.child,_=0,M=0;if(g)for(var R=m.child;R!==null;)_|=R.lanes|R.childLanes,M|=R.subtreeFlags&65011712,M|=R.flags&65011712,R.return=m,R=R.sibling;else for(R=m.child;R!==null;)_|=R.lanes|R.childLanes,M|=R.subtreeFlags,M|=R.flags,R.return=m,R=R.sibling;return m.subtreeFlags|=M,m.childLanes=_,g}function pae(m,g,_){var M=g.pendingProps;switch(EM(g),g.tag){case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Jr(g),null;case 1:return Jr(g),null;case 3:return _=g.stateNode,M=null,m!==null&&(M=m.memoizedState.cache),g.memoizedState.cache!==M&&(g.flags|=2048),Kc($i),ue(),_.pendingContext&&(_.context=_.pendingContext,_.pendingContext=null),(m===null||m.child===null)&&(ap(g)?Zc(g):m===null||m.memoizedState.isDehydrated&&(g.flags&256)===0||(g.flags|=1024,jM())),Jr(g),null;case 26:var R=g.type,O=g.memoizedState;return m===null?(Zc(g),O!==null?(Jr(g),B5(g,O)):(Jr(g),_A(g,R,null,M,_))):O?O!==m.memoizedState?(Zc(g),Jr(g),B5(g,O)):(Jr(g),g.flags&=-16777217):(m=m.memoizedProps,m!==M&&Zc(g),Jr(g),_A(g,R,m,M,_)),null;case 27:if(Te(g),_=he.current,R=g.type,m!==null&&g.stateNode!=null)m.memoizedProps!==M&&Zc(g);else{if(!M){if(g.stateNode===null)throw Error(r(166));return Jr(g),null}m=Z.current,ap(g)?vI(g):(m=Kz(R,M,_),g.stateNode=m,Zc(g))}return Jr(g),null;case 5:if(Te(g),R=g.type,m!==null&&g.stateNode!=null)m.memoizedProps!==M&&Zc(g);else{if(!M){if(g.stateNode===null)throw Error(r(166));return Jr(g),null}if(O=Z.current,ap(g))vI(g);else{var ne=jw(he.current);switch(O){case 1:O=ne.createElementNS("http://www.w3.org/2000/svg",R);break;case 2:O=ne.createElementNS("http://www.w3.org/1998/Math/MathML",R);break;default:switch(R){case"svg":O=ne.createElementNS("http://www.w3.org/2000/svg",R);break;case"math":O=ne.createElementNS("http://www.w3.org/1998/Math/MathML",R);break;case"script":O=ne.createElement("div"),O.innerHTML=" - + +
From a6b1f6fc284bfbb49f12ec01a8b6cc0f862ae597 Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 15 Feb 2026 08:25:32 +0100 Subject: [PATCH 5/8] Updated update_website_wiki.sh --- update_website_wiki.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/update_website_wiki.sh b/update_website_wiki.sh index 779b9059a..b35e2a44a 100755 --- a/update_website_wiki.sh +++ b/update_website_wiki.sh @@ -21,6 +21,7 @@ git commit -m "Updated website" git push cd ../spoolbuddy-wiki +git pull git add . git commit -m "Updated Wiki" git push From 46f2b8ee0da50c8430ae704db9e32dc977a68645 Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 15 Feb 2026 08:33:59 +0100 Subject: [PATCH 6/8] fix: AMS slot configure modal not pre-populating black filament color The color pre-population logic skipped hex 000000 (black) to guard against empty/uninitialized slots, but empty slots already don't pass trayColor to the modal. Removed the unnecessary check so black filament color is now pre-populated like any other color. --- CHANGELOG.md | 1 + frontend/src/components/ConfigureAmsSlotModal.tsx | 4 ++-- static/assets/{index-CS3Lw7Ok.js => index-C3i3HoUc.js} | 2 +- static/index.html | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) rename static/assets/{index-CS3Lw7Ok.js => index-C3i3HoUc.js} (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 177709b6c..fd9606533 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ All notable changes to Bambuddy will be documented in this file. - **RFID Spool Data Erased by Periodic AMS Updates** — Periodic MQTT push-all responses cleared `tag_uid` and `tray_uuid` fields because they were included in the "always update" list. These fields are now preserved during updates and only cleared when a spool is physically removed (slot clearing detected by empty `tray_type`). This fixes the AMS "eye" icon disappearing for RFID spools after startup. - **AMS Slot Configuration Overwrites RFID Spool State** — Configuring an AMS slot for an RFID-detected Bambu Lab spool sent `ams_set_filament_setting`, which replaced the firmware's RFID-managed filament config with a manual one — causing the slicer's "eye" icon to change to a "pen" icon. Now detects RFID spools and skips the filament setting command, only sending K-profile selection. - **K-Profile Selection Corrupts Existing Profiles on X1C/P1S** — The `extrusion_cali_sel` command included a `setting_id` field that BambuStudio never sends, causing firmware to mislink calibration data. The `extrusion_cali_set` command was sent unconditionally, overwriting existing profile metadata. Now `setting_id` is removed from selection commands, and `extrusion_cali_set` is only sent when no existing profile is selected (`cali_idx < 0`). +- **AMS Slot Configure — Black Filament Color Not Pre-Populated** — When re-opening the Configure AMS Slot modal for a slot with black filament, the color field was empty despite the preset and K-profile being correctly pre-selected. The color pre-population logic excluded hex `000000` (black) as a guard against empty slots, but empty slots already skip color data entirely. Removed the unnecessary check so black is now pre-populated like any other color. ### Improved - **SQLite WAL Mode for Database Reliability** — Database now uses Write-Ahead Logging (WAL) mode with a 5-second busy timeout, reducing "database is locked" errors under concurrent access. WAL mode allows simultaneous reads during writes, improving responsiveness for multi-printer setups. Automatically enabled on startup. diff --git a/frontend/src/components/ConfigureAmsSlotModal.tsx b/frontend/src/components/ConfigureAmsSlotModal.tsx index 75497ffa4..f52fbd88f 100644 --- a/frontend/src/components/ConfigureAmsSlotModal.tsx +++ b/frontend/src/components/ConfigureAmsSlotModal.tsx @@ -693,10 +693,10 @@ export function ConfigureAmsSlotModal({ } } - // Pre-populate color from current slot + // Pre-populate color from current slot (black is valid — empty slots don't pass trayColor) if (slotInfo.trayColor) { const hex = slotInfo.trayColor.slice(0, 6); - if (hex && hex !== '000000') { + if (hex) { setColorHex(hex); } } diff --git a/static/assets/index-CS3Lw7Ok.js b/static/assets/index-C3i3HoUc.js similarity index 99% rename from static/assets/index-CS3Lw7Ok.js rename to static/assets/index-C3i3HoUc.js index 69ebe21ea..a36052742 100644 --- a/static/assets/index-CS3Lw7Ok.js +++ b/static/assets/index-C3i3HoUc.js @@ -8482,7 +8482,7 @@ void main() { border-l-[5px] border-l-transparent border-r-[5px] border-r-transparent border-t-[5px] border-t-bambu-dark-tertiary - `})]})]})}function gHe({isOpen:t,onClose:e,tagUid:n,trayUuid:r,printerId:i,amsId:s,trayId:o}){const{t:l}=Ut(),c=Zt(),{showToast:u}=hn(),[d,h]=w.useState(""),{data:f,isLoading:p}=et({queryKey:["inventory-spools"],queryFn:()=>ae.getSpools(!1),enabled:t}),x=w.useMemo(()=>f?f.filter(b=>{if(b.tag_uid||b.tray_uuid)return!1;if(!d)return!0;const y=d.toLowerCase();return b.material.toLowerCase().includes(y)||b.brand&&b.brand.toLowerCase().includes(y)||b.color_name&&b.color_name.toLowerCase().includes(y)}):[],[f,d]),v=ot({mutationFn:b=>ae.linkTagToSpool(b,{tag_uid:n||void 0,tray_uuid:r||void 0,tag_type:r?"bambulab":"generic",data_origin:"nfc_link"}),onSuccess:()=>{c.invalidateQueries({queryKey:["inventory-spools"]}),c.invalidateQueries({queryKey:["inventory-assignments"]}),u(l("inventory.tagLinked"),"success"),e()},onError:b=>{u(b.message||l("inventory.tagLinkFailed"),"error")}});return t?a.jsxs("div",{className:"fixed inset-0 z-50 flex items-center justify-center",children:[a.jsx("div",{className:"absolute inset-0 bg-black/60 backdrop-blur-sm",onClick:e}),a.jsxs("div",{className:"relative bg-bambu-dark-secondary rounded-xl shadow-xl w-full max-w-md mx-4 max-h-[80vh] flex flex-col border border-bambu-dark-tertiary",children:[a.jsxs("div",{className:"flex items-center justify-between p-4 border-b border-white/10",children:[a.jsxs("div",{children:[a.jsxs("h3",{className:"text-lg font-semibold text-white flex items-center gap-2",children:[a.jsx(Vb,{className:"w-5 h-5 text-bambu-green"}),l("inventory.linkToSpool")]}),a.jsxs("p",{className:"text-xs text-bambu-gray mt-1",children:["AMS ",s," T",o," · Printer #",i]})]}),a.jsx("button",{onClick:e,className:"p-1 text-bambu-gray hover:text-white rounded transition-colors",children:a.jsx(Rt,{className:"w-5 h-5"})})]}),a.jsxs("div",{className:"p-4 border-b border-white/10",children:[a.jsxs("div",{className:"relative",children:[a.jsx(Br,{className:"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray"}),a.jsx("input",{type:"text",value:d,onChange:b=>h(b.target.value),placeholder:l("inventory.searchSpools"),className:"w-full pl-9 pr-3 py-2 bg-bambu-dark rounded-lg border border-white/10 text-white text-sm placeholder:text-bambu-gray focus:outline-none focus:border-bambu-green"})]}),(n||r)&&a.jsxs("p",{className:"text-xs text-bambu-gray mt-2 font-mono truncate",title:n||r,children:["Tag: ",n||r]})]}),a.jsx("div",{className:"flex-1 overflow-y-auto p-2 min-h-0",children:p?a.jsx("div",{className:"flex justify-center py-8",children:a.jsx(it,{className:"w-6 h-6 animate-spin text-bambu-green"})}):x.length===0?a.jsx("p",{className:"text-center text-bambu-gray py-8 text-sm",children:l("inventory.noSpoolsMatch")}):x.map(b=>a.jsxs("button",{onClick:()=>v.mutate(b.id),disabled:v.isPending,className:"w-full flex items-center gap-3 p-3 rounded-lg hover:bg-white/5 transition-colors text-left",children:[a.jsx("span",{className:"w-6 h-6 rounded-full border border-white/20 flex-shrink-0",style:{backgroundColor:b.rgba?`#${b.rgba.substring(0,6)}`:"#808080"}}),a.jsxs("div",{className:"flex-1 min-w-0",children:[a.jsxs("div",{className:"text-sm text-white font-medium truncate",children:[b.brand?`${b.brand} `:"",b.material,b.subtype?` ${b.subtype}`:""]}),a.jsxs("div",{className:"text-xs text-bambu-gray truncate",children:[b.color_name||"No color"," · #",b.id]})]}),a.jsxs("span",{className:"text-xs text-bambu-gray",children:[Math.round(b.label_weight-b.weight_used),"g"]})]},b.id))}),a.jsx("div",{className:"p-4 border-t border-white/10 flex justify-end",children:a.jsx(Te,{variant:"ghost",onClick:e,children:l("inventory.cancel")||"Cancel"})})]})]}):null}function bHe({isOpen:t,onClose:e,printerId:n,amsId:r,trayId:i,trayInfo:s}){const{t:o}=Ut(),l=Zt(),{showToast:c}=hn(),[u,d]=w.useState(null),[h,f]=w.useState(""),{data:p,isLoading:x}=et({queryKey:["inventory-spools"],queryFn:()=>ae.getSpools(),enabled:t}),{data:v}=et({queryKey:["spool-assignments"],queryFn:()=>ae.getAssignments(),enabled:t}),b=ot({mutationFn:T=>ae.assignSpool({spool_id:T,printer_id:n,ams_id:r,tray_id:i}),onSuccess:T=>{l.setQueryData(["spool-assignments"],C=>{const j=(C||[]).filter(E=>!(E.printer_id===n&&E.ams_id===r&&E.tray_id===i));return j.push(T),j}),l.invalidateQueries({queryKey:["spool-assignments"]}),c(o("inventory.assignSuccess"),"success"),e()},onError:T=>{c(`${o("inventory.assignFailed")}: ${T.message}`,"error")}});if(!t)return null;const y=new Set((v||[]).filter(T=>!(T.printer_id===n&&T.ams_id===r&&T.tray_id===i)).map(T=>T.spool_id)),S=r===254||r===255,N=p?.filter(T=>!y.has(T.id)&&(S||!T.tag_uid&&!T.tray_uuid)),M=N?.filter(T=>{if(!h)return!0;const C=h.toLowerCase();return T.material.toLowerCase().includes(C)||(T.brand?.toLowerCase().includes(C)??!1)||(T.color_name?.toLowerCase().includes(C)??!1)||(T.subtype?.toLowerCase().includes(C)??!1)}),k=()=>{u&&b.mutate(u)};return a.jsxs("div",{className:"fixed inset-0 z-50 flex items-center justify-center",children:[a.jsx("div",{className:"absolute inset-0 bg-black/60 backdrop-blur-sm",onClick:e}),a.jsxs("div",{className:"relative w-full max-w-md mx-4 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-xl shadow-2xl",children:[a.jsxs("div",{className:"flex items-center justify-between p-4 border-b border-bambu-dark-tertiary",children:[a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(ra,{className:"w-5 h-5 text-bambu-green"}),a.jsx("h2",{className:"text-lg font-semibold text-white",children:o("inventory.assignSpool")})]}),a.jsx("button",{onClick:e,className:"p-1 text-bambu-gray hover:text-white rounded transition-colors",children:a.jsx(Rt,{className:"w-5 h-5"})})]}),a.jsxs("div",{className:"p-4 space-y-4",children:[s&&a.jsxs("div",{className:"p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary",children:[a.jsxs("p",{className:"text-xs text-bambu-gray mb-1",children:[o("inventory.selectSpool"),":"]}),a.jsxs("div",{className:"flex items-center gap-2",children:[s.color&&a.jsx("span",{className:"w-4 h-4 rounded-full border border-white/20",style:{backgroundColor:`#${s.color}`}}),a.jsx("span",{className:"text-white font-medium",children:s.type||o("ams.emptySlot")}),a.jsxs("span",{className:"text-bambu-gray",children:["(",s.location,")"]})]})]}),a.jsxs("div",{className:"relative",children:[a.jsx(Br,{className:"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray"}),a.jsx("input",{type:"text",value:h,onChange:T=>f(T.target.value),placeholder:o("inventory.searchSpools"),className:"w-full pl-9 pr-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm placeholder:text-bambu-gray focus:outline-none focus:border-bambu-green"})]}),a.jsx("div",{children:x?a.jsx("div",{className:"flex justify-center py-8",children:a.jsx(it,{className:"w-6 h-6 text-bambu-green animate-spin"})}):M&&M.length>0?a.jsx("div",{className:"max-h-64 overflow-y-auto space-y-2",children:M.map(T=>a.jsx("button",{onClick:()=>d(T.id),className:`w-full p-3 rounded-lg border text-left transition-colors ${u===T.id?"bg-bambu-green/20 border-bambu-green":"bg-bambu-dark border-bambu-dark-tertiary hover:border-bambu-gray"}`,children:a.jsxs("div",{className:"flex items-center gap-2",children:[T.rgba&&a.jsx("span",{className:"w-4 h-4 rounded-full border border-white/20 flex-shrink-0",style:{backgroundColor:`#${T.rgba.substring(0,6)}`}}),a.jsxs("div",{className:"flex-1 min-w-0",children:[a.jsxs("p",{className:"text-white font-medium truncate",children:[T.brand?`${T.brand} `:"",T.material,T.subtype?` ${T.subtype}`:""]}),a.jsxs("p",{className:"text-xs text-bambu-gray",children:[T.color_name||"",T.label_weight?` - ${T.label_weight}g`:"",T.weight_used>0?` (${Math.round(T.weight_used)}g used)`:""]})]}),u===T.id&&a.jsx(Yr,{className:"w-4 h-4 text-bambu-green flex-shrink-0"})]})},T.id))}):N&&N.length===0?a.jsx("div",{className:"text-center py-8 text-bambu-gray",children:a.jsx("p",{children:o("inventory.noManualSpools")})}):a.jsx("div",{className:"text-center py-8 text-bambu-gray",children:a.jsx("p",{children:o("inventory.noSpoolsMatch")})})})]}),a.jsxs("div",{className:"flex justify-end gap-2 p-4 border-t border-bambu-dark-tertiary",children:[a.jsx(Te,{variant:"secondary",onClick:e,children:o("common.cancel")}),a.jsx(Te,{onClick:k,disabled:!u||b.isPending,children:b.isPending?a.jsxs(a.Fragment,{children:[a.jsx(it,{className:"w-4 h-4 animate-spin"}),o("inventory.assigning")]}):a.jsxs(a.Fragment,{children:[a.jsx(ra,{className:"w-4 h-4"}),o("inventory.assignSpool")]})})]}),b.isError&&a.jsx("div",{className:"mx-4 mb-4 p-2 bg-red-500/20 border border-red-500/50 rounded text-sm text-red-400",children:b.error.message})]})]})}function xHe(t,e){if(t===255)return"External";let n,r=!1;t>=128&&t<=135?(n=t-128,r=!0):t>=0&&t<=3?(n=t,r=e===1):n=0,n=Math.max(0,Math.min(n,7));const i=String.fromCharCode(65+n);return r?`HT-${i}`:`AMS-${i}`}function MS(t){const e=t.includes("_")?t.split("_")[0]:t;return e.startsWith("GFS")?"GF"+e.slice(3):(e.startsWith("PFUS")||e.startsWith("PFSP"),e)}const yHe=["PLA","PETG","ABS","ASA","TPU","PC","PA","NYLON","PVA","HIPS","PP","PET"];function Q7(t){const e=t.replace(/@.+$/,"").trim(),n=e.toUpperCase();for(const i of yHe){const s=new RegExp(`\\b${i}\\b`,"i");if(s.test(n)){const o=e.split(s),l=o[0]?.trim()||"",c=o[1]?.trim()||"";return{material:i,brand:l,variant:c}}}const r=e.split(/\s+/);return r.length>=2?{material:r[1],brand:r[0],variant:r.slice(2).join(" ")}:{material:e,brand:"",variant:""}}function vHe(t){return!t.startsWith("GF")&&!t.startsWith("P1")}const wHe={white:"FFFFFF",black:"000000",red:"FF0000",green:"00FF00",blue:"0000FF",yellow:"FFFF00",cyan:"00FFFF",magenta:"FF00FF",orange:"FFA500",purple:"800080",pink:"FFC0CB",brown:"8B4513",gray:"808080",grey:"808080","jade white":"FFFEF2",ivory:"FFFFF0",beige:"F5F5DC",cream:"FFFDD0",silver:"C0C0C0",gold:"FFD700",bronze:"CD7F32",copper:"B87333",navy:"000080",teal:"008080",olive:"808000",maroon:"800000",coral:"FF7F50",salmon:"FA8072",lime:"32CD32",mint:"98FF98","forest green":"228B22","sky blue":"87CEEB","royal blue":"4169E1",turquoise:"40E0D0",lavender:"E6E6FA",violet:"EE82EE",plum:"DDA0DD",tan:"D2B48C",chocolate:"D2691E",charcoal:"36454F",slate:"708090",transparent:"000000",natural:"F5F5DC",wood:"DEB887"},_He=[{name:"White",hex:"FFFFFF"},{name:"Black",hex:"000000"},{name:"Red",hex:"FF0000"},{name:"Blue",hex:"0000FF"},{name:"Green",hex:"00AA00"},{name:"Yellow",hex:"FFFF00"},{name:"Orange",hex:"FFA500"},{name:"Gray",hex:"808080"}],SHe=[{name:"Cyan",hex:"00FFFF"},{name:"Magenta",hex:"FF00FF"},{name:"Purple",hex:"800080"},{name:"Pink",hex:"FFC0CB"},{name:"Brown",hex:"8B4513"},{name:"Beige",hex:"F5F5DC"},{name:"Navy",hex:"000080"},{name:"Teal",hex:"008080"},{name:"Lime",hex:"32CD32"},{name:"Gold",hex:"FFD700"},{name:"Silver",hex:"C0C0C0"},{name:"Maroon",hex:"800000"},{name:"Olive",hex:"808000"},{name:"Coral",hex:"FF7F50"},{name:"Salmon",hex:"FA8072"},{name:"Turquoise",hex:"40E0D0"},{name:"Violet",hex:"EE82EE"},{name:"Indigo",hex:"4B0082"},{name:"Chocolate",hex:"D2691E"},{name:"Tan",hex:"D2B48C"},{name:"Slate",hex:"708090"},{name:"Charcoal",hex:"36454F"},{name:"Ivory",hex:"FFFFF0"},{name:"Cream",hex:"FFFDD0"}];function NHe(t){const e=t.toLowerCase().trim();return wHe[e]||null}function CHe(t){const e=t.indexOf("@");if(e<0)return null;const r=t.slice(e+1).trim().match(/^BBL\s+(.+?)(?:\s+[\d.]+\s*nozzle)?$/i);return r?r[1].trim():null}function kHe({isOpen:t,onClose:e,printerId:n,slotInfo:r,nozzleDiameter:i="0.4",printerModel:s,onSuccess:o}){const{t:l}=Ut(),[c,u]=w.useState(""),[d,h]=w.useState(null),[f,p]=w.useState(""),[x,v]=w.useState(""),[b,y]=w.useState(""),[S,N]=w.useState(!1),[M,k]=w.useState(!1),{data:T,isLoading:C,isError:j}=et({queryKey:["cloudSettings"],queryFn:()=>ae.getCloudSettings(),enabled:t,retry:!1}),{data:E,isLoading:P}=et({queryKey:["localPresets"],queryFn:()=>ae.getLocalPresets(),enabled:t}),{data:U,isLoading:L}=et({queryKey:["builtinFilaments"],queryFn:()=>ae.getBuiltinFilaments(),enabled:t,staleTime:1/0}),{data:B,isLoading:D}=et({queryKey:["kprofiles",n,i],queryFn:()=>ae.getKProfiles(n,i),enabled:t&&!!n}),{data:$}=et({queryKey:["colorCatalog"],queryFn:()=>ae.getColorCatalog(),enabled:t,staleTime:1/0}),q=ot({mutationFn:async()=>{if(!c)throw new Error("No filament preset selected");const ue=c.startsWith("local_"),Re=c.startsWith("builtin_"),le=ue?parseInt(c.replace("local_",""),10):null,se=Re?c.replace("builtin_",""):null,Ne=ue?E?.filament.find(ge=>ge.id===le):null,J=Re?U?.find(ge=>ge.filament_id===se):null,Le=!ue&&!Re?T?.filament.find(ge=>ge.setting_id===c):null;if(!ue&&!Re&&!Le)throw new Error("Selected preset not found");if(ue&&!Ne)throw new Error("Selected local preset not found");if(Re&&!J)throw new Error("Selected builtin preset not found");const V=ue?Ne.name:Re?J.name:Le.name,Y=Q7(V),ce=d?.slot_id??-1,H=f||r.trayColor?.slice(0,6)||"FFFFFF",Z=V.replace(/@.+$/,"").trim();let de,pe;if(ue)de="",pe="";else if(Re)de=se,pe="";else if(de=MS(c),pe=c,!c.startsWith("GFS"))try{const ge=await ae.getCloudSettingDetail(c);ge.filament_id?de=ge.filament_id:ge.base_id&&(de=MS(ge.base_id),console.log(`Derived tray_info_idx from base_id: ${ge.base_id} -> ${de}`))}catch(ge){console.warn("Failed to fetch preset detail for filament_id:",ge)}let ve=ue&&Ne?.nozzle_temp_min?Ne.nozzle_temp_min:190,ee=ue&&Ne?.nozzle_temp_max?Ne.nozzle_temp_max:230;if(!ue||Re||!Ne?.nozzle_temp_min&&!Ne?.nozzle_temp_max){const ge=(ue&&Ne?.filament_type||Y.material).toUpperCase();ge.includes("PLA")?(ve=190,ee=230):ge.includes("PETG")?(ve=220,ee=260):ge.includes("ABS")||ge.includes("ASA")?(ve=240,ee=280):ge.includes("TPU")?(ve=200,ee=240):ge.includes("PC")?(ve=260,ee=300):(ge.includes("PA")||ge.includes("NYLON"))&&(ve=250,ee=290)}const z=d?.k_value?parseFloat(d.k_value):0,F=ue?Ne?.filament_type||Y.material||"PLA":Y.material||"PLA",W=await ae.configureAmsSlot(n,r.amsId,r.trayId,{tray_info_idx:de,tray_type:F,tray_sub_brands:Z,tray_color:H+"FF",nozzle_temp_min:ve,nozzle_temp_max:ee,cali_idx:ce,nozzle_diameter:i,setting_id:pe,kprofile_filament_id:d?.filament_id,kprofile_setting_id:d?.setting_id||void 0,k_value:z}),me=ue?`local_${le}`:Re?`builtin_${se}`:c,Se=ue?"local":Re?"builtin":"cloud";try{await ae.saveSlotPreset(n,r.amsId,r.trayId,me,Z,Se)}catch(ge){console.warn("Failed to save slot preset mapping:",ge)}return W},onSuccess:()=>{N(!0),o?.(),setTimeout(()=>{N(!1),e()},1500)}}),I=ot({mutationFn:async()=>ae.resetAmsSlot(n,r.amsId,r.trayId),onSuccess:()=>{N(!0),o?.(),setTimeout(()=>{N(!1),e()},1500)}}),K=w.useMemo(()=>{const ue=b.toLowerCase(),Re=[],le=new Set,se=r.savedPresetId,Ne=r.trayInfoIdx;if(T?.filament)for(const J of T.filament){le.add(J.setting_id);const Le=se===J.setting_id||Ne&&(J.setting_id===Ne||MS(J.setting_id)===Ne);if(!(!Le&&ue&&!J.name.toLowerCase().includes(ue))){if(!Le&&s){const V=CHe(J.name);if(V&&V.toUpperCase()!==s.toUpperCase())continue}Re.push({id:J.setting_id,name:J.name,source:"cloud",isUser:vHe(J.setting_id)})}}if(E?.filament)for(const J of E.filament){const Le=`local_${J.id}`,V=se===Le;if(!(!V&&ue&&!J.name.toLowerCase().includes(ue))){if(!V&&s&&J.compatible_printers){const Y=J.compatible_printers.split(";").map(ce=>{const H=ce.trim(),Z=H.match(/^BBL\s+(.+)/i);return Z?Z[1].trim().toUpperCase():H.toUpperCase()}).filter(Boolean);if(Y.length>0&&!Y.includes(s.toUpperCase()))continue}Re.push({id:Le,name:J.name,source:"local",isUser:!1})}}if(U)for(const J of U){if(le.has(J.filament_id))continue;const Le=J.filament_id.startsWith("GF")?"GFS"+J.filament_id.slice(2):J.filament_id;le.has(Le)||(!ue||J.name.toLowerCase().includes(ue))&&Re.push({id:`builtin_${J.filament_id}`,name:J.name,source:"builtin",isUser:!1})}return Re.sort((J,Le)=>{const V={cloud:0,local:1,builtin:2};return J.source!==Le.source?V[J.source]-V[Le.source]:J.isUser&&!Le.isUser?-1:!J.isUser&&Le.isUser?1:J.name.localeCompare(Le.name)})},[T?.filament,E?.filament,U,b,s,r.savedPresetId,r.trayInfoIdx]),Q=w.useMemo(()=>{if(!c)return null;let ue=null;if(c.startsWith("local_")){const se=parseInt(c.replace("local_",""),10);ue=E?.filament.find(J=>J.id===se)?.name||null}else if(c.startsWith("builtin_")){const se=c.replace("builtin_","");ue=U?.find(J=>J.filament_id===se)?.name||null}else T?.filament&&(ue=T.filament.find(Ne=>Ne.setting_id===c)?.name||null);if(!ue)return null;let Re=ue.replace(/@.+$/,"").trim();Re.startsWith("# ")&&(Re=Re.slice(2).trim());const le=Q7(Re);return{fullName:Re,material:le.material,brand:le.brand}},[c,T?.filament,E?.filament,U]),ie=Q?.fullName||"",G=w.useMemo(()=>{if(!$||!Q)return[];const{fullName:ue,brand:Re}=Q,le=ue.replace(/^(Bambu\s*(Lab)?|eSUN|Polymaker|Overture|Sunlu|Hatchbox)\s*/i,"").trim();return $.filter(se=>{const Ne=(se.material||"").toUpperCase(),J=se.manufacturer.toUpperCase();if(!(Ne===le.toUpperCase()||Ne.includes(le.toUpperCase())||le.toUpperCase().includes(Ne)))return!1;if(Re){const V=Re.toUpperCase();if(!J.includes(V)&&!V.includes(J))return!1}return!0})},[$,Q]),X=w.useMemo(()=>{if(!B?.profiles||!Q)return[];const{fullName:ue,material:Re,brand:le}=Q,se=ue.toUpperCase(),Ne=Re.toUpperCase(),J=le.toUpperCase();if(!Ne||Ne.length<2)return[];const Le=B.profiles.filter(Y=>{const ce=Y.name.toUpperCase();if(J)return!(!ce.includes(J)||!ce.includes(Ne));if(ce.includes(se)||ce.includes(Ne))return!0;const Z={NYLON:["PA","PA-CF","PA6"],PA:["NYLON"]}[Ne]||[];for(const de of Z)if(ce.includes(de))return!0;return!1}),V=new Map;for(const Y of Le){const ce=`${Y.name}|${Y.k_value}`,H=V.get(ce);H?r.extruderId!==void 0&&Y.extruder_id===r.extruderId&&H.extruder_id!==r.extruderId&&V.set(ce,Y):V.set(ce,Y)}return Array.from(V.values())},[B?.profiles,Q]);w.useEffect(()=>{if(t){if(r.savedPresetId)u(r.savedPresetId);else if(r.trayInfoIdx&&T?.filament){let ue=T.filament.find(Re=>Re.setting_id===r.trayInfoIdx);ue||(ue=T.filament.find(Re=>MS(Re.setting_id)===r.trayInfoIdx)),ue&&u(ue.setting_id)}if(r.trayColor){const ue=r.trayColor.slice(0,6);ue&&ue!=="000000"&&p(ue)}}else u(""),h(null),p(""),v(""),y(""),N(!1)},[t,r.savedPresetId,r.trayInfoIdx,r.trayColor,T?.filament]),w.useEffect(()=>{if(X.length>0){if(r.caliIdx!=null&&r.caliIdx>0){const ue=X.find(Re=>Re.slot_id===r.caliIdx);if(ue){h(ue);return}}h(X[0])}else h(null)},[c,X,r.caliIdx]);const ne=w.useCallback(ue=>{ue.key==="Escape"&&e()},[e]);if(w.useEffect(()=>{if(t)return document.addEventListener("keydown",ne),()=>document.removeEventListener("keydown",ne)},[t,ne]),!t)return null;const te=C&&!j||P||L||D,xe=c&&!q.isPending,he=f||r.trayColor?.slice(0,6)||"FFFFFF";return a.jsxs("div",{className:"fixed inset-0 z-50 flex items-center justify-center",children:[a.jsx("div",{className:"absolute inset-0 bg-black/60 backdrop-blur-sm",onClick:e}),a.jsxs("div",{className:"relative w-full max-w-lg mx-4 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-xl shadow-2xl",children:[a.jsxs("div",{className:"flex items-center justify-between p-4 border-b border-bambu-dark-tertiary",children:[a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(nd,{className:"w-5 h-5 text-bambu-blue"}),a.jsx("h2",{className:"text-lg font-semibold text-white",children:l("configureAmsSlot.title")})]}),a.jsx("button",{onClick:e,className:"p-1 text-bambu-gray hover:text-white rounded transition-colors",children:a.jsx(Rt,{className:"w-5 h-5"})})]}),a.jsxs("div",{className:"p-4 space-y-4 max-h-[60vh] overflow-y-auto",children:[S&&a.jsx("div",{className:"absolute inset-0 bg-bambu-dark-secondary/95 z-10 flex items-center justify-center rounded-xl",children:a.jsxs("div",{className:"text-center space-y-3",children:[a.jsx(ub,{className:"w-16 h-16 text-bambu-green mx-auto"}),a.jsx("p",{className:"text-lg font-semibold text-white",children:l("configureAmsSlot.slotConfigured")}),a.jsx("p",{className:"text-sm text-bambu-gray",children:l("configureAmsSlot.settingsSentToPrinter")})]})}),a.jsxs("div",{className:"p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary",children:[a.jsx("p",{className:"text-xs text-bambu-gray mb-1",children:l("configureAmsSlot.configuringSlot")}),a.jsxs("div",{className:"flex items-center gap-2",children:[r.trayColor&&a.jsx("span",{className:"w-4 h-4 rounded-full border border-white/20",style:{backgroundColor:`#${r.trayColor.slice(0,6)}`}}),a.jsx("span",{className:"text-white font-medium",children:l("configureAmsSlot.slotLabel",{ams:xHe(r.amsId,r.trayCount),slot:r.trayId+1})}),r.traySubBrands&&a.jsxs("span",{className:"text-bambu-gray",children:["(",r.traySubBrands,")"]})]})]}),te?a.jsx("div",{className:"flex justify-center py-8",children:a.jsx(it,{className:"w-6 h-6 text-bambu-green animate-spin"})}):a.jsxs(a.Fragment,{children:[a.jsxs("div",{children:[a.jsxs("label",{className:"block text-sm text-bambu-gray mb-2",children:[l("configureAmsSlot.filamentProfile")," ",a.jsx("span",{className:"text-red-400",children:"*"})]}),a.jsxs("div",{className:"relative",children:[a.jsx("input",{type:"text",placeholder:l("configureAmsSlot.searchPresets"),value:b,onChange:ue=>y(ue.target.value),className:"w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder:text-bambu-gray focus:border-bambu-green focus:outline-none mb-2"}),a.jsx("div",{className:"max-h-48 overflow-y-auto space-y-1",children:K.length===0?a.jsx("p",{className:"text-center py-4 text-bambu-gray",children:T?.filament?.length===0&&!E?.filament?.length?l("configureAmsSlot.noPresetsAvailable"):l("configureAmsSlot.noMatchingPresets")}):K.map(ue=>a.jsx("button",{ref:c===ue.id?Re=>{Re?.scrollIntoView({block:"nearest"})}:void 0,onClick:()=>u(ue.id),className:`w-full p-2 rounded-lg border text-left transition-colors ${c===ue.id?"bg-bambu-green/20 border-bambu-green":"bg-bambu-dark border-bambu-dark-tertiary hover:border-bambu-gray"}`,children:a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsx("span",{className:"text-white text-sm truncate",children:ue.name}),a.jsxs("div",{className:"flex items-center gap-1 flex-shrink-0",children:[ue.source==="local"&&a.jsx("span",{className:"text-xs px-1.5 py-0.5 rounded bg-green-500/20 text-green-400",children:l("profiles.localProfiles.badge")}),ue.source==="builtin"&&a.jsx("span",{className:"text-xs px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400",children:l("configureAmsSlot.builtin")}),ue.isUser&&a.jsx("span",{className:"text-xs px-1.5 py-0.5 rounded bg-bambu-blue/20 text-bambu-blue",children:l("configureAmsSlot.custom")})]})]})},ue.id))})]})]}),a.jsxs("div",{children:[a.jsxs("label",{className:"block text-sm text-bambu-gray mb-2",children:[l("configureAmsSlot.kProfileLabel"),ie&&a.jsx("span",{className:"ml-2 text-xs text-bambu-blue",children:l("configureAmsSlot.filteringFor",{material:ie})})]}),X.length>0?a.jsxs("div",{className:"relative",children:[a.jsxs("select",{value:d?.name||"",onChange:ue=>{const Re=X.find(le=>le.name===ue.target.value);h(Re||null)},className:"w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none appearance-none pr-10",children:[a.jsx("option",{value:"",children:l("configureAmsSlot.noKProfile")}),X.map(ue=>a.jsxs("option",{value:ue.name,children:[ue.name," (K=",ue.k_value,")"]},`${ue.name}-${ue.extruder_id}`))]}),a.jsx(Jn,{className:"absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none"})]}):c?a.jsx("p",{className:"text-sm text-bambu-gray italic py-2",children:l("configureAmsSlot.noMatchingKProfiles")}):a.jsx("span",{className:"inline-block text-xs px-2 py-1 rounded bg-amber-500/20 text-amber-400 border border-amber-500/30",children:l("configureAmsSlot.selectFilamentFirst")}),d&&a.jsx("p",{className:"text-xs text-bambu-green mt-1",children:l("configureAmsSlot.kFromCalibration",{value:d.k_value})})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm text-bambu-gray mb-2",children:l("configureAmsSlot.customColorLabel")}),G.length>0&&a.jsxs("div",{className:"mb-3",children:[a.jsx("p",{className:"text-xs text-bambu-gray mb-1.5",children:l("configureAmsSlot.presetColors",{name:Q?.fullName})}),a.jsx("div",{className:"flex flex-wrap gap-1.5",children:G.map(ue=>a.jsxs("button",{onClick:()=>{const Re=ue.hex_color.replace("#","").toUpperCase();p(Re),v(ue.color_name)},className:`h-7 px-2 rounded-md border-2 transition-all flex items-center gap-1.5 ${f===ue.hex_color.replace("#","").toUpperCase()?"border-bambu-green scale-105":"border-white/20 hover:border-white/40"}`,title:ue.color_name,children:[a.jsx("span",{className:"w-4 h-4 rounded-full border border-white/30 flex-shrink-0",style:{backgroundColor:ue.hex_color}}),a.jsx("span",{className:"text-xs text-white/80 whitespace-nowrap",children:ue.color_name})]},ue.id))})]}),a.jsxs("div",{className:"flex flex-wrap gap-1.5 mb-2",children:[_He.map(ue=>a.jsx("button",{onClick:()=>{p(ue.hex),v(ue.name)},className:`w-7 h-7 rounded-md border-2 transition-all ${f===ue.hex?"border-bambu-green scale-110":"border-white/20 hover:border-white/40"}`,style:{backgroundColor:`#${ue.hex}`},title:ue.name},ue.hex)),a.jsx("button",{onClick:()=>k(!M),className:"w-7 h-7 rounded-md border-2 border-white/20 hover:border-white/40 flex items-center justify-center text-white/60 hover:text-white/80 transition-all text-xs",title:l(M?"configureAmsSlot.showLessColors":"configureAmsSlot.showMoreColors"),children:M?"−":"+"})]}),M&&a.jsx("div",{className:"flex flex-wrap gap-1.5 mb-2",children:SHe.map(ue=>a.jsx("button",{onClick:()=>{p(ue.hex),v(ue.name)},className:`w-7 h-7 rounded-md border-2 transition-all ${f===ue.hex?"border-bambu-green scale-110":"border-white/20 hover:border-white/40"}`,style:{backgroundColor:`#${ue.hex}`},title:ue.name},ue.hex))}),a.jsxs("div",{className:"flex gap-2 items-center",children:[a.jsx("div",{className:"w-10 h-10 rounded-lg border-2 border-white/20 flex-shrink-0",style:{backgroundColor:`#${he}`}}),a.jsx("input",{type:"text",placeholder:l("configureAmsSlot.colorPlaceholder"),value:x,onChange:ue=>{const Re=ue.target.value;v(Re);const le=NHe(Re);if(le)p(le);else{const se=Re.replace(/[^0-9A-Fa-f]/g,"").toUpperCase();se.length===6?p(se):se.length===3&&p(se.split("").map(Ne=>Ne+Ne).join(""))}},className:"flex-1 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder:text-bambu-gray focus:border-bambu-green focus:outline-none text-sm"}),f&&a.jsx("button",{onClick:()=>{p(""),v("")},className:"px-2 py-1 text-xs text-bambu-gray hover:text-white bg-bambu-dark-tertiary rounded",title:l("configureAmsSlot.clearCustomColor"),children:l("configureAmsSlot.clear")})]}),f&&a.jsx("p",{className:"text-xs text-bambu-gray mt-1.5",children:l("configureAmsSlot.hexLabel",{hex:f})})]})]})]}),a.jsxs("div",{className:"flex justify-between p-4 border-t border-bambu-dark-tertiary",children:[a.jsx(Te,{variant:"secondary",onClick:()=>I.mutate(),disabled:I.isPending||q.isPending,className:"text-red-400 hover:text-red-300 hover:bg-red-500/10",children:I.isPending?a.jsxs(a.Fragment,{children:[a.jsx(it,{className:"w-4 h-4 animate-spin"}),l("configureAmsSlot.resetting")]}):a.jsxs(a.Fragment,{children:[a.jsx(Do,{className:"w-4 h-4"}),l("configureAmsSlot.resetSlot")]})}),a.jsxs("div",{className:"flex gap-2",children:[a.jsx(Te,{variant:"secondary",onClick:e,children:l("configureAmsSlot.cancel")}),a.jsx(Te,{onClick:()=>q.mutate(),disabled:!xe,children:q.isPending?a.jsxs(a.Fragment,{children:[a.jsx(it,{className:"w-4 h-4 animate-spin"}),l("configureAmsSlot.configuring")]}):a.jsxs(a.Fragment,{children:[a.jsx(nd,{className:"w-4 h-4"}),l("configureAmsSlot.configureSlot")]})})]})]}),(q.isError||I.isError)&&a.jsx("div",{className:"mx-4 mb-4 p-2 bg-red-500/20 border border-red-500/50 rounded text-sm text-red-400",children:q.error?.message||I.error?.message})]})]})}const Z7={"A00-W1":"Jade White","A00-P0":"Beige","A00-D2":"Light Gray","A00-Y0":"Yellow","A00-Y2":"Sunflower Yellow","A00-A1":"Pumpkin Orange","A00-A0":"Orange","A00-Y4":"Gold","A00-G3":"Bright Green","A00-G1":"Bambu Green","A00-G2":"Mistletoe Green","A00-R3":"Hot Pink","A00-P6":"Magenta","A00-R0":"Red","A00-R2":"Maroon Red","A00-P5":"Purple","A00-P2":"Indigo Purple","A00-B5":"Turquoise","A00-B8":"Cyan","A00-B3":"Cobalt Blue","A00-N0":"Brown","A00-N1":"Cocoa Brown","A00-Y3":"Bronze","A00-D0":"Gray","A00-D1":"Silver","A00-B1":"Blue Grey","A00-D3":"Dark Gray","A00-K0":"Black","A00-M3":"Pink Citrus","A00-M6":"Dusk Glare","A00-M0":"Arctic Whisper","A00-M1":"Solar Breeze","A00-M5":"Blueberry Bubblegum","A00-M4":"Mint Lime","A00-M2":"Ocean to Meadow","A00-M7":"Cotton Candy Cloud","A18-K0":"Black","A18-D0":"Gray","A18-W0":"White","A18-R0":"Red","A18-Y0":"Yellow","A18-B0":"Cyan","A18-B1":"Blue","A18-P0":"Matte Beige","A01-W2":"Ivory White","A01-W3":"Bone White","A01-Y2":"Lemon Yellow","A01-A2":"Mandarin Orange","A01-P3":"Sakura Pink","A01-P4":"Lilac Purple","A01-R3":"Plum","A01-R1":"Scarlet Red","A01-R4":"Dark Red","A01-G0":"Apple Green","A01-G1":"Grass Green","A01-G7":"Dark Green","A01-B4":"Ice Blue","A01-B0":"Sky Blue","A01-B3":"Marine Blue","A01-B6":"Dark Blue","A01-Y3":"Desert Tan","A01-N1":"Latte Brown","A01-N3":"Caramel","A01-R2":"Terracotta","A01-N2":"Dark Brown","A01-N0":"Dark Chocolate","A01-D3":"Ash Gray","A01-D0":"Nardo Gray","A01-K1":"Charcoal","A12-G0":"Green","A12-R0":"Pink","A12-A0":"Orange","A12-Y0":"Yellow","A12-B0":"Blue","A07-R5":"Red Granite","A07-D4":"White Marble","A11-W0":"White","A11-K0":"Black","A08-G3":"Alpine Green Sparkle","A08-D5":"Slate Gray Sparkle","A08-B7":"Royal Purple Sparkle","A08-R2":"Crimson Red Sparkle","A08-K2":"Onyx Black Sparkle","A08-Y1":"Classic Gold Sparkle","A02-B2":"Cobalt Blue Metallic","A02-G2":"Oxide Green Metallic","A02-Y1":"Iridium Gold Metallic","A02-D2":"Iron Gray Metallic","A17-B1":"Blue","A17-A0":"Orange","A17-P0":"Purple","A06-Y1":"Gold","A06-D0":"Titan Gray","A06-D1":"Silver","A06-W0":"White","A06-R0":"Candy Red","A06-G0":"Candy Green","A06-G1":"Mint","A06-B1":"Blue","A06-B0":"Baby Blue","A06-P0":"Purple","A06-R1":"Rose Gold","A06-R2":"Pink","A06-Y0":"Champagne","A05-M8":"Dawn Radiance","A05-M4":"Aurora Purple","A05-M1":"South Beach","A05-T3":"Neon City","A05-T2":"Midnight Blaze","A05-T1":"Gilded Rose","A05-T4":"Blue Hawaii","A05-T5":"Velvet Eclipse","A15-B0":"Purple","A15-G0":"Green","A15-G1":"Nebulae","A15-R0":"Brown","A16-K0":"Black Walnut","A16-R0":"Rosewood","A16-N0":"Clay Brown","A16-G0":"Classic Birch","A16-W0":"White Oak","A16-Y0":"Ochre Yellow","A50-D6":"Lava Gray","A50-K0":"Black","A50-B6":"Royal Blue","A10-W0":"White","A10-D0":"Gray","A09-B5":"Lavender Blue","A09-B4":"Light Blue","A09-A0":"Orange","A09-D1":"Silver","A09-R3":"Vermilion Red","A09-Y0":"Yellow","G02-K0":"Black","G02-W0":"White","G02-R0":"Red","G02-D0":"Gray","G02-D1":"Dark Gray","G02-Y1":"Cream","G02-Y0":"Yellow","G02-A0":"Orange","G02-N1":"Peanut Brown","G02-G1":"Lime Green","G02-G0":"Green","G02-G2":"Forest Green","G02-B1":"Lake Blue","G02-B0":"Blue","G01-G1":"Translucent Teal","G01-B0":"Translucent Light Blue","G01-C0":"Clear","G01-D0":"Translucent Gray","G01-G0":"Translucent Olive","G01-N0":"Translucent Brown","G01-A0":"Translucent Orange","G01-P1":"Translucent Pink","G01-P0":"Translucent Purple","G50-P7":"Violet Purple","G50-K0":"Black","B00-D1":"Silver","B00-K0":"Black","B00-W0":"White","B00-G6":"Bambu Green","B00-G7":"Olive","B00-Y1":"Tangerine Yellow","B00-A0":"Orange","B00-R0":"Red","B00-B4":"Azure","B00-B0":"Blue","B00-B6":"Navy Blue","B50-A0":"Orange","B50-K0":"Black","B01-W0":"White","B01-K0":"Black","B01-D0":"Gray","B02-W0":"White","C00-C1":"Transparent","C00-C0":"Clear Black","C00-K0":"Black","C00-W0":"White","C01-K0":"Black","U02-B0":"Blue","U02-D0":"Gray","U02-K0":"Black","N04-K0":"Black","N08-K0":"Black","S02-W0":"Nature","S02-W1":"White","S05-C0":"Black","S06-W0":"White","S03-G1":"Green","S04-Y0":"Clear"},MHe={W0:"White",W1:"Jade White",W2:"Ivory White",W3:"Bone White",Y0:"Yellow",Y1:"Gold",Y2:"Sunflower Yellow",Y3:"Bronze",Y4:"Gold",A0:"Orange",A1:"Pumpkin Orange",A2:"Mandarin Orange",R0:"Red",R1:"Scarlet Red",R2:"Maroon Red",R3:"Hot Pink",R4:"Dark Red",R5:"Red Granite",P0:"Beige",P1:"Pink",P2:"Indigo Purple",P3:"Sakura Pink",P4:"Lilac Purple",P5:"Purple",P6:"Magenta",P7:"Violet Purple",B0:"Blue",B1:"Blue Grey",B2:"Cobalt Blue",B3:"Cobalt Blue",B4:"Ice Blue",B5:"Turquoise",B6:"Navy Blue",B7:"Royal Purple",B8:"Cyan",G0:"Green",G1:"Grass Green",G2:"Mistletoe Green",G3:"Bright Green",G6:"Bambu Green",G7:"Dark Green",N0:"Brown",N1:"Peanut Brown",N2:"Dark Brown",N3:"Caramel",D0:"Gray",D1:"Silver",D2:"Light Gray",D3:"Dark Gray",D4:"White Marble",D5:"Slate Gray",D6:"Lava Gray",K0:"Black",K1:"Charcoal",K2:"Onyx Black",C0:"Clear Black",C1:"Transparent",M0:"Arctic Whisper",M1:"Solar Breeze",M2:"Ocean to Meadow",M3:"Pink Citrus",M4:"Aurora Purple",M5:"Blueberry Bubblegum",M6:"Dusk Glare",M7:"Cotton Candy Cloud",M8:"Dawn Radiance",T1:"Gilded Rose",T2:"Midnight Blaze",T3:"Neon City",T4:"Blue Hawaii",T5:"Velvet Eclipse"};function XP(t){if(!t)return null;if(Z7[t])return Z7[t];const e=t.split("-");if(e.length<2)return null;const n=e[1];return MHe[n]||null}function YP(t){if(!t||t.length<6)return"Unknown";const e=parseInt(t.substring(0,2),16),n=parseInt(t.substring(2,4),16),r=parseInt(t.substring(4,6),16),i=Math.max(e,n,r)/255,s=Math.min(e,n,r)/255,o=(i+s)/2;let l=0,c=0;if(i!==s){const u=i-s;c=o>.5?u/(2-i-s):u/(i+s);const d=e/255,h=n/255,f=r/255;i===d?l=((h-f)/u+(h.85?"White":c<.15?o<.4?"Dark Gray":o>.6?"Light Gray":"Gray":l>=15&&l<45&&o<.45||l>=45&&l<70&&o<.4?"Brown":l<15||l>=345?"Red":l<45?"Orange":l<70?"Yellow":l<150?"Green":l<200?"Cyan":l<260?"Blue":l<290?"Purple":"Pink"}function QP(t){return(t??.02).toFixed(3)}function J7({side:t}){const{mode:e}=rp(),n=e==="dark"?"#1a4d2e":"#e7f5e9";return a.jsx("span",{className:"inline-flex items-center justify-center w-4 h-4 text-[10px] font-bold rounded",style:{backgroundColor:n,color:"#00ae42"},children:t})}function b4(t){if(!t||t==="00000000"||t.length<6)return null;const e=t.slice(0,2),n=t.slice(2,4),r=t.slice(4,6),i=t.length>=8?parseInt(t.slice(6,8),16)/255:1;return i===0?null:`rgba(${parseInt(e,16)}, ${parseInt(n,16)}, ${parseInt(r,16)}, ${i})`}function AHe(t){if(!t||t.length<6)return!1;const e=parseInt(t.slice(0,2),16),n=parseInt(t.slice(2,4),16),r=parseInt(t.slice(4,6),16);return(.299*e+.587*n+.114*r)/255>.6}function nne(t,e){if(!t)return"";if(t.includes("hardened"))return e("printers.nozzleHardenedSteel");if(t.includes("stainless"))return e("printers.nozzleStainlessSteel");if(t.includes("tungsten"))return e("printers.nozzleTungstenCarbide");if(t.length>=4){const n=t.slice(2,4);if(n==="00")return e("printers.nozzleStainlessSteel");if(n==="01")return e("printers.nozzleHardenedSteel");if(n==="05")return e("printers.nozzleTungstenCarbide")}return t==="00"?e("printers.nozzleStainlessSteel"):t==="01"?e("printers.nozzleHardenedSteel"):t==="05"?e("printers.nozzleTungstenCarbide"):t.startsWith("H")?e("printers.nozzleHardenedSteel"):t}function rne(t,e){return t?t.startsWith("HH")?e("printers.nozzleHighFlow"):t.startsWith("HS")?e("printers.nozzleStandardFlow"):"":""}function ine({slot:t,index:e,activeStatus:n,filamentName:r,children:i}){const{t:s}=Ut(),[o,l]=w.useState(!1),[c,u]=w.useState("top"),d=w.useRef(null),h=w.useRef(null),f=w.useRef(null),p=!t.nozzle_diameter&&!t.nozzle_type,x=t.stat===1;w.useEffect(()=>{if(o&&d.current&&h.current){const M=d.current.getBoundingClientRect(),k=h.current.offsetHeight,C=M.top-56,j=window.innerHeight-M.bottom;CC?u("bottom"):u("top")}},[o]);const v=()=>{f.current&&clearTimeout(f.current),f.current=setTimeout(()=>l(!0),80)},b=()=>{f.current&&clearTimeout(f.current),f.current=setTimeout(()=>l(!1),100)};w.useEffect(()=>()=>{f.current&&clearTimeout(f.current)},[]);const y=b4(t.filament_color),S=nne(t.nozzle_type,s),N=rne(t.nozzle_type,s);return a.jsxs("div",{ref:d,className:"relative",onMouseEnter:v,onMouseLeave:b,children:[i,o&&a.jsxs("div",{ref:h,className:` + `})]})]})}function gHe({isOpen:t,onClose:e,tagUid:n,trayUuid:r,printerId:i,amsId:s,trayId:o}){const{t:l}=Ut(),c=Zt(),{showToast:u}=hn(),[d,h]=w.useState(""),{data:f,isLoading:p}=et({queryKey:["inventory-spools"],queryFn:()=>ae.getSpools(!1),enabled:t}),x=w.useMemo(()=>f?f.filter(b=>{if(b.tag_uid||b.tray_uuid)return!1;if(!d)return!0;const y=d.toLowerCase();return b.material.toLowerCase().includes(y)||b.brand&&b.brand.toLowerCase().includes(y)||b.color_name&&b.color_name.toLowerCase().includes(y)}):[],[f,d]),v=ot({mutationFn:b=>ae.linkTagToSpool(b,{tag_uid:n||void 0,tray_uuid:r||void 0,tag_type:r?"bambulab":"generic",data_origin:"nfc_link"}),onSuccess:()=>{c.invalidateQueries({queryKey:["inventory-spools"]}),c.invalidateQueries({queryKey:["inventory-assignments"]}),u(l("inventory.tagLinked"),"success"),e()},onError:b=>{u(b.message||l("inventory.tagLinkFailed"),"error")}});return t?a.jsxs("div",{className:"fixed inset-0 z-50 flex items-center justify-center",children:[a.jsx("div",{className:"absolute inset-0 bg-black/60 backdrop-blur-sm",onClick:e}),a.jsxs("div",{className:"relative bg-bambu-dark-secondary rounded-xl shadow-xl w-full max-w-md mx-4 max-h-[80vh] flex flex-col border border-bambu-dark-tertiary",children:[a.jsxs("div",{className:"flex items-center justify-between p-4 border-b border-white/10",children:[a.jsxs("div",{children:[a.jsxs("h3",{className:"text-lg font-semibold text-white flex items-center gap-2",children:[a.jsx(Vb,{className:"w-5 h-5 text-bambu-green"}),l("inventory.linkToSpool")]}),a.jsxs("p",{className:"text-xs text-bambu-gray mt-1",children:["AMS ",s," T",o," · Printer #",i]})]}),a.jsx("button",{onClick:e,className:"p-1 text-bambu-gray hover:text-white rounded transition-colors",children:a.jsx(Rt,{className:"w-5 h-5"})})]}),a.jsxs("div",{className:"p-4 border-b border-white/10",children:[a.jsxs("div",{className:"relative",children:[a.jsx(Br,{className:"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray"}),a.jsx("input",{type:"text",value:d,onChange:b=>h(b.target.value),placeholder:l("inventory.searchSpools"),className:"w-full pl-9 pr-3 py-2 bg-bambu-dark rounded-lg border border-white/10 text-white text-sm placeholder:text-bambu-gray focus:outline-none focus:border-bambu-green"})]}),(n||r)&&a.jsxs("p",{className:"text-xs text-bambu-gray mt-2 font-mono truncate",title:n||r,children:["Tag: ",n||r]})]}),a.jsx("div",{className:"flex-1 overflow-y-auto p-2 min-h-0",children:p?a.jsx("div",{className:"flex justify-center py-8",children:a.jsx(it,{className:"w-6 h-6 animate-spin text-bambu-green"})}):x.length===0?a.jsx("p",{className:"text-center text-bambu-gray py-8 text-sm",children:l("inventory.noSpoolsMatch")}):x.map(b=>a.jsxs("button",{onClick:()=>v.mutate(b.id),disabled:v.isPending,className:"w-full flex items-center gap-3 p-3 rounded-lg hover:bg-white/5 transition-colors text-left",children:[a.jsx("span",{className:"w-6 h-6 rounded-full border border-white/20 flex-shrink-0",style:{backgroundColor:b.rgba?`#${b.rgba.substring(0,6)}`:"#808080"}}),a.jsxs("div",{className:"flex-1 min-w-0",children:[a.jsxs("div",{className:"text-sm text-white font-medium truncate",children:[b.brand?`${b.brand} `:"",b.material,b.subtype?` ${b.subtype}`:""]}),a.jsxs("div",{className:"text-xs text-bambu-gray truncate",children:[b.color_name||"No color"," · #",b.id]})]}),a.jsxs("span",{className:"text-xs text-bambu-gray",children:[Math.round(b.label_weight-b.weight_used),"g"]})]},b.id))}),a.jsx("div",{className:"p-4 border-t border-white/10 flex justify-end",children:a.jsx(Te,{variant:"ghost",onClick:e,children:l("inventory.cancel")||"Cancel"})})]})]}):null}function bHe({isOpen:t,onClose:e,printerId:n,amsId:r,trayId:i,trayInfo:s}){const{t:o}=Ut(),l=Zt(),{showToast:c}=hn(),[u,d]=w.useState(null),[h,f]=w.useState(""),{data:p,isLoading:x}=et({queryKey:["inventory-spools"],queryFn:()=>ae.getSpools(),enabled:t}),{data:v}=et({queryKey:["spool-assignments"],queryFn:()=>ae.getAssignments(),enabled:t}),b=ot({mutationFn:T=>ae.assignSpool({spool_id:T,printer_id:n,ams_id:r,tray_id:i}),onSuccess:T=>{l.setQueryData(["spool-assignments"],C=>{const j=(C||[]).filter(E=>!(E.printer_id===n&&E.ams_id===r&&E.tray_id===i));return j.push(T),j}),l.invalidateQueries({queryKey:["spool-assignments"]}),c(o("inventory.assignSuccess"),"success"),e()},onError:T=>{c(`${o("inventory.assignFailed")}: ${T.message}`,"error")}});if(!t)return null;const y=new Set((v||[]).filter(T=>!(T.printer_id===n&&T.ams_id===r&&T.tray_id===i)).map(T=>T.spool_id)),S=r===254||r===255,N=p?.filter(T=>!y.has(T.id)&&(S||!T.tag_uid&&!T.tray_uuid)),M=N?.filter(T=>{if(!h)return!0;const C=h.toLowerCase();return T.material.toLowerCase().includes(C)||(T.brand?.toLowerCase().includes(C)??!1)||(T.color_name?.toLowerCase().includes(C)??!1)||(T.subtype?.toLowerCase().includes(C)??!1)}),k=()=>{u&&b.mutate(u)};return a.jsxs("div",{className:"fixed inset-0 z-50 flex items-center justify-center",children:[a.jsx("div",{className:"absolute inset-0 bg-black/60 backdrop-blur-sm",onClick:e}),a.jsxs("div",{className:"relative w-full max-w-md mx-4 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-xl shadow-2xl",children:[a.jsxs("div",{className:"flex items-center justify-between p-4 border-b border-bambu-dark-tertiary",children:[a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(ra,{className:"w-5 h-5 text-bambu-green"}),a.jsx("h2",{className:"text-lg font-semibold text-white",children:o("inventory.assignSpool")})]}),a.jsx("button",{onClick:e,className:"p-1 text-bambu-gray hover:text-white rounded transition-colors",children:a.jsx(Rt,{className:"w-5 h-5"})})]}),a.jsxs("div",{className:"p-4 space-y-4",children:[s&&a.jsxs("div",{className:"p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary",children:[a.jsxs("p",{className:"text-xs text-bambu-gray mb-1",children:[o("inventory.selectSpool"),":"]}),a.jsxs("div",{className:"flex items-center gap-2",children:[s.color&&a.jsx("span",{className:"w-4 h-4 rounded-full border border-white/20",style:{backgroundColor:`#${s.color}`}}),a.jsx("span",{className:"text-white font-medium",children:s.type||o("ams.emptySlot")}),a.jsxs("span",{className:"text-bambu-gray",children:["(",s.location,")"]})]})]}),a.jsxs("div",{className:"relative",children:[a.jsx(Br,{className:"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray"}),a.jsx("input",{type:"text",value:h,onChange:T=>f(T.target.value),placeholder:o("inventory.searchSpools"),className:"w-full pl-9 pr-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm placeholder:text-bambu-gray focus:outline-none focus:border-bambu-green"})]}),a.jsx("div",{children:x?a.jsx("div",{className:"flex justify-center py-8",children:a.jsx(it,{className:"w-6 h-6 text-bambu-green animate-spin"})}):M&&M.length>0?a.jsx("div",{className:"max-h-64 overflow-y-auto space-y-2",children:M.map(T=>a.jsx("button",{onClick:()=>d(T.id),className:`w-full p-3 rounded-lg border text-left transition-colors ${u===T.id?"bg-bambu-green/20 border-bambu-green":"bg-bambu-dark border-bambu-dark-tertiary hover:border-bambu-gray"}`,children:a.jsxs("div",{className:"flex items-center gap-2",children:[T.rgba&&a.jsx("span",{className:"w-4 h-4 rounded-full border border-white/20 flex-shrink-0",style:{backgroundColor:`#${T.rgba.substring(0,6)}`}}),a.jsxs("div",{className:"flex-1 min-w-0",children:[a.jsxs("p",{className:"text-white font-medium truncate",children:[T.brand?`${T.brand} `:"",T.material,T.subtype?` ${T.subtype}`:""]}),a.jsxs("p",{className:"text-xs text-bambu-gray",children:[T.color_name||"",T.label_weight?` - ${T.label_weight}g`:"",T.weight_used>0?` (${Math.round(T.weight_used)}g used)`:""]})]}),u===T.id&&a.jsx(Yr,{className:"w-4 h-4 text-bambu-green flex-shrink-0"})]})},T.id))}):N&&N.length===0?a.jsx("div",{className:"text-center py-8 text-bambu-gray",children:a.jsx("p",{children:o("inventory.noManualSpools")})}):a.jsx("div",{className:"text-center py-8 text-bambu-gray",children:a.jsx("p",{children:o("inventory.noSpoolsMatch")})})})]}),a.jsxs("div",{className:"flex justify-end gap-2 p-4 border-t border-bambu-dark-tertiary",children:[a.jsx(Te,{variant:"secondary",onClick:e,children:o("common.cancel")}),a.jsx(Te,{onClick:k,disabled:!u||b.isPending,children:b.isPending?a.jsxs(a.Fragment,{children:[a.jsx(it,{className:"w-4 h-4 animate-spin"}),o("inventory.assigning")]}):a.jsxs(a.Fragment,{children:[a.jsx(ra,{className:"w-4 h-4"}),o("inventory.assignSpool")]})})]}),b.isError&&a.jsx("div",{className:"mx-4 mb-4 p-2 bg-red-500/20 border border-red-500/50 rounded text-sm text-red-400",children:b.error.message})]})]})}function xHe(t,e){if(t===255)return"External";let n,r=!1;t>=128&&t<=135?(n=t-128,r=!0):t>=0&&t<=3?(n=t,r=e===1):n=0,n=Math.max(0,Math.min(n,7));const i=String.fromCharCode(65+n);return r?`HT-${i}`:`AMS-${i}`}function MS(t){const e=t.includes("_")?t.split("_")[0]:t;return e.startsWith("GFS")?"GF"+e.slice(3):(e.startsWith("PFUS")||e.startsWith("PFSP"),e)}const yHe=["PLA","PETG","ABS","ASA","TPU","PC","PA","NYLON","PVA","HIPS","PP","PET"];function Q7(t){const e=t.replace(/@.+$/,"").trim(),n=e.toUpperCase();for(const i of yHe){const s=new RegExp(`\\b${i}\\b`,"i");if(s.test(n)){const o=e.split(s),l=o[0]?.trim()||"",c=o[1]?.trim()||"";return{material:i,brand:l,variant:c}}}const r=e.split(/\s+/);return r.length>=2?{material:r[1],brand:r[0],variant:r.slice(2).join(" ")}:{material:e,brand:"",variant:""}}function vHe(t){return!t.startsWith("GF")&&!t.startsWith("P1")}const wHe={white:"FFFFFF",black:"000000",red:"FF0000",green:"00FF00",blue:"0000FF",yellow:"FFFF00",cyan:"00FFFF",magenta:"FF00FF",orange:"FFA500",purple:"800080",pink:"FFC0CB",brown:"8B4513",gray:"808080",grey:"808080","jade white":"FFFEF2",ivory:"FFFFF0",beige:"F5F5DC",cream:"FFFDD0",silver:"C0C0C0",gold:"FFD700",bronze:"CD7F32",copper:"B87333",navy:"000080",teal:"008080",olive:"808000",maroon:"800000",coral:"FF7F50",salmon:"FA8072",lime:"32CD32",mint:"98FF98","forest green":"228B22","sky blue":"87CEEB","royal blue":"4169E1",turquoise:"40E0D0",lavender:"E6E6FA",violet:"EE82EE",plum:"DDA0DD",tan:"D2B48C",chocolate:"D2691E",charcoal:"36454F",slate:"708090",transparent:"000000",natural:"F5F5DC",wood:"DEB887"},_He=[{name:"White",hex:"FFFFFF"},{name:"Black",hex:"000000"},{name:"Red",hex:"FF0000"},{name:"Blue",hex:"0000FF"},{name:"Green",hex:"00AA00"},{name:"Yellow",hex:"FFFF00"},{name:"Orange",hex:"FFA500"},{name:"Gray",hex:"808080"}],SHe=[{name:"Cyan",hex:"00FFFF"},{name:"Magenta",hex:"FF00FF"},{name:"Purple",hex:"800080"},{name:"Pink",hex:"FFC0CB"},{name:"Brown",hex:"8B4513"},{name:"Beige",hex:"F5F5DC"},{name:"Navy",hex:"000080"},{name:"Teal",hex:"008080"},{name:"Lime",hex:"32CD32"},{name:"Gold",hex:"FFD700"},{name:"Silver",hex:"C0C0C0"},{name:"Maroon",hex:"800000"},{name:"Olive",hex:"808000"},{name:"Coral",hex:"FF7F50"},{name:"Salmon",hex:"FA8072"},{name:"Turquoise",hex:"40E0D0"},{name:"Violet",hex:"EE82EE"},{name:"Indigo",hex:"4B0082"},{name:"Chocolate",hex:"D2691E"},{name:"Tan",hex:"D2B48C"},{name:"Slate",hex:"708090"},{name:"Charcoal",hex:"36454F"},{name:"Ivory",hex:"FFFFF0"},{name:"Cream",hex:"FFFDD0"}];function NHe(t){const e=t.toLowerCase().trim();return wHe[e]||null}function CHe(t){const e=t.indexOf("@");if(e<0)return null;const r=t.slice(e+1).trim().match(/^BBL\s+(.+?)(?:\s+[\d.]+\s*nozzle)?$/i);return r?r[1].trim():null}function kHe({isOpen:t,onClose:e,printerId:n,slotInfo:r,nozzleDiameter:i="0.4",printerModel:s,onSuccess:o}){const{t:l}=Ut(),[c,u]=w.useState(""),[d,h]=w.useState(null),[f,p]=w.useState(""),[x,v]=w.useState(""),[b,y]=w.useState(""),[S,N]=w.useState(!1),[M,k]=w.useState(!1),{data:T,isLoading:C,isError:j}=et({queryKey:["cloudSettings"],queryFn:()=>ae.getCloudSettings(),enabled:t,retry:!1}),{data:E,isLoading:P}=et({queryKey:["localPresets"],queryFn:()=>ae.getLocalPresets(),enabled:t}),{data:U,isLoading:L}=et({queryKey:["builtinFilaments"],queryFn:()=>ae.getBuiltinFilaments(),enabled:t,staleTime:1/0}),{data:B,isLoading:D}=et({queryKey:["kprofiles",n,i],queryFn:()=>ae.getKProfiles(n,i),enabled:t&&!!n}),{data:$}=et({queryKey:["colorCatalog"],queryFn:()=>ae.getColorCatalog(),enabled:t,staleTime:1/0}),q=ot({mutationFn:async()=>{if(!c)throw new Error("No filament preset selected");const ue=c.startsWith("local_"),Re=c.startsWith("builtin_"),le=ue?parseInt(c.replace("local_",""),10):null,se=Re?c.replace("builtin_",""):null,Ne=ue?E?.filament.find(ge=>ge.id===le):null,J=Re?U?.find(ge=>ge.filament_id===se):null,Le=!ue&&!Re?T?.filament.find(ge=>ge.setting_id===c):null;if(!ue&&!Re&&!Le)throw new Error("Selected preset not found");if(ue&&!Ne)throw new Error("Selected local preset not found");if(Re&&!J)throw new Error("Selected builtin preset not found");const V=ue?Ne.name:Re?J.name:Le.name,Y=Q7(V),ce=d?.slot_id??-1,H=f||r.trayColor?.slice(0,6)||"FFFFFF",Z=V.replace(/@.+$/,"").trim();let de,pe;if(ue)de="",pe="";else if(Re)de=se,pe="";else if(de=MS(c),pe=c,!c.startsWith("GFS"))try{const ge=await ae.getCloudSettingDetail(c);ge.filament_id?de=ge.filament_id:ge.base_id&&(de=MS(ge.base_id),console.log(`Derived tray_info_idx from base_id: ${ge.base_id} -> ${de}`))}catch(ge){console.warn("Failed to fetch preset detail for filament_id:",ge)}let ve=ue&&Ne?.nozzle_temp_min?Ne.nozzle_temp_min:190,ee=ue&&Ne?.nozzle_temp_max?Ne.nozzle_temp_max:230;if(!ue||Re||!Ne?.nozzle_temp_min&&!Ne?.nozzle_temp_max){const ge=(ue&&Ne?.filament_type||Y.material).toUpperCase();ge.includes("PLA")?(ve=190,ee=230):ge.includes("PETG")?(ve=220,ee=260):ge.includes("ABS")||ge.includes("ASA")?(ve=240,ee=280):ge.includes("TPU")?(ve=200,ee=240):ge.includes("PC")?(ve=260,ee=300):(ge.includes("PA")||ge.includes("NYLON"))&&(ve=250,ee=290)}const z=d?.k_value?parseFloat(d.k_value):0,F=ue?Ne?.filament_type||Y.material||"PLA":Y.material||"PLA",W=await ae.configureAmsSlot(n,r.amsId,r.trayId,{tray_info_idx:de,tray_type:F,tray_sub_brands:Z,tray_color:H+"FF",nozzle_temp_min:ve,nozzle_temp_max:ee,cali_idx:ce,nozzle_diameter:i,setting_id:pe,kprofile_filament_id:d?.filament_id,kprofile_setting_id:d?.setting_id||void 0,k_value:z}),me=ue?`local_${le}`:Re?`builtin_${se}`:c,Se=ue?"local":Re?"builtin":"cloud";try{await ae.saveSlotPreset(n,r.amsId,r.trayId,me,Z,Se)}catch(ge){console.warn("Failed to save slot preset mapping:",ge)}return W},onSuccess:()=>{N(!0),o?.(),setTimeout(()=>{N(!1),e()},1500)}}),I=ot({mutationFn:async()=>ae.resetAmsSlot(n,r.amsId,r.trayId),onSuccess:()=>{N(!0),o?.(),setTimeout(()=>{N(!1),e()},1500)}}),K=w.useMemo(()=>{const ue=b.toLowerCase(),Re=[],le=new Set,se=r.savedPresetId,Ne=r.trayInfoIdx;if(T?.filament)for(const J of T.filament){le.add(J.setting_id);const Le=se===J.setting_id||Ne&&(J.setting_id===Ne||MS(J.setting_id)===Ne);if(!(!Le&&ue&&!J.name.toLowerCase().includes(ue))){if(!Le&&s){const V=CHe(J.name);if(V&&V.toUpperCase()!==s.toUpperCase())continue}Re.push({id:J.setting_id,name:J.name,source:"cloud",isUser:vHe(J.setting_id)})}}if(E?.filament)for(const J of E.filament){const Le=`local_${J.id}`,V=se===Le;if(!(!V&&ue&&!J.name.toLowerCase().includes(ue))){if(!V&&s&&J.compatible_printers){const Y=J.compatible_printers.split(";").map(ce=>{const H=ce.trim(),Z=H.match(/^BBL\s+(.+)/i);return Z?Z[1].trim().toUpperCase():H.toUpperCase()}).filter(Boolean);if(Y.length>0&&!Y.includes(s.toUpperCase()))continue}Re.push({id:Le,name:J.name,source:"local",isUser:!1})}}if(U)for(const J of U){if(le.has(J.filament_id))continue;const Le=J.filament_id.startsWith("GF")?"GFS"+J.filament_id.slice(2):J.filament_id;le.has(Le)||(!ue||J.name.toLowerCase().includes(ue))&&Re.push({id:`builtin_${J.filament_id}`,name:J.name,source:"builtin",isUser:!1})}return Re.sort((J,Le)=>{const V={cloud:0,local:1,builtin:2};return J.source!==Le.source?V[J.source]-V[Le.source]:J.isUser&&!Le.isUser?-1:!J.isUser&&Le.isUser?1:J.name.localeCompare(Le.name)})},[T?.filament,E?.filament,U,b,s,r.savedPresetId,r.trayInfoIdx]),Q=w.useMemo(()=>{if(!c)return null;let ue=null;if(c.startsWith("local_")){const se=parseInt(c.replace("local_",""),10);ue=E?.filament.find(J=>J.id===se)?.name||null}else if(c.startsWith("builtin_")){const se=c.replace("builtin_","");ue=U?.find(J=>J.filament_id===se)?.name||null}else T?.filament&&(ue=T.filament.find(Ne=>Ne.setting_id===c)?.name||null);if(!ue)return null;let Re=ue.replace(/@.+$/,"").trim();Re.startsWith("# ")&&(Re=Re.slice(2).trim());const le=Q7(Re);return{fullName:Re,material:le.material,brand:le.brand}},[c,T?.filament,E?.filament,U]),ie=Q?.fullName||"",G=w.useMemo(()=>{if(!$||!Q)return[];const{fullName:ue,brand:Re}=Q,le=ue.replace(/^(Bambu\s*(Lab)?|eSUN|Polymaker|Overture|Sunlu|Hatchbox)\s*/i,"").trim();return $.filter(se=>{const Ne=(se.material||"").toUpperCase(),J=se.manufacturer.toUpperCase();if(!(Ne===le.toUpperCase()||Ne.includes(le.toUpperCase())||le.toUpperCase().includes(Ne)))return!1;if(Re){const V=Re.toUpperCase();if(!J.includes(V)&&!V.includes(J))return!1}return!0})},[$,Q]),X=w.useMemo(()=>{if(!B?.profiles||!Q)return[];const{fullName:ue,material:Re,brand:le}=Q,se=ue.toUpperCase(),Ne=Re.toUpperCase(),J=le.toUpperCase();if(!Ne||Ne.length<2)return[];const Le=B.profiles.filter(Y=>{const ce=Y.name.toUpperCase();if(J)return!(!ce.includes(J)||!ce.includes(Ne));if(ce.includes(se)||ce.includes(Ne))return!0;const Z={NYLON:["PA","PA-CF","PA6"],PA:["NYLON"]}[Ne]||[];for(const de of Z)if(ce.includes(de))return!0;return!1}),V=new Map;for(const Y of Le){const ce=`${Y.name}|${Y.k_value}`,H=V.get(ce);H?r.extruderId!==void 0&&Y.extruder_id===r.extruderId&&H.extruder_id!==r.extruderId&&V.set(ce,Y):V.set(ce,Y)}return Array.from(V.values())},[B?.profiles,Q,r.extruderId]);w.useEffect(()=>{if(t){if(r.savedPresetId)u(r.savedPresetId);else if(r.trayInfoIdx&&T?.filament){let ue=T.filament.find(Re=>Re.setting_id===r.trayInfoIdx);ue||(ue=T.filament.find(Re=>MS(Re.setting_id)===r.trayInfoIdx)),ue&&u(ue.setting_id)}if(r.trayColor){const ue=r.trayColor.slice(0,6);ue&&p(ue)}}else u(""),h(null),p(""),v(""),y(""),N(!1)},[t,r.savedPresetId,r.trayInfoIdx,r.trayColor,T?.filament]),w.useEffect(()=>{if(X.length>0){if(r.caliIdx!=null&&r.caliIdx>0){const ue=X.find(Re=>Re.slot_id===r.caliIdx);if(ue){h(ue);return}}h(X[0])}else h(null)},[c,X,r.caliIdx]);const ne=w.useCallback(ue=>{ue.key==="Escape"&&e()},[e]);if(w.useEffect(()=>{if(t)return document.addEventListener("keydown",ne),()=>document.removeEventListener("keydown",ne)},[t,ne]),!t)return null;const te=C&&!j||P||L||D,xe=c&&!q.isPending,he=f||r.trayColor?.slice(0,6)||"FFFFFF";return a.jsxs("div",{className:"fixed inset-0 z-50 flex items-center justify-center",children:[a.jsx("div",{className:"absolute inset-0 bg-black/60 backdrop-blur-sm",onClick:e}),a.jsxs("div",{className:"relative w-full max-w-lg mx-4 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-xl shadow-2xl",children:[a.jsxs("div",{className:"flex items-center justify-between p-4 border-b border-bambu-dark-tertiary",children:[a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx(nd,{className:"w-5 h-5 text-bambu-blue"}),a.jsx("h2",{className:"text-lg font-semibold text-white",children:l("configureAmsSlot.title")})]}),a.jsx("button",{onClick:e,className:"p-1 text-bambu-gray hover:text-white rounded transition-colors",children:a.jsx(Rt,{className:"w-5 h-5"})})]}),a.jsxs("div",{className:"p-4 space-y-4 max-h-[60vh] overflow-y-auto",children:[S&&a.jsx("div",{className:"absolute inset-0 bg-bambu-dark-secondary/95 z-10 flex items-center justify-center rounded-xl",children:a.jsxs("div",{className:"text-center space-y-3",children:[a.jsx(ub,{className:"w-16 h-16 text-bambu-green mx-auto"}),a.jsx("p",{className:"text-lg font-semibold text-white",children:l("configureAmsSlot.slotConfigured")}),a.jsx("p",{className:"text-sm text-bambu-gray",children:l("configureAmsSlot.settingsSentToPrinter")})]})}),a.jsxs("div",{className:"p-3 bg-bambu-dark rounded-lg border border-bambu-dark-tertiary",children:[a.jsx("p",{className:"text-xs text-bambu-gray mb-1",children:l("configureAmsSlot.configuringSlot")}),a.jsxs("div",{className:"flex items-center gap-2",children:[r.trayColor&&a.jsx("span",{className:"w-4 h-4 rounded-full border border-white/20",style:{backgroundColor:`#${r.trayColor.slice(0,6)}`}}),a.jsx("span",{className:"text-white font-medium",children:l("configureAmsSlot.slotLabel",{ams:xHe(r.amsId,r.trayCount),slot:r.trayId+1})}),r.traySubBrands&&a.jsxs("span",{className:"text-bambu-gray",children:["(",r.traySubBrands,")"]})]})]}),te?a.jsx("div",{className:"flex justify-center py-8",children:a.jsx(it,{className:"w-6 h-6 text-bambu-green animate-spin"})}):a.jsxs(a.Fragment,{children:[a.jsxs("div",{children:[a.jsxs("label",{className:"block text-sm text-bambu-gray mb-2",children:[l("configureAmsSlot.filamentProfile")," ",a.jsx("span",{className:"text-red-400",children:"*"})]}),a.jsxs("div",{className:"relative",children:[a.jsx("input",{type:"text",placeholder:l("configureAmsSlot.searchPresets"),value:b,onChange:ue=>y(ue.target.value),className:"w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder:text-bambu-gray focus:border-bambu-green focus:outline-none mb-2"}),a.jsx("div",{className:"max-h-48 overflow-y-auto space-y-1",children:K.length===0?a.jsx("p",{className:"text-center py-4 text-bambu-gray",children:T?.filament?.length===0&&!E?.filament?.length?l("configureAmsSlot.noPresetsAvailable"):l("configureAmsSlot.noMatchingPresets")}):K.map(ue=>a.jsx("button",{ref:c===ue.id?Re=>{Re?.scrollIntoView({block:"nearest"})}:void 0,onClick:()=>u(ue.id),className:`w-full p-2 rounded-lg border text-left transition-colors ${c===ue.id?"bg-bambu-green/20 border-bambu-green":"bg-bambu-dark border-bambu-dark-tertiary hover:border-bambu-gray"}`,children:a.jsxs("div",{className:"flex items-center justify-between",children:[a.jsx("span",{className:"text-white text-sm truncate",children:ue.name}),a.jsxs("div",{className:"flex items-center gap-1 flex-shrink-0",children:[ue.source==="local"&&a.jsx("span",{className:"text-xs px-1.5 py-0.5 rounded bg-green-500/20 text-green-400",children:l("profiles.localProfiles.badge")}),ue.source==="builtin"&&a.jsx("span",{className:"text-xs px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400",children:l("configureAmsSlot.builtin")}),ue.isUser&&a.jsx("span",{className:"text-xs px-1.5 py-0.5 rounded bg-bambu-blue/20 text-bambu-blue",children:l("configureAmsSlot.custom")})]})]})},ue.id))})]})]}),a.jsxs("div",{children:[a.jsxs("label",{className:"block text-sm text-bambu-gray mb-2",children:[l("configureAmsSlot.kProfileLabel"),ie&&a.jsx("span",{className:"ml-2 text-xs text-bambu-blue",children:l("configureAmsSlot.filteringFor",{material:ie})})]}),X.length>0?a.jsxs("div",{className:"relative",children:[a.jsxs("select",{value:d?.name||"",onChange:ue=>{const Re=X.find(le=>le.name===ue.target.value);h(Re||null)},className:"w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none appearance-none pr-10",children:[a.jsx("option",{value:"",children:l("configureAmsSlot.noKProfile")}),X.map(ue=>a.jsxs("option",{value:ue.name,children:[ue.name," (K=",ue.k_value,")"]},`${ue.name}-${ue.extruder_id}`))]}),a.jsx(Jn,{className:"absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none"})]}):c?a.jsx("p",{className:"text-sm text-bambu-gray italic py-2",children:l("configureAmsSlot.noMatchingKProfiles")}):a.jsx("span",{className:"inline-block text-xs px-2 py-1 rounded bg-amber-500/20 text-amber-400 border border-amber-500/30",children:l("configureAmsSlot.selectFilamentFirst")}),d&&a.jsx("p",{className:"text-xs text-bambu-green mt-1",children:l("configureAmsSlot.kFromCalibration",{value:d.k_value})})]}),a.jsxs("div",{children:[a.jsx("label",{className:"block text-sm text-bambu-gray mb-2",children:l("configureAmsSlot.customColorLabel")}),G.length>0&&a.jsxs("div",{className:"mb-3",children:[a.jsx("p",{className:"text-xs text-bambu-gray mb-1.5",children:l("configureAmsSlot.presetColors",{name:Q?.fullName})}),a.jsx("div",{className:"flex flex-wrap gap-1.5",children:G.map(ue=>a.jsxs("button",{onClick:()=>{const Re=ue.hex_color.replace("#","").toUpperCase();p(Re),v(ue.color_name)},className:`h-7 px-2 rounded-md border-2 transition-all flex items-center gap-1.5 ${f===ue.hex_color.replace("#","").toUpperCase()?"border-bambu-green scale-105":"border-white/20 hover:border-white/40"}`,title:ue.color_name,children:[a.jsx("span",{className:"w-4 h-4 rounded-full border border-white/30 flex-shrink-0",style:{backgroundColor:ue.hex_color}}),a.jsx("span",{className:"text-xs text-white/80 whitespace-nowrap",children:ue.color_name})]},ue.id))})]}),a.jsxs("div",{className:"flex flex-wrap gap-1.5 mb-2",children:[_He.map(ue=>a.jsx("button",{onClick:()=>{p(ue.hex),v(ue.name)},className:`w-7 h-7 rounded-md border-2 transition-all ${f===ue.hex?"border-bambu-green scale-110":"border-white/20 hover:border-white/40"}`,style:{backgroundColor:`#${ue.hex}`},title:ue.name},ue.hex)),a.jsx("button",{onClick:()=>k(!M),className:"w-7 h-7 rounded-md border-2 border-white/20 hover:border-white/40 flex items-center justify-center text-white/60 hover:text-white/80 transition-all text-xs",title:l(M?"configureAmsSlot.showLessColors":"configureAmsSlot.showMoreColors"),children:M?"−":"+"})]}),M&&a.jsx("div",{className:"flex flex-wrap gap-1.5 mb-2",children:SHe.map(ue=>a.jsx("button",{onClick:()=>{p(ue.hex),v(ue.name)},className:`w-7 h-7 rounded-md border-2 transition-all ${f===ue.hex?"border-bambu-green scale-110":"border-white/20 hover:border-white/40"}`,style:{backgroundColor:`#${ue.hex}`},title:ue.name},ue.hex))}),a.jsxs("div",{className:"flex gap-2 items-center",children:[a.jsx("div",{className:"w-10 h-10 rounded-lg border-2 border-white/20 flex-shrink-0",style:{backgroundColor:`#${he}`}}),a.jsx("input",{type:"text",placeholder:l("configureAmsSlot.colorPlaceholder"),value:x,onChange:ue=>{const Re=ue.target.value;v(Re);const le=NHe(Re);if(le)p(le);else{const se=Re.replace(/[^0-9A-Fa-f]/g,"").toUpperCase();se.length===6?p(se):se.length===3&&p(se.split("").map(Ne=>Ne+Ne).join(""))}},className:"flex-1 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder:text-bambu-gray focus:border-bambu-green focus:outline-none text-sm"}),f&&a.jsx("button",{onClick:()=>{p(""),v("")},className:"px-2 py-1 text-xs text-bambu-gray hover:text-white bg-bambu-dark-tertiary rounded",title:l("configureAmsSlot.clearCustomColor"),children:l("configureAmsSlot.clear")})]}),f&&a.jsx("p",{className:"text-xs text-bambu-gray mt-1.5",children:l("configureAmsSlot.hexLabel",{hex:f})})]})]})]}),a.jsxs("div",{className:"flex justify-between p-4 border-t border-bambu-dark-tertiary",children:[a.jsx(Te,{variant:"secondary",onClick:()=>I.mutate(),disabled:I.isPending||q.isPending,className:"text-red-400 hover:text-red-300 hover:bg-red-500/10",children:I.isPending?a.jsxs(a.Fragment,{children:[a.jsx(it,{className:"w-4 h-4 animate-spin"}),l("configureAmsSlot.resetting")]}):a.jsxs(a.Fragment,{children:[a.jsx(Do,{className:"w-4 h-4"}),l("configureAmsSlot.resetSlot")]})}),a.jsxs("div",{className:"flex gap-2",children:[a.jsx(Te,{variant:"secondary",onClick:e,children:l("configureAmsSlot.cancel")}),a.jsx(Te,{onClick:()=>q.mutate(),disabled:!xe,children:q.isPending?a.jsxs(a.Fragment,{children:[a.jsx(it,{className:"w-4 h-4 animate-spin"}),l("configureAmsSlot.configuring")]}):a.jsxs(a.Fragment,{children:[a.jsx(nd,{className:"w-4 h-4"}),l("configureAmsSlot.configureSlot")]})})]})]}),(q.isError||I.isError)&&a.jsx("div",{className:"mx-4 mb-4 p-2 bg-red-500/20 border border-red-500/50 rounded text-sm text-red-400",children:q.error?.message||I.error?.message})]})]})}const Z7={"A00-W1":"Jade White","A00-P0":"Beige","A00-D2":"Light Gray","A00-Y0":"Yellow","A00-Y2":"Sunflower Yellow","A00-A1":"Pumpkin Orange","A00-A0":"Orange","A00-Y4":"Gold","A00-G3":"Bright Green","A00-G1":"Bambu Green","A00-G2":"Mistletoe Green","A00-R3":"Hot Pink","A00-P6":"Magenta","A00-R0":"Red","A00-R2":"Maroon Red","A00-P5":"Purple","A00-P2":"Indigo Purple","A00-B5":"Turquoise","A00-B8":"Cyan","A00-B3":"Cobalt Blue","A00-N0":"Brown","A00-N1":"Cocoa Brown","A00-Y3":"Bronze","A00-D0":"Gray","A00-D1":"Silver","A00-B1":"Blue Grey","A00-D3":"Dark Gray","A00-K0":"Black","A00-M3":"Pink Citrus","A00-M6":"Dusk Glare","A00-M0":"Arctic Whisper","A00-M1":"Solar Breeze","A00-M5":"Blueberry Bubblegum","A00-M4":"Mint Lime","A00-M2":"Ocean to Meadow","A00-M7":"Cotton Candy Cloud","A18-K0":"Black","A18-D0":"Gray","A18-W0":"White","A18-R0":"Red","A18-Y0":"Yellow","A18-B0":"Cyan","A18-B1":"Blue","A18-P0":"Matte Beige","A01-W2":"Ivory White","A01-W3":"Bone White","A01-Y2":"Lemon Yellow","A01-A2":"Mandarin Orange","A01-P3":"Sakura Pink","A01-P4":"Lilac Purple","A01-R3":"Plum","A01-R1":"Scarlet Red","A01-R4":"Dark Red","A01-G0":"Apple Green","A01-G1":"Grass Green","A01-G7":"Dark Green","A01-B4":"Ice Blue","A01-B0":"Sky Blue","A01-B3":"Marine Blue","A01-B6":"Dark Blue","A01-Y3":"Desert Tan","A01-N1":"Latte Brown","A01-N3":"Caramel","A01-R2":"Terracotta","A01-N2":"Dark Brown","A01-N0":"Dark Chocolate","A01-D3":"Ash Gray","A01-D0":"Nardo Gray","A01-K1":"Charcoal","A12-G0":"Green","A12-R0":"Pink","A12-A0":"Orange","A12-Y0":"Yellow","A12-B0":"Blue","A07-R5":"Red Granite","A07-D4":"White Marble","A11-W0":"White","A11-K0":"Black","A08-G3":"Alpine Green Sparkle","A08-D5":"Slate Gray Sparkle","A08-B7":"Royal Purple Sparkle","A08-R2":"Crimson Red Sparkle","A08-K2":"Onyx Black Sparkle","A08-Y1":"Classic Gold Sparkle","A02-B2":"Cobalt Blue Metallic","A02-G2":"Oxide Green Metallic","A02-Y1":"Iridium Gold Metallic","A02-D2":"Iron Gray Metallic","A17-B1":"Blue","A17-A0":"Orange","A17-P0":"Purple","A06-Y1":"Gold","A06-D0":"Titan Gray","A06-D1":"Silver","A06-W0":"White","A06-R0":"Candy Red","A06-G0":"Candy Green","A06-G1":"Mint","A06-B1":"Blue","A06-B0":"Baby Blue","A06-P0":"Purple","A06-R1":"Rose Gold","A06-R2":"Pink","A06-Y0":"Champagne","A05-M8":"Dawn Radiance","A05-M4":"Aurora Purple","A05-M1":"South Beach","A05-T3":"Neon City","A05-T2":"Midnight Blaze","A05-T1":"Gilded Rose","A05-T4":"Blue Hawaii","A05-T5":"Velvet Eclipse","A15-B0":"Purple","A15-G0":"Green","A15-G1":"Nebulae","A15-R0":"Brown","A16-K0":"Black Walnut","A16-R0":"Rosewood","A16-N0":"Clay Brown","A16-G0":"Classic Birch","A16-W0":"White Oak","A16-Y0":"Ochre Yellow","A50-D6":"Lava Gray","A50-K0":"Black","A50-B6":"Royal Blue","A10-W0":"White","A10-D0":"Gray","A09-B5":"Lavender Blue","A09-B4":"Light Blue","A09-A0":"Orange","A09-D1":"Silver","A09-R3":"Vermilion Red","A09-Y0":"Yellow","G02-K0":"Black","G02-W0":"White","G02-R0":"Red","G02-D0":"Gray","G02-D1":"Dark Gray","G02-Y1":"Cream","G02-Y0":"Yellow","G02-A0":"Orange","G02-N1":"Peanut Brown","G02-G1":"Lime Green","G02-G0":"Green","G02-G2":"Forest Green","G02-B1":"Lake Blue","G02-B0":"Blue","G01-G1":"Translucent Teal","G01-B0":"Translucent Light Blue","G01-C0":"Clear","G01-D0":"Translucent Gray","G01-G0":"Translucent Olive","G01-N0":"Translucent Brown","G01-A0":"Translucent Orange","G01-P1":"Translucent Pink","G01-P0":"Translucent Purple","G50-P7":"Violet Purple","G50-K0":"Black","B00-D1":"Silver","B00-K0":"Black","B00-W0":"White","B00-G6":"Bambu Green","B00-G7":"Olive","B00-Y1":"Tangerine Yellow","B00-A0":"Orange","B00-R0":"Red","B00-B4":"Azure","B00-B0":"Blue","B00-B6":"Navy Blue","B50-A0":"Orange","B50-K0":"Black","B01-W0":"White","B01-K0":"Black","B01-D0":"Gray","B02-W0":"White","C00-C1":"Transparent","C00-C0":"Clear Black","C00-K0":"Black","C00-W0":"White","C01-K0":"Black","U02-B0":"Blue","U02-D0":"Gray","U02-K0":"Black","N04-K0":"Black","N08-K0":"Black","S02-W0":"Nature","S02-W1":"White","S05-C0":"Black","S06-W0":"White","S03-G1":"Green","S04-Y0":"Clear"},MHe={W0:"White",W1:"Jade White",W2:"Ivory White",W3:"Bone White",Y0:"Yellow",Y1:"Gold",Y2:"Sunflower Yellow",Y3:"Bronze",Y4:"Gold",A0:"Orange",A1:"Pumpkin Orange",A2:"Mandarin Orange",R0:"Red",R1:"Scarlet Red",R2:"Maroon Red",R3:"Hot Pink",R4:"Dark Red",R5:"Red Granite",P0:"Beige",P1:"Pink",P2:"Indigo Purple",P3:"Sakura Pink",P4:"Lilac Purple",P5:"Purple",P6:"Magenta",P7:"Violet Purple",B0:"Blue",B1:"Blue Grey",B2:"Cobalt Blue",B3:"Cobalt Blue",B4:"Ice Blue",B5:"Turquoise",B6:"Navy Blue",B7:"Royal Purple",B8:"Cyan",G0:"Green",G1:"Grass Green",G2:"Mistletoe Green",G3:"Bright Green",G6:"Bambu Green",G7:"Dark Green",N0:"Brown",N1:"Peanut Brown",N2:"Dark Brown",N3:"Caramel",D0:"Gray",D1:"Silver",D2:"Light Gray",D3:"Dark Gray",D4:"White Marble",D5:"Slate Gray",D6:"Lava Gray",K0:"Black",K1:"Charcoal",K2:"Onyx Black",C0:"Clear Black",C1:"Transparent",M0:"Arctic Whisper",M1:"Solar Breeze",M2:"Ocean to Meadow",M3:"Pink Citrus",M4:"Aurora Purple",M5:"Blueberry Bubblegum",M6:"Dusk Glare",M7:"Cotton Candy Cloud",M8:"Dawn Radiance",T1:"Gilded Rose",T2:"Midnight Blaze",T3:"Neon City",T4:"Blue Hawaii",T5:"Velvet Eclipse"};function XP(t){if(!t)return null;if(Z7[t])return Z7[t];const e=t.split("-");if(e.length<2)return null;const n=e[1];return MHe[n]||null}function YP(t){if(!t||t.length<6)return"Unknown";const e=parseInt(t.substring(0,2),16),n=parseInt(t.substring(2,4),16),r=parseInt(t.substring(4,6),16),i=Math.max(e,n,r)/255,s=Math.min(e,n,r)/255,o=(i+s)/2;let l=0,c=0;if(i!==s){const u=i-s;c=o>.5?u/(2-i-s):u/(i+s);const d=e/255,h=n/255,f=r/255;i===d?l=((h-f)/u+(h.85?"White":c<.15?o<.4?"Dark Gray":o>.6?"Light Gray":"Gray":l>=15&&l<45&&o<.45||l>=45&&l<70&&o<.4?"Brown":l<15||l>=345?"Red":l<45?"Orange":l<70?"Yellow":l<150?"Green":l<200?"Cyan":l<260?"Blue":l<290?"Purple":"Pink"}function QP(t){return(t??.02).toFixed(3)}function J7({side:t}){const{mode:e}=rp(),n=e==="dark"?"#1a4d2e":"#e7f5e9";return a.jsx("span",{className:"inline-flex items-center justify-center w-4 h-4 text-[10px] font-bold rounded",style:{backgroundColor:n,color:"#00ae42"},children:t})}function b4(t){if(!t||t==="00000000"||t.length<6)return null;const e=t.slice(0,2),n=t.slice(2,4),r=t.slice(4,6),i=t.length>=8?parseInt(t.slice(6,8),16)/255:1;return i===0?null:`rgba(${parseInt(e,16)}, ${parseInt(n,16)}, ${parseInt(r,16)}, ${i})`}function AHe(t){if(!t||t.length<6)return!1;const e=parseInt(t.slice(0,2),16),n=parseInt(t.slice(2,4),16),r=parseInt(t.slice(4,6),16);return(.299*e+.587*n+.114*r)/255>.6}function nne(t,e){if(!t)return"";if(t.includes("hardened"))return e("printers.nozzleHardenedSteel");if(t.includes("stainless"))return e("printers.nozzleStainlessSteel");if(t.includes("tungsten"))return e("printers.nozzleTungstenCarbide");if(t.length>=4){const n=t.slice(2,4);if(n==="00")return e("printers.nozzleStainlessSteel");if(n==="01")return e("printers.nozzleHardenedSteel");if(n==="05")return e("printers.nozzleTungstenCarbide")}return t==="00"?e("printers.nozzleStainlessSteel"):t==="01"?e("printers.nozzleHardenedSteel"):t==="05"?e("printers.nozzleTungstenCarbide"):t.startsWith("H")?e("printers.nozzleHardenedSteel"):t}function rne(t,e){return t?t.startsWith("HH")?e("printers.nozzleHighFlow"):t.startsWith("HS")?e("printers.nozzleStandardFlow"):"":""}function ine({slot:t,index:e,activeStatus:n,filamentName:r,children:i}){const{t:s}=Ut(),[o,l]=w.useState(!1),[c,u]=w.useState("top"),d=w.useRef(null),h=w.useRef(null),f=w.useRef(null),p=!t.nozzle_diameter&&!t.nozzle_type,x=t.stat===1;w.useEffect(()=>{if(o&&d.current&&h.current){const M=d.current.getBoundingClientRect(),k=h.current.offsetHeight,C=M.top-56,j=window.innerHeight-M.bottom;CC?u("bottom"):u("top")}},[o]);const v=()=>{f.current&&clearTimeout(f.current),f.current=setTimeout(()=>l(!0),80)},b=()=>{f.current&&clearTimeout(f.current),f.current=setTimeout(()=>l(!1),100)};w.useEffect(()=>()=>{f.current&&clearTimeout(f.current)},[]);const y=b4(t.filament_color),S=nne(t.nozzle_type,s),N=rne(t.nozzle_type,s);return a.jsxs("div",{ref:d,className:"relative",onMouseEnter:v,onMouseLeave:b,children:[i,o&&a.jsxs("div",{ref:h,className:` absolute left-1/2 -translate-x-1/2 z-50 ${c==="top"?"bottom-full mb-2":"top-full mt-2"} animate-in fade-in-0 zoom-in-95 duration-150 diff --git a/static/index.html b/static/index.html index 2330089ed..f731634d0 100644 --- a/static/index.html +++ b/static/index.html @@ -23,7 +23,7 @@ - + From d67f1a1c038e11792fd011f5beab4d1eb385f324 Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 15 Feb 2026 08:40:12 +0100 Subject: [PATCH 7/8] fix: inventory spool weight_used reset by AMS remain% sync The on_ams_change() handler recalculated weight_used from the AMS remain% sensor on every status update, overwriting precise values from the usage tracker. For small prints on large spools (e.g. 1.6g on 1000g), the integer-resolution sensor (10g steps) still reported 100% remain, resetting weight_used back to 0. The sync now only increases weight_used, never decreases it. --- CHANGELOG.md | 1 + backend/app/main.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd9606533..1fdd0f7f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ All notable changes to Bambuddy will be documented in this file. - **AMS Slot Configuration Overwrites RFID Spool State** — Configuring an AMS slot for an RFID-detected Bambu Lab spool sent `ams_set_filament_setting`, which replaced the firmware's RFID-managed filament config with a manual one — causing the slicer's "eye" icon to change to a "pen" icon. Now detects RFID spools and skips the filament setting command, only sending K-profile selection. - **K-Profile Selection Corrupts Existing Profiles on X1C/P1S** — The `extrusion_cali_sel` command included a `setting_id` field that BambuStudio never sends, causing firmware to mislink calibration data. The `extrusion_cali_set` command was sent unconditionally, overwriting existing profile metadata. Now `setting_id` is removed from selection commands, and `extrusion_cali_set` is only sent when no existing profile is selected (`cali_idx < 0`). - **AMS Slot Configure — Black Filament Color Not Pre-Populated** — When re-opening the Configure AMS Slot modal for a slot with black filament, the color field was empty despite the preset and K-profile being correctly pre-selected. The color pre-population logic excluded hex `000000` (black) as a guard against empty slots, but empty slots already skip color data entirely. Removed the unnecessary check so black is now pre-populated like any other color. +- **Inventory Spool Weight Resets After Print Completes** — After a print, the usage tracker correctly updated `weight_used` (e.g., +1.6g), but periodic AMS status updates recalculated `weight_used` from the AMS remain% sensor and overwrote the precise value. For small prints on large spools (e.g., 1.6g on 1000g), the AMS remain% stays at 100% (integer resolution = 10g steps), resetting `weight_used` back to 0. The AMS weight sync now only increases `weight_used`, never decreases it, preserving precise values from the usage tracker. ### Improved - **SQLite WAL Mode for Database Reliability** — Database now uses Write-Ahead Logging (WAL) mode with a 5-second busy timeout, reducing "database is locked" errors under concurrent access. WAL mode allows simultaneous reads during writes, improving responsiveness for multi-printer setups. Automatically enabled on startup. diff --git a/backend/app/main.py b/backend/app/main.py index 94400e70c..e27c844cd 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -675,7 +675,9 @@ async def on_ams_change(printer_id: int, ams_data: list): ) existing_assignment = existing.scalar_one_or_none() if existing_assignment: - # Sync spool weight_used from AMS remain if valid + # Sync spool weight_used from AMS remain — only INCREASE, never decrease. + # The AMS remain% is low-resolution (integer %, i.e. 10g steps for 1kg spool) + # and must not overwrite precise values from the usage tracker (3MF/G-code). remain_raw = tray.get("remain") if remain_raw is not None and existing_assignment.spool: try: @@ -685,11 +687,12 @@ async def on_ams_change(printer_id: int, ams_data: list): if 0 <= remain_val <= 100: lw = existing_assignment.spool.label_weight or 1000 new_used = round(lw * (100 - remain_val) / 100.0, 1) - if abs((existing_assignment.spool.weight_used or 0) - new_used) > 1: + current_used = existing_assignment.spool.weight_used or 0 + if new_used > current_used + 1: logger.info( "Weight sync: spool %d weight_used %s -> %s (remain=%d)", existing_assignment.spool_id, - existing_assignment.spool.weight_used, + current_used, new_used, remain_val, ) From 6e56aa7ab3c7f302e763a72fed2a9d8d221835e0 Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 15 Feb 2026 09:12:34 +0100 Subject: [PATCH 8/8] Inventory UX improvements, color catalog expansion, settings polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace browser confirm() with styled ConfirmModal for inventory delete (danger) and archive (warning) actions - Expand default color catalog from 258 to 638 entries (6 → 20 brands) with measured hex codes from FilamentColors.xyz - Expand eSUN from 10 generic placeholders to 79 measured colors across 10 material lines - Add built-in inventory note about third-party spool assignment - Increase Spool/Color Catalog settings card height (400px → 600px) - Add i18n keys for all new strings (en, de, fr, it, ja) - Update wiki inventory docs with catalog management details --- CHANGELOG.md | 4 + backend/app/core/catalog_defaults.py | 493 +++++++++++++++++- .../src/components/ColorCatalogSettings.tsx | 2 +- .../src/components/SpoolCatalogSettings.tsx | 2 +- frontend/src/components/SpoolmanSettings.tsx | 1 + frontend/src/i18n/locales/de.ts | 2 + frontend/src/i18n/locales/en.ts | 2 + frontend/src/i18n/locales/fr.ts | 2 + frontend/src/i18n/locales/ja.ts | 2 + frontend/src/pages/InventoryPage.tsx | 29 +- .../{index-C3i3HoUc.js => index-CEmIWZ6o.js} | 94 ++-- static/assets/index-CggumZ2v.css | 1 + static/assets/index-D--TAtCz.css | 1 - static/index.html | 4 +- 14 files changed, 570 insertions(+), 69 deletions(-) rename static/assets/{index-C3i3HoUc.js => index-CEmIWZ6o.js} (57%) create mode 100644 static/assets/index-CggumZ2v.css delete mode 100644 static/assets/index-D--TAtCz.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fdd0f7f3..52f5f54f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,10 @@ All notable changes to Bambuddy will be documented in this file. - **Skip Objects: Confirmation Dialog** ([#346](https://github.com/maziggy/bambuddy/issues/346)) — Added a warning confirmation modal before skipping an object during a print. Shows the object name and warns the action is irreversible. Prevents accidentally skipping the wrong object. Translated in all 4 locales (en, de, ja, it). - **Additional Currency Options** ([#329](https://github.com/maziggy/bambuddy/issues/329), [#333](https://github.com/maziggy/bambuddy/issues/333)) — Added 17 additional currencies to the cost tracking dropdown: HKD, INR, KRW, SEK, NOK, DKK, PLN, BRL, TWD, SGD, NZD, MXN, CZK, THB, ZAR, RUB. - **Move Email Settings Under Authentication Tab** — Renamed the settings "Users" tab to "Authentication" and moved the standalone "Global Email" tab into it as an "Email Authentication" sub-tab. Groups email/SMTP configuration with user management where it logically belongs. Legacy `?tab=email` URLs are handled automatically. +- **Inventory — Confirmation Modals for Delete & Archive** — The inventory page now uses the app's styled confirmation modal for both delete and archive actions. Previously, delete used the browser's native `confirm()` dialog and archive had no confirmation at all. Delete shows a danger-styled modal, archive shows a warning-styled modal. Translated in all 5 locales (en, de, fr, it, ja). +- **Default Color Catalog Expanded to 638 Colors Across 20 Brands** — The built-in filament color catalog has been expanded from 258 entries (6 brands) to 638 entries (20 brands). Added Overture, Sunlu, Creality, Elegoo, Jayo, Inland, Eryone, ColorFabb, Fillamentum, FormFutura, Fiberlogy, MatterHackers, Protopasta, 3DXTECH, and Sakata3D. eSUN expanded from 10 generic placeholder entries to 79 measured colors across 10 material lines (PLA+, Pro PLA+, PLA, PLA Silk, PLA Metal, PLA-ST, PETG, PETG-HS, ABS, ABS+). All hex codes sourced from FilamentColors.xyz measured swatches. +- **Settings — Built-in Inventory Feature Note** — Added a note in Settings > Filament > Built-in Inventory that third-party spools can be assigned to inventory spools for tracking. +- **Catalog Settings Cards Taller** — Spool Catalog and Color Catalog settings panels increased from 400px to 600px max height for better browsability with the expanded default catalogs. ## [0.1.9] - 2026-02-10 diff --git a/backend/app/core/catalog_defaults.py b/backend/app/core/catalog_defaults.py index 18625a60e..7fafa9eec 100644 --- a/backend/app/core/catalog_defaults.py +++ b/backend/app/core/catalog_defaults.py @@ -330,17 +330,95 @@ DEFAULT_COLOR_CATALOG: list[tuple[str, str, str, str]] = [ ("Prusament", "Azure Blue", "#007FFF", "PLA"), ("Prusament", "Royal Blue", "#4169E1", "PLA"), ("Prusament", "Mystic Purple", "#7B68EE", "PLA"), - # eSUN PLA+ - ("eSUN", "White", "#FFFFFF", "PLA+"), - ("eSUN", "Black", "#000000", "PLA+"), - ("eSUN", "Grey", "#808080", "PLA+"), - ("eSUN", "Red", "#FF0000", "PLA+"), - ("eSUN", "Blue", "#0000FF", "PLA+"), - ("eSUN", "Green", "#00FF00", "PLA+"), - ("eSUN", "Yellow", "#FFFF00", "PLA+"), - ("eSUN", "Orange", "#FFA500", "PLA+"), - ("eSUN", "Purple", "#800080", "PLA+"), - ("eSUN", "Pink", "#FFC0CB", "PLA+"), + # eSUN PLA+ (from FilamentColors.xyz measured swatches) + ("eSUN", "Beige", "#ECCAB0", "PLA+"), + ("eSUN", "Black", "#373838", "PLA+"), + ("eSUN", "Blue", "#054795", "PLA+"), + ("eSUN", "Bone White", "#C2BAA7", "PLA+"), + ("eSUN", "Brown", "#6F513C", "PLA+"), + ("eSUN", "Cool White", "#E1E4E5", "PLA+"), + ("eSUN", "Dark Blue", "#2F314D", "PLA+"), + ("eSUN", "Fire Engine Red", "#91202B", "PLA+"), + ("eSUN", "Gold", "#C99B26", "PLA+"), + ("eSUN", "Gray", "#697480", "PLA+"), + ("eSUN", "Green", "#015E58", "PLA+"), + ("eSUN", "Grey", "#5F6574", "PLA+"), + ("eSUN", "Light Blue", "#48BFD5", "PLA+"), + ("eSUN", "Light Brown", "#A27556", "PLA+"), + ("eSUN", "Luminous Blue", "#C8CAC8", "PLA+"), + ("eSUN", "Magenta", "#DA3B6C", "PLA+"), + ("eSUN", "Olive Green", "#555B45", "PLA+"), + ("eSUN", "Orange", "#EF7749", "PLA+"), + ("eSUN", "Peak Green", "#A1DA7C", "PLA+"), + ("eSUN", "Pink", "#E78397", "PLA+"), + ("eSUN", "Purple", "#8350A4", "PLA+"), + ("eSUN", "Red", "#C4402A", "PLA+"), + ("eSUN", "Silver", "#8B8889", "PLA+"), + ("eSUN", "Skin", "#E3C7AF", "PLA+"), + ("eSUN", "White", "#E1E9E9", "PLA+"), + ("eSUN", "Yellow", "#FBCE2B", "PLA+"), + # eSUN Pro PLA+ + ("eSUN", "Blue", "#065AA1", "Pro PLA+"), + # eSUN PLA + ("eSUN", "Glow in the Dark", "#C5C2AB", "PLA"), + ("eSUN", "Marble White", "#B5BCC0", "PLA"), + ("eSUN", "Natural Wood", "#EBCFA6", "PLA"), + ("eSUN", "Pine Green", "#375C49", "PLA"), + ("eSUN", "UV Change Purple", "#CABBA9", "PLA"), + ("eSUN", "eTwinkling Blue", "#115CAF", "PLA"), + ("eSUN", "eStars Galaxy Black", "#403936", "PLA"), + # eSUN PLA Silk + ("eSUN", "Silk Blue", "#2275AA", "PLA Silk"), + ("eSUN", "Silk Bronze", "#829172", "PLA Silk"), + ("eSUN", "Silk Copper", "#AE6B2F", "PLA Silk"), + ("eSUN", "Silk Cyan", "#34A7CF", "PLA Silk"), + ("eSUN", "Silk Dark Yellow", "#D4A62E", "PLA Silk"), + ("eSUN", "Silk Gold", "#C48E2F", "PLA Silk"), + ("eSUN", "Silk Green", "#7FCB43", "PLA Silk"), + ("eSUN", "Silk Jacinth", "#DA8061", "PLA Silk"), + ("eSUN", "Silk Lime", "#C1D762", "PLA Silk"), + ("eSUN", "Silk Magic Green Blue", "#508669", "PLA Silk"), + ("eSUN", "Silk Purple", "#905295", "PLA Silk"), + ("eSUN", "Silk Red", "#C94830", "PLA Silk"), + ("eSUN", "Silk Rose Gold", "#C7886B", "PLA Silk"), + ("eSUN", "Silk Silver", "#B5C1C5", "PLA Silk"), + ("eSUN", "Silk Violet", "#B93CA1", "PLA Silk"), + ("eSUN", "Silk White", "#E3E0DB", "PLA Silk"), + ("eSUN", "Silk Yellow", "#DED74B", "PLA Silk"), + # eSUN PLA Metal + ("eSUN", "Bronze", "#917F57", "PLA Metal"), + # eSUN PLA-ST + ("eSUN", "Grey", "#626C77", "PLA-ST"), + # eSUN PETG + ("eSUN", "Black", "#353434", "PETG"), + ("eSUN", "Magenta", "#E03E76", "PETG"), + ("eSUN", "Solid Blue", "#1A6FB4", "PETG"), + ("eSUN", "Solid Green", "#008A58", "PETG"), + ("eSUN", "Solid Purple", "#7A4795", "PETG"), + ("eSUN", "Solid White", "#F4F1F1", "PETG"), + ("eSUN", "Solid Yellow", "#F0CA41", "PETG"), + ("eSUN", "Translucent Green", "#378041", "PETG"), + ("eSUN", "Translucent Orange", "#DD7135", "PETG"), + ("eSUN", "White", "#E7EDED", "PETG"), + # eSUN PETG-HS (High Speed) + ("eSUN", "Black", "#424445", "PETG-HS"), + ("eSUN", "Solid Blue", "#1A6FB4", "PETG-HS"), + # eSUN ABS + ("eSUN", "Black", "#3F3A3F", "ABS"), + ("eSUN", "Brown", "#624741", "ABS"), + ("eSUN", "Natural", "#D9E3DD", "ABS"), + ("eSUN", "Pine Green", "#3C694E", "ABS"), + ("eSUN", "Pink", "#E86477", "ABS"), + ("eSUN", "Red", "#A74237", "ABS"), + ("eSUN", "Silver", "#838080", "ABS"), + # eSUN ABS+ + ("eSUN", "Gray", "#616777", "ABS+"), + ("eSUN", "Green", "#018068", "ABS+"), + ("eSUN", "Natural", "#E4DEC9", "ABS+"), + ("eSUN", "Orange", "#EE7845", "ABS+"), + ("eSUN", "Silver", "#7F807E", "ABS+"), + ("eSUN", "White", "#E1E1DF", "ABS+"), + ("eSUN", "Yellow", "#D3BC0F", "ABS+"), # Hatchbox PLA ("Hatchbox", "White", "#FFFFFF", "PLA"), ("Hatchbox", "Black", "#000000", "PLA"), @@ -354,4 +432,397 @@ DEFAULT_COLOR_CATALOG: list[tuple[str, str, str, str]] = [ ("Hatchbox", "Pink", "#FFC0CB", "PLA"), ("Hatchbox", "True Blue", "#0073CF", "PLA"), ("Hatchbox", "True Green", "#008000", "PLA"), + # Overture PLA (from FilamentColors.xyz measured swatches) + ("Overture", "Black", "#2B292E", "PLA"), + ("Overture", "Blue", "#034070", "PLA"), + ("Overture", "Cement Gray", "#48494A", "PLA"), + ("Overture", "Dark Blue", "#124775", "PLA"), + ("Overture", "Fresh Red", "#C01F1D", "PLA"), + ("Overture", "Gray Blue", "#6D8790", "PLA"), + ("Overture", "Green", "#318C49", "PLA"), + ("Overture", "Highlight Yellow", "#FBF93C", "PLA"), + ("Overture", "Light Blue", "#7CC4D5", "PLA"), + ("Overture", "Light Gray", "#8F9694", "PLA"), + ("Overture", "Neon Green Air", "#C5ED33", "PLA"), + ("Overture", "Olive Green", "#8F843D", "PLA"), + ("Overture", "Pink", "#DC99B4", "PLA"), + ("Overture", "Red", "#C9341A", "PLA"), + ("Overture", "Royal Gold", "#C58F31", "PLA"), + ("Overture", "Space Grey", "#797779", "PLA"), + ("Overture", "White", "#E7EBE3", "PLA"), + # Overture PLA Matte + ("Overture", "Black", "#3F3E41", "PLA Matte"), + ("Overture", "Blue", "#277EAB", "PLA Matte"), + ("Overture", "Brick Red", "#AE4848", "PLA Matte"), + ("Overture", "Green", "#5EAE73", "PLA Matte"), + ("Overture", "Light Grey", "#919598", "PLA Matte"), + ("Overture", "Light Brown", "#BF9C80", "PLA Matte"), + ("Overture", "Light Green", "#A1C1A5", "PLA Matte"), + ("Overture", "Olive Green", "#B59837", "PLA Matte"), + ("Overture", "Orange", "#F59752", "PLA Matte"), + ("Overture", "Pink", "#EBBDCE", "PLA Matte"), + ("Overture", "Purple", "#978DC5", "PLA Matte"), + ("Overture", "White", "#E1E4DD", "PLA Matte"), + ("Overture", "Yellow", "#FFD359", "PLA Matte"), + # Overture PLA Pro + ("Overture", "Digital Blue", "#008FBE", "PLA Pro"), + ("Overture", "Light Blue", "#68C8DB", "PLA Pro"), + ("Overture", "Orange", "#F27C1B", "PLA Pro"), + ("Overture", "Purple", "#7B5DB0", "PLA Pro"), + ("Overture", "Red", "#E62F18", "PLA Pro"), + ("Overture", "Yellow", "#DFB233", "PLA Pro"), + # Overture PETG + ("Overture", "Black", "#2F2821", "PETG"), + ("Overture", "Blue", "#225291", "PETG"), + ("Overture", "Clear", "#BEC3C5", "PETG"), + ("Overture", "Pink", "#E0A1BA", "PETG"), + ("Overture", "Purple", "#67518F", "PETG"), + ("Overture", "Rock White", "#C2C8C9", "PETG"), + ("Overture", "Red", "#AB291B", "PETG"), + ("Overture", "Space Grey", "#80817E", "PETG"), + ("Overture", "Translucent Blue", "#38487B", "PETG"), + ("Overture", "White", "#E7E9E7", "PETG"), + ("Overture", "Yellow", "#E6B93C", "PETG"), + # Overture ABS + ("Overture", "Diamond Gray", "#5D5F5F", "ABS"), + ("Overture", "Diamond Purple", "#6B649D", "ABS"), + # Overture Silk PLA + ("Overture", "Gold", "#CA9B52", "Silk PLA"), + ("Overture", "Neon Green", "#C2D74D", "Silk PLA"), + ("Overture", "Copper", "#B27052", "Silk PLA"), + # Overture Glow PLA + ("Overture", "Glow Blue", "#4EA2AA", "Glow PLA"), + ("Overture", "Glow Orange", "#C2895E", "Glow PLA"), + ("Overture", "Glow Red", "#C27B7D", "Glow PLA"), + ("Overture", "Glow Yellow", "#E3F079", "Glow PLA"), + # Sunlu PLA (from FilamentColors.xyz measured swatches) + ("Sunlu", "Black", "#3C3C3C", "PLA"), + ("Sunlu", "Blue", "#006AB8", "PLA"), + ("Sunlu", "Cherry Red", "#EA4A5D", "PLA"), + ("Sunlu", "Glow in the Dark", "#CBCAB8", "PLA"), + ("Sunlu", "Green Mint", "#4CCB9A", "PLA"), + ("Sunlu", "Grey", "#6B6E6E", "PLA"), + ("Sunlu", "Orange", "#E77932", "PLA"), + ("Sunlu", "Red", "#AC3637", "PLA"), + ("Sunlu", "Sky Blue", "#0CB7CC", "PLA"), + ("Sunlu", "Sunny Orange", "#FF7235", "PLA"), + ("Sunlu", "Transparent", "#C8C7BF", "PLA"), + ("Sunlu", "Transparent Orange", "#DB7F42", "PLA"), + ("Sunlu", "White", "#DEDFD9", "PLA"), + ("Sunlu", "Wood", "#D5BA95", "PLA"), + # Sunlu PLA Silk + ("Sunlu", "Silk Black", "#737272", "PLA Silk"), + ("Sunlu", "Silk Green", "#34C0A5", "PLA Silk"), + ("Sunlu", "Silk Red", "#CD5C62", "PLA Silk"), + ("Sunlu", "Silky Silver", "#C6CBD0", "PLA Silk"), + # Sunlu PLA Meta + ("Sunlu", "Blue", "#00B2CC", "PLA Meta"), + ("Sunlu", "Mint Green", "#03A490", "PLA Meta"), + ("Sunlu", "Sakura Pink", "#F5B5C2", "PLA Meta"), + ("Sunlu", "Taro Purple", "#A69ED0", "PLA Meta"), + # Sunlu PLA+ + ("Sunlu", "Beige", "#DDBCAC", "PLA+"), + ("Sunlu", "Black", "#3A3B3B", "PLA+"), + ("Sunlu", "Blue", "#0063A0", "PLA+"), + ("Sunlu", "Green", "#4EE349", "PLA+"), + ("Sunlu", "Light Gold", "#D3943D", "PLA+"), + ("Sunlu", "Mint Green", "#00B39A", "PLA+"), + ("Sunlu", "Orange", "#ED7432", "PLA+"), + ("Sunlu", "Pure Yellow", "#FFBD2C", "PLA+"), + ("Sunlu", "Purple", "#8887C5", "PLA+"), + ("Sunlu", "Red", "#B34044", "PLA+"), + ("Sunlu", "Silk Blue", "#33ACD4", "PLA+"), + ("Sunlu", "Silk Brass", "#F1A050", "PLA+"), + ("Sunlu", "Silk Pink", "#FFCAD9", "PLA+"), + ("Sunlu", "Silk White", "#EEEFE7", "PLA+"), + ("Sunlu", "Skin", "#F7BEA1", "PLA+"), + ("Sunlu", "White", "#E6E6E2", "PLA+"), + # Sunlu PETG + ("Sunlu", "Black", "#3F4141", "PETG"), + ("Sunlu", "Blue", "#0068AB", "PETG"), + ("Sunlu", "Green", "#67DB25", "PETG"), + ("Sunlu", "Olive Green", "#707D63", "PETG"), + ("Sunlu", "Transparent", "#BAB9B4", "PETG"), + ("Sunlu", "White", "#DBDDD9", "PETG"), + # Sunlu ABS + ("Sunlu", "Black", "#404142", "ABS"), + # Creality Hyper PLA (from FilamentColors.xyz measured swatches) + ("Creality", "Black", "#282C2C", "Hyper PLA"), + ("Creality", "Blue", "#0881BE", "Hyper PLA"), + ("Creality", "Grey", "#7A7C7C", "Hyper PLA"), + ("Creality", "Purple", "#B0347E", "Hyper PLA"), + ("Creality", "Red", "#C32E2F", "Hyper PLA"), + ("Creality", "White", "#DEE4E1", "Hyper PLA"), + # Creality Hyper PLA-CF + ("Creality", "Black", "#322F2D", "Hyper PLA-CF"), + # Creality PLA + ("Creality", "Gray", "#8F9395", "PLA"), + ("Creality", "White", "#E1DFD0", "PLA"), + # Creality PETG + ("Creality", "White", "#E3E5E1", "PETG"), + # Creality Silk PLA + ("Creality", "Blue-Green", "#479B7D", "Silk PLA"), + # Elegoo PLA (from FilamentColors.xyz measured swatches) + ("Elegoo", "Black", "#282929", "PLA"), + ("Elegoo", "Clear", "#BEBBBF", "PLA"), + ("Elegoo", "Galaxy Black", "#32464E", "PLA"), + ("Elegoo", "Galaxy Purple", "#3A2F6F", "PLA"), + ("Elegoo", "Grey", "#B5B7B7", "PLA"), + ("Elegoo", "Peacock Blue", "#21606B", "PLA"), + ("Elegoo", "Sky Blue", "#46C8D4", "PLA"), + # Elegoo PLA+ + ("Elegoo", "Black", "#343132", "PLA+"), + ("Elegoo", "Orange", "#CC6A2F", "PLA+"), + ("Elegoo", "Purple", "#6E45A7", "PLA+"), + # Elegoo Silk PLA + ("Elegoo", "Coral Pink", "#DB6E6D", "Silk PLA"), + ("Elegoo", "Gold", "#E2AC00", "Silk PLA"), + ("Elegoo", "Silver", "#93969B", "Silk PLA"), + # Jayo PLA+ (from FilamentColors.xyz measured swatches) + ("Jayo", "Black", "#2F2E2D", "PLA+"), + ("Jayo", "Cherry Red", "#C43536", "PLA+"), + ("Jayo", "White", "#D9E0E7", "PLA+"), + # Inland PLA (from FilamentColors.xyz measured swatches) + ("Inland", "Black", "#27272C", "PLA"), + ("Inland", "Blue", "#044482", "PLA"), + ("Inland", "Coral", "#C16062", "PLA"), + ("Inland", "Egyptian Blue", "#075AAC", "PLA"), + ("Inland", "Gold", "#D7B536", "PLA"), + ("Inland", "Green", "#407166", "PLA"), + ("Inland", "Grey", "#6F7983", "PLA"), + ("Inland", "Light Blue", "#3CA4B8", "PLA"), + ("Inland", "Military Green", "#5B6D37", "PLA"), + ("Inland", "Pink", "#FC97AF", "PLA"), + ("Inland", "Red", "#C43220", "PLA"), + ("Inland", "Silver", "#8A8F92", "PLA"), + ("Inland", "True Red", "#B13137", "PLA"), + ("Inland", "White", "#E0E3E3", "PLA"), + ("Inland", "Wood", "#DEB98F", "PLA"), + # Inland PLA+ + ("Inland", "Black", "#2B272B", "PLA+"), + ("Inland", "Blue", "#054990", "PLA+"), + ("Inland", "Bone White", "#ABA18F", "PLA+"), + ("Inland", "Dark Blue", "#2C3353", "PLA+"), + ("Inland", "Light Blue", "#079FBF", "PLA+"), + ("Inland", "Magenta", "#DE2B60", "PLA+"), + ("Inland", "Orange", "#FB8B5A", "PLA+"), + ("Inland", "Pink", "#F291A4", "PLA+"), + ("Inland", "Purple", "#744FA0", "PLA+"), + ("Inland", "Silver", "#868A8B", "PLA+"), + ("Inland", "White", "#E3E5E5", "PLA+"), + ("Inland", "Yellow", "#F8D008", "PLA+"), + # Inland PETG + ("Inland", "Blue", "#084480", "PETG"), + ("Inland", "Green", "#2B783E", "PETG"), + ("Inland", "Magenta", "#E14170", "PETG"), + ("Inland", "Transparent", "#D1D6D1", "PETG"), + ("Inland", "True Red", "#97392B", "PETG"), + # Inland ABS + ("Inland", "Grey", "#8A97A2", "ABS"), + ("Inland", "Light Blue", "#6CBECF", "ABS"), + ("Inland", "Orange", "#E8712F", "ABS"), + # Inland Tough PLA + ("Inland", "Light Gray", "#8D9497", "Tough PLA"), + ("Inland", "Yellow", "#FFBB3F", "Tough PLA"), + # Eryone PLA (from FilamentColors.xyz measured swatches) + ("Eryone", "Galaxy Purple", "#60617B", "PLA"), + ("Eryone", "Galaxy Red", "#8E3332", "PLA"), + ("Eryone", "Glow in the Dark", "#C2C1AF", "PLA"), + ("Eryone", "Ivory White", "#DCDCD3", "PLA"), + ("Eryone", "Silk Blue", "#64A9D3", "PLA"), + ("Eryone", "Silk Copper", "#B36A50", "PLA"), + ("Eryone", "Silk Gold", "#D5983D", "PLA"), + ("Eryone", "Silk Gold Copper", "#D69366", "PLA"), + ("Eryone", "Silk Gold Silver", "#ABA787", "PLA"), + ("Eryone", "Ultra Silk Black", "#5B6264", "PLA"), + ("Eryone", "Ultra Silk Copper", "#B46A4D", "PLA"), + ("Eryone", "Ultra Silk Silver", "#999BA5", "PLA"), + # Eryone PLA+ + ("Eryone", "Army Green", "#5D644D", "PLA+"), + # Eryone ASA + ("Eryone", "Black", "#414446", "ASA"), + # Eryone PLA Wood + ("Eryone", "Light Wood", "#A5886E", "PLA Wood"), + # ColorFabb PLA (from FilamentColors.xyz measured swatches) + ("ColorFabb", "Stonefill Light Gray", "#A9B2B7", "PLA"), + ("ColorFabb", "WoodFill", "#B89775", "PLA"), + # ColorFabb PLA/PHA + ("ColorFabb", "CopperFill", "#9D7465", "PLA/PHA"), + ("ColorFabb", "CorkFill", "#7F6150", "PLA/PHA"), + ("ColorFabb", "Natural", "#CFCFC2", "PLA/PHA"), + # ColorFabb XT + ("ColorFabb", "Light Gray", "#BFC5BE", "XT"), + ("ColorFabb", "Black", "#3B3635", "XT"), + # Fillamentum PLA Extrafill (from FilamentColors.xyz measured swatches) + ("Fillamentum", "Baby Blue", "#B9D7DC", "PLA Extrafill"), + ("Fillamentum", "Chocolate Brown", "#5B4A45", "PLA Extrafill"), + ("Fillamentum", "Cobalt Blue", "#333D5C", "PLA Extrafill"), + ("Fillamentum", "Crystal Clear Smaragd Green", "#028D77", "PLA Extrafill"), + ("Fillamentum", "Everybody's Magenta", "#E1347D", "PLA Extrafill"), + ("Fillamentum", "Gold Happens", "#BC994D", "PLA Extrafill"), + ("Fillamentum", "Mukha", "#A88866", "PLA Extrafill"), + ("Fillamentum", "Pearl Night Blue", "#045589", "PLA Extrafill"), + ("Fillamentum", "Pearl Ruby Red", "#791F2A", "PLA Extrafill"), + ("Fillamentum", "Rapunzel Silver", "#AFAFB0", "PLA Extrafill"), + ("Fillamentum", "Vertigo Cherry", "#752F38", "PLA Extrafill"), + ("Fillamentum", "Vertigo Galaxy", "#333928", "PLA Extrafill"), + ("Fillamentum", "Vertigo Grey", "#5A5963", "PLA Extrafill"), + ("Fillamentum", "Vertigo Starlight", "#343A4F", "PLA Extrafill"), + ("Fillamentum", "Wizard's Voodoo", "#3F465E", "PLA Extrafill"), + # Fillamentum PLA (Crystal Clear / Timberfill / Vertigo lines) + ("Fillamentum", "Crystal Clear", "#EBECF2", "PLA"), + ("Fillamentum", "Crystal Clear Amethyst Purple", "#9F99BC", "PLA"), + ("Fillamentum", "Crystal Clear Iceland Blue", "#82BBCD", "PLA"), + ("Fillamentum", "Crystal Clear Tangerine Orange", "#ECD082", "PLA"), + ("Fillamentum", "Lilac", "#A99FCF", "PLA"), + ("Fillamentum", "Timberfill Cinnamon", "#AC7C67", "PLA"), + ("Fillamentum", "Timberfill Rosewood", "#6A564E", "PLA"), + ("Fillamentum", "Vertigo Jade", "#217F60", "PLA"), + # Fillamentum ASA Extrafill + ("Fillamentum", "Anthracite Grey", "#4B4F50", "ASA Extrafill"), + ("Fillamentum", "Green Grass", "#678653", "ASA Extrafill"), + ("Fillamentum", "Grey Blue", "#495965", "ASA Extrafill"), + ("Fillamentum", "Metallic Grey", "#878A8C", "ASA Extrafill"), + ("Fillamentum", "Sky Blue", "#0783B6", "ASA Extrafill"), + ("Fillamentum", "Snow White", "#F3F3EF", "ASA Extrafill"), + ("Fillamentum", "Traffic Black", "#3B3B3F", "ASA Extrafill"), + ("Fillamentum", "Traffic White", "#E9E7DA", "ASA Extrafill"), + ("Fillamentum", "White Aluminium", "#9CA1A2", "ASA Extrafill"), + # Fillamentum CPE HG100 + ("Fillamentum", "Black Soul", "#292B27", "CPE HG100"), + ("Fillamentum", "Ghost White", "#E5E8E7", "CPE HG100"), + ("Fillamentum", "Natural", "#DCE4DF", "CPE HG100"), + # Fillamentum Flexfill TPU 98A + ("Fillamentum", "Blue Transparent", "#047990", "Flexfill TPU 98A"), + ("Fillamentum", "Carrot Orange", "#EA6E21", "Flexfill TPU 98A"), + ("Fillamentum", "Metallic Grey", "#8F8E8F", "Flexfill TPU 98A"), + ("Fillamentum", "Pistachio Green", "#A7BE36", "Flexfill TPU 98A"), + ("Fillamentum", "Signal Red", "#9A2222", "Flexfill TPU 98A"), + ("Fillamentum", "Traffic Black", "#26262A", "Flexfill TPU 98A"), + ("Fillamentum", "Vertigo Grey", "#515150", "Flexfill TPU 98A"), + # FormFutura PLA (from FilamentColors.xyz measured swatches) + ("FormFutura", "Basalt Grey", "#5B5F61", "PLA"), + ("FormFutura", "Dark Blue", "#084B86", "PLA"), + ("FormFutura", "Galaxy Champagne Gold", "#AE9D83", "PLA"), + ("FormFutura", "Gold High Gloss", "#C89B4B", "PLA"), + ("FormFutura", "High Gloss White", "#D0D7D8", "PLA"), + ("FormFutura", "Magenta High Gloss", "#B94474", "PLA"), + ("FormFutura", "Stonefil Terracotta", "#BD634C", "PLA"), + ("FormFutura", "Yellow Green", "#7AA837", "PLA"), + # FormFutura ePLA + ("FormFutura", "Pure Orange", "#FA9145", "EasyFil PLA"), + # FormFutura rPLA + ("FormFutura", "ReForm Black", "#3A3B3B", "ReForm rPLA"), + ("FormFutura", "ReForm White", "#F3F3EC", "ReForm rPLA"), + # Fiberlogy PLA (from FilamentColors.xyz measured swatches) + ("Fiberlogy", "Mineral White", "#E2D9CD", "PLA"), + ("Fiberlogy", "Aurora", "#3C4452", "Easy PLA"), + ("Fiberlogy", "Army Green", "#535F4F", "Impact PLA"), + # Fiberlogy ASA + ("Fiberlogy", "Olive Green", "#61634B", "ASA"), + # Fiberlogy Easy PETG + ("Fiberlogy", "White", "#F2F2EE", "Easy PETG"), + # Fiberlogy FiberSilk + ("Fiberlogy", "Green", "#A2D780", "FiberSilk Metallic"), + # MatterHackers Build PLA (from FilamentColors.xyz measured swatches) + ("MatterHackers", "Blue", "#044786", "Build PLA"), + ("MatterHackers", "Magenta", "#CD4263", "Build PLA"), + ("MatterHackers", "Red", "#C4351B", "Build PLA"), + ("MatterHackers", "Shiny Gold", "#DFAC1E", "Build PLA"), + ("MatterHackers", "Silky Copper", "#C76F35", "Build PLA"), + ("MatterHackers", "Silky Silver", "#BDBDB8", "Build PLA"), + ("MatterHackers", "Silky Teal", "#078EBC", "Build PLA"), + ("MatterHackers", "Silky Yellow", "#EDB554", "Build PLA"), + ("MatterHackers", "Yellow", "#EBC100", "Build PLA"), + # MatterHackers PLA + ("MatterHackers", "Gold", "#E7AC37", "PLA"), + ("MatterHackers", "Lime Green", "#75BA52", "PLA"), + ("MatterHackers", "Pearl White", "#D4DCDD", "PLA"), + ("MatterHackers", "Red", "#E54931", "PLA"), + # MatterHackers Pro PLA + ("MatterHackers", "Electric Pink", "#F35886", "Pro PLA"), + ("MatterHackers", "Jet Gray", "#474B4C", "Pro PLA"), + # MatterHackers PETG + ("MatterHackers", "Clear", "#D5DDDA", "PETG"), + ("MatterHackers", "White", "#E9EBEF", "PETG"), + # MatterHackers NylonX / NylonG + ("MatterHackers", "Black", "#3D3C38", "NylonX"), + ("MatterHackers", "White", "#DCDED9", "NylonG"), + # Protopasta HTPLA (from FilamentColors.xyz measured swatches) + ("Protopasta", "Atikam Teal", "#135859", "HTPLA"), + ("Protopasta", "Blood of My Enemies", "#7A1A23", "HTPLA"), + ("Protopasta", "Blue Opaque", "#044A86", "HTPLA"), + ("Protopasta", "Blue Wonder Glitter Flake", "#20556F", "HTPLA"), + ("Protopasta", "Bobbi's Purple Iris", "#542B5C", "HTPLA"), + ("Protopasta", "Brass Composite", "#8C7A4F", "HTPLA"), + ("Protopasta", "Bronze Composite", "#635146", "HTPLA"), + ("Protopasta", "Candy Apple Metallic Red", "#A32423", "HTPLA"), + ("Protopasta", "Cloverleaf Metallic Green", "#245A3F", "HTPLA"), + ("Protopasta", "Copper Composite", "#976252", "HTPLA"), + ("Protopasta", "Cupid's Crush Metallic Pink", "#EA8699", "HTPLA"), + ("Protopasta", "Double Espresso Metallic Brown", "#6B473B", "HTPLA"), + ("Protopasta", "Dragon Fruit Smoothie", "#B3295F", "HTPLA"), + ("Protopasta", "Dragon Scale Purple", "#8A80AC", "HTPLA"), + ("Protopasta", "Dusty Smoke", "#8F9491", "HTPLA"), + ("Protopasta", "Electric Lemonade Metallic Yellow", "#E4CA6B", "HTPLA"), + ("Protopasta", "Empire Strikes Metallic Black", "#393B3B", "HTPLA"), + ("Protopasta", "Fluorescent Yellow", "#D4DC3A", "HTPLA"), + ("Protopasta", "Galactic Empire Metallic Purple", "#3B3F5D", "HTPLA"), + ("Protopasta", "Glitter's Mane", "#128C93", "HTPLA"), + ("Protopasta", "Gold Dust Glitter Flake", "#BFAE6D", "HTPLA"), + ("Protopasta", "Good as Gold", "#9A774B", "HTPLA"), + ("Protopasta", "Good Old Gray", "#6D737B", "HTPLA"), + ("Protopasta", "Green Glowing Natural", "#D4D3AD", "HTPLA"), + ("Protopasta", "Heartthrob Red Metallic", "#7E3030", "HTPLA"), + ("Protopasta", "Joel's Highfive Blue", "#056B9A", "HTPLA"), + ("Protopasta", "Lootsef Green", "#8FB841", "HTPLA"), + ("Protopasta", "Luke's Proton Purple", "#7D3F59", "HTPLA"), + ("Protopasta", "Mahogany", "#7F5D4F", "HTPLA"), + ("Protopasta", "Matte Fiber Black", "#3F3F3E", "HTPLA"), + ("Protopasta", "Matte Fiber Daffodil", "#B79868", "HTPLA"), + ("Protopasta", "Matte Fiber Gray", "#767A7D", "HTPLA"), + ("Protopasta", "Matte Fiber Walnut", "#6F5D4E", "HTPLA"), + ("Protopasta", "Matte Fiber White", "#F4EADB", "HTPLA"), + ("Protopasta", "Mermaid's Tale Metallic Teal", "#026768", "HTPLA"), + ("Protopasta", "Moonstruck White Satin", "#DCE4DD", "HTPLA"), + ("Protopasta", "Obsidian", "#474743", "HTPLA"), + ("Protopasta", "Opaque Black", "#312F30", "HTPLA"), + ("Protopasta", "Opaque Natural", "#CBD0D0", "HTPLA"), + ("Protopasta", "Opaque White", "#DFE4E2", "HTPLA"), + ("Protopasta", "Orange Papaya Smoothie", "#C57231", "HTPLA"), + ("Protopasta", "Out of Darts Orange", "#E58429", "HTPLA"), + ("Protopasta", "Pineapple Banana Smoothie", "#D0A645", "HTPLA"), + ("Protopasta", "Pretty in Pink Pearl", "#CE95AE", "HTPLA"), + ("Protopasta", "Red Hot Cinnamon", "#7C4448", "HTPLA"), + ("Protopasta", "Red Opaque", "#972425", "HTPLA"), + ("Protopasta", "Second to None Silver", "#B4B6B6", "HTPLA"), + ("Protopasta", "Sparkling Spruce", "#47614C", "HTPLA"), + ("Protopasta", "Stardust Glitter Flake", "#AAB1AE", "HTPLA"), + ("Protopasta", "Summertime Green", "#89A78A", "HTPLA"), + ("Protopasta", "Tangerine Orange Metallic Gold", "#C05834", "HTPLA"), + ("Protopasta", "Translucent Iridescent Ice", "#C4CBC8", "HTPLA"), + ("Protopasta", "Translucent Silver Smoke", "#A5ACA9", "HTPLA"), + ("Protopasta", "Unicorn Tears White Glitter", "#D7DEDE", "HTPLA"), + ("Protopasta", "What Karat? Smooth Gold", "#CD974B", "HTPLA"), + ("Protopasta", "White", "#F6F5F0", "HTPLA"), + ("Protopasta", "White Marble", "#C6CECF", "HTPLA"), + ("Protopasta", "Winter Blue Glitter Flake", "#056F9D", "HTPLA"), + # Protopasta PLA + ("Protopasta", "Black", "#323132", "PLA"), + ("Protopasta", "Conductive", "#373838", "PLA"), + ("Protopasta", "Iron Composite", "#555451", "PLA"), + ("Protopasta", "Natural", "#E1DFD6", "PLA"), + ("Protopasta", "Steel Composite", "#676561", "PLA"), + # Protopasta Carbon Fiber PLA + ("Protopasta", "Black", "#424140", "Carbon Fiber PLA"), + # 3DXTECH (from FilamentColors.xyz measured swatches) + ("3DXTECH", "Natural", "#DED7C6", "ASA"), + ("3DXTECH", "Black", "#444342", "Carbon Fiber PLA"), + ("3DXTECH", "Venom", "#CACC19", "ECOMAX PLA"), + ("3DXTECH", "Simubone", "#EAE0CB", "PLA"), + ("3DXTECH", "Blue Frost", "#B1C1C5", "rPETG"), + # Sakata3D PLA (from FilamentColors.xyz measured swatches) + ("Sakata3D", "Red", "#B63A32", "PLA"), + ("Sakata3D", "Silk Sunset", "#F49545", "PLA"), + ("Sakata3D", "Surf Green", "#00C1A8", "PLA"), ] diff --git a/frontend/src/components/ColorCatalogSettings.tsx b/frontend/src/components/ColorCatalogSettings.tsx index 710245ec0..b5b6e8ef3 100644 --- a/frontend/src/components/ColorCatalogSettings.tsx +++ b/frontend/src/components/ColorCatalogSettings.tsx @@ -436,7 +436,7 @@ export function ColorCatalogSettings() { {t('common.loading')}
) : ( -
+
diff --git a/frontend/src/components/SpoolCatalogSettings.tsx b/frontend/src/components/SpoolCatalogSettings.tsx index 9cecee779..a8856f1e4 100644 --- a/frontend/src/components/SpoolCatalogSettings.tsx +++ b/frontend/src/components/SpoolCatalogSettings.tsx @@ -271,7 +271,7 @@ export function SpoolCatalogSettings() { {t('common.loading')} ) : ( -
+
diff --git a/frontend/src/components/SpoolmanSettings.tsx b/frontend/src/components/SpoolmanSettings.tsx index 6a77f9074..bea973974 100644 --- a/frontend/src/components/SpoolmanSettings.tsx +++ b/frontend/src/components/SpoolmanSettings.tsx @@ -243,6 +243,7 @@ export function SpoolmanSettings() {
  • {t('settings.builtInFeatureRfid')}
  • {t('settings.builtInFeatureUsage')}
  • {t('settings.builtInFeatureCatalog')}
  • +
  • {t('settings.builtInFeatureThirdParty')}
  • diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index c166b4347..a7bd69f44 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -1170,6 +1170,7 @@ export default { builtInFeatureRfid: 'Erkennt automatisch Bambu Lab RFID-Spulen im AMS', builtInFeatureUsage: 'Erfasst den Filamentverbrauch pro Druck', builtInFeatureCatalog: 'Spulen, Farben und K-Faktor-Profile verwalten', + builtInFeatureThirdParty: 'Drittanbieter-Spulen können Inventarspulen zugewiesen werden', // Spoolman settings spoolmanUrl: 'Spoolman URL', spoolmanUrlHint: 'URL Ihres Spoolman-Servers (z.B. http://localhost:7912)', @@ -2498,6 +2499,7 @@ export default { spoolArchived: 'Spule archiviert', spoolRestored: 'Spule wiederhergestellt', deleteConfirm: 'Möchten Sie diese Spule wirklich löschen? Dies kann nicht rückgängig gemacht werden.', + archiveConfirm: 'Möchten Sie diese Spule wirklich archivieren?', advancedSettings: 'Erweiterte Einstellungen', filamentInfoTab: 'Filament-Info', paProfileTab: 'PA-Profil', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index b1e1d9bbf..d9db5d33d 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1170,6 +1170,7 @@ export default { builtInFeatureRfid: 'Automatically detects Bambu Lab RFID spools in AMS', builtInFeatureUsage: 'Tracks filament consumption per print', builtInFeatureCatalog: 'Manage spools, colors, and K-factor profiles', + builtInFeatureThirdParty: 'Third-party spools can be assigned to inventory spools', // Spoolman settings spoolmanUrl: 'Spoolman URL', spoolmanUrlHint: 'URL of your Spoolman server (e.g., http://localhost:7912)', @@ -2498,6 +2499,7 @@ export default { spoolArchived: 'Spool archived', spoolRestored: 'Spool restored', deleteConfirm: 'Are you sure you want to delete this spool? This cannot be undone.', + archiveConfirm: 'Are you sure you want to archive this spool?', advancedSettings: 'Advanced Settings', // Tabs filamentInfoTab: 'Filament Info', diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index 766ce4c2b..e2af45b11 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -1166,6 +1166,7 @@ export default { builtInFeatureRfid: 'Détecte auto les bobines RFID Bambu Lab dans l\'AMS', builtInFeatureUsage: 'Suit la consommation par impression', builtInFeatureCatalog: 'Gère bobines, couleurs et profils facteur K', + builtInFeatureThirdParty: 'Les bobines tierces peuvent être assignées aux bobines d\'inventaire', // Spoolman settings spoolmanUrl: 'URL Spoolman', spoolmanUrlHint: 'URL de votre serveur Spoolman (ex: http://localhost:7912)', @@ -2494,6 +2495,7 @@ export default { spoolArchived: 'Bobine archivée', spoolRestored: 'Bobine restaurée', deleteConfirm: 'Supprimer définitivement cette bobine ?', + archiveConfirm: 'Voulez-vous vraiment archiver cette bobine ?', advancedSettings: 'Paramètres Avancés', // Tabs filamentInfoTab: 'Infos Filament', diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 8adb9d74f..4333be1c8 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -1410,6 +1410,7 @@ export default { builtInFeatureRfid: 'AMS内のBambu Lab RFIDスプールを自動検出', builtInFeatureUsage: 'プリントごとのフィラメント消費量を追跡', builtInFeatureCatalog: 'スプール、カラー、K値プロファイルを管理', + builtInFeatureThirdParty: 'サードパーティ製スプールをインベントリスプールに割り当て可能', // Spoolman設定 spoolmanUrl: 'Spoolman URL', spoolmanUrlHint: 'Spoolmanサーバーのurl(例:http://localhost:7912)', @@ -2429,6 +2430,7 @@ export default { spoolArchived: 'スプールをアーカイブしました', spoolRestored: 'スプールを復元しました', deleteConfirm: 'このスプールを削除しますか?この操作は元に戻せません。', + archiveConfirm: 'このスプールをアーカイブしますか?', advancedSettings: '詳細設定', filamentInfoTab: 'フィラメント情報', paProfileTab: 'PAプロファイル', diff --git a/frontend/src/pages/InventoryPage.tsx b/frontend/src/pages/InventoryPage.tsx index c2ea72f26..86d4437dc 100644 --- a/frontend/src/pages/InventoryPage.tsx +++ b/frontend/src/pages/InventoryPage.tsx @@ -11,6 +11,7 @@ import { api } from '../api/client'; import type { InventorySpool, SpoolAssignment } from '../api/client'; import { Button } from '../components/Button'; import { SpoolFormModal } from '../components/SpoolFormModal'; +import { ConfirmModal } from '../components/ConfirmModal'; import { ColumnConfigModal, type ColumnConfig } from '../components/ColumnConfigModal'; import { useToast } from '../contexts/ToastContext'; import { resolveSpoolColorName } from '../utils/colors'; @@ -303,6 +304,7 @@ export default function InventoryPage() { const queryClient = useQueryClient(); const { showToast } = useToast(); const [formModal, setFormModal] = useState<{ spool?: InventorySpool | null } | null>(null); + const [confirmAction, setConfirmAction] = useState<{ type: 'delete' | 'archive'; spoolId: number } | null>(null); // Filter state const [archiveFilter, setArchiveFilter] = useState('active'); @@ -947,7 +949,7 @@ export default function InventoryPage() { ) : ( )}