diff --git a/CHANGELOG.md b/CHANGELOG.md index efe5f50bc..e1a58d33e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,9 +16,11 @@ All notable changes to Bambuddy will be documented in this file. - **H2D Tray Disambiguation Triggers on Single-Nozzle Printers** — The `tray_now <= 3` check for H2D dual-nozzle disambiguation matched any printer loading from AMS 0 (trays 0-3). On P2S, X1C, and X1E with multiple AMS units, this caused warning log spam every second. Now uses a persistent `_is_dual_nozzle` flag detected from `device.extruder.info` (>= 2 entries), which only dual-nozzle printers (H2D, H2D Pro) report. - **AMS-HT Snow Slot Mismatch Log Spam on H2D** — The snow-based tray_now disambiguation computed `snow_slot = -1` for AMS-HT trays (IDs 128-135), causing a "slot mismatch" debug log on every MQTT update even though the result was correct. Now correctly computes `snow_slot = 0` for AMS-HT single-slot units. - **Color Tooltip Clipped Behind Adjacent Swatches** — Color swatch hover tooltips in the spool form were rendered behind neighboring swatches due to missing z-index on the hover state. Added `hover:z-20` and tooltip `z-20` classes. +- **Print Queue Shows UUID Hash Instead of Filename** ([#438](https://github.com/maziggy/bambuddy/issues/438)) — When printing a library file, the Print Queue and archive displayed the UUID-hex disk filename (e.g., `c65887535303404eba1525176a0f78dc`) instead of the original human-readable name. Library files are stored on disk with UUID filenames for uniqueness, but `archive_print()` used the disk path as the display name. Now passes the original `LibraryFile.filename` through to `archive_print()` from both the print scheduler and the direct-print-from-library flow, so the archive's `filename`, `print_name`, and directory name all use the human-readable name. -- **Usage Tracking Wrong Spool on Dual-Nozzle / Multi-AMS Printers** ([#364](https://github.com/maziggy/bambuddy/issues/364)) — On H2C, H2D Pro, and other dual-nozzle printers with multiple AMS units, the usage tracker attributed filament consumption to the wrong spools. The MQTT `mapping` field — a per-print array that maps slicer filament slots to physical AMS trays — was preserved in state but never parsed or used. The tracker fell back to `slot_id - 1` as the global tray ID, which is incorrect when AMS hardware IDs differ from sequential indices (e.g., AMS-HT units with ID 128). Now decodes the MQTT mapping field from its snow encoding (`ams_hw_id * 256 + local_slot`) into bambuddy global tray IDs and uses it as a universal mapping source — working for all printer models and all print sources (slicer, queue, reprint) without relying on `tray_now` disambiguation. +- **Usage Tracking Wrong Spool on Dual-Nozzle / Multi-AMS Printers** ([#364](https://github.com/maziggy/bambuddy/issues/364)) — On H2C, H2D Pro, and other dual-nozzle printers with multiple AMS units, the usage tracker attributed filament consumption to the wrong spools. The MQTT `mapping` field — a per-print array that maps slicer filament slots to physical AMS trays — was preserved in state but never parsed or used. The tracker fell back to `slot_id - 1` as the global tray ID, which is incorrect when AMS hardware IDs differ from sequential indices (e.g., AMS-HT units with ID 128). Now decodes the MQTT mapping field from its snow encoding (`ams_hw_id * 256 + local_slot`) into bambuddy global tray IDs and uses it as a universal mapping source — working for all printer models and all print sources (slicer, queue, reprint) without relying on `tray_now` disambiguation. For printers that don't provide the MQTT mapping field (A1, A1 Mini, P1S, P2S), a color-matching fallback compares 3MF filament slot colors against AMS tray colors to resolve the correct slot-to-tray mapping. Gracefully returns no match when colors are ambiguous (duplicate tray colors) or unavailable. - **npm audit: suppress moderate ajv ReDoS finding** — Added `audit-level=high` to `frontend/.npmrc` so `npm audit` exits cleanly. The ajv@6 ReDoS (GHSA-2g4f-4pwh-qvx6) is a transitive dependency of eslint@9 with no patched v6 release; ajv@8 override breaks eslint. The vulnerability requires crafted `$data` schema input — not an attack vector in a linting config. +- **npm audit: fix minimatch ReDoS finding** — Added an npm override for `minimatch@^10.2.1` in `package.json` to resolve the high-severity ReDoS (GHSA-3ppc-4f35-3m26) affecting minimatch@3.x/9.x pulled in transitively by eslint@9, typescript-eslint, and @vitest/coverage-v8. Eslint@9 pins minimatch@3.x with no patched release; eslint@10 upgrades to minimatch@10 but is not yet available. The override forces the patched version across the tree. Verified lint, build, and all tests pass. - **Spool Form Allows Empty Brand & Subtype** ([#417](https://github.com/maziggy/bambuddy/issues/417)) — The spool add/edit modal did not require Brand or Subtype fields, allowing spools to be saved without them. When such a spool was assigned to an AMS slot, the `tray_sub_brands` sent to the printer was incomplete (e.g., just "PETG" instead of "PETG Basic"), causing BambuStudio to not recognize the filament profile. Brand and Subtype are now mandatory fields with validation errors shown on submit. - **Open in Slicer Fails When Authentication Enabled** ([#421](https://github.com/maziggy/bambuddy/issues/421)) — The "Open in Slicer" buttons for BambuStudio and OrcaSlicer failed with "importing failed" when authentication was enabled. Slicer protocol handlers (`bambustudio://`, `orcaslicer://`) launch the slicer app which fetches the file via HTTP — but cannot send authentication headers, so the global auth middleware returned 401. Additionally, the URL format was wrong on Linux (used the macOS-only `bambustudioopen://` scheme instead of `bambustudio://open?file=`). Fixed with short-lived, single-use download tokens: the frontend fetches a token via an authenticated POST endpoint, then builds a `/dl/{token}/{filename}` URL that the slicer can access without auth headers. The token is validated server-side (5-minute expiry, single-use). Platform-specific URL formats now match the actual slicer source code: macOS uses `bambustudioopen://` with URL encoding, Windows/Linux use `bambustudio://open?file=`, and OrcaSlicer uses `orcaslicer://open?file=`. diff --git a/backend/app/api/routes/library.py b/backend/app/api/routes/library.py index dac910fcc..69d7f5233 100644 --- a/backend/app/api/routes/library.py +++ b/backend/app/api/routes/library.py @@ -1795,6 +1795,7 @@ async def print_library_file( archive = await archive_service.archive_print( printer_id=printer_id, source_file=file_path, + original_filename=lib_file.filename, ) if not archive: diff --git a/backend/app/services/archive.py b/backend/app/services/archive.py index dafa5911a..147cbc26f 100644 --- a/backend/app/services/archive.py +++ b/backend/app/services/archive.py @@ -831,6 +831,7 @@ class ArchiveService: source_file: Path, print_data: dict | None = None, created_by_id: int | None = None, + original_filename: str | None = None, ) -> PrintArchive | None: """Archive a 3MF file with metadata. @@ -839,6 +840,8 @@ class ArchiveService: source_file: Path to the 3MF file print_data: Print data from MQTT (optional) created_by_id: User ID who created this archive (optional, for user tracking) + original_filename: Original human-readable filename (optional, for library files + stored with UUID names) """ # Verify printer exists if specified if printer_id is not None: @@ -849,7 +852,8 @@ class ArchiveService: # Create archive directory structure timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - archive_name = f"{timestamp}_{source_file.stem}" + display_stem = Path(original_filename).stem if original_filename else source_file.stem + archive_name = f"{timestamp}_{display_stem}" # Use "unassigned" folder for archives without a printer printer_folder = str(printer_id) if printer_id is not None else "unassigned" archive_dir = settings.archive_dir / printer_folder / archive_name @@ -923,12 +927,12 @@ class ArchiveService: # Create archive record archive = PrintArchive( printer_id=printer_id, - filename=source_file.name, + filename=original_filename or source_file.name, file_path=str(dest_file.relative_to(settings.base_dir)), file_size=dest_file.stat().st_size, content_hash=content_hash, thumbnail_path=thumbnail_path, - print_name=metadata.get("print_name") or source_file.stem, + print_name=metadata.get("print_name") or display_stem, print_time_seconds=metadata.get("print_time_seconds"), filament_used_grams=metadata.get("filament_used_grams"), filament_type=metadata.get("filament_type"), diff --git a/backend/app/services/print_scheduler.py b/backend/app/services/print_scheduler.py index bc510d76f..d5fb3afc6 100644 --- a/backend/app/services/print_scheduler.py +++ b/backend/app/services/print_scheduler.py @@ -915,6 +915,7 @@ class PrintScheduler: archive = await archive_service.archive_print( printer_id=item.printer_id, source_file=file_path, + original_filename=filename, ) if archive: item.archive_id = archive.id diff --git a/backend/app/services/usage_tracker.py b/backend/app/services/usage_tracker.py index 335e8d60b..a3658c0e8 100644 --- a/backend/app/services/usage_tracker.py +++ b/backend/app/services/usage_tracker.py @@ -63,6 +63,90 @@ def _decode_mqtt_mapping(mapping_raw: list | None) -> list[int] | None: return result +def _match_slots_by_color( + filament_usage: list[dict], + ams_raw: dict | list | None, +) -> list[int] | None: + """Match 3MF filament slots to AMS trays by color. + + Fallback mapping for printers that don't provide the MQTT mapping field + or request topic subscription (e.g. A1, A1 Mini, P1S, P2S). + + Compares the 3MF slicer filament color (per slot) against each AMS tray's + color to find a unique match. Only returns a mapping if every used slot + matches exactly one tray (no ambiguity). + + Args: + filament_usage: List of 3MF slot dicts with 'slot_id', 'color', 'type' + ams_raw: raw_data["ams"] dict or list from printer state + + Returns: + List of global tray IDs indexed by slicer slot (0-based), or None. + """ + if not filament_usage or not ams_raw: + return None + + ams_data = ams_raw.get("ams", []) if isinstance(ams_raw, dict) else ams_raw if isinstance(ams_raw, list) else [] + if not ams_data: + return None + + # Build map of normalized color → list of global tray IDs + color_to_trays: dict[str, list[int]] = {} + for ams_unit in ams_data: + ams_id = int(ams_unit.get("id", 0)) + for tray in ams_unit.get("tray", []): + tray_id = int(tray.get("id", 0)) + tray_color = tray.get("tray_color", "") + tray_type = tray.get("tray_type", "") + if not tray_color or not tray_type: + continue + # Normalize AMS color: strip alpha (last 2 chars), lowercase + norm = tray_color[:6].lower() if len(tray_color) >= 6 else tray_color.lower() + if ams_id >= 128: + global_id = ams_id # AMS-HT + else: + global_id = ams_id * 4 + tray_id + color_to_trays.setdefault(norm, []).append(global_id) + + if not color_to_trays: + return None + + # Find max slot_id to size the result array + max_slot = max(u.get("slot_id", 0) for u in filament_usage) + if max_slot <= 0: + return None + + result = [-1] * max_slot + used_trays: set[int] = set() + + for usage in filament_usage: + slot_id = usage.get("slot_id", 0) + if slot_id <= 0: + continue + slot_color = usage.get("color", "").lstrip("#").lower() + if len(slot_color) < 6: + return None # Can't match without a valid color + + slot_color = slot_color[:6] # Strip alpha if present + candidates = color_to_trays.get(slot_color, []) + # Filter out trays already claimed by another slot + available = [t for t in candidates if t not in used_trays] + + if len(available) != 1: + # Ambiguous (multiple trays with same color) or no match + return None + + result[slot_id - 1] = available[0] + used_trays.add(available[0]) + + # Only return if at least one valid mapping exists + if all(v < 0 for v in result): + return None + + logger.info("[UsageTracker] Color-matched slot_to_tray: %s", result) + return result + + @dataclass class PrintSession: printer_id: int @@ -392,13 +476,23 @@ async def _track_from_3mf( except (json.JSONDecodeError, TypeError): pass + # 4. Color-match 3MF filament slots to AMS trays (for printers without mapping field) + if not slot_to_tray: + state = printer_manager.get_status(printer_id) + raw_data = getattr(state, "raw_data", None) if state else None + if raw_data: + matched = _match_slots_by_color(filament_usage, raw_data.get("ams")) + if matched: + slot_to_tray = matched + mapping_source = "color_match" + logger.info( "[UsageTracker] 3MF: slot_to_tray=%s (source: %s)", slot_to_tray, mapping_source or "none", ) - # 3. For single-filament non-queue prints, use tray_now from printer state + # 5. For single-filament non-queue prints, use tray_now from printer state # Priority: tray_now_at_start > current tray_now > last_loaded_tray > vt_tray check nonzero_slots = [u for u in filament_usage if u.get("used_g", 0) > 0] tray_now_override: int | None = None diff --git a/backend/tests/unit/test_usage_tracker.py b/backend/tests/unit/test_usage_tracker.py index 1d7a72019..1628fe6bb 100644 --- a/backend/tests/unit/test_usage_tracker.py +++ b/backend/tests/unit/test_usage_tracker.py @@ -15,6 +15,7 @@ from backend.app.services.usage_tracker import ( PrintSession, _active_sessions, _decode_mqtt_mapping, + _match_slots_by_color, _track_from_3mf, on_print_complete, on_print_start, @@ -886,6 +887,161 @@ class TestDecodeMqttMapping: assert _decode_mqtt_mapping(["foo", 0]) == [-1, 0] +class TestMatchSlotsByColor: + """Tests for _match_slots_by_color() — color-based filament slot to AMS tray matching.""" + + def _ams(self, trays): + """Build AMS data from list of (ams_id, tray_id, color_hex, tray_type) tuples.""" + units: dict[int, list] = {} + for ams_id, tray_id, color, tray_type in trays: + units.setdefault(ams_id, []).append({"id": tray_id, "tray_color": color, "tray_type": tray_type}) + return [{"id": aid, "tray": t} for aid, t in units.items()] + + def _usage(self, slots): + """Build filament_usage from list of (slot_id, color_hex) tuples.""" + return [{"slot_id": sid, "used_g": 10.0, "type": "PLA", "color": color} for sid, color in slots] + + def test_none_inputs(self): + assert _match_slots_by_color(None, None) is None + assert _match_slots_by_color([], None) is None + assert _match_slots_by_color(None, {"ams": []}) is None + + def test_empty_ams(self): + usage = self._usage([(1, "#FF0000")]) + assert _match_slots_by_color(usage, {"ams": []}) is None + + def test_single_slot_single_tray(self): + """One 3MF slot matches one AMS tray by color.""" + ams = self._ams([(0, 0, "FF0000FF", "PLA")]) + usage = self._usage([(1, "#FF0000")]) + assert _match_slots_by_color(usage, {"ams": ams}) == [0] + + def test_a1_mini_three_colors(self): + """A1 Mini: 3 slots match 3 distinct AMS trays.""" + ams = self._ams( + [ + (0, 0, "FF0000FF", "PLA"), # Red + (0, 1, "00FF00FF", "PLA"), # Green + (0, 2, "0000FFFF", "PLA"), # Blue + ] + ) + usage = self._usage([(1, "#FF0000"), (2, "#00FF00"), (3, "#0000FF")]) + assert _match_slots_by_color(usage, {"ams": ams}) == [0, 1, 2] + + def test_dual_ams_p2s_like(self): + """P2S with dual AMS: slots from second AMS unit.""" + ams = self._ams( + [ + (0, 0, "AAAAAAFF", "PLA"), + (0, 1, "BBBBBBFF", "PLA"), + (1, 0, "CC0000FF", "PETG"), # global_id=4 + (1, 1, "00CC00FF", "PETG"), # global_id=5 + ] + ) + usage = self._usage([(1, "#CC0000"), (2, "#00CC00")]) + assert _match_slots_by_color(usage, {"ams": ams}) == [4, 5] + + def test_ams_ht_global_id(self): + """AMS-HT (ams_id >= 128) uses raw ams_id as global tray ID.""" + ams = self._ams( + [ + (0, 0, "FF0000FF", "PLA"), + (128, 0, "0000FFFF", "PLA"), # AMS-HT → global_id=128 + ] + ) + usage = self._usage([(1, "#FF0000"), (2, "#0000FF")]) + assert _match_slots_by_color(usage, {"ams": ams}) == [0, 128] + + def test_ambiguous_same_color_returns_none(self): + """Two trays with the same color → ambiguous → None.""" + ams = self._ams( + [ + (0, 0, "FF0000FF", "PLA"), + (0, 1, "FF0000FF", "PLA"), # Same red + ] + ) + usage = self._usage([(1, "#FF0000")]) + assert _match_slots_by_color(usage, {"ams": ams}) is None + + def test_no_matching_color_returns_none(self): + """3MF slot color not found in any AMS tray → None.""" + ams = self._ams([(0, 0, "00FF00FF", "PLA")]) + usage = self._usage([(1, "#FF0000")]) # Red, but AMS has green + assert _match_slots_by_color(usage, {"ams": ams}) is None + + def test_color_normalization_strips_alpha(self): + """AMS colors (RRGGBBAA) and 3MF colors (#RRGGBB) match after normalization.""" + ams = self._ams([(0, 0, "AABBCC80", "PLA")]) # 8-char with alpha + usage = self._usage([(1, "#AABBCC")]) # 6-char with # + assert _match_slots_by_color(usage, {"ams": ams}) == [0] + + def test_case_insensitive(self): + """Color matching is case-insensitive.""" + ams = self._ams([(0, 0, "aaBBccFF", "PLA")]) + usage = self._usage([(1, "#AAbbCC")]) + assert _match_slots_by_color(usage, {"ams": ams}) == [0] + + def test_empty_tray_color_skipped(self): + """Trays with empty color are skipped (not matched).""" + ams = self._ams( + [ + (0, 0, "", "PLA"), + (0, 1, "FF0000FF", "PLA"), + ] + ) + usage = self._usage([(1, "#FF0000")]) + assert _match_slots_by_color(usage, {"ams": ams}) == [1] + + def test_empty_tray_type_skipped(self): + """Trays with empty tray_type are skipped (unloaded slot).""" + ams = self._ams( + [ + (0, 0, "FF0000FF", ""), # Empty slot + (0, 1, "FF0000FF", "PLA"), # Loaded slot + ] + ) + usage = self._usage([(1, "#FF0000")]) + assert _match_slots_by_color(usage, {"ams": ams}) == [1] + + def test_short_slot_color_returns_none(self): + """3MF slot with color < 6 chars → can't match → None.""" + ams = self._ams([(0, 0, "FF0000FF", "PLA")]) + usage = [{"slot_id": 1, "used_g": 10.0, "type": "PLA", "color": "#FFF"}] + assert _match_slots_by_color(usage, {"ams": ams}) is None + + def test_slot_id_zero_skipped(self): + """Slots with slot_id=0 are skipped.""" + ams = self._ams([(0, 0, "FF0000FF", "PLA")]) + usage = [{"slot_id": 0, "used_g": 10.0, "type": "PLA", "color": "#FF0000"}] + assert _match_slots_by_color(usage, {"ams": ams}) is None + + def test_ams_data_as_list(self): + """Handles ams_raw as a plain list (some printer models).""" + ams_list = [{"id": 0, "tray": [{"id": 0, "tray_color": "FF0000FF", "tray_type": "PLA"}]}] + usage = self._usage([(1, "#FF0000")]) + assert _match_slots_by_color(usage, ams_list) == [0] + + def test_same_color_two_trays_disambiguated_by_usage(self): + """Two trays same color, two slots same color → unique assignment via used_trays tracking.""" + ams = self._ams( + [ + (0, 0, "FF0000FF", "PLA"), + (0, 1, "FF0000FF", "PLA"), + ] + ) + # Two slots both wanting red — first gets tray 0, second gets tray 1? No. + # When first slot takes the only available, second has 1 left → should work + usage = self._usage([(1, "#FF0000"), (2, "#FF0000")]) + # First slot: candidates=[0,1], available=[0,1], len!=1 → None + assert _match_slots_by_color(usage, {"ams": ams}) is None + + def test_dict_wrapper_with_ams_key(self): + """Standard dict format with 'ams' key.""" + ams_data = {"ams": [{"id": 0, "tray": [{"id": 0, "tray_color": "00FF00FF", "tray_type": "PLA"}]}]} + usage = self._usage([(1, "#00FF00")]) + assert _match_slots_by_color(usage, ams_data) == [0] + + class TestMqttMappingIntegration: """Integration tests: MQTT mapping field used in _track_from_3mf.""" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e54b1060e..668b206f4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3317,32 +3317,6 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -3723,11 +3697,13 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", + "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", "dev": true, - "license": "MIT" + "engines": { + "node": "20 || >=22" + } }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", @@ -3740,14 +3716,15 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", "dev": true, - "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, "node_modules/browserslist": { @@ -4003,13 +3980,6 @@ "node": ">= 0.8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -5033,32 +5003,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", @@ -6138,16 +6082,18 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz", + "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "*" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { @@ -7481,32 +7427,6 @@ "node": ">=18" } }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/three": { "version": "0.181.2", "resolved": "https://registry.npmjs.org/three/-/three-0.181.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 688ea421a..ae68b308c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,6 +41,9 @@ "recharts": "^3.5.1", "three": "^0.181.2" }, + "overrides": { + "minimatch": "^10.2.1" + }, "devDependencies": { "@eslint/js": "^9.39.1", "@tailwindcss/postcss": "^4.1.17", diff --git a/frontend/src/__tests__/utils/slicer.test.ts b/frontend/src/__tests__/utils/slicer.test.ts index 66eb71764..94016e366 100644 --- a/frontend/src/__tests__/utils/slicer.test.ts +++ b/frontend/src/__tests__/utils/slicer.test.ts @@ -50,8 +50,8 @@ describe('slicer utility', () => { openInSlicer('http://localhost:8000/file.3mf', 'bambu_studio'); expect(appendSpy).toHaveBeenCalled(); - expect(createdLink.href).toContain('bambustudio://'); - expect(createdLink.href).toContain(encodeURIComponent('http://localhost:8000/file.3mf')); + expect(createdLink.href).toContain('bambustudio://open?file='); + expect(createdLink.href).toContain('http://localhost:8000/file.3mf'); expect(clickSpy).toHaveBeenCalled(); expect(removeSpy).toHaveBeenCalled(); }); @@ -63,11 +63,11 @@ describe('slicer utility', () => { expect(createdLink.href).toContain('bambustudioopen://'); }); - it('uses bambustudioopen:// protocol on Linux for bambu_studio', () => { + it('uses bambustudio://open?file= on Linux for bambu_studio', () => { vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (X11; Linux x86_64)'); openInSlicer('http://localhost:8000/file.3mf', 'bambu_studio'); - expect(createdLink.href).toContain('bambustudioopen://'); + expect(createdLink.href).toContain('bambustudio://open?file='); }); it('uses orcaslicer:// protocol for orcaslicer on all platforms', () => { diff --git a/install/docker-install.sh b/install/docker-install.sh index 9e52ae536..11131e1c2 100755 --- a/install/docker-install.sh +++ b/install/docker-install.sh @@ -14,6 +14,7 @@ # --tz TIMEZONE Timezone (default: system timezone or UTC) # --build Build from source instead of using pre-built image # --yes, -y Non-interactive mode, accept defaults +# --redirect-990 Add iptables redirect from 990 -> 9990 (Linux only) # --help, -h Show this help message # @@ -42,6 +43,7 @@ BUILD_FROM_SOURCE="false" NON_INTERACTIVE="false" OS_TYPE="" DOCKER_CMD="" +REDIRECT_990="false" # ----------------------------------------------------------------------------- # Helper Functions @@ -140,6 +142,7 @@ show_help() { echo " --tz TIMEZONE Timezone (default: system timezone or UTC)" echo " --build Build from source instead of using pre-built image" echo " --yes, -y Non-interactive mode, accept defaults" + echo " --redirect-990 Add iptables redirect from 990 -> 9990 (Linux only)" echo " --help, -h Show this help message" echo "" echo "Examples:" @@ -380,6 +383,10 @@ parse_args() { NON_INTERACTIVE="true" shift ;; + --redirect-990) + REDIRECT_990="true" + shift + ;; --help|-h) show_help ;; @@ -391,6 +398,53 @@ parse_args() { done } +check_sudo() { + if ! command -v sudo &>/dev/null; then + log_error "sudo is required for iptables redirect but is not installed. Skipping iptables redirect." + return 1 + fi + if ! command -v iptables &>/dev/null; then + log_error "iptables is required for iptables redirect but is not installed. Skipping iptables redirect." + return 1 + fi + return 0 +} + +configure_iptables_redirect() { + if [[ "$OS_TYPE" != "linux" ]]; then + log_warn "iptables redirect only supported on Linux. Skipping." + return + fi + + if [[ "$REDIRECT_990" != "true" ]]; then + return + fi + + if ! check_sudo; then + return + fi + + log_info "Configuring iptables redirect: 990 -> 9990" + + # Check if rule already exists + if sudo iptables -t nat -C PREROUTING -p tcp --dport 990 -j REDIRECT --to-port 9990 2>/dev/null; then + log_warn "PREROUTING rule already exists. Skipping." + else + sudo iptables -t nat -A PREROUTING -p tcp --dport 990 -j REDIRECT --to-port 9990 + log_success "Added PREROUTING redirect rule" + fi + + if sudo iptables -t nat -C OUTPUT -o lo -p tcp --dport 990 -j REDIRECT --to-port 9990 2>/dev/null; then + log_warn "OUTPUT rule already exists. Skipping." + else + sudo iptables -t nat -A OUTPUT -o lo -p tcp --dport 990 -j REDIRECT --to-port 9990 + log_success "Added OUTPUT redirect rule" + fi + + log_warn "Note: iptables rules are NOT persistent after reboot." + log_warn "To persist them, install iptables-persistent or use a firewall manager." +} + gather_config() { echo "" echo -e "${BOLD}Installation Configuration${NC}" @@ -412,6 +466,20 @@ gather_config() { prompt "Bind address" "$DEFAULT_BIND_ADDRESS" BIND_ADDRESS fi + # Redirect port 990 -> 9990 (Linux only) + if [[ "$OS_TYPE" == "linux" ]]; then + if [[ "$NON_INTERACTIVE" == "true" ]]; then + # In non-interactive mode, only set REDIRECT_990 if explicitly passed + : # Do nothing, REDIRECT_990 is set only if --redirect-990 is passed + elif [[ "$REDIRECT_990" != "true" ]]; then + echo "" + echo "Optional network configuration:" + if prompt_yes_no "Add iptables redirect (990 -> 9990)?" "n"; then + REDIRECT_990="true" + fi + fi + fi + # Timezone detect_timezone prompt "Timezone" "$TIMEZONE" TIMEZONE @@ -432,6 +500,7 @@ gather_config() { echo -e " Bind address: ${GREEN}$BIND_ADDRESS${NC}" echo -e " Timezone: ${GREEN}$TIMEZONE${NC}" echo -e " Build source: ${GREEN}$BUILD_FROM_SOURCE${NC}" + echo -e " Redirect 990: ${GREEN}$REDIRECT_990${NC}" echo "" if ! prompt_yes_no "Proceed with installation?" "y"; then @@ -493,6 +562,7 @@ main() { create_env_file customize_compose start_container + configure_iptables_redirect # Done! echo "" @@ -531,6 +601,13 @@ main() { echo -e " ${BOLD}Documentation:${NC} ${CYAN}https://wiki.bambuddy.cool${NC}" echo "" + # Warn about iptables persistence + if [[ "$REDIRECT_990" == "true" ]] && [[ "$OS_TYPE" == "linux" ]]; then + echo -e " ${YELLOW}Note:${NC} iptables redirect rules do NOT survive reboot." + echo -e " Install 'iptables-persistent' if persistence is required." + echo "" + fi + if [[ "$OS_TYPE" == "macos" ]]; then echo -e " ${YELLOW}Note:${NC} Printer discovery may not work with Docker Desktop." echo -e " Add printers manually using their IP address." diff --git a/static/assets/index-BTn6l3An.js b/static/assets/index-DqAJp9_u.js similarity index 99% rename from static/assets/index-BTn6l3An.js rename to static/assets/index-DqAJp9_u.js index 96bac7f6d..cd981484b 100644 --- a/static/assets/index-BTn6l3An.js +++ b/static/assets/index-DqAJp9_u.js @@ -8500,7 +8500,7 @@ void main() { border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent ${c==="top"?"top-full border-t-[6px] border-t-bambu-dark-tertiary":"bottom-full border-b-[6px] border-b-bambu-dark-tertiary"} - `})]})]})}function BHe({slots:t,filamentInfo:e}){const{t:n}=Vt(),r=t.filter(l=>l.id>=2),i=6,s=r.length>0?Math.min(...r.map(l=>l.id)):16,o=Array.from({length:i},(l,c)=>r.find(u=>u.id===s+c)??{id:-(c+1),nozzle_type:"",nozzle_diameter:"",wear:null,stat:null,max_temp:0,serial_number:"",filament_color:"",filament_id:"",filament_type:""});return a.jsxs("div",{className:"text-center px-2.5 py-1.5 bg-bambu-dark rounded-lg flex-[2_1_190px] flex flex-col justify-center",children:[a.jsx("p",{className:"text-[9px] text-bambu-gray mb-1",children:n("printers.nozzleRack")}),a.jsx("div",{className:"flex gap-[3px] justify-center",children:o.map((l,c)=>{const u=!l.nozzle_diameter&&!l.nozzle_type,d=u?null:N4(l.filament_color),h=d?zHe(l.filament_color):!1;return a.jsx(pne,{slot:l,index:c,filamentName:l.filament_id?e?.[l.filament_id]?.name:void 0,children:a.jsx("div",{className:`w-7 h-7 rounded flex items-center justify-center cursor-default transition-colors border-b-2 ${u?"bg-bambu-dark-tertiary/20 border-bambu-dark-tertiary/20":"bg-bambu-dark-tertiary/40 border-bambu-dark-tertiary/40"}`,style:d?{backgroundColor:d}:void 0,children:a.jsx("span",{className:`text-[10px] font-semibold ${u?"text-bambu-gray/30":h?"text-black/80":"text-white"}`,style:d&&!h?{textShadow:"0 1px 3px rgba(0,0,0,0.9)"}:void 0,children:u?"—":l.nozzle_diameter||"?"})})},l.id>=0?l.id:`empty-${c}`)})})]})}function c9({className:t}){return a.jsx("svg",{className:t,viewBox:"0 0 36 54",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:a.jsx("path",{d:"M17.8131 0.00538C18.4463 -0.15091 20.3648 3.14642 20.8264 3.84781C25.4187 10.816 35.3089 26.9368 35.9383 34.8694C37.4182 53.5822 11.882 61.3357 2.53721 45.3789C-1.73471 38.0791 0.016 32.2049 3.178 25.0232C6.99221 16.3662 12.6411 7.90372 17.8131 0.00538ZM18.3738 7.24807L17.5881 7.48441C14.4452 12.9431 10.917 18.2341 8.19369 23.9368C4.6808 31.29 1.18317 38.5479 7.69403 45.5657C17.3058 55.9228 34.9847 46.8808 31.4604 32.8681C29.2558 24.0969 22.4207 15.2913 18.3776 7.24807H18.3738Z",fill:"#C3C2C1"})})}function HHe({className:t}){return a.jsxs("svg",{className:t,viewBox:"0 0 35 53",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[a.jsx("path",{d:"M17.3165 0.0038C17.932 -0.14959 19.7971 3.08645 20.2458 3.77481C24.7103 10.6135 34.3251 26.4346 34.937 34.2198C36.3757 52.5848 11.5505 60.1942 2.46584 44.534C-1.68714 37.3735 0.0148 31.6085 3.08879 24.5603C6.79681 16.0605 12.2884 7.75907 17.3165 0.0038ZM17.8615 7.11561L17.0977 7.34755C14.0423 12.7048 10.6124 17.8974 7.96483 23.4941C4.54975 30.7107 1.14949 37.8337 7.47908 44.721C16.8233 54.8856 34.01 46.0117 30.5838 32.2595C28.4405 23.6512 21.7957 15.0093 17.8652 7.11561H17.8615Z",fill:"#C3C2C1"}),a.jsx("path",{d:"M5.03547 30.112C9.64453 30.4936 11.632 35.7985 16.4154 35.791C19.6339 35.7873 20.2161 33.2283 22.3853 31.6197C31.6776 24.7286 33.5835 37.4894 27.9881 44.4254C18.1878 56.5653 -1.16063 44.6013 5.03917 30.1158L5.03547 30.112Z",fill:"#1F8FEB"})]})}function $He({className:t}){return a.jsxs("svg",{className:t,viewBox:"0 0 36 54",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[a.jsx("path",{d:"M17.9625 4.48059L4.77216 26.3154L2.08228 40.2175L10.0224 50.8414H23.1594L33.3246 42.1693V30.2455L17.9625 4.48059Z",fill:"#1F8FEB"}),a.jsx("path",{d:"M17.7948 0.00538C18.4273 -0.15091 20.3438 3.14642 20.8048 3.84781C25.3921 10.816 35.2715 26.9368 35.9001 34.8694C37.3784 53.5822 11.8702 61.3357 2.53562 45.3789C-1.73163 38.0829 0.0134 32.2087 3.1757 25.027C6.98574 16.3662 12.6284 7.90372 17.7948 0.00538ZM18.3549 7.24807L17.57 7.48441C14.4306 12.9431 10.9063 18.2341 8.1859 23.9368C4.67686 31.29 1.18305 38.5479 7.68679 45.5657C17.2881 55.9228 34.9476 46.8808 31.4271 32.8681C29.2249 24.0969 22.3974 15.2913 18.3587 7.24807H18.3549Z",fill:"#C3C2C1"})]})}function VHe({className:t}){return a.jsxs("svg",{className:t,viewBox:"0 0 12 20",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[a.jsx("path",{d:"M6 0.5C4.6 0.5 3.5 1.6 3.5 3V12.1C2.6 12.8 2 13.9 2 15C2 17.2 3.8 19 6 19C8.2 19 10 17.2 10 15C10 13.9 9.4 12.8 8.5 12.1V3C8.5 1.6 7.4 0.5 6 0.5Z",stroke:"#C3C2C1",strokeWidth:"1",fill:"none"}),a.jsx("circle",{cx:"6",cy:"15",r:"2.5",stroke:"#C3C2C1",strokeWidth:"1",fill:"none"})]})}function qHe({className:t}){return a.jsxs("svg",{className:t,viewBox:"0 0 12 20",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[a.jsx("rect",{x:"4.5",y:"8",width:"3",height:"4.5",fill:"#d4a017",rx:"0.5"}),a.jsx("circle",{cx:"6",cy:"15",r:"2",fill:"#d4a017"}),a.jsx("path",{d:"M6 0.5C4.6 0.5 3.5 1.6 3.5 3V12.1C2.6 12.8 2 13.9 2 15C2 17.2 3.8 19 6 19C8.2 19 10 17.2 10 15C10 13.9 9.4 12.8 8.5 12.1V3C8.5 1.6 7.4 0.5 6 0.5Z",stroke:"#C3C2C1",strokeWidth:"1",fill:"none"})]})}function GHe({className:t}){return a.jsxs("svg",{className:t,viewBox:"0 0 12 20",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[a.jsx("rect",{x:"4.5",y:"3",width:"3",height:"9.5",fill:"#c62828",rx:"0.5"}),a.jsx("circle",{cx:"6",cy:"15",r:"2",fill:"#c62828"}),a.jsx("path",{d:"M6 0.5C4.6 0.5 3.5 1.6 3.5 3V12.1C2.6 12.8 2 13.9 2 15C2 17.2 3.8 19 6 19C8.2 19 10 17.2 10 15C10 13.9 9.4 12.8 8.5 12.1V3C8.5 1.6 7.4 0.5 6 0.5Z",stroke:"#C3C2C1",strokeWidth:"1",fill:"none"})]})}function sj({className:t,color:e,isHeating:n}){const i={"text-orange-400":"#fb923c","text-blue-400":"#60a5fa","text-green-400":"#4ade80"}[e]||"#888",s=n?{filter:`drop-shadow(0 0 4px ${i}) drop-shadow(0 0 8px ${i})`}:{};return n?a.jsxs("svg",{className:t,style:s,viewBox:"0 0 12 20",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[a.jsx("rect",{x:"4.5",y:"3",width:"3",height:"9.5",fill:i,rx:"0.5"}),a.jsx("circle",{cx:"6",cy:"15",r:"2",fill:i}),a.jsx("path",{d:"M6 0.5C4.6 0.5 3.5 1.6 3.5 3V12.1C2.6 12.8 2 13.9 2 15C2 17.2 3.8 19 6 19C8.2 19 10 17.2 10 15C10 13.9 9.4 12.8 8.5 12.1V3C8.5 1.6 7.4 0.5 6 0.5Z",stroke:i,strokeWidth:"1",fill:"none"})]}):a.jsxs("svg",{className:t,viewBox:"0 0 12 20",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[a.jsx("path",{d:"M6 0.5C4.6 0.5 3.5 1.6 3.5 3V12.1C2.6 12.8 2 13.9 2 15C2 17.2 3.8 19 6 19C8.2 19 10 17.2 10 15C10 13.9 9.4 12.8 8.5 12.1V3C8.5 1.6 7.4 0.5 6 0.5Z",stroke:i,strokeWidth:"1",fill:"none"}),a.jsx("circle",{cx:"6",cy:"15",r:"2.5",stroke:i,strokeWidth:"1",fill:"none"})]})}function u9({humidity:t,goodThreshold:e=40,fairThreshold:n=60,onClick:r,compact:i}){const s=typeof t=="string"?parseInt(t,10):t,o=typeof e=="number"?e:40,l=typeof n=="number"?n:60;let c,u;isNaN(s)?(c="#C3C2C1",u="Unknown"):s<=o?(c="#22a352",u="Good"):s<=l?(c="#d4a017",u="Fair"):(c="#c62828",u="Bad");let d;return isNaN(s)||s<=o?d=c9:s<=l?d=HHe:d=$He,a.jsxs("button",{type:"button",onClick:r,className:`flex items-center gap-1 ${r?"cursor-pointer hover:opacity-80 transition-opacity":""}`,title:`Humidity: ${s}% - ${u}${r?" (click for history)":""}`,children:[a.jsx(d,{className:i?"w-2.5 h-3":"w-3 h-4"}),a.jsxs("span",{className:`font-medium tabular-nums ${i?"text-[10px]":"text-xs"}`,style:{color:c},children:[s,"%"]})]})}function d9({temp:t,goodThreshold:e=28,fairThreshold:n=35,onClick:r,compact:i}){const s=typeof e=="number"?e:28,o=typeof n=="number"?n:35;let l,c,u;return t<=s?(l="#22a352",c="Good",u=VHe):t<=o?(l="#d4a017",c="Fair",u=qHe):(l="#c62828",c="Bad",u=GHe),a.jsxs("button",{type:"button",onClick:r,className:`flex items-center gap-1 ${r?"cursor-pointer hover:opacity-80 transition-opacity":""}`,title:`Temperature: ${t}°C - ${c}${r?" (click for history)":""}`,children:[a.jsx(u,{className:i?"w-2.5 h-3":"w-3 h-4"}),a.jsxs("span",{className:`tabular-nums text-right ${i?"text-[10px] w-8":"w-12"}`,style:{color:l},children:[t,"°C"]})]})}function ih(t,e){const n=typeof t=="string"?parseInt(t,10):t,r=isNaN(n)?0:n,i=e===1,s=r>=128?r-128:r,o=String.fromCharCode(65+s);return i?`HT-${o}`:`AMS-${o}`}function oj(t){return t>50?"#00ae42":t>=15?"#f59e0b":"#ef4444"}function lj(t){return!t?.remaining_weight||!t?.filament_weight||t.filament_weight<=0?null:Math.min(100,Math.round(t.remaining_weight/t.filament_weight*100))}function WHe(t){if(!t)return"/img/printers/default.png";const e=t.toLowerCase().replace(/\s+/g,"");return e.includes("x1e")?"/img/printers/x1e.png":e.includes("x1c")||e.includes("x1carbon")||e.includes("x1")?"/img/printers/x1c.png":e.includes("h2d")?"/img/printers/h2d.png":e.includes("h2c")?"/img/printers/h2c.png":e.includes("h2s")?"/img/printers/h2d.png":e.includes("p2s")||e.includes("p1s")?"/img/printers/p1s.png":e.includes("p1p")?"/img/printers/p1p.png":e.includes("a1mini")?"/img/printers/a1mini.png":e.includes("a1")?"/img/printers/a1.png":"/img/printers/default.png"}function KHe(t){return t>=-50?{labelKey:"printers.wifiSignal.excellent",color:"text-bambu-green",bars:4}:t>=-60?{labelKey:"printers.wifiSignal.good",color:"text-bambu-green",bars:3}:t>=-70?{labelKey:"printers.wifiSignal.fair",color:"text-yellow-400",bars:2}:t>=-80?{labelKey:"printers.wifiSignal.weak",color:"text-orange-400",bars:1}:{labelKey:"printers.wifiSignal.veryWeak",color:"text-red-400",bars:1}}function cj(t){return t?!!(t.tray_uuid&&t.tray_uuid!=="00000000000000000000000000000000"||t.tag_uid&&t.tag_uid!=="0000000000000000"):!1}function XHe({url:t,printName:e}){const{t:n}=Vt(),[r,i]=w.useState(!1),[s,o]=w.useState(!1),[l,c]=w.useState(!1),u=w.useMemo(()=>{if(!t)return null;const d=t.includes("?")?"&":"?";return`${t}${d}v=${encodeURIComponent(e||Date.now().toString())}`},[t,e]);return w.useEffect(()=>{i(!1),o(!1)},[u]),a.jsxs(a.Fragment,{children:[a.jsx("div",{className:`w-20 h-20 flex-shrink-0 rounded-lg overflow-hidden bg-bambu-dark-tertiary flex items-center justify-center ${u&&r?"cursor-pointer":""}`,onClick:()=>u&&r&&c(!0),children:u&&!s?a.jsxs(a.Fragment,{children:[a.jsx("img",{src:u,alt:n("printers.printPreview"),className:`w-full h-full object-cover ${r?"block":"hidden"}`,onLoad:()=>i(!0),onError:()=>o(!0)}),!r&&a.jsx(Ji,{className:"w-8 h-8 text-bambu-gray"})]}):a.jsx(Ji,{className:"w-8 h-8 text-bambu-gray"})}),l&&u&&a.jsx("div",{className:"fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-8",onClick:()=>c(!1),children:a.jsxs("div",{className:"relative max-w-2xl max-h-full",children:[a.jsx("img",{src:u,alt:n("printers.printPreview"),className:"max-w-full max-h-[80vh] rounded-lg shadow-2xl"}),e&&a.jsx("p",{className:"text-white text-center mt-4 text-lg",children:e})]})})]})}function YHe({printers:t}){const{t:e}=Vt(),n=tn(),[r,i]=w.useState(0);w.useEffect(()=>{let l=!1;const c=n.getQueryCache().subscribe(()=>{l||(l=!0,requestAnimationFrame(()=>{i(u=>u+1),l=!1}))});return()=>c()},[n]);const{counts:s,nextFinish:o}=w.useMemo(()=>{let l=0,c=0,u=0,d=0,h=null,f=null,p=0;return t?.forEach(x=>{const y=n.getQueryData(["printerStatus",x.id]);y===void 0?d++:y.connected?y.state==="RUNNING"?(l++,y.remaining_time!=null&&y.remaining_time>0&&(f===null||y.remaining_time0?"bg-bambu-green":"bg-gray-500"}`}),a.jsxs("span",{className:"text-bambu-gray",children:[a.jsx("span",{className:"text-white font-medium",children:s.idle})," ",e("printers.status.available").toLowerCase()]})]}),s.printing>0&&a.jsxs("div",{className:"flex items-center gap-1.5",children:[a.jsx("div",{className:"w-2 h-2 rounded-full bg-bambu-green animate-pulse"}),a.jsxs("span",{className:"text-bambu-gray",children:[a.jsx("span",{className:"text-white font-medium",children:s.printing})," ",e("printers.status.printing").toLowerCase()]})]}),s.offline>0&&a.jsxs("div",{className:"flex items-center gap-1.5",children:[a.jsx("div",{className:"w-2 h-2 rounded-full bg-gray-400"}),a.jsxs("span",{className:"text-bambu-gray",children:[a.jsx("span",{className:"text-white font-medium",children:s.offline})," ",e("printers.status.offline").toLowerCase()]})]}),o&&a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"w-px h-4 bg-bambu-dark-tertiary"}),a.jsxs("div",{className:"flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-2",children:[a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsxs("span",{className:"text-bambu-green font-medium",children:[e("printers.nextAvailable"),":"]}),a.jsx("span",{className:"text-white font-medium",children:o.name})]}),a.jsxs("div",{className:"flex items-center gap-2 w-full sm:w-auto",children:[a.jsx("div",{className:"w-full sm:w-16 bg-bambu-dark-tertiary rounded-full h-1.5",children:a.jsx("div",{className:"bg-bambu-green h-1.5 rounded-full transition-all",style:{width:`${o.progress}%`}})}),a.jsxs("span",{className:"text-white font-medium",children:[Math.round(o.progress),"%"]}),a.jsxs("span",{className:"text-bambu-gray",children:["(",Zs(o.remainingMin*60),")"]})]})]})]})]}):null}function h9(t,e){if(e)return e;switch(t){case"RUNNING":return"Printing";case"PAUSE":return"Paused";case"FINISH":return"Finished";case"FAILED":return"Failed";case"IDLE":return"Idle";default:return t?t.charAt(0)+t.slice(1).toLowerCase():"Idle"}}function PR(t){return t?{O1D:"H2D",O1E:"H2D Pro",O2D:"H2D Pro",O1C:"H2C",O1S:"H2S","BL-P001":"X1C","BL-P002":"X1","BL-P003":"X1E",C11:"P1S",C12:"P1P",C13:"P2S",N2S:"A1",N1:"A1 Mini",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"}[t]||t:""}function m9({printer:t,hideIfDisconnected:e,maintenanceInfo:n,viewMode:r="expanded",cardSize:i=2,amsThresholds:s,spoolmanEnabled:o=!1,hasUnlinkedSpools:l=!1,linkedSpools:c,spoolmanUrl:u,onGetAssignment:d,onUnassignSpool:h,timeFormat:f="system",cameraViewMode:p="window",onOpenEmbeddedCamera:x,checkPrinterFirmware:y=!0}){const{t:b}=Vt(),v=tn(),_=$c(),{showToast:N}=hn(),{hasPermission:T}=Ir(),[k,M]=w.useState(!1),[C,j]=w.useState(!1),[P,E]=w.useState(!0),[U,O]=w.useState(!1),[B,R]=w.useState(!1),[q,G]=w.useState(!1),[z,K]=w.useState(!1),[Y,oe]=w.useState(!1),[$,X]=w.useState(!1),[ee,J]=w.useState(!1),[Se,pe]=w.useState(!1),[he,je]=w.useState(!1),[de,ce]=w.useState(!1),[_e,ie]=w.useState(null),[Fe,V]=w.useState(null),[W,ue]=w.useState(null),[Q,se]=w.useState(null),[Z,Ne]=w.useState(!1),[ye,fe]=w.useState(null),[L,D]=w.useState(!1),[H,me]=w.useState(!1),[xe,be]=w.useState(null),[Ue,Oe]=w.useState(!1),[Qe,rt]=w.useState(!1),{data:we}=Ze({queryKey:["printerStatus",t.id],queryFn:()=>ne.getPrinterStatus(t.id),refetchInterval:3e4}),{data:ve}=Ze({queryKey:["firmwareUpdate",t.id],queryFn:()=>t1.checkPrinterUpdate(t.id),staleTime:300*1e3,refetchInterval:300*1e3,enabled:y&&T("firmware:read")}),le=w.useMemo(()=>{const bt=new Set;if(we?.ams)for(const Pt of we.ams)for(const Hn of Pt.tray||[])Hn.tray_info_idx&&bt.add(Hn.tray_info_idx);for(const Pt of we?.vt_tray??[])Pt.tray_info_idx&&bt.add(Pt.tray_info_idx);if(we?.nozzle_rack)for(const Pt of we.nozzle_rack)Pt.filament_id&&bt.add(Pt.filament_id);return Array.from(bt)},[we?.ams,we?.vt_tray,we?.nozzle_rack]),{data:Le}=Ze({queryKey:["filamentInfo",le],queryFn:()=>ne.getFilamentInfo(le),enabled:le.length>0,staleTime:300*1e3}),{data:Me}=Ze({queryKey:["slotPresets",t.id],queryFn:()=>ne.getSlotPresets(t.id),staleTime:120*1e3}),[$e,Ae]=w.useState(null);w.useEffect(()=>{we?.wifi_signal!=null&&Ae(we.wifi_signal)},[we?.wifi_signal]);const Je=we?.wifi_signal??$e,Ee=w.useRef(void 0);w.useEffect(()=>{we?.connected!==void 0&&(Ee.current=we.connected)},[we?.connected]);const Xe=we?.connected??Ee.current,De=w.useRef({});w.useEffect(()=>{we?.ams_extruder_map&&Object.keys(we.ams_extruder_map).length>0&&(De.current=we.ams_extruder_map)},[we?.ams_extruder_map]);const Re=we?.ams_extruder_map&&Object.keys(we.ams_extruder_map).length>0?we.ams_extruder_map:De.current,Ge=w.useRef([]);w.useEffect(()=>{we?.ams&&we.ams.length>0&&(Ge.current=we.ams)},[we?.ams]);const pt=we?.ams&&we.ams.length>0?we.ams:Ge.current,Tt=w.useRef(void 0),Rt=we?.tray_now;Rt!==void 0&&Rt!==255?Tt.current=Rt:Rt===255&&(Tt.current=void 0);const Wt=Rt!==void 0&&Rt!==255?Rt:Tt.current,{data:Mt}=Ze({queryKey:["smartPlugByPrinter",t.id],queryFn:()=>ne.getSmartPlugByPrinter(t.id)}),{data:Ie}=Ze({queryKey:["scriptPlugsByPrinter",t.id],queryFn:()=>ne.getScriptPlugsByPrinter(t.id)}),{data:xt}=Ze({queryKey:["smartPlugStatus",Mt?.id],queryFn:()=>Mt?ne.getSmartPlugStatus(Mt.id):null,enabled:!!Mt,refetchInterval:1e4}),{data:wt}=Ze({queryKey:["queue",t.id,"pending"],queryFn:()=>ne.getQueue(t.id,"pending")}),Bt=wt?.length||0,{data:Pn}=Ze({queryKey:["queue",t.id,"printing"],queryFn:()=>ne.getQueue(t.id,"printing"),enabled:we?.state==="RUNNING"}),{data:pr}=Ze({queryKey:["currentPrintUser",t.id],queryFn:()=>ne.getCurrentPrintUser(t.id),enabled:we?.state==="RUNNING"}),Zn=Pn?.[0]?.created_by_username||pr?.username,{data:Jn}=Ze({queryKey:["archives",t.id,"last"],queryFn:()=>ne.getArchives(t.id,1,0),enabled:we?.connected&&we?.state!=="RUNNING"}),jn=Jn?.[0],cr=e&&Xe===!1,Dn=ot({mutationFn:bt=>ne.deletePrinter(t.id,bt.deleteArchives),onSuccess:()=>{v.invalidateQueries({queryKey:["printers"]}),v.invalidateQueries({queryKey:["archives"]}),v.invalidateQueries({queryKey:["maintenanceOverview"]})},onError:bt=>N(bt.message||b("printers.toast.failedToDelete"),"error")}),ai=ot({mutationFn:()=>ne.connectPrinter(t.id),onSuccess:()=>{v.invalidateQueries({queryKey:["printerStatus",t.id]})}}),re=ot({mutationFn:bt=>Mt?ne.controlSmartPlug(Mt.id,bt):Promise.reject("No plug"),onSuccess:()=>{v.invalidateQueries({queryKey:["smartPlugStatus",Mt?.id]})}}),He=ot({mutationFn:bt=>Mt?ne.updateSmartPlug(Mt.id,{auto_off:bt}):Promise.reject("No plug"),onSuccess:()=>{v.invalidateQueries({queryKey:["smartPlugByPrinter",t.id]}),v.invalidateQueries({queryKey:["smart-plugs"]})}}),et=ot({mutationFn:bt=>ne.controlSmartPlug(bt,"on"),onSuccess:()=>{N(b("printers.toast.scriptTriggered"))},onError:bt=>N(bt.message||b("printers.toast.failedToRunScript"),"error")}),lt=ot({mutationFn:()=>ne.stopPrint(t.id),onSuccess:()=>{N(b("printers.toast.printStopped")),v.invalidateQueries({queryKey:["printerStatus",t.id]})},onError:bt=>N(bt.message||b("printers.toast.failedToStopPrint"),"error")}),dt=ot({mutationFn:()=>ne.pausePrint(t.id),onSuccess:()=>{N(b("printers.toast.printPaused")),v.invalidateQueries({queryKey:["printerStatus",t.id]})},onError:bt=>N(bt.message||b("printers.toast.failedToPausePrint"),"error")}),Lt=ot({mutationFn:()=>ne.resumePrint(t.id),onSuccess:()=>{N(b("printers.toast.printResumed")),v.invalidateQueries({queryKey:["printerStatus",t.id]})},onError:bt=>N(bt.message||b("printers.toast.failedToResumePrint"),"error")}),ge=ot({mutationFn:bt=>ne.setChamberLight(t.id,bt),onMutate:async bt=>{await v.cancelQueries({queryKey:["printerStatus",t.id]});const Pt=v.getQueryData(["printerStatus",t.id]);return v.setQueryData(["printerStatus",t.id],Hn=>({...Hn,chamber_light:bt})),{previousStatus:Pt}},onSuccess:(bt,Pt)=>{N(`Chamber light ${Pt?"on":"off"}`)},onError:(bt,Pt,Hn)=>{Hn?.previousStatus&&v.setQueryData(["printerStatus",t.id],Hn.previousStatus),N(bt.message||b("printers.toast.failedToControlChamberLight"),"error")}}),ze=ot({mutationFn:bt=>ne.updatePrinter(t.id,{plate_detection_enabled:bt}),onSuccess:()=>{v.invalidateQueries({queryKey:["printers"]}),N(ze.variables?b("printers.toast.plateCheckEnabled"):b("printers.toast.plateCheckDisabled"))},onError:bt=>N(bt.message||b("printers.toast.failedToUpdateSetting"),"error")}),qe=(we?.state==="RUNNING"||we?.state==="PAUSE"||we?.state==="PAUSED")&&(we?.printable_objects_count??0)>=2,{data:Ve}=Ze({queryKey:["printableObjects",t.id],queryFn:()=>ne.getPrintableObjects(t.id),enabled:de||qe,refetchInterval:de?5e3:qe?3e4:!1}),[Pe,st]=w.useState(null),tt=w.useRef(!1),at=w.useRef(null),At=w.useRef(!1),Kt=ot({mutationFn:({amsId:bt,slotId:Pt})=>ne.refreshAmsSlot(t.id,bt,Pt),onMutate:({amsId:bt,slotId:Pt})=>{at.current&&clearTimeout(at.current),tt.current=!1,At.current=!1,st({amsId:bt,slotId:Pt}),setTimeout(()=>{At.current=!0},2e3),at.current=setTimeout(()=>{st(null)},3e4)},onSuccess:bt=>{N(bt.message||b("printers.toast.rfidRereadInitiated"))},onError:bt=>{N(bt.message||b("printers.toast.failedToRereadRfid"),"error"),at.current&&clearTimeout(at.current),st(null)}}),[Ot,jt]=w.useState(null),[Ht,Zt]=w.useState(null),rn=async()=>{try{const bt=await ne.getPlateReferences(t.id);jt(bt)}catch{}},An=()=>{ze.mutate(!t.plate_detection_enabled)},pn=async()=>{D(!0),fe(null);const bt=we?.chamber_light===!1;rt(bt),bt&&(await ne.setChamberLight(t.id,!0),await new Promise(Pt=>setTimeout(Pt,2500)));try{const Pt=await ne.checkPlateEmpty(t.id,{includeDebugImage:!0});fe(Pt),rn()}catch(Pt){N(Pt instanceof Error?Pt.message:b("printers.toast.failedToCheckPlate"),"error"),bt&&(await ne.setChamberLight(t.id,!1),rt(!1))}finally{D(!1)}},Xt=w.useCallback(async()=>{fe(null),Qe&&(await ne.setChamberLight(t.id,!1),rt(!1))},[Qe,t.id]),on=async bt=>{me(!0);try{const Pt=await ne.calibratePlateDetection(t.id,{label:bt});if(Pt.success){N(Pt.message||b("printers.toast.calibrationSaved"),"success"),rn();const Hn=await ne.checkPlateEmpty(t.id,{includeDebugImage:!0});fe(Hn)}else N(Pt.message||b("printers.toast.calibrationFailed"),"error")}catch(Pt){N(Pt instanceof Error?Pt.message:b("printers.toast.calibrationFailed"),"error")}finally{me(!1)}},fn=async(bt,Pt)=>{try{await ne.updatePlateReferenceLabel(t.id,bt,Pt),Zt(null),rn()}catch(Hn){N(Hn instanceof Error?Hn.message:b("printers.toast.failedToUpdateLabel"),"error")}},Ar=async bt=>{try{await ne.deletePlateReference(t.id,bt),N(b("printers.toast.referenceDeleted"),"success"),rn();const Pt=await ne.checkPlateEmpty(t.id,{includeDebugImage:!0});fe(Pt)}catch(Pt){N(Pt instanceof Error?Pt.message:b("printers.toast.failedToDeleteReference"),"error")}},Qr=async()=>{if(xe){Oe(!0);try{await ne.updatePrinter(t.id,{plate_detection_roi:xe}),N(b("printers.toast.detectionAreaSaved"),"success"),be(null);const bt=await ne.checkPlateEmpty(t.id,{includeDebugImage:!0});fe(bt)}catch(bt){N(bt instanceof Error?bt.message:b("printers.toast.failedToSaveDetectionArea"),"error")}finally{Oe(!1)}}};w.useEffect(()=>{const bt=Pt=>{Pt.key==="Escape"&&ye&&Xt()};return window.addEventListener("keydown",bt),()=>window.removeEventListener("keydown",bt)},[ye,Xt]);const gn=w.useRef(null);w.useEffect(()=>{if(!Pe)return;const bt=we?.ams_status_main??0;return bt!==0&&(tt.current=!0,gn.current&&(clearTimeout(gn.current),gn.current=null)),tt.current&&bt===0&&(At.current?(at.current&&clearTimeout(at.current),st(null)):gn.current||(gn.current=setTimeout(()=>{at.current&&clearTimeout(at.current),st(null)},2e3))),()=>{gn.current&&clearTimeout(gn.current)}},[we?.ams_status_main,Pe]);const[Zr,Kn]=w.useState(null);if(cr)return null;const vi=()=>{switch(i){case 1:return"w-10 h-10";case 2:return"w-14 h-14";case 3:return"w-16 h-16";case 4:return"w-20 h-20";default:return"w-14 h-14"}},mi=()=>{switch(i){case 1:return"text-base truncate";case 2:return"text-lg";case 3:return"text-xl";case 4:return"text-2xl";default:return"text-lg"}},fi=()=>{switch(i){case 1:return"mb-2";case 2:return"mb-4";case 3:return"mb-5";case 4:return"mb-6";default:return"mb-4"}};return a.jsxs(vt,{className:"relative",children:[a.jsxs(Nt,{className:i>=3?"p-5":"",children:[a.jsxs("div",{className:fi(),children:[a.jsxs("div",{className:"flex items-start justify-between gap-2",children:[a.jsxs("div",{className:"flex items-center gap-3 min-w-0 flex-1",children:[a.jsx("img",{src:WHe(t.model),alt:t.model||b("common.printer"),className:`object-contain rounded-lg bg-bambu-dark flex-shrink-0 ${vi()}`}),a.jsxs("div",{className:"min-w-0 flex-1",children:[a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("h3",{className:`font-semibold text-white ${mi()}`,children:t.name}),r==="compact"&&a.jsx("div",{className:`w-2 h-2 rounded-full flex-shrink-0 ${we?.connected?"bg-status-ok":"bg-status-error"}`,title:we?.connected?b("printers.connection.connected"):b("printers.connection.offline")})]}),a.jsxs("p",{className:"text-sm text-bambu-gray",children:[t.model||"Unknown Model",r==="expanded"&&we?.nozzles&&we.nozzles[0]?.nozzle_diameter&&a.jsxs("span",{className:"ml-1.5 text-bambu-gray",title:we.nozzles[0].nozzle_type||"Nozzle",children:["• ",we.nozzles[0].nozzle_diameter,"mm"]}),r==="expanded"&&n&&n.total_print_hours>0&&a.jsxs("span",{className:"ml-2 text-bambu-gray",children:[a.jsx(Wn,{className:"w-3 h-3 inline-block mr-1"}),Math.round(n.total_print_hours),"h"]})]})]})]}),a.jsxs("div",{className:"relative flex-shrink-0",children:[a.jsx(ke,{variant:"ghost",size:"sm",onClick:()=>M(!k),children:a.jsx(Eh,{className:"w-4 h-4"})}),k&&a.jsxs("div",{className:"absolute right-0 mt-2 w-48 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg shadow-lg z-20",children:[a.jsxs("button",{className:`w-full px-4 py-2 text-left text-sm flex items-center gap-2 ${T("printers:update")?"hover:bg-bambu-dark-tertiary":"opacity-50 cursor-not-allowed"}`,onClick:()=>{T("printers:update")&&(O(!0),M(!1))},title:T("printers:update")?void 0:b("printers.permission.noEdit"),children:[a.jsx(Vi,{className:"w-4 h-4"}),b("common.edit")]}),a.jsxs("button",{className:"w-full px-4 py-2 text-left text-sm hover:bg-bambu-dark-tertiary flex items-center gap-2",onClick:()=>{ai.mutate(),M(!1)},children:[a.jsx(Cr,{className:"w-4 h-4"}),b("printers.reconnect")]}),a.jsxs("button",{className:"w-full px-4 py-2 text-left text-sm hover:bg-bambu-dark-tertiary flex items-center gap-2",onClick:()=>{G(!0),M(!1)},children:[a.jsx(wX,{className:"w-4 h-4"}),b("printers.mqttDebug")]}),a.jsxs("button",{className:`w-full px-4 py-2 text-left text-sm flex items-center gap-2 ${T("printers:delete")?"text-red-400 hover:bg-bambu-dark-tertiary":"text-red-400/50 cursor-not-allowed"}`,onClick:()=>{T("printers:delete")&&(j(!0),M(!1))},title:T("printers:delete")?void 0:b("printers.permission.noDelete"),children:[a.jsx(Jt,{className:"w-4 h-4"}),b("common.delete")]})]})]})]}),r==="expanded"&&a.jsxs("div",{className:"flex flex-wrap items-center gap-2 mt-2",children:[a.jsxs("span",{className:`flex items-center gap-1.5 px-2 py-1 rounded-full text-xs ${we?.connected?"bg-status-ok/20 text-status-ok":"bg-status-error/20 text-status-error"}`,children:[we?.connected?a.jsx(Kb,{className:"w-3 h-3"}):a.jsx(hk,{className:"w-3 h-3"}),we?.connected?b("printers.connection.connected"):b("printers.connection.offline")]}),we?.connected&&Je!=null&&a.jsxs("span",{className:`flex items-center gap-1 px-2 py-1 rounded-full text-xs ${Je>=-50||Je>=-60?"bg-status-ok/20 text-status-ok":Je>=-70?"bg-status-warning/20 text-status-warning":Je>=-80?"bg-orange-500/20 text-orange-600":"bg-status-error/20 text-status-error"}`,title:`WiFi: ${Je} dBm - ${b(KHe(Je).labelKey)}`,children:[a.jsx(qfe,{className:"w-3 h-3"}),Je,"dBm"]}),we?.connected&&(()=>{const bt=we.hms_errors?dTe(we.hms_errors):[];return a.jsxs("button",{onClick:()=>X(!0),className:`flex items-center gap-1 px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80 transition-opacity ${bt.length>0?bt.some(Pt=>Pt.severity<=2)?"bg-status-error/20 text-status-error":"bg-status-warning/20 text-status-warning":"bg-status-ok/20 text-status-ok"}`,title:b("printers.clickToViewHmsErrors"),children:[a.jsx(kn,{className:"w-3 h-3"}),bt.length>0?bt.length:"OK"]})})(),n&&a.jsxs("button",{onClick:()=>_("/maintenance"),className:`flex items-center gap-1 px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80 transition-opacity ${n.due_count>0?"bg-status-error/20 text-status-error":n.warning_count>0?"bg-status-warning/20 text-status-warning":"bg-status-ok/20 text-status-ok"}`,title:n.due_count>0||n.warning_count>0?`${n.due_count>0?`${n.due_count} maintenance due`:""}${n.due_count>0&&n.warning_count>0?", ":""}${n.warning_count>0?`${n.warning_count} due soon`:""} - Click to view`:b("printers.maintenanceUpToDate"),children:[a.jsx(Of,{className:"w-3 h-3"}),n.due_count>0||n.warning_count>0?n.due_count+n.warning_count:"OK"]}),Bt>0&&a.jsxs("button",{onClick:()=>_("/queue"),className:"flex items-center gap-1 px-2 py-1 rounded-full text-xs bg-purple-500/20 text-purple-400 hover:opacity-80 transition-opacity",title:b("printers.queue.inQueue",{count:Bt}),children:[a.jsx(ha,{className:"w-3 h-3"}),Bt]}),y&&ve?.current_version&&ve?.latest_version?a.jsxs("button",{onClick:()=>Ne(!0),className:`flex items-center gap-1 px-2 py-1 rounded-full text-xs hover:opacity-80 transition-opacity ${ve.update_available?"bg-orange-500/20 text-orange-400":"bg-status-ok/20 text-status-ok"}`,title:ve.update_available?b("printers.firmwareUpdateAvailable",{current:ve.current_version,latest:ve.latest_version}):b("printers.firmwareUpToDate",{version:ve.current_version}),children:[ve.update_available?a.jsx(di,{className:"w-3 h-3"}):a.jsx(xr,{className:"w-3 h-3"}),ve.current_version]}):we?.firmware_version?a.jsx("span",{className:"flex items-center gap-1 px-2 py-1 rounded-full text-xs bg-bambu-dark-tertiary/50 text-bambu-gray",children:we.firmware_version}):null]})]}),C&&a.jsx("div",{className:"fixed inset-0 bg-black/50 flex items-center justify-center z-50",children:a.jsx(vt,{className:"w-full max-w-md mx-4",children:a.jsxs(Nt,{children:[a.jsxs("div",{className:"flex items-start gap-3 mb-4",children:[a.jsx("div",{className:"p-2 rounded-full bg-red-500/20",children:a.jsx(kn,{className:"w-5 h-5 text-red-400"})}),a.jsxs("div",{children:[a.jsx("h3",{className:"text-lg font-semibold text-white",children:b("printers.confirm.deleteTitle")}),a.jsx("p",{className:"text-sm text-bambu-gray mt-1",children:b("printers.confirm.deleteMessage",{name:t.name})})]})]}),a.jsx("div",{className:"bg-bambu-dark rounded-lg p-3 mb-4",children:a.jsxs("label",{className:"flex items-start gap-3 cursor-pointer",children:[a.jsx("input",{type:"checkbox",checked:P,onChange:bt=>E(bt.target.checked),className:"mt-0.5 w-4 h-4 rounded border-bambu-gray bg-bambu-dark-secondary text-bambu-green focus:ring-bambu-green focus:ring-offset-0"}),a.jsxs("div",{children:[a.jsx("span",{className:"text-sm text-white",children:b("printers.deleteArchives")}),a.jsx("p",{className:"text-xs text-bambu-gray mt-0.5",children:b(P?"printers.confirm.deleteArchivesNote":"printers.confirm.keepArchivesNote")})]})]})}),a.jsxs("div",{className:"flex justify-end gap-2",children:[a.jsx(ke,{variant:"secondary",onClick:()=>{j(!1),E(!0)},children:b("common.cancel")}),a.jsx(ke,{variant:"danger",onClick:()=>{Dn.mutate({deleteArchives:P}),j(!1),E(!0)},children:"Delete"})]})]})})}),we?.connected&&a.jsxs(a.Fragment,{children:[r==="compact"?a.jsx("div",{className:"mt-2",children:we.state==="RUNNING"?a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("div",{className:"flex-1 bg-bambu-dark-tertiary rounded-full h-1.5",children:a.jsx("div",{className:"bg-bambu-green h-1.5 rounded-full transition-all",style:{width:`${we.progress||0}%`}})}),a.jsxs("span",{className:"text-xs text-white",children:[Math.round(we.progress||0),"%"]})]}):a.jsx("p",{className:"text-xs text-bambu-gray",children:h9(we.state,we.stg_cur_name)})}):a.jsxs(a.Fragment,{children:[a.jsxs("div",{className:"mb-4 p-3 bg-bambu-dark rounded-lg relative",children:[a.jsxs("button",{onClick:()=>ce(!0),disabled:!(we.state==="RUNNING"||we.state==="PAUSE"||we.state==="PAUSED")||(we.printable_objects_count??0)<2||!T("printers:control"),className:`absolute top-2 right-2 p-1.5 rounded transition-colors z-10 ${(we.state==="RUNNING"||we.state==="PAUSE"||we.state==="PAUSED")&&(we.printable_objects_count??0)>=2&&T("printers:control")?"text-bambu-gray hover:text-white hover:bg-white/10":"text-bambu-gray/30 cursor-not-allowed"}`,title:T("printers:control")?we.state==="RUNNING"||we.state==="PAUSE"||we.state==="PAUSED"?(we.printable_objects_count??0)>=2?b("printers.skipObjects.tooltip"):b("printers.skipObjects.requiresMultiple"):b("printers.skipObjects.onlyWhilePrinting"):b("printers.permission.noControl"),children:[a.jsx(Nk,{className:"w-4 h-4"}),Ve&&Ve.skipped_count>0&&a.jsx("span",{className:"absolute -top-1 -right-1 min-w-[16px] h-4 px-1 flex items-center justify-center text-[10px] font-bold bg-red-500 text-white rounded-full",children:Ve.skipped_count})]}),a.jsxs("div",{className:"flex gap-3",children:[a.jsx(XHe,{url:we.state==="RUNNING"?we.cover_url:null,printName:we.state==="RUNNING"&&(we.subtask_name||we.current_print)||void 0}),a.jsx("div",{className:"flex-1 min-w-0",children:we.current_print&&we.state==="RUNNING"?a.jsxs(a.Fragment,{children:[a.jsx("p",{className:"text-sm text-bambu-gray mb-1",children:we.stg_cur_name||"Printing"}),a.jsx("p",{className:"text-white text-sm mb-2 truncate",children:we.subtask_name||we.current_print}),a.jsxs("div",{className:"flex items-center justify-between text-sm",children:[a.jsx("div",{className:"flex-1 bg-bambu-dark-tertiary rounded-full h-2 mr-3",children:a.jsx("div",{className:"bg-bambu-green h-2 rounded-full transition-all",style:{width:`${we.progress||0}%`}})}),a.jsxs("span",{className:"text-white",children:[Math.round(we.progress||0),"%"]})]}),a.jsxs("div",{className:"flex items-center gap-3 mt-2 text-xs text-bambu-gray",children:[we.remaining_time!=null&&we.remaining_time>0&&a.jsxs(a.Fragment,{children:[a.jsxs("span",{className:"flex items-center gap-1",children:[a.jsx(Wn,{className:"w-3 h-3"}),Zs(we.remaining_time*60)]}),a.jsxs("span",{className:"text-bambu-green font-medium",title:b("printers.estimatedCompletion"),children:["ETA ",VF(we.remaining_time,f,b)]})]}),we.layer_num!=null&&we.total_layers!=null&&we.total_layers>0&&a.jsxs("span",{className:"flex items-center gap-1",children:[a.jsx(ha,{className:"w-3 h-3"}),we.layer_num,"/",we.total_layers]}),Zn&&a.jsxs("span",{className:"flex items-center gap-1",title:`Started by ${Zn}`,children:[a.jsx(rd,{className:"w-3 h-3"}),Zn]})]})]}):a.jsxs(a.Fragment,{children:[a.jsx("p",{className:"text-sm text-bambu-gray mb-1",children:b("printers.sort.status")}),a.jsx("p",{className:"text-white text-sm mb-2",children:h9(we.state,we.stg_cur_name)}),a.jsxs("div",{className:"flex items-center justify-between text-sm",children:[a.jsx("div",{className:"flex-1 bg-bambu-dark-tertiary rounded-full h-2 mr-3",children:a.jsx("div",{className:"bg-bambu-dark-tertiary h-2 rounded-full"})}),a.jsx("span",{className:"text-bambu-gray",children:"—"})]}),jn?a.jsxs("p",{className:"text-xs text-bambu-gray mt-2 truncate",title:jn.print_name||jn.filename,children:["Last: ",jn.print_name||jn.filename,jn.completed_at&&a.jsxs("span",{className:"ml-1 text-bambu-gray/60",children:["• ",bb(jn.completed_at,{month:"short",day:"numeric"})]})]}):a.jsx("p",{className:"text-xs text-bambu-gray mt-2",children:b("printers.readyToPrint")})]})})]})]}),a.jsx(fTe,{printerId:t.id,printerState:we.state,plateCleared:we.plate_cleared})]}),we.temperatures&&r==="expanded"&&(()=>{const bt=we.temperatures.nozzle_heating||we.temperatures.nozzle_2_heating||!1,Pt=we.temperatures.bed_heating||!1,Hn=we.temperatures.chamber_heating||!1,Ct=t.nozzle_count===2||we.temperatures.nozzle_2!==void 0,Nn=we.active_extruder===1?"L":"R",si=we.nozzle_rack?.find(Na=>Na.id===1),Fn=we.nozzle_rack?.find(Na=>Na.id===0),aa=Fn||si;return a.jsxs("div",{className:"flex items-stretch gap-1.5 flex-wrap",children:[a.jsxs("div",{className:"text-center px-2 py-1.5 bg-bambu-dark rounded-lg flex-1 flex flex-col justify-center items-center",children:[a.jsx(sj,{className:"w-3.5 h-3.5 mb-0.5",color:"text-orange-400",isHeating:bt}),we.temperatures.nozzle_2!==void 0?a.jsxs(a.Fragment,{children:[a.jsx("p",{className:"text-[9px] text-bambu-gray",children:"L / R"}),a.jsxs("p",{className:"text-[11px] text-white",children:[Math.round(we.temperatures.nozzle||0),"° / ",Math.round(we.temperatures.nozzle_2||0),"°"]})]}):aa?a.jsx(pne,{slot:aa,index:0,activeStatus:!0,filamentName:aa.filament_id?Le?.[aa.filament_id]?.name:void 0,children:a.jsxs("div",{className:"cursor-default",children:[a.jsx("p",{className:"text-[9px] text-bambu-gray",children:b("printers.temperatures.nozzle")}),a.jsxs("p",{className:"text-[11px] text-white",children:[Math.round(we.temperatures.nozzle||0),"°C"]})]})}):a.jsxs(a.Fragment,{children:[a.jsx("p",{className:"text-[9px] text-bambu-gray",children:b("printers.temperatures.nozzle")}),a.jsxs("p",{className:"text-[11px] text-white",children:[Math.round(we.temperatures.nozzle||0),"°C"]})]})]}),a.jsxs("div",{className:"text-center px-2 py-1.5 bg-bambu-dark rounded-lg flex-1 flex flex-col justify-center items-center",children:[a.jsx(sj,{className:"w-3.5 h-3.5 mb-0.5",color:"text-blue-400",isHeating:Pt}),a.jsx("p",{className:"text-[9px] text-bambu-gray",children:b("printers.temperatures.bed")}),a.jsxs("p",{className:"text-[11px] text-white",children:[Math.round(we.temperatures.bed||0),"°C"]})]}),we.temperatures.chamber!==void 0&&a.jsxs("div",{className:"text-center px-2 py-1.5 bg-bambu-dark rounded-lg flex-1 flex flex-col justify-center items-center",children:[a.jsx(sj,{className:"w-3.5 h-3.5 mb-0.5",color:"text-green-400",isHeating:Hn}),a.jsx("p",{className:"text-[9px] text-bambu-gray",children:b("printers.temperatures.chamber")}),a.jsxs("p",{className:"text-[11px] text-white",children:[Math.round(we.temperatures.chamber||0),"°C"]})]}),Ct&&a.jsx(UHe,{leftSlot:si,rightSlot:Fn,activeNozzle:Nn,filamentInfo:Le,children:a.jsxs("div",{className:"text-center px-3 py-1.5 bg-bambu-dark rounded-lg h-full flex flex-col justify-center items-center cursor-default",title:b("printers.activeNozzle",{nozzle:b(Nn==="L"?"common.left":"common.right")}),children:[a.jsxs("div",{className:"flex items-center gap-2 mb-1",children:[a.jsxs("span",{className:`text-[11px] font-bold ${Nn==="L"?"text-amber-400":"text-gray-500"}`,children:["L",si?.nozzle_diameter?` ${si.nozzle_diameter}`:""]}),a.jsx("span",{className:"text-[9px] text-bambu-gray/40",children:"·"}),a.jsxs("span",{className:`text-[11px] font-bold ${Nn==="R"?"text-amber-400":"text-gray-500"}`,children:["R",Fn?.nozzle_diameter?` ${Fn.nozzle_diameter}`:""]})]}),a.jsx("p",{className:"text-[9px] text-bambu-gray",children:b("printers.temperatures.nozzle")})]})}),we.nozzle_rack&&we.nozzle_rack.some(Na=>Na.id>=2)&&a.jsx(BHe,{slots:we.nozzle_rack,filamentInfo:Le})]})})(),r==="expanded"&&(()=>{const bt=we.state==="RUNNING",Pt=we.state==="PAUSED"||we.state==="PAUSE",Hn=bt||Pt,Ct=lt.isPending||dt.isPending||Lt.isPending,Nn=we.cooling_fan_speed,si=we.big_fan1_speed,Fn=we.big_fan2_speed;return a.jsxs("div",{className:"mt-3",children:[a.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[a.jsx("span",{className:"text-[10px] uppercase tracking-wider text-bambu-gray font-medium",children:b("printers.controls")}),a.jsx("div",{className:"flex-1 h-px bg-bambu-dark-tertiary/30"})]}),a.jsxs("div",{className:"flex items-center justify-between gap-2 max-[550px]:items-start",children:[a.jsxs("div",{className:"flex items-center gap-2 min-w-0 max-[550px]:flex-wrap max-[550px]:items-start max-[550px]:gap-1.5",children:[a.jsxs("div",{className:`flex items-center gap-1 px-1.5 py-1 rounded ${Nn&&Nn>0?"bg-cyan-500/10":"bg-bambu-dark"}`,title:b("printers.fans.partCooling"),children:[a.jsx(hX,{className:`w-3.5 h-3.5 ${Nn&&Nn>0?"text-cyan-400":"text-bambu-gray/50"}`}),a.jsxs("span",{className:`text-[10px] ${Nn&&Nn>0?"text-cyan-400":"text-bambu-gray/50"}`,children:[Nn??0,"%"]})]}),a.jsxs("div",{className:`flex items-center gap-1 px-1.5 py-1 rounded ${si&&si>0?"bg-blue-500/10":"bg-bambu-dark"}`,title:b("printers.fans.auxiliary"),children:[a.jsx(_X,{className:`w-3.5 h-3.5 ${si&&si>0?"text-blue-400":"text-bambu-gray/50"}`}),a.jsxs("span",{className:`text-[10px] ${si&&si>0?"text-blue-400":"text-bambu-gray/50"}`,children:[si??0,"%"]})]}),a.jsxs("div",{className:`flex items-center gap-1 px-1.5 py-1 rounded ${Fn&&Fn>0?"bg-green-500/10":"bg-bambu-dark"}`,title:b("printers.fans.chamber"),children:[a.jsx(dde,{className:`w-3.5 h-3.5 ${Fn&&Fn>0?"text-green-400":"text-bambu-gray/50"}`}),a.jsxs("span",{className:`text-[10px] ${Fn&&Fn>0?"text-green-400":"text-bambu-gray/50"}`,children:[Fn??0,"%"]})]})]}),a.jsxs("div",{className:"flex items-center gap-2 flex-shrink-0 max-[550px]:self-start",children:[a.jsxs("button",{onClick:()=>J(!0),disabled:!Hn||Ct||!T("printers:control"),className:` + `})]})]})}function BHe({slots:t,filamentInfo:e}){const{t:n}=Vt(),r=t.filter(l=>l.id>=2),i=6,s=r.length>0?Math.min(...r.map(l=>l.id)):16,o=Array.from({length:i},(l,c)=>r.find(u=>u.id===s+c)??{id:-(c+1),nozzle_type:"",nozzle_diameter:"",wear:null,stat:null,max_temp:0,serial_number:"",filament_color:"",filament_id:"",filament_type:""});return a.jsxs("div",{className:"text-center px-2.5 py-1.5 bg-bambu-dark rounded-lg flex-[2_1_190px] flex flex-col justify-center",children:[a.jsx("p",{className:"text-[9px] text-bambu-gray mb-1",children:n("printers.nozzleRack")}),a.jsx("div",{className:"flex gap-[3px] justify-center",children:o.map((l,c)=>{const u=!l.nozzle_diameter&&!l.nozzle_type,d=u?null:N4(l.filament_color),h=d?zHe(l.filament_color):!1;return a.jsx(pne,{slot:l,index:c,filamentName:l.filament_id?e?.[l.filament_id]?.name:void 0,children:a.jsx("div",{className:`w-7 h-7 rounded flex items-center justify-center cursor-default transition-colors border-b-2 ${u?"bg-bambu-dark-tertiary/20 border-bambu-dark-tertiary/20":"bg-bambu-dark-tertiary/40 border-bambu-dark-tertiary/40"}`,style:d?{backgroundColor:d}:void 0,children:a.jsx("span",{className:`text-[10px] font-semibold ${u?"text-bambu-gray/30":h?"text-black/80":"text-white"}`,style:d&&!h?{textShadow:"0 1px 3px rgba(0,0,0,0.9)"}:void 0,children:u?"—":l.nozzle_diameter||"?"})})},l.id>=0?l.id:`empty-${c}`)})})]})}function c9({className:t}){return a.jsx("svg",{className:t,viewBox:"0 0 36 54",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:a.jsx("path",{d:"M17.8131 0.00538C18.4463 -0.15091 20.3648 3.14642 20.8264 3.84781C25.4187 10.816 35.3089 26.9368 35.9383 34.8694C37.4182 53.5822 11.882 61.3357 2.53721 45.3789C-1.73471 38.0791 0.016 32.2049 3.178 25.0232C6.99221 16.3662 12.6411 7.90372 17.8131 0.00538ZM18.3738 7.24807L17.5881 7.48441C14.4452 12.9431 10.917 18.2341 8.19369 23.9368C4.6808 31.29 1.18317 38.5479 7.69403 45.5657C17.3058 55.9228 34.9847 46.8808 31.4604 32.8681C29.2558 24.0969 22.4207 15.2913 18.3776 7.24807H18.3738Z",fill:"#C3C2C1"})})}function HHe({className:t}){return a.jsxs("svg",{className:t,viewBox:"0 0 35 53",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[a.jsx("path",{d:"M17.3165 0.0038C17.932 -0.14959 19.7971 3.08645 20.2458 3.77481C24.7103 10.6135 34.3251 26.4346 34.937 34.2198C36.3757 52.5848 11.5505 60.1942 2.46584 44.534C-1.68714 37.3735 0.0148 31.6085 3.08879 24.5603C6.79681 16.0605 12.2884 7.75907 17.3165 0.0038ZM17.8615 7.11561L17.0977 7.34755C14.0423 12.7048 10.6124 17.8974 7.96483 23.4941C4.54975 30.7107 1.14949 37.8337 7.47908 44.721C16.8233 54.8856 34.01 46.0117 30.5838 32.2595C28.4405 23.6512 21.7957 15.0093 17.8652 7.11561H17.8615Z",fill:"#C3C2C1"}),a.jsx("path",{d:"M5.03547 30.112C9.64453 30.4936 11.632 35.7985 16.4154 35.791C19.6339 35.7873 20.2161 33.2283 22.3853 31.6197C31.6776 24.7286 33.5835 37.4894 27.9881 44.4254C18.1878 56.5653 -1.16063 44.6013 5.03917 30.1158L5.03547 30.112Z",fill:"#1F8FEB"})]})}function $He({className:t}){return a.jsxs("svg",{className:t,viewBox:"0 0 36 54",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[a.jsx("path",{d:"M17.9625 4.48059L4.77216 26.3154L2.08228 40.2175L10.0224 50.8414H23.1594L33.3246 42.1693V30.2455L17.9625 4.48059Z",fill:"#1F8FEB"}),a.jsx("path",{d:"M17.7948 0.00538C18.4273 -0.15091 20.3438 3.14642 20.8048 3.84781C25.3921 10.816 35.2715 26.9368 35.9001 34.8694C37.3784 53.5822 11.8702 61.3357 2.53562 45.3789C-1.73163 38.0829 0.0134 32.2087 3.1757 25.027C6.98574 16.3662 12.6284 7.90372 17.7948 0.00538ZM18.3549 7.24807L17.57 7.48441C14.4306 12.9431 10.9063 18.2341 8.1859 23.9368C4.67686 31.29 1.18305 38.5479 7.68679 45.5657C17.2881 55.9228 34.9476 46.8808 31.4271 32.8681C29.2249 24.0969 22.3974 15.2913 18.3587 7.24807H18.3549Z",fill:"#C3C2C1"})]})}function VHe({className:t}){return a.jsxs("svg",{className:t,viewBox:"0 0 12 20",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[a.jsx("path",{d:"M6 0.5C4.6 0.5 3.5 1.6 3.5 3V12.1C2.6 12.8 2 13.9 2 15C2 17.2 3.8 19 6 19C8.2 19 10 17.2 10 15C10 13.9 9.4 12.8 8.5 12.1V3C8.5 1.6 7.4 0.5 6 0.5Z",stroke:"#C3C2C1",strokeWidth:"1",fill:"none"}),a.jsx("circle",{cx:"6",cy:"15",r:"2.5",stroke:"#C3C2C1",strokeWidth:"1",fill:"none"})]})}function qHe({className:t}){return a.jsxs("svg",{className:t,viewBox:"0 0 12 20",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[a.jsx("rect",{x:"4.5",y:"8",width:"3",height:"4.5",fill:"#d4a017",rx:"0.5"}),a.jsx("circle",{cx:"6",cy:"15",r:"2",fill:"#d4a017"}),a.jsx("path",{d:"M6 0.5C4.6 0.5 3.5 1.6 3.5 3V12.1C2.6 12.8 2 13.9 2 15C2 17.2 3.8 19 6 19C8.2 19 10 17.2 10 15C10 13.9 9.4 12.8 8.5 12.1V3C8.5 1.6 7.4 0.5 6 0.5Z",stroke:"#C3C2C1",strokeWidth:"1",fill:"none"})]})}function GHe({className:t}){return a.jsxs("svg",{className:t,viewBox:"0 0 12 20",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[a.jsx("rect",{x:"4.5",y:"3",width:"3",height:"9.5",fill:"#c62828",rx:"0.5"}),a.jsx("circle",{cx:"6",cy:"15",r:"2",fill:"#c62828"}),a.jsx("path",{d:"M6 0.5C4.6 0.5 3.5 1.6 3.5 3V12.1C2.6 12.8 2 13.9 2 15C2 17.2 3.8 19 6 19C8.2 19 10 17.2 10 15C10 13.9 9.4 12.8 8.5 12.1V3C8.5 1.6 7.4 0.5 6 0.5Z",stroke:"#C3C2C1",strokeWidth:"1",fill:"none"})]})}function sj({className:t,color:e,isHeating:n}){const i={"text-orange-400":"#fb923c","text-blue-400":"#60a5fa","text-green-400":"#4ade80"}[e]||"#888",s=n?{filter:`drop-shadow(0 0 4px ${i}) drop-shadow(0 0 8px ${i})`}:{};return n?a.jsxs("svg",{className:t,style:s,viewBox:"0 0 12 20",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[a.jsx("rect",{x:"4.5",y:"3",width:"3",height:"9.5",fill:i,rx:"0.5"}),a.jsx("circle",{cx:"6",cy:"15",r:"2",fill:i}),a.jsx("path",{d:"M6 0.5C4.6 0.5 3.5 1.6 3.5 3V12.1C2.6 12.8 2 13.9 2 15C2 17.2 3.8 19 6 19C8.2 19 10 17.2 10 15C10 13.9 9.4 12.8 8.5 12.1V3C8.5 1.6 7.4 0.5 6 0.5Z",stroke:i,strokeWidth:"1",fill:"none"})]}):a.jsxs("svg",{className:t,viewBox:"0 0 12 20",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[a.jsx("path",{d:"M6 0.5C4.6 0.5 3.5 1.6 3.5 3V12.1C2.6 12.8 2 13.9 2 15C2 17.2 3.8 19 6 19C8.2 19 10 17.2 10 15C10 13.9 9.4 12.8 8.5 12.1V3C8.5 1.6 7.4 0.5 6 0.5Z",stroke:i,strokeWidth:"1",fill:"none"}),a.jsx("circle",{cx:"6",cy:"15",r:"2.5",stroke:i,strokeWidth:"1",fill:"none"})]})}function u9({humidity:t,goodThreshold:e=40,fairThreshold:n=60,onClick:r,compact:i}){const s=typeof t=="string"?parseInt(t,10):t,o=typeof e=="number"?e:40,l=typeof n=="number"?n:60;let c,u;isNaN(s)?(c="#C3C2C1",u="Unknown"):s<=o?(c="#22a352",u="Good"):s<=l?(c="#d4a017",u="Fair"):(c="#c62828",u="Bad");let d;return isNaN(s)||s<=o?d=c9:s<=l?d=HHe:d=$He,a.jsxs("button",{type:"button",onClick:r,className:`flex items-center gap-1 ${r?"cursor-pointer hover:opacity-80 transition-opacity":""}`,title:`Humidity: ${s}% - ${u}${r?" (click for history)":""}`,children:[a.jsx(d,{className:i?"w-2.5 h-3":"w-3 h-4"}),a.jsxs("span",{className:`font-medium tabular-nums ${i?"text-[10px]":"text-xs"}`,style:{color:c},children:[s,"%"]})]})}function d9({temp:t,goodThreshold:e=28,fairThreshold:n=35,onClick:r,compact:i}){const s=typeof e=="number"?e:28,o=typeof n=="number"?n:35;let l,c,u;return t<=s?(l="#22a352",c="Good",u=VHe):t<=o?(l="#d4a017",c="Fair",u=qHe):(l="#c62828",c="Bad",u=GHe),a.jsxs("button",{type:"button",onClick:r,className:`flex items-center gap-1 ${r?"cursor-pointer hover:opacity-80 transition-opacity":""}`,title:`Temperature: ${t}°C - ${c}${r?" (click for history)":""}`,children:[a.jsx(u,{className:i?"w-2.5 h-3":"w-3 h-4"}),a.jsxs("span",{className:`tabular-nums text-right ${i?"text-[10px] w-8":"w-12"}`,style:{color:l},children:[t,"°C"]})]})}function ih(t,e){const n=typeof t=="string"?parseInt(t,10):t,r=isNaN(n)?0:n,i=e===1,s=r>=128?r-128:r,o=String.fromCharCode(65+s);return i?`HT-${o}`:`AMS-${o}`}function oj(t){return t>50?"#00ae42":t>=15?"#f59e0b":"#ef4444"}function lj(t){return!t?.remaining_weight||!t?.filament_weight||t.filament_weight<=0?null:Math.min(100,Math.round(t.remaining_weight/t.filament_weight*100))}function WHe(t){if(!t)return"/img/printers/default.png";const e=t.toLowerCase().replace(/\s+/g,"");return e.includes("x1e")?"/img/printers/x1e.png":e.includes("x1c")||e.includes("x1carbon")||e.includes("x1")?"/img/printers/x1c.png":e.includes("h2dpro")||e.includes("h2d-pro")?"/img/printers/h2dpro.png":e.includes("h2d")?"/img/printers/h2d.png":e.includes("h2c")?"/img/printers/h2c.png":e.includes("h2s")?"/img/printers/h2d.png":e.includes("p2s")||e.includes("p1s")?"/img/printers/p1s.png":e.includes("p1p")?"/img/printers/p1p.png":e.includes("a1mini")?"/img/printers/a1mini.png":e.includes("a1")?"/img/printers/a1.png":"/img/printers/default.png"}function KHe(t){return t>=-50?{labelKey:"printers.wifiSignal.excellent",color:"text-bambu-green",bars:4}:t>=-60?{labelKey:"printers.wifiSignal.good",color:"text-bambu-green",bars:3}:t>=-70?{labelKey:"printers.wifiSignal.fair",color:"text-yellow-400",bars:2}:t>=-80?{labelKey:"printers.wifiSignal.weak",color:"text-orange-400",bars:1}:{labelKey:"printers.wifiSignal.veryWeak",color:"text-red-400",bars:1}}function cj(t){return t?!!(t.tray_uuid&&t.tray_uuid!=="00000000000000000000000000000000"||t.tag_uid&&t.tag_uid!=="0000000000000000"):!1}function XHe({url:t,printName:e}){const{t:n}=Vt(),[r,i]=w.useState(!1),[s,o]=w.useState(!1),[l,c]=w.useState(!1),u=w.useMemo(()=>{if(!t)return null;const d=t.includes("?")?"&":"?";return`${t}${d}v=${encodeURIComponent(e||Date.now().toString())}`},[t,e]);return w.useEffect(()=>{i(!1),o(!1)},[u]),a.jsxs(a.Fragment,{children:[a.jsx("div",{className:`w-20 h-20 flex-shrink-0 rounded-lg overflow-hidden bg-bambu-dark-tertiary flex items-center justify-center ${u&&r?"cursor-pointer":""}`,onClick:()=>u&&r&&c(!0),children:u&&!s?a.jsxs(a.Fragment,{children:[a.jsx("img",{src:u,alt:n("printers.printPreview"),className:`w-full h-full object-cover ${r?"block":"hidden"}`,onLoad:()=>i(!0),onError:()=>o(!0)}),!r&&a.jsx(Ji,{className:"w-8 h-8 text-bambu-gray"})]}):a.jsx(Ji,{className:"w-8 h-8 text-bambu-gray"})}),l&&u&&a.jsx("div",{className:"fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-8",onClick:()=>c(!1),children:a.jsxs("div",{className:"relative max-w-2xl max-h-full",children:[a.jsx("img",{src:u,alt:n("printers.printPreview"),className:"max-w-full max-h-[80vh] rounded-lg shadow-2xl"}),e&&a.jsx("p",{className:"text-white text-center mt-4 text-lg",children:e})]})})]})}function YHe({printers:t}){const{t:e}=Vt(),n=tn(),[r,i]=w.useState(0);w.useEffect(()=>{let l=!1;const c=n.getQueryCache().subscribe(()=>{l||(l=!0,requestAnimationFrame(()=>{i(u=>u+1),l=!1}))});return()=>c()},[n]);const{counts:s,nextFinish:o}=w.useMemo(()=>{let l=0,c=0,u=0,d=0,h=null,f=null,p=0;return t?.forEach(x=>{const y=n.getQueryData(["printerStatus",x.id]);y===void 0?d++:y.connected?y.state==="RUNNING"?(l++,y.remaining_time!=null&&y.remaining_time>0&&(f===null||y.remaining_time0?"bg-bambu-green":"bg-gray-500"}`}),a.jsxs("span",{className:"text-bambu-gray",children:[a.jsx("span",{className:"text-white font-medium",children:s.idle})," ",e("printers.status.available").toLowerCase()]})]}),s.printing>0&&a.jsxs("div",{className:"flex items-center gap-1.5",children:[a.jsx("div",{className:"w-2 h-2 rounded-full bg-bambu-green animate-pulse"}),a.jsxs("span",{className:"text-bambu-gray",children:[a.jsx("span",{className:"text-white font-medium",children:s.printing})," ",e("printers.status.printing").toLowerCase()]})]}),s.offline>0&&a.jsxs("div",{className:"flex items-center gap-1.5",children:[a.jsx("div",{className:"w-2 h-2 rounded-full bg-gray-400"}),a.jsxs("span",{className:"text-bambu-gray",children:[a.jsx("span",{className:"text-white font-medium",children:s.offline})," ",e("printers.status.offline").toLowerCase()]})]}),o&&a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"w-px h-4 bg-bambu-dark-tertiary"}),a.jsxs("div",{className:"flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-2",children:[a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsxs("span",{className:"text-bambu-green font-medium",children:[e("printers.nextAvailable"),":"]}),a.jsx("span",{className:"text-white font-medium",children:o.name})]}),a.jsxs("div",{className:"flex items-center gap-2 w-full sm:w-auto",children:[a.jsx("div",{className:"w-full sm:w-16 bg-bambu-dark-tertiary rounded-full h-1.5",children:a.jsx("div",{className:"bg-bambu-green h-1.5 rounded-full transition-all",style:{width:`${o.progress}%`}})}),a.jsxs("span",{className:"text-white font-medium",children:[Math.round(o.progress),"%"]}),a.jsxs("span",{className:"text-bambu-gray",children:["(",Zs(o.remainingMin*60),")"]})]})]})]})]}):null}function h9(t,e){if(e)return e;switch(t){case"RUNNING":return"Printing";case"PAUSE":return"Paused";case"FINISH":return"Finished";case"FAILED":return"Failed";case"IDLE":return"Idle";default:return t?t.charAt(0)+t.slice(1).toLowerCase():"Idle"}}function PR(t){return t?{O1D:"H2D",O1E:"H2D Pro",O2D:"H2D Pro",O1C:"H2C",O1S:"H2S","BL-P001":"X1C","BL-P002":"X1","BL-P003":"X1E",C11:"P1S",C12:"P1P",C13:"P2S",N2S:"A1",N1:"A1 Mini",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"}[t]||t:""}function m9({printer:t,hideIfDisconnected:e,maintenanceInfo:n,viewMode:r="expanded",cardSize:i=2,amsThresholds:s,spoolmanEnabled:o=!1,hasUnlinkedSpools:l=!1,linkedSpools:c,spoolmanUrl:u,onGetAssignment:d,onUnassignSpool:h,timeFormat:f="system",cameraViewMode:p="window",onOpenEmbeddedCamera:x,checkPrinterFirmware:y=!0}){const{t:b}=Vt(),v=tn(),_=$c(),{showToast:N}=hn(),{hasPermission:T}=Ir(),[k,M]=w.useState(!1),[C,j]=w.useState(!1),[P,E]=w.useState(!0),[U,O]=w.useState(!1),[B,R]=w.useState(!1),[q,G]=w.useState(!1),[z,K]=w.useState(!1),[Y,oe]=w.useState(!1),[$,X]=w.useState(!1),[ee,J]=w.useState(!1),[Se,pe]=w.useState(!1),[he,je]=w.useState(!1),[de,ce]=w.useState(!1),[_e,ie]=w.useState(null),[Fe,V]=w.useState(null),[W,ue]=w.useState(null),[Q,se]=w.useState(null),[Z,Ne]=w.useState(!1),[ye,fe]=w.useState(null),[L,D]=w.useState(!1),[H,me]=w.useState(!1),[xe,be]=w.useState(null),[Ue,Oe]=w.useState(!1),[Qe,rt]=w.useState(!1),{data:we}=Ze({queryKey:["printerStatus",t.id],queryFn:()=>ne.getPrinterStatus(t.id),refetchInterval:3e4}),{data:ve}=Ze({queryKey:["firmwareUpdate",t.id],queryFn:()=>t1.checkPrinterUpdate(t.id),staleTime:300*1e3,refetchInterval:300*1e3,enabled:y&&T("firmware:read")}),le=w.useMemo(()=>{const bt=new Set;if(we?.ams)for(const Pt of we.ams)for(const Hn of Pt.tray||[])Hn.tray_info_idx&&bt.add(Hn.tray_info_idx);for(const Pt of we?.vt_tray??[])Pt.tray_info_idx&&bt.add(Pt.tray_info_idx);if(we?.nozzle_rack)for(const Pt of we.nozzle_rack)Pt.filament_id&&bt.add(Pt.filament_id);return Array.from(bt)},[we?.ams,we?.vt_tray,we?.nozzle_rack]),{data:Le}=Ze({queryKey:["filamentInfo",le],queryFn:()=>ne.getFilamentInfo(le),enabled:le.length>0,staleTime:300*1e3}),{data:Me}=Ze({queryKey:["slotPresets",t.id],queryFn:()=>ne.getSlotPresets(t.id),staleTime:120*1e3}),[$e,Ae]=w.useState(null);w.useEffect(()=>{we?.wifi_signal!=null&&Ae(we.wifi_signal)},[we?.wifi_signal]);const Je=we?.wifi_signal??$e,Ee=w.useRef(void 0);w.useEffect(()=>{we?.connected!==void 0&&(Ee.current=we.connected)},[we?.connected]);const Xe=we?.connected??Ee.current,De=w.useRef({});w.useEffect(()=>{we?.ams_extruder_map&&Object.keys(we.ams_extruder_map).length>0&&(De.current=we.ams_extruder_map)},[we?.ams_extruder_map]);const Re=we?.ams_extruder_map&&Object.keys(we.ams_extruder_map).length>0?we.ams_extruder_map:De.current,Ge=w.useRef([]);w.useEffect(()=>{we?.ams&&we.ams.length>0&&(Ge.current=we.ams)},[we?.ams]);const pt=we?.ams&&we.ams.length>0?we.ams:Ge.current,Tt=w.useRef(void 0),Rt=we?.tray_now;Rt!==void 0&&Rt!==255?Tt.current=Rt:Rt===255&&(Tt.current=void 0);const Wt=Rt!==void 0&&Rt!==255?Rt:Tt.current,{data:Mt}=Ze({queryKey:["smartPlugByPrinter",t.id],queryFn:()=>ne.getSmartPlugByPrinter(t.id)}),{data:Ie}=Ze({queryKey:["scriptPlugsByPrinter",t.id],queryFn:()=>ne.getScriptPlugsByPrinter(t.id)}),{data:xt}=Ze({queryKey:["smartPlugStatus",Mt?.id],queryFn:()=>Mt?ne.getSmartPlugStatus(Mt.id):null,enabled:!!Mt,refetchInterval:1e4}),{data:wt}=Ze({queryKey:["queue",t.id,"pending"],queryFn:()=>ne.getQueue(t.id,"pending")}),Bt=wt?.length||0,{data:Pn}=Ze({queryKey:["queue",t.id,"printing"],queryFn:()=>ne.getQueue(t.id,"printing"),enabled:we?.state==="RUNNING"}),{data:pr}=Ze({queryKey:["currentPrintUser",t.id],queryFn:()=>ne.getCurrentPrintUser(t.id),enabled:we?.state==="RUNNING"}),Zn=Pn?.[0]?.created_by_username||pr?.username,{data:Jn}=Ze({queryKey:["archives",t.id,"last"],queryFn:()=>ne.getArchives(t.id,1,0),enabled:we?.connected&&we?.state!=="RUNNING"}),jn=Jn?.[0],cr=e&&Xe===!1,Dn=ot({mutationFn:bt=>ne.deletePrinter(t.id,bt.deleteArchives),onSuccess:()=>{v.invalidateQueries({queryKey:["printers"]}),v.invalidateQueries({queryKey:["archives"]}),v.invalidateQueries({queryKey:["maintenanceOverview"]})},onError:bt=>N(bt.message||b("printers.toast.failedToDelete"),"error")}),ai=ot({mutationFn:()=>ne.connectPrinter(t.id),onSuccess:()=>{v.invalidateQueries({queryKey:["printerStatus",t.id]})}}),re=ot({mutationFn:bt=>Mt?ne.controlSmartPlug(Mt.id,bt):Promise.reject("No plug"),onSuccess:()=>{v.invalidateQueries({queryKey:["smartPlugStatus",Mt?.id]})}}),He=ot({mutationFn:bt=>Mt?ne.updateSmartPlug(Mt.id,{auto_off:bt}):Promise.reject("No plug"),onSuccess:()=>{v.invalidateQueries({queryKey:["smartPlugByPrinter",t.id]}),v.invalidateQueries({queryKey:["smart-plugs"]})}}),et=ot({mutationFn:bt=>ne.controlSmartPlug(bt,"on"),onSuccess:()=>{N(b("printers.toast.scriptTriggered"))},onError:bt=>N(bt.message||b("printers.toast.failedToRunScript"),"error")}),lt=ot({mutationFn:()=>ne.stopPrint(t.id),onSuccess:()=>{N(b("printers.toast.printStopped")),v.invalidateQueries({queryKey:["printerStatus",t.id]})},onError:bt=>N(bt.message||b("printers.toast.failedToStopPrint"),"error")}),dt=ot({mutationFn:()=>ne.pausePrint(t.id),onSuccess:()=>{N(b("printers.toast.printPaused")),v.invalidateQueries({queryKey:["printerStatus",t.id]})},onError:bt=>N(bt.message||b("printers.toast.failedToPausePrint"),"error")}),Lt=ot({mutationFn:()=>ne.resumePrint(t.id),onSuccess:()=>{N(b("printers.toast.printResumed")),v.invalidateQueries({queryKey:["printerStatus",t.id]})},onError:bt=>N(bt.message||b("printers.toast.failedToResumePrint"),"error")}),ge=ot({mutationFn:bt=>ne.setChamberLight(t.id,bt),onMutate:async bt=>{await v.cancelQueries({queryKey:["printerStatus",t.id]});const Pt=v.getQueryData(["printerStatus",t.id]);return v.setQueryData(["printerStatus",t.id],Hn=>({...Hn,chamber_light:bt})),{previousStatus:Pt}},onSuccess:(bt,Pt)=>{N(`Chamber light ${Pt?"on":"off"}`)},onError:(bt,Pt,Hn)=>{Hn?.previousStatus&&v.setQueryData(["printerStatus",t.id],Hn.previousStatus),N(bt.message||b("printers.toast.failedToControlChamberLight"),"error")}}),ze=ot({mutationFn:bt=>ne.updatePrinter(t.id,{plate_detection_enabled:bt}),onSuccess:()=>{v.invalidateQueries({queryKey:["printers"]}),N(ze.variables?b("printers.toast.plateCheckEnabled"):b("printers.toast.plateCheckDisabled"))},onError:bt=>N(bt.message||b("printers.toast.failedToUpdateSetting"),"error")}),qe=(we?.state==="RUNNING"||we?.state==="PAUSE"||we?.state==="PAUSED")&&(we?.printable_objects_count??0)>=2,{data:Ve}=Ze({queryKey:["printableObjects",t.id],queryFn:()=>ne.getPrintableObjects(t.id),enabled:de||qe,refetchInterval:de?5e3:qe?3e4:!1}),[Pe,st]=w.useState(null),tt=w.useRef(!1),at=w.useRef(null),At=w.useRef(!1),Kt=ot({mutationFn:({amsId:bt,slotId:Pt})=>ne.refreshAmsSlot(t.id,bt,Pt),onMutate:({amsId:bt,slotId:Pt})=>{at.current&&clearTimeout(at.current),tt.current=!1,At.current=!1,st({amsId:bt,slotId:Pt}),setTimeout(()=>{At.current=!0},2e3),at.current=setTimeout(()=>{st(null)},3e4)},onSuccess:bt=>{N(bt.message||b("printers.toast.rfidRereadInitiated"))},onError:bt=>{N(bt.message||b("printers.toast.failedToRereadRfid"),"error"),at.current&&clearTimeout(at.current),st(null)}}),[Ot,jt]=w.useState(null),[Ht,Zt]=w.useState(null),rn=async()=>{try{const bt=await ne.getPlateReferences(t.id);jt(bt)}catch{}},An=()=>{ze.mutate(!t.plate_detection_enabled)},pn=async()=>{D(!0),fe(null);const bt=we?.chamber_light===!1;rt(bt),bt&&(await ne.setChamberLight(t.id,!0),await new Promise(Pt=>setTimeout(Pt,2500)));try{const Pt=await ne.checkPlateEmpty(t.id,{includeDebugImage:!0});fe(Pt),rn()}catch(Pt){N(Pt instanceof Error?Pt.message:b("printers.toast.failedToCheckPlate"),"error"),bt&&(await ne.setChamberLight(t.id,!1),rt(!1))}finally{D(!1)}},Xt=w.useCallback(async()=>{fe(null),Qe&&(await ne.setChamberLight(t.id,!1),rt(!1))},[Qe,t.id]),on=async bt=>{me(!0);try{const Pt=await ne.calibratePlateDetection(t.id,{label:bt});if(Pt.success){N(Pt.message||b("printers.toast.calibrationSaved"),"success"),rn();const Hn=await ne.checkPlateEmpty(t.id,{includeDebugImage:!0});fe(Hn)}else N(Pt.message||b("printers.toast.calibrationFailed"),"error")}catch(Pt){N(Pt instanceof Error?Pt.message:b("printers.toast.calibrationFailed"),"error")}finally{me(!1)}},fn=async(bt,Pt)=>{try{await ne.updatePlateReferenceLabel(t.id,bt,Pt),Zt(null),rn()}catch(Hn){N(Hn instanceof Error?Hn.message:b("printers.toast.failedToUpdateLabel"),"error")}},Ar=async bt=>{try{await ne.deletePlateReference(t.id,bt),N(b("printers.toast.referenceDeleted"),"success"),rn();const Pt=await ne.checkPlateEmpty(t.id,{includeDebugImage:!0});fe(Pt)}catch(Pt){N(Pt instanceof Error?Pt.message:b("printers.toast.failedToDeleteReference"),"error")}},Qr=async()=>{if(xe){Oe(!0);try{await ne.updatePrinter(t.id,{plate_detection_roi:xe}),N(b("printers.toast.detectionAreaSaved"),"success"),be(null);const bt=await ne.checkPlateEmpty(t.id,{includeDebugImage:!0});fe(bt)}catch(bt){N(bt instanceof Error?bt.message:b("printers.toast.failedToSaveDetectionArea"),"error")}finally{Oe(!1)}}};w.useEffect(()=>{const bt=Pt=>{Pt.key==="Escape"&&ye&&Xt()};return window.addEventListener("keydown",bt),()=>window.removeEventListener("keydown",bt)},[ye,Xt]);const gn=w.useRef(null);w.useEffect(()=>{if(!Pe)return;const bt=we?.ams_status_main??0;return bt!==0&&(tt.current=!0,gn.current&&(clearTimeout(gn.current),gn.current=null)),tt.current&&bt===0&&(At.current?(at.current&&clearTimeout(at.current),st(null)):gn.current||(gn.current=setTimeout(()=>{at.current&&clearTimeout(at.current),st(null)},2e3))),()=>{gn.current&&clearTimeout(gn.current)}},[we?.ams_status_main,Pe]);const[Zr,Kn]=w.useState(null);if(cr)return null;const vi=()=>{switch(i){case 1:return"w-10 h-10";case 2:return"w-14 h-14";case 3:return"w-16 h-16";case 4:return"w-20 h-20";default:return"w-14 h-14"}},mi=()=>{switch(i){case 1:return"text-base truncate";case 2:return"text-lg";case 3:return"text-xl";case 4:return"text-2xl";default:return"text-lg"}},fi=()=>{switch(i){case 1:return"mb-2";case 2:return"mb-4";case 3:return"mb-5";case 4:return"mb-6";default:return"mb-4"}};return a.jsxs(vt,{className:"relative",children:[a.jsxs(Nt,{className:i>=3?"p-5":"",children:[a.jsxs("div",{className:fi(),children:[a.jsxs("div",{className:"flex items-start justify-between gap-2",children:[a.jsxs("div",{className:"flex items-center gap-3 min-w-0 flex-1",children:[a.jsx("img",{src:WHe(t.model),alt:t.model||b("common.printer"),className:`object-contain rounded-lg bg-bambu-dark flex-shrink-0 ${vi()}`}),a.jsxs("div",{className:"min-w-0 flex-1",children:[a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("h3",{className:`font-semibold text-white ${mi()}`,children:t.name}),r==="compact"&&a.jsx("div",{className:`w-2 h-2 rounded-full flex-shrink-0 ${we?.connected?"bg-status-ok":"bg-status-error"}`,title:we?.connected?b("printers.connection.connected"):b("printers.connection.offline")})]}),a.jsxs("p",{className:"text-sm text-bambu-gray",children:[t.model||"Unknown Model",r==="expanded"&&we?.nozzles&&we.nozzles[0]?.nozzle_diameter&&a.jsxs("span",{className:"ml-1.5 text-bambu-gray",title:we.nozzles[0].nozzle_type||"Nozzle",children:["• ",we.nozzles[0].nozzle_diameter,"mm"]}),r==="expanded"&&n&&n.total_print_hours>0&&a.jsxs("span",{className:"ml-2 text-bambu-gray",children:[a.jsx(Wn,{className:"w-3 h-3 inline-block mr-1"}),Math.round(n.total_print_hours),"h"]})]})]})]}),a.jsxs("div",{className:"relative flex-shrink-0",children:[a.jsx(ke,{variant:"ghost",size:"sm",onClick:()=>M(!k),children:a.jsx(Eh,{className:"w-4 h-4"})}),k&&a.jsxs("div",{className:"absolute right-0 mt-2 w-48 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg shadow-lg z-20",children:[a.jsxs("button",{className:`w-full px-4 py-2 text-left text-sm flex items-center gap-2 ${T("printers:update")?"hover:bg-bambu-dark-tertiary":"opacity-50 cursor-not-allowed"}`,onClick:()=>{T("printers:update")&&(O(!0),M(!1))},title:T("printers:update")?void 0:b("printers.permission.noEdit"),children:[a.jsx(Vi,{className:"w-4 h-4"}),b("common.edit")]}),a.jsxs("button",{className:"w-full px-4 py-2 text-left text-sm hover:bg-bambu-dark-tertiary flex items-center gap-2",onClick:()=>{ai.mutate(),M(!1)},children:[a.jsx(Cr,{className:"w-4 h-4"}),b("printers.reconnect")]}),a.jsxs("button",{className:"w-full px-4 py-2 text-left text-sm hover:bg-bambu-dark-tertiary flex items-center gap-2",onClick:()=>{G(!0),M(!1)},children:[a.jsx(wX,{className:"w-4 h-4"}),b("printers.mqttDebug")]}),a.jsxs("button",{className:`w-full px-4 py-2 text-left text-sm flex items-center gap-2 ${T("printers:delete")?"text-red-400 hover:bg-bambu-dark-tertiary":"text-red-400/50 cursor-not-allowed"}`,onClick:()=>{T("printers:delete")&&(j(!0),M(!1))},title:T("printers:delete")?void 0:b("printers.permission.noDelete"),children:[a.jsx(Jt,{className:"w-4 h-4"}),b("common.delete")]})]})]})]}),r==="expanded"&&a.jsxs("div",{className:"flex flex-wrap items-center gap-2 mt-2",children:[a.jsxs("span",{className:`flex items-center gap-1.5 px-2 py-1 rounded-full text-xs ${we?.connected?"bg-status-ok/20 text-status-ok":"bg-status-error/20 text-status-error"}`,children:[we?.connected?a.jsx(Kb,{className:"w-3 h-3"}):a.jsx(hk,{className:"w-3 h-3"}),we?.connected?b("printers.connection.connected"):b("printers.connection.offline")]}),we?.connected&&Je!=null&&a.jsxs("span",{className:`flex items-center gap-1 px-2 py-1 rounded-full text-xs ${Je>=-50||Je>=-60?"bg-status-ok/20 text-status-ok":Je>=-70?"bg-status-warning/20 text-status-warning":Je>=-80?"bg-orange-500/20 text-orange-600":"bg-status-error/20 text-status-error"}`,title:`WiFi: ${Je} dBm - ${b(KHe(Je).labelKey)}`,children:[a.jsx(qfe,{className:"w-3 h-3"}),Je,"dBm"]}),we?.connected&&(()=>{const bt=we.hms_errors?dTe(we.hms_errors):[];return a.jsxs("button",{onClick:()=>X(!0),className:`flex items-center gap-1 px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80 transition-opacity ${bt.length>0?bt.some(Pt=>Pt.severity<=2)?"bg-status-error/20 text-status-error":"bg-status-warning/20 text-status-warning":"bg-status-ok/20 text-status-ok"}`,title:b("printers.clickToViewHmsErrors"),children:[a.jsx(kn,{className:"w-3 h-3"}),bt.length>0?bt.length:"OK"]})})(),n&&a.jsxs("button",{onClick:()=>_("/maintenance"),className:`flex items-center gap-1 px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80 transition-opacity ${n.due_count>0?"bg-status-error/20 text-status-error":n.warning_count>0?"bg-status-warning/20 text-status-warning":"bg-status-ok/20 text-status-ok"}`,title:n.due_count>0||n.warning_count>0?`${n.due_count>0?`${n.due_count} maintenance due`:""}${n.due_count>0&&n.warning_count>0?", ":""}${n.warning_count>0?`${n.warning_count} due soon`:""} - Click to view`:b("printers.maintenanceUpToDate"),children:[a.jsx(Of,{className:"w-3 h-3"}),n.due_count>0||n.warning_count>0?n.due_count+n.warning_count:"OK"]}),Bt>0&&a.jsxs("button",{onClick:()=>_("/queue"),className:"flex items-center gap-1 px-2 py-1 rounded-full text-xs bg-purple-500/20 text-purple-400 hover:opacity-80 transition-opacity",title:b("printers.queue.inQueue",{count:Bt}),children:[a.jsx(ha,{className:"w-3 h-3"}),Bt]}),y&&ve?.current_version&&ve?.latest_version?a.jsxs("button",{onClick:()=>Ne(!0),className:`flex items-center gap-1 px-2 py-1 rounded-full text-xs hover:opacity-80 transition-opacity ${ve.update_available?"bg-orange-500/20 text-orange-400":"bg-status-ok/20 text-status-ok"}`,title:ve.update_available?b("printers.firmwareUpdateAvailable",{current:ve.current_version,latest:ve.latest_version}):b("printers.firmwareUpToDate",{version:ve.current_version}),children:[ve.update_available?a.jsx(di,{className:"w-3 h-3"}):a.jsx(xr,{className:"w-3 h-3"}),ve.current_version]}):we?.firmware_version?a.jsx("span",{className:"flex items-center gap-1 px-2 py-1 rounded-full text-xs bg-bambu-dark-tertiary/50 text-bambu-gray",children:we.firmware_version}):null]})]}),C&&a.jsx("div",{className:"fixed inset-0 bg-black/50 flex items-center justify-center z-50",children:a.jsx(vt,{className:"w-full max-w-md mx-4",children:a.jsxs(Nt,{children:[a.jsxs("div",{className:"flex items-start gap-3 mb-4",children:[a.jsx("div",{className:"p-2 rounded-full bg-red-500/20",children:a.jsx(kn,{className:"w-5 h-5 text-red-400"})}),a.jsxs("div",{children:[a.jsx("h3",{className:"text-lg font-semibold text-white",children:b("printers.confirm.deleteTitle")}),a.jsx("p",{className:"text-sm text-bambu-gray mt-1",children:b("printers.confirm.deleteMessage",{name:t.name})})]})]}),a.jsx("div",{className:"bg-bambu-dark rounded-lg p-3 mb-4",children:a.jsxs("label",{className:"flex items-start gap-3 cursor-pointer",children:[a.jsx("input",{type:"checkbox",checked:P,onChange:bt=>E(bt.target.checked),className:"mt-0.5 w-4 h-4 rounded border-bambu-gray bg-bambu-dark-secondary text-bambu-green focus:ring-bambu-green focus:ring-offset-0"}),a.jsxs("div",{children:[a.jsx("span",{className:"text-sm text-white",children:b("printers.deleteArchives")}),a.jsx("p",{className:"text-xs text-bambu-gray mt-0.5",children:b(P?"printers.confirm.deleteArchivesNote":"printers.confirm.keepArchivesNote")})]})]})}),a.jsxs("div",{className:"flex justify-end gap-2",children:[a.jsx(ke,{variant:"secondary",onClick:()=>{j(!1),E(!0)},children:b("common.cancel")}),a.jsx(ke,{variant:"danger",onClick:()=>{Dn.mutate({deleteArchives:P}),j(!1),E(!0)},children:"Delete"})]})]})})}),we?.connected&&a.jsxs(a.Fragment,{children:[r==="compact"?a.jsx("div",{className:"mt-2",children:we.state==="RUNNING"?a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("div",{className:"flex-1 bg-bambu-dark-tertiary rounded-full h-1.5",children:a.jsx("div",{className:"bg-bambu-green h-1.5 rounded-full transition-all",style:{width:`${we.progress||0}%`}})}),a.jsxs("span",{className:"text-xs text-white",children:[Math.round(we.progress||0),"%"]})]}):a.jsx("p",{className:"text-xs text-bambu-gray",children:h9(we.state,we.stg_cur_name)})}):a.jsxs(a.Fragment,{children:[a.jsxs("div",{className:"mb-4 p-3 bg-bambu-dark rounded-lg relative",children:[a.jsxs("button",{onClick:()=>ce(!0),disabled:!(we.state==="RUNNING"||we.state==="PAUSE"||we.state==="PAUSED")||(we.printable_objects_count??0)<2||!T("printers:control"),className:`absolute top-2 right-2 p-1.5 rounded transition-colors z-10 ${(we.state==="RUNNING"||we.state==="PAUSE"||we.state==="PAUSED")&&(we.printable_objects_count??0)>=2&&T("printers:control")?"text-bambu-gray hover:text-white hover:bg-white/10":"text-bambu-gray/30 cursor-not-allowed"}`,title:T("printers:control")?we.state==="RUNNING"||we.state==="PAUSE"||we.state==="PAUSED"?(we.printable_objects_count??0)>=2?b("printers.skipObjects.tooltip"):b("printers.skipObjects.requiresMultiple"):b("printers.skipObjects.onlyWhilePrinting"):b("printers.permission.noControl"),children:[a.jsx(Nk,{className:"w-4 h-4"}),Ve&&Ve.skipped_count>0&&a.jsx("span",{className:"absolute -top-1 -right-1 min-w-[16px] h-4 px-1 flex items-center justify-center text-[10px] font-bold bg-red-500 text-white rounded-full",children:Ve.skipped_count})]}),a.jsxs("div",{className:"flex gap-3",children:[a.jsx(XHe,{url:we.state==="RUNNING"?we.cover_url:null,printName:we.state==="RUNNING"&&(we.subtask_name||we.current_print)||void 0}),a.jsx("div",{className:"flex-1 min-w-0",children:we.current_print&&we.state==="RUNNING"?a.jsxs(a.Fragment,{children:[a.jsx("p",{className:"text-sm text-bambu-gray mb-1",children:we.stg_cur_name||"Printing"}),a.jsx("p",{className:"text-white text-sm mb-2 truncate",children:we.subtask_name||we.current_print}),a.jsxs("div",{className:"flex items-center justify-between text-sm",children:[a.jsx("div",{className:"flex-1 bg-bambu-dark-tertiary rounded-full h-2 mr-3",children:a.jsx("div",{className:"bg-bambu-green h-2 rounded-full transition-all",style:{width:`${we.progress||0}%`}})}),a.jsxs("span",{className:"text-white",children:[Math.round(we.progress||0),"%"]})]}),a.jsxs("div",{className:"flex items-center gap-3 mt-2 text-xs text-bambu-gray",children:[we.remaining_time!=null&&we.remaining_time>0&&a.jsxs(a.Fragment,{children:[a.jsxs("span",{className:"flex items-center gap-1",children:[a.jsx(Wn,{className:"w-3 h-3"}),Zs(we.remaining_time*60)]}),a.jsxs("span",{className:"text-bambu-green font-medium",title:b("printers.estimatedCompletion"),children:["ETA ",VF(we.remaining_time,f,b)]})]}),we.layer_num!=null&&we.total_layers!=null&&we.total_layers>0&&a.jsxs("span",{className:"flex items-center gap-1",children:[a.jsx(ha,{className:"w-3 h-3"}),we.layer_num,"/",we.total_layers]}),Zn&&a.jsxs("span",{className:"flex items-center gap-1",title:`Started by ${Zn}`,children:[a.jsx(rd,{className:"w-3 h-3"}),Zn]})]})]}):a.jsxs(a.Fragment,{children:[a.jsx("p",{className:"text-sm text-bambu-gray mb-1",children:b("printers.sort.status")}),a.jsx("p",{className:"text-white text-sm mb-2",children:h9(we.state,we.stg_cur_name)}),a.jsxs("div",{className:"flex items-center justify-between text-sm",children:[a.jsx("div",{className:"flex-1 bg-bambu-dark-tertiary rounded-full h-2 mr-3",children:a.jsx("div",{className:"bg-bambu-dark-tertiary h-2 rounded-full"})}),a.jsx("span",{className:"text-bambu-gray",children:"—"})]}),jn?a.jsxs("p",{className:"text-xs text-bambu-gray mt-2 truncate",title:jn.print_name||jn.filename,children:["Last: ",jn.print_name||jn.filename,jn.completed_at&&a.jsxs("span",{className:"ml-1 text-bambu-gray/60",children:["• ",bb(jn.completed_at,{month:"short",day:"numeric"})]})]}):a.jsx("p",{className:"text-xs text-bambu-gray mt-2",children:b("printers.readyToPrint")})]})})]})]}),a.jsx(fTe,{printerId:t.id,printerState:we.state,plateCleared:we.plate_cleared})]}),we.temperatures&&r==="expanded"&&(()=>{const bt=we.temperatures.nozzle_heating||we.temperatures.nozzle_2_heating||!1,Pt=we.temperatures.bed_heating||!1,Hn=we.temperatures.chamber_heating||!1,Ct=t.nozzle_count===2||we.temperatures.nozzle_2!==void 0,Nn=we.active_extruder===1?"L":"R",si=we.nozzle_rack?.find(Na=>Na.id===1),Fn=we.nozzle_rack?.find(Na=>Na.id===0),aa=Fn||si;return a.jsxs("div",{className:"flex items-stretch gap-1.5 flex-wrap",children:[a.jsxs("div",{className:"text-center px-2 py-1.5 bg-bambu-dark rounded-lg flex-1 flex flex-col justify-center items-center",children:[a.jsx(sj,{className:"w-3.5 h-3.5 mb-0.5",color:"text-orange-400",isHeating:bt}),we.temperatures.nozzle_2!==void 0?a.jsxs(a.Fragment,{children:[a.jsx("p",{className:"text-[9px] text-bambu-gray",children:"L / R"}),a.jsxs("p",{className:"text-[11px] text-white",children:[Math.round(we.temperatures.nozzle||0),"° / ",Math.round(we.temperatures.nozzle_2||0),"°"]})]}):aa?a.jsx(pne,{slot:aa,index:0,activeStatus:!0,filamentName:aa.filament_id?Le?.[aa.filament_id]?.name:void 0,children:a.jsxs("div",{className:"cursor-default",children:[a.jsx("p",{className:"text-[9px] text-bambu-gray",children:b("printers.temperatures.nozzle")}),a.jsxs("p",{className:"text-[11px] text-white",children:[Math.round(we.temperatures.nozzle||0),"°C"]})]})}):a.jsxs(a.Fragment,{children:[a.jsx("p",{className:"text-[9px] text-bambu-gray",children:b("printers.temperatures.nozzle")}),a.jsxs("p",{className:"text-[11px] text-white",children:[Math.round(we.temperatures.nozzle||0),"°C"]})]})]}),a.jsxs("div",{className:"text-center px-2 py-1.5 bg-bambu-dark rounded-lg flex-1 flex flex-col justify-center items-center",children:[a.jsx(sj,{className:"w-3.5 h-3.5 mb-0.5",color:"text-blue-400",isHeating:Pt}),a.jsx("p",{className:"text-[9px] text-bambu-gray",children:b("printers.temperatures.bed")}),a.jsxs("p",{className:"text-[11px] text-white",children:[Math.round(we.temperatures.bed||0),"°C"]})]}),we.temperatures.chamber!==void 0&&a.jsxs("div",{className:"text-center px-2 py-1.5 bg-bambu-dark rounded-lg flex-1 flex flex-col justify-center items-center",children:[a.jsx(sj,{className:"w-3.5 h-3.5 mb-0.5",color:"text-green-400",isHeating:Hn}),a.jsx("p",{className:"text-[9px] text-bambu-gray",children:b("printers.temperatures.chamber")}),a.jsxs("p",{className:"text-[11px] text-white",children:[Math.round(we.temperatures.chamber||0),"°C"]})]}),Ct&&a.jsx(UHe,{leftSlot:si,rightSlot:Fn,activeNozzle:Nn,filamentInfo:Le,children:a.jsxs("div",{className:"text-center px-3 py-1.5 bg-bambu-dark rounded-lg h-full flex flex-col justify-center items-center cursor-default",title:b("printers.activeNozzle",{nozzle:b(Nn==="L"?"common.left":"common.right")}),children:[a.jsxs("div",{className:"flex items-center gap-2 mb-1",children:[a.jsxs("span",{className:`text-[11px] font-bold ${Nn==="L"?"text-amber-400":"text-gray-500"}`,children:["L",si?.nozzle_diameter?` ${si.nozzle_diameter}`:""]}),a.jsx("span",{className:"text-[9px] text-bambu-gray/40",children:"·"}),a.jsxs("span",{className:`text-[11px] font-bold ${Nn==="R"?"text-amber-400":"text-gray-500"}`,children:["R",Fn?.nozzle_diameter?` ${Fn.nozzle_diameter}`:""]})]}),a.jsx("p",{className:"text-[9px] text-bambu-gray",children:b("printers.temperatures.nozzle")})]})}),we.nozzle_rack&&we.nozzle_rack.some(Na=>Na.id>=2)&&a.jsx(BHe,{slots:we.nozzle_rack,filamentInfo:Le})]})})(),r==="expanded"&&(()=>{const bt=we.state==="RUNNING",Pt=we.state==="PAUSED"||we.state==="PAUSE",Hn=bt||Pt,Ct=lt.isPending||dt.isPending||Lt.isPending,Nn=we.cooling_fan_speed,si=we.big_fan1_speed,Fn=we.big_fan2_speed;return a.jsxs("div",{className:"mt-3",children:[a.jsxs("div",{className:"flex items-center gap-2 mb-2",children:[a.jsx("span",{className:"text-[10px] uppercase tracking-wider text-bambu-gray font-medium",children:b("printers.controls")}),a.jsx("div",{className:"flex-1 h-px bg-bambu-dark-tertiary/30"})]}),a.jsxs("div",{className:"flex items-center justify-between gap-2 max-[550px]:items-start",children:[a.jsxs("div",{className:"flex items-center gap-2 min-w-0 max-[550px]:flex-wrap max-[550px]:items-start max-[550px]:gap-1.5",children:[a.jsxs("div",{className:`flex items-center gap-1 px-1.5 py-1 rounded ${Nn&&Nn>0?"bg-cyan-500/10":"bg-bambu-dark"}`,title:b("printers.fans.partCooling"),children:[a.jsx(hX,{className:`w-3.5 h-3.5 ${Nn&&Nn>0?"text-cyan-400":"text-bambu-gray/50"}`}),a.jsxs("span",{className:`text-[10px] ${Nn&&Nn>0?"text-cyan-400":"text-bambu-gray/50"}`,children:[Nn??0,"%"]})]}),a.jsxs("div",{className:`flex items-center gap-1 px-1.5 py-1 rounded ${si&&si>0?"bg-blue-500/10":"bg-bambu-dark"}`,title:b("printers.fans.auxiliary"),children:[a.jsx(_X,{className:`w-3.5 h-3.5 ${si&&si>0?"text-blue-400":"text-bambu-gray/50"}`}),a.jsxs("span",{className:`text-[10px] ${si&&si>0?"text-blue-400":"text-bambu-gray/50"}`,children:[si??0,"%"]})]}),a.jsxs("div",{className:`flex items-center gap-1 px-1.5 py-1 rounded ${Fn&&Fn>0?"bg-green-500/10":"bg-bambu-dark"}`,title:b("printers.fans.chamber"),children:[a.jsx(dde,{className:`w-3.5 h-3.5 ${Fn&&Fn>0?"text-green-400":"text-bambu-gray/50"}`}),a.jsxs("span",{className:`text-[10px] ${Fn&&Fn>0?"text-green-400":"text-bambu-gray/50"}`,children:[Fn??0,"%"]})]})]}),a.jsxs("div",{className:"flex items-center gap-2 flex-shrink-0 max-[550px]:self-start",children:[a.jsxs("button",{onClick:()=>J(!0),disabled:!Hn||Ct||!T("printers:control"),className:` flex items-center justify-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${Hn&&T("printers:control")?"bg-red-500/20 text-red-400 hover:bg-red-500/30":"bg-bambu-dark text-bambu-gray/50 cursor-not-allowed"} diff --git a/static/index.html b/static/index.html index ec3e4e200..acb66068e 100644 --- a/static/index.html +++ b/static/index.html @@ -23,7 +23,7 @@ - +