Просмотр исходного кода

Add HMS error notifications with translations and plate detection fixes

New Features:
- HMS error notifications with human-readable messages (853 error codes)
- Plate not empty notification category (separate toggle in settings)
- Module-friendly names: Print/Task, AMS/Filament, Nozzle/Extruder, etc.
- Backup/restore now includes plate calibration reference images

Fixes:
- Plate calibration images now persist after restart in Docker
- Telegram notifications no longer fail on error codes with underscores
- Plate detection combo button: main toggles detection, chevron opens modal

Tests:
- Added HMS error code tests (10 tests)
- Implemented Dashboard, FileManagerModal, UploadModal tests (previously skipped)
- Added notification service tests for HMS errors and plate detection
- Fixed AuthContext unmount memory leak in tests
maziggy 4 месяцев назад
Родитель
Сommit
b9495877fc

+ 14 - 1
CHANGELOG.md

@@ -5,6 +5,17 @@ All notable changes to Bambuddy will be documented in this file.
 ## [0.1.6] - Not released
 
 ### New Features
+- **HMS Error Notifications** - Get notified when printer errors occur (Issue #84):
+  - Automatic notifications for HMS errors (AMS issues, nozzle problems, etc.)
+  - Human-readable error messages (853 error codes translated)
+  - Friendly error type names (Print/Task, AMS/Filament, Nozzle/Extruder, Motion Controller, Chamber)
+  - Deduplication prevents spam from repeated error messages
+  - Publishes to MQTT relay for home automation integrations
+  - New "Printer Error" toggle in notification provider settings
+- **Plate Not Empty Notification** - Dedicated notification category for build plate detection:
+  - New toggle in notification provider settings (enabled by default)
+  - Sends immediately (bypasses quiet hours and digest mode)
+  - Separate from general printer errors for granular control
 - **USB Camera Support** - Connect USB webcams directly to your Bambuddy host:
   - New "USB Camera (V4L2)" option in external camera settings
   - Auto-detection of available USB cameras via V4L2
@@ -20,7 +31,7 @@ All notable changes to Bambuddy will be documented in this file.
   - Reference management: View thumbnails, add labels, delete references
   - Works with both built-in and external cameras
   - Uses buffered camera frames when stream is active (no blocking)
-  - Split button UI: Main button opens calibration modal, chevron toggles detection on/off
+  - Split button UI: Main button toggles detection on/off, chevron opens calibration modal
   - Green visual indicator when plate detection is enabled
   - Included in backup/restore
 - **Project Import/Export** - Export and import projects with full file support (Issue #152):
@@ -52,6 +63,8 @@ All notable changes to Bambuddy will be documented in this file.
   - Bulk delete for multiple files at once
 
 ### Fixes
+- **Plate Calibration Persistence** - Fixed plate detection reference images not persisting after restart in Docker deployments
+- **Telegram Notification Parsing** - Fixed Telegram markdown parsing errors when messages contain underscores (e.g., error codes)
 - **Settings API PATCH Method** - Added PATCH support to `/api/settings` for Home Assistant rest_command compatibility (Issue #152)
 - **P2S Empty Archive Tiles** - Fixed FTP file search for printers without SD card (Issue #146):
   - Added root folder `/` to search paths when looking for 3MF files

+ 2 - 0
README.md

@@ -113,6 +113,8 @@
 - Quiet hours & daily digest
 - Customizable message templates
 - Print finish photo URL in notifications
+- HMS error alerts (AMS, nozzle, etc.)
+- Build plate detection alerts
 
 ### 🔧 Integrations
 - [Spoolman](https://github.com/Donkie/Spoolman) filament sync

+ 4 - 0
backend/app/api/routes/notifications.py

@@ -51,6 +51,8 @@ def _provider_to_dict(provider: NotificationProvider) -> dict:
         # AMS-HT environmental alarms
         "on_ams_ht_humidity_high": provider.on_ams_ht_humidity_high,
         "on_ams_ht_temperature_high": provider.on_ams_ht_temperature_high,
+        # Build plate detection
+        "on_plate_not_empty": provider.on_plate_not_empty,
         # Quiet hours
         "quiet_hours_enabled": provider.quiet_hours_enabled,
         "quiet_hours_start": provider.quiet_hours_start,
@@ -112,6 +114,8 @@ async def create_notification_provider(
         # AMS-HT environmental alarms
         on_ams_ht_humidity_high=provider_data.on_ams_ht_humidity_high,
         on_ams_ht_temperature_high=provider_data.on_ams_ht_temperature_high,
+        # Build plate detection
+        on_plate_not_empty=provider_data.on_plate_not_empty,
         # Quiet hours
         quiet_hours_enabled=provider_data.quiet_hours_enabled,
         quiet_hours_start=provider_data.quiet_hours_start,

+ 29 - 1
backend/app/api/routes/settings.py

@@ -242,6 +242,7 @@ async def export_backup(
     include_smart_plugs: bool = Query(True, description="Include smart plugs"),
     include_external_links: bool = Query(True, description="Include external sidebar links"),
     include_printers: bool = Query(False, description="Include printers (without access codes)"),
+    include_plate_calibration: bool = Query(False, description="Include plate detection reference images"),
     include_filaments: bool = Query(False, description="Include filament inventory"),
     include_maintenance: bool = Query(
         False, description="Include maintenance types, per-printer settings, and history"
@@ -305,6 +306,7 @@ async def export_backup(
                     "on_ams_temperature_high": getattr(p, "on_ams_temperature_high", False),
                     "on_ams_ht_humidity_high": getattr(p, "on_ams_ht_humidity_high", False),
                     "on_ams_ht_temperature_high": getattr(p, "on_ams_ht_temperature_high", False),
+                    "on_plate_not_empty": getattr(p, "on_plate_not_empty", True),
                     "quiet_hours_enabled": p.quiet_hours_enabled,
                     "quiet_hours_start": p.quiet_hours_start,
                     "quiet_hours_end": p.quiet_hours_end,
@@ -429,6 +431,17 @@ async def export_backup(
         if include_access_codes:
             backup["included"].append("access_codes")
 
+    # Plate calibration references (requires include_printers)
+    if include_printers and include_plate_calibration:
+        plate_cal_dir = app_settings.plate_calibration_dir
+        if plate_cal_dir.exists():
+            backup["plate_calibration"] = []
+            for cal_file in plate_cal_dir.iterdir():
+                if cal_file.is_file():
+                    backup["plate_calibration"].append(cal_file.name)
+            if backup["plate_calibration"]:
+                backup["included"].append("plate_calibration")
+
     # Filaments
     if include_filaments:
         result = await db.execute(select(Filament))
@@ -588,6 +601,14 @@ async def export_backup(
                 if icon_path.exists():
                     backup_files.append((link_data["custom_icon_path"], icon_path))
 
+    # Add plate calibration reference images
+    if "plate_calibration" in backup:
+        plate_cal_dir = app_settings.plate_calibration_dir
+        for filename in backup["plate_calibration"]:
+            file_path = plate_cal_dir / filename
+            if file_path.exists():
+                backup_files.append((f"plate_calibration/{filename}", file_path))
+
     # Print archives with file paths for ZIP
     if include_archives:
         result = await db.execute(select(PrintArchive))
@@ -882,7 +903,12 @@ async def import_backup(
                         # Ensure path is safe (no path traversal)
                         if ".." in zip_path or zip_path.startswith("/"):
                             continue
-                        target_path = base_dir / zip_path
+                        # Plate calibration files go to plate_calibration_dir (may differ from base_dir)
+                        if zip_path.startswith("plate_calibration/"):
+                            filename = zip_path.replace("plate_calibration/", "", 1)
+                            target_path = app_settings.plate_calibration_dir / filename
+                        else:
+                            target_path = base_dir / zip_path
                         target_path.parent.mkdir(parents=True, exist_ok=True)
                         with zf.open(zip_path) as src, open(target_path, "wb") as dst:
                             dst.write(src.read())
@@ -1064,6 +1090,7 @@ async def import_backup(
                     existing.on_ams_temperature_high = provider_data.get("on_ams_temperature_high", False)
                     existing.on_ams_ht_humidity_high = provider_data.get("on_ams_ht_humidity_high", False)
                     existing.on_ams_ht_temperature_high = provider_data.get("on_ams_ht_temperature_high", False)
+                    existing.on_plate_not_empty = provider_data.get("on_plate_not_empty", True)
                     existing.quiet_hours_enabled = provider_data.get("quiet_hours_enabled", False)
                     existing.quiet_hours_start = provider_data.get("quiet_hours_start")
                     existing.quiet_hours_end = provider_data.get("quiet_hours_end")
@@ -1093,6 +1120,7 @@ async def import_backup(
                     on_ams_temperature_high=provider_data.get("on_ams_temperature_high", False),
                     on_ams_ht_humidity_high=provider_data.get("on_ams_ht_humidity_high", False),
                     on_ams_ht_temperature_high=provider_data.get("on_ams_ht_temperature_high", False),
+                    on_plate_not_empty=provider_data.get("on_plate_not_empty", True),
                     quiet_hours_enabled=provider_data.get("quiet_hours_enabled", False),
                     quiet_hours_start=provider_data.get("quiet_hours_start"),
                     quiet_hours_end=provider_data.get("quiet_hours_end"),

+ 7 - 0
backend/app/core/config.py

@@ -16,6 +16,11 @@ _app_dir = Path(__file__).resolve().parent.parent.parent.parent
 _data_dir_env = os.environ.get("DATA_DIR")
 _data_dir = Path(_data_dir_env) if _data_dir_env else _app_dir
 
+# Plate calibration directory - special handling to maintain backwards compatibility
+# Docker: DATA_DIR/plate_calibration (e.g., /data/plate_calibration)
+# Local dev: project_root/data/plate_calibration (original location)
+_plate_cal_dir = Path(_data_dir_env) / "plate_calibration" if _data_dir_env else _app_dir / "data" / "plate_calibration"
+
 # Log directory - use LOG_DIR env var if set, otherwise use app_dir/logs
 _log_dir_env = os.environ.get("LOG_DIR")
 _log_dir = Path(_log_dir_env) if _log_dir_env else _app_dir / "logs"
@@ -52,6 +57,7 @@ class Settings(BaseSettings):
     # Paths
     base_dir: Path = _data_dir  # For backwards compatibility
     archive_dir: Path = _data_dir / "archive"
+    plate_calibration_dir: Path = _plate_cal_dir  # Plate detection references
     static_dir: Path = _app_dir / "static"  # Static files are part of app, not data
     log_dir: Path = _log_dir
     database_url: str = f"sqlite+aiosqlite:///{_db_path}"
@@ -72,6 +78,7 @@ settings = Settings()
 
 # Ensure directories exist
 settings.archive_dir.mkdir(exist_ok=True)
+settings.plate_calibration_dir.mkdir(exist_ok=True)
 settings.static_dir.mkdir(exist_ok=True)
 if settings.log_to_file:
     settings.log_dir.mkdir(exist_ok=True)

+ 33 - 16
backend/app/core/database.py

@@ -288,6 +288,12 @@ async def run_migrations(conn):
     except Exception:
         pass
 
+    # Migration: Add plate not empty notification column to notification_providers
+    try:
+        await conn.execute(text("ALTER TABLE notification_providers ADD COLUMN on_plate_not_empty BOOLEAN DEFAULT 1"))
+    except Exception:
+        pass
+
     # Migration: Add notes column to projects (Phase 2)
     try:
         await conn.execute(text("ALTER TABLE projects ADD COLUMN notes TEXT"))
@@ -761,21 +767,32 @@ async def seed_notification_templates():
     from backend.app.models.notification_template import DEFAULT_TEMPLATES, NotificationTemplate
 
     async with async_session() as session:
-        # Check if templates already exist
-        result = await session.execute(select(NotificationTemplate).limit(1))
-        if result.scalar_one_or_none() is not None:
-            # Templates already seeded
-            return
-
-        # Insert default templates
-        for template_data in DEFAULT_TEMPLATES:
-            template = NotificationTemplate(
-                event_type=template_data["event_type"],
-                name=template_data["name"],
-                title_template=template_data["title_template"],
-                body_template=template_data["body_template"],
-                is_default=True,
-            )
-            session.add(template)
+        # Get existing template event types
+        result = await session.execute(select(NotificationTemplate.event_type))
+        existing_types = {row[0] for row in result.fetchall()}
+
+        if not existing_types:
+            # No templates exist - insert all defaults
+            for template_data in DEFAULT_TEMPLATES:
+                template = NotificationTemplate(
+                    event_type=template_data["event_type"],
+                    name=template_data["name"],
+                    title_template=template_data["title_template"],
+                    body_template=template_data["body_template"],
+                    is_default=True,
+                )
+                session.add(template)
+        else:
+            # Templates exist - only add missing ones
+            for template_data in DEFAULT_TEMPLATES:
+                if template_data["event_type"] not in existing_types:
+                    template = NotificationTemplate(
+                        event_type=template_data["event_type"],
+                        name=template_data["name"],
+                        title_template=template_data["title_template"],
+                        body_template=template_data["body_template"],
+                        is_default=True,
+                    )
+                    session.add(template)
 
         await session.commit()

+ 85 - 4
backend/app/main.py

@@ -239,6 +239,10 @@ _reprint_archives: set[int] = set()
 # Milestones are 25, 50, 75. Value of 0 means no milestone notified yet for current print.
 _last_progress_milestone: dict[int, int] = {}
 
+# Track HMS errors that have been notified: {printer_id: set of error codes}
+# This prevents sending duplicate notifications for the same error
+_notified_hms_errors: dict[int, set[str]] = {}
+
 
 async def _get_plug_energy(plug, db) -> dict | None:
     """Get energy from plug regardless of type (Tasmota or Home Assistant).
@@ -444,6 +448,85 @@ async def on_printer_status_change(printer_id: int, state: PrinterState):
         # Reset milestone tracking when print restarts or new print begins
         _last_progress_milestone[printer_id] = 0
 
+    # Check for new HMS errors and send notifications
+    current_hms_errors = getattr(state, "hms_errors", []) or []
+    if current_hms_errors:
+        # Build set of current error codes (using attr for uniqueness)
+        current_error_codes = {f"{e.attr:08x}" for e in current_hms_errors}
+        previously_notified = _notified_hms_errors.get(printer_id, set())
+
+        # Find new errors that haven't been notified yet
+        new_error_codes = current_error_codes - previously_notified
+
+        if new_error_codes:
+            # Get the actual new errors for the notification
+            new_errors = [e for e in current_hms_errors if f"{e.attr:08x}" in new_error_codes]
+
+            try:
+                async with async_session() as db:
+                    from backend.app.models.printer import Printer
+
+                    result = await db.execute(select(Printer).where(Printer.id == printer_id))
+                    printer = result.scalar_one_or_none()
+                    printer_name = printer.name if printer else f"Printer {printer_id}"
+
+                    # Format error details for notification
+                    # Module 0x07 = AMS/Filament, 0x05 = Nozzle, 0x0C = Motion Controller, etc.
+                    module_names = {
+                        0x03: "Print/Task",
+                        0x05: "Nozzle/Extruder",
+                        0x07: "AMS/Filament",
+                        0x0C: "Motion Controller",
+                        0x12: "Chamber",
+                    }
+
+                    from backend.app.services.hms_errors import get_error_description
+
+                    for error in new_errors:
+                        module_name = module_names.get(error.module, f"Module 0x{error.module:02X}")
+                        # Build short code like "0700_8010"
+                        error_code_int = int(error.code.replace("0x", ""), 16) if error.code else 0
+                        short_code = f"{(error.attr >> 16) & 0xFFFF:04X}_{error_code_int:04X}"
+
+                        error_type = f"{module_name} Error"
+                        # Look up human-readable description
+                        description = get_error_description(short_code)
+                        error_detail = description if description else f"Error code: {short_code}"
+
+                        await notification_service.on_printer_error(
+                            printer_id, printer_name, error_type, db, error_detail
+                        )
+
+                    logging.getLogger(__name__).info(
+                        f"[HMS] Sent notification for {len(new_errors)} new error(s) on printer {printer_id}"
+                    )
+
+                    # Also publish to MQTT relay
+                    printer_info = printer_manager.get_printer(printer_id)
+                    if printer_info:
+                        errors_data = [
+                            {
+                                "code": e.code,
+                                "attr": e.attr,
+                                "module": e.module,
+                                "severity": e.severity,
+                            }
+                            for e in new_errors
+                        ]
+                        await mqtt_relay.on_printer_error(
+                            printer_id, printer_info.name, printer_info.serial_number, errors_data
+                        )
+
+            except Exception as e:
+                logging.getLogger(__name__).warning(f"HMS error notification failed: {e}")
+
+            # Update tracking with all current errors
+            _notified_hms_errors[printer_id] = current_error_codes
+    else:
+        # No HMS errors - clear tracking so future errors get notified
+        if printer_id in _notified_hms_errors:
+            _notified_hms_errors.pop(printer_id, None)
+
     await ws_manager.send_printer_status(
         printer_id,
         printer_state_to_dict(state, printer_id, printer_manager.get_model(printer_id)),
@@ -684,13 +767,11 @@ async def on_print_start(printer_id: int, data: dict):
 
                     # Also send push notification
                     try:
-                        await notification_service.send_notification(
+                        await notification_service.on_plate_not_empty(
                             printer_id=printer_id,
                             printer_name=printer.name,
-                            event_type="plate_not_empty",
-                            title=f"⚠️ {printer.name}: Plate Not Empty!",
-                            body="Objects detected on build plate. Print has been paused. Please clear the plate and resume.",
                             db=db,
+                            difference_percent=plate_result.difference_percent,
                         )
                     except Exception as notif_err:
                         logger.warning(f"[PLATE CHECK] Failed to send notification: {notif_err}")

+ 3 - 0
backend/app/models/notification.py

@@ -80,6 +80,9 @@ class NotificationProvider(Base):
     on_ams_ht_humidity_high = Column(Boolean, default=False)  # AMS-HT humidity above threshold
     on_ams_ht_temperature_high = Column(Boolean, default=False)  # AMS-HT temperature above threshold
 
+    # Event triggers - Build plate detection
+    on_plate_not_empty = Column(Boolean, default=True)  # Objects detected on plate before print
+
     # Quiet hours (do not disturb)
     quiet_hours_enabled = Column(Boolean, default=False)
     quiet_hours_start = Column(String(5), nullable=True)  # HH:MM format, e.g., "22:00"

+ 6 - 0
backend/app/models/notification_template.py

@@ -67,6 +67,12 @@ DEFAULT_TEMPLATES = [
         "title_template": "Printer Error: {error_type}",
         "body_template": "{printer}\n{error_detail}",
     },
+    {
+        "event_type": "plate_not_empty",
+        "name": "Plate Not Empty",
+        "title_template": "Plate Not Empty - Print Paused",
+        "body_template": "{printer}: Objects detected on build plate. Print has been paused. Clear plate and resume.",
+    },
     {
         "event_type": "filament_low",
         "name": "Filament Low",

+ 6 - 0
backend/app/schemas/notification.py

@@ -50,6 +50,9 @@ class NotificationProviderBase(BaseModel):
         default=False, description="Notify when AMS-HT temperature exceeds threshold"
     )
 
+    # Event triggers - Build plate detection
+    on_plate_not_empty: bool = Field(default=True, description="Notify when objects detected on plate before print")
+
     # Quiet hours
     quiet_hours_enabled: bool = Field(default=False, description="Enable quiet hours")
     quiet_hours_start: str | None = Field(default=None, description="Start time in HH:MM format")
@@ -114,6 +117,9 @@ class NotificationProviderUpdate(BaseModel):
     on_ams_ht_humidity_high: bool | None = None
     on_ams_ht_temperature_high: bool | None = None
 
+    # Event triggers - Build plate detection
+    on_plate_not_empty: bool | None = None
+
     # Quiet hours
     quiet_hours_enabled: bool | None = None
     quiet_hours_start: str | None = None

+ 875 - 0
backend/app/services/hms_errors.py

@@ -0,0 +1,875 @@
+"""HMS Error Code Descriptions.
+
+Auto-generated from frontend/src/components/HMSErrorModal.tsx
+Source: https://github.com/greghesp/ha-bambulab
+"""
+
+# HMS error code to human-readable description mapping
+# Format: "XXXX_YYYY" where XXXX is module code, YYYY is error code
+HMS_ERROR_DESCRIPTIONS: dict[str, str] = {
+    "0300_4000": "Z axis homing failed; the task has been stopped.",
+    "0300_4001": "The printer timed out waiting for the nozzle to cool down before homing.",
+    "0300_4002": "Auto Bed Leveling failed; the task has been stopped.",
+    "0300_4005": "The hotend cooling fan speed is abnormal.",
+    "0300_4006": "The nozzle is clogged.",
+    "0300_4008": "The AMS failed to change filament.",
+    "0300_4009": "Homing XY axis failed.",
+    "0300_400A": "Mechanical resonance frequency identification failed.",
+    "0300_400B": "Internal communication exception",
+    "0300_400C": "The task was canceled.",
+    "0300_400D": "Resume failed after power loss.",
+    "0300_400E": "The motor self-check failed.",
+    "0300_400F": "The power supply voltage does not match the printer.",
+    "0300_4010": "Nozzle offset calibration failed.",
+    "0300_4011": "Flow Dynamics Calibration failed; please reinitiate printing or calibration.",
+    "0300_4013": "Printing cannot be initiated while AMS is drying.",
+    "0300_4014": "Homing Z axis failed: temperature control abnormality.",
+    "0300_4015": "Nozzle clumping detection calibration failed. Please go to 'Assistant' for troubleshooting.",
+    "0300_4016": "Nozzle cleaning failed. Please click the Assistant for troubleshooting.",
+    "0300_401F": "The hotend is not installed, and the toolhead cannot perform homing. Please install the hotend and then continue.",
+    "0300_4020": "The nozzle presence detection failed. Please check the Assistant for details.",
+    "0300_4021": "Nozzle offset calibration sensor signal abnormality detected. Please check the sensor and retry.",
+    "0300_4042": "The Laser Safety Window is not properly installed. The task has been stopped.",
+    "0300_4044": "The Flame Sensor is abnormal. The sensor may be short-circuited. Please troubleshoot the issue before starting a print job.",
+    "0300_404B": "Task aborted because the front door or top cover is open.",
+    "0300_404D": "The current temperature of the hotend, heatbed, or chamber is too high. Please wait for it to cool down to room temperature before restarting the task.",
+    "0300_4050": "Liveview Camera calibration timeout; please restart the printer.",
+    "0300_4052": "Blade Z-axis homing failed",
+    "0300_4057": "Z-axis step loss detected. The task has stopped. Please check if there are any obstructions beneath the heatbed.",
+    "0300_4066": "Calibration of motion precision failed.",
+    "0300_4067": "Calibration result is over the threshold.",
+    "0300_4068": "Step loss occurred during the motion accuracy enhancement process. Please try again.",
+    "0300_8000": "Printing was paused for unknown reason. You can select 'Resume' to resume the print job.",
+    "0300_8001": "Printing was paused by the user. You can select 'Resume' to continue printing.",
+    "0300_8002": "First layer defects were detected by the Micro Lidar. Please check the quality of the printed model before continuing your print.",
+    "0300_8003": "Spaghetti defects were detected by the AI Print Monitoring. Please check the quality of the printed model before continuing your print.",
+    "0300_8004": "Filament ran out. Please load new filament.",
+    "0300_8005": "Toolhead front cover fell off. Please remount the front cover and check to make sure your print is going okay.",
+    "0300_8006": "The build plate marker was not detected. Please confirm the build plate is correctly positioned on the heatbed with all four corners aligned, and the marker is visible.",
+    "0300_8007": "There was an unfinished print job when the printer lost power. If the model is still adhered to the build plate, you can try resuming the print job.",
+    "0300_8008": "Nozzle temperature malfunction",
+    "0300_8009": "Heatbed temperature malfunction",
+    "0300_800A": "A Filament pile-up was detected by AI Print Monitoring. Please clean filament from the waste chute.",
+    "0300_800B": "The cutter is stuck. Please make sure the cutter handle is out and check the filament sensor cable connection.",
+    "0300_800C": "Skipped step detected: auto-recover complete; please resume print and check if there are any layer shift problems.",
+    "0300_800D": "Detected that the extruder is not extruding normally. If the defects are acceptable, select 'Resume' to resume the print job.",
+    "0300_800E": "The print file is not available. Please check to see if the storage media has been removed.",
+    "0300_800F": "The door seems to be open, so printing was paused.",
+    "0300_8010": "The hotend cooling fan speed is abnormal.",
+    "0300_8011": "Detected build plate is not the same as the Gcode file. Please adjust slicer settings or use the correct plate.",
+    "0300_8013": "Printing paused due to the pause command added to the printing file.",
+    "0300_8014": "The nozzle is covered with filament, or the build plate is installed incorrectly. Please cancel this print and clean the nozzle or adjust the build plate according to the actual status. You can als...",
+    "0300_8015": "The filament on external spool has run out; please load new filament. If the filament is loaded, please select 'Resume'.",
+    "0300_8016": "The nozzle is clogged with filament. Please cancel this print and clean the nozzle or select 'Resume' to resume the print job.",
+    "0300_8017": "Foreign objects detected on heatbed. Please check and clean the heatbed. Then, select 'Resume' to resume the print job.",
+    "0300_8018": "Chamber temperature malfunction.",
+    "0300_8019": "No build plate is placed.",
+    "0300_801A": "Filament extrusion error; please check the assistant for troubleshooting. After resolving the issue, decide whether to cancel or resume the print job based on the actual print status.",
+    "0300_801B": "Nozzle temperature problem detected. Refer to Assistant to re-connect the hotend connector. POWER OFF the printer before this operation to avoid short circuits.",
+    "0300_801C": "The extrusion resistance is abnormal. The extruder may be clogged; please refer to the assistant. After trouble shooting, you can select 'Resume' to resume the print job.",
+    "0300_801D": "The extruder servo motor position sensor is malfunctioning. Please power off the printer first and check if the connection cable is loose.",
+    "0300_801E": "The extrusion motor is overloaded, please check the Assistant for details.",
+    "0300_8021": "The nozzle may not be installed or not properly installed. Please ensure the nozzle is correctly installed before proceeding.",
+    "0300_8022": "The heatbed may be obstructed while moving downward. Please clear any objects beneath the heatbed and check for any resistance or jamming during its movement.",
+    "0300_8028": "Nozzle offset calibration sensor error. If using a single hotend or the calibration function is disabled, you may ignore this and continue printing; otherwise, it is recommended to check the sensor...",
+    "0300_8041": "Platform detection timeout: please restart the printer.",
+    "0300_8042": "Task paused because the door is open.",
+    "0300_8043": "The laser module is abnormal.",
+    "0300_8044": "Fire was detected inside the chamber.",
+    "0300_8045": "Material detection timeout: please restart the printer.",
+    "0300_8046": "Foreign object detect timeout: please restart the printer.",
+    "0300_8047": "Quick-release lever detection time out: please restart the printer.",
+    "0300_8048": "Laser Module unlock has timed out, and the task cannot proceed. Please restart the printer and try again.",
+    "0300_8049": "The current plate is invalid.",
+    "0300_804A": "Emergency stop button improperly installed. Please reinstall according to the Wiki before proceeding.",
+    "0300_804B": "Task paused. The Laser Safety Window is open.",
+    "0300_804E": "This is a printing task. Please detach the Laser/Cutting Module from the Toolhead.",
+    "0300_804F": "The loading/unloading process is currently ongoing. Please stop the process or remove the laser/cutting module.",
+    "0300_8050": "This device does not support the 40W Laser Module. Please remove it or replace it with a 10W Laser Module.",
+    "0300_8051": "The cutting module has dropped or the cutting module cable is disconnected; please check the module.",
+    "0300_8053": "Laser module detected. Please install the right nozzle correctly to ensure proper Laser Module Mounting Calibration.",
+    "0300_8054": "Please place the paper required for Print Then Cut.",
+    "0300_8055": "The module mounted on the toolhead does not match the task. Please install the correct module.",
+    "0300_8057": "The rotary attachment is disconnected. Please ensure it is properly installed and the cable is securely plugged in.",
+    "0300_8058": "The rotary attachment is detected. Please remove it before continuing.",
+    "0300_8061": "The mode of Airflow System failed to activate; check the air door condition.",
+    "0300_8062": "The chamber temperature is too high. It may be due to high environmental temperature.",
+    "0300_8063": "The chamber temperature is too high. Please open the top cover and front door to cool down.",
+    "0300_8064": "The chamber temperature is too high. Please open the top cover and front door to cool down. (Open door detection for this print job will be set to 'Notification' level)",
+    "0300_8065": "The temperature of the MC module is too high. Please check the Wiki for possible explanations.",
+    "0300_8071": "The Toolhead Enhanced Cooling Fan module is malfunctioning.",
+    "0300_807D": "Fire Extinguisher not detected, the automatic extinguishing function will be unavailable.",
+    "0300_807E": "Fire Extinguisher not detected, the automatic extinguishing function will be unavailable.",
+    "0300_807F": "Fire Extinguisher is malfunctioning.",
+    "0300_8080": "Fire extinguisher motor reset failed.",
+    "0300_8081": "Fire extinguisher cylinder not installed. Please confirm on the extinguisher page.",
+    "0300_8082": "The Fire Extinguisher Gas Cylinder is empty.",
+    "0300_C012": "Please heat the nozzle to above 170°C.",
+    "0300_C056": "A minor fire was detected inside the chamber, and the Auto Fire Extinguishing process has been aborted.",
+    "0300_C070": "The fire extinguisher has been detected and is ready for use after the laser module is connected.",
+    "0500_4001": "Failed to connect to Bambu Cloud. Please check your network connection.",
+    "0500_4002": "Unsupported print file path or name. Please resend the print job.",
+    "0500_4003": "Printing stopped because the printer was unable to parse the file. Please resend your print job.",
+    "0500_4004": "Device is busy and cannot start new task. Please wait for current task to complete before sending new task.",
+    "0500_4005": "Print jobs are not allowed to be sent while updating firmware.",
+    "0500_4006": "There is not enough free storage space for the print job. Restoring to factory settings can free up available space.",
+    "0500_4007": "The device requires a repair upgrade, and printing is currently unavailable.",
+    "0500_4008": "Starting printing failed; please power cycle the printer and resend the print job.",
+    "0500_4009": "Print jobs are not allowed to be sent while updating logs.",
+    "0500_400A": "The file name is not supported. Please rename and restart the print job.",
+    "0500_400B": "There was a problem downloading a file. Please check your network connection and resend the print job.",
+    "0500_400C": "Please insert a MicroSD card and restart the print job.",
+    "0500_400D": "Please run a self-test and restart the print job.",
+    "0500_400E": "Printing was cancelled.",
+    "0500_400F": "AMS is initializing and cannot be upgraded at the moment. Please try again later.",
+    "0500_4010": "AMS is drying and cannot be upgraded at the moment. Please try again later.",
+    "0500_4011": "The printer is loading or unloading filament and cannot be upgraded at the moment. Please try again later.",
+    "0500_4012": "The device is printing and cannot be upgraded at the moment. Please try again later.",
+    "0500_4013": "AMS is in operation and cannot be upgraded at the moment. Please try again when it is idle.",
+    "0500_4014": "Slicing for the print job failed. Please check your settings and restart the print job.",
+    "0500_4015": "There is not enough free storage space for the print job. Please format or clear files from the MicroSD card to free up space.",
+    "0500_4016": "The MicroSD Card is write-protected. Please replace the MicroSD Card.",
+    "0500_4017": "Binding failed. Please retry or restart the printer and retry.",
+    "0500_4018": "Binding configuration information parsing failed; please try again.",
+    "0500_4019": "The printer has already been bound. Please unbind it and try again.",
+    "0500_401A": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the pri...",
+    "0500_401B": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+    "0500_401C": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer support.",
+    "0500_401D": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
+    "0500_401E": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+    "0500_401F": "Authorization timed out. Please make sure that your phone or PC has access to the internet, and ensure that the Bambu Studio/Bambu Handy APP is running in the foreground during the binding operation.",
+    "0500_4020": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
+    "0500_4021": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
+    "0500_4022": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+    "0500_4023": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
+    "0500_4024": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the pri...",
+    "0500_4025": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+    "0500_4026": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
+    "0500_4027": "Cloud access failed; this may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
+    "0500_4028": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+    "0500_4029": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer support.",
+    "0500_402A": "Failed to connect to the router, which may be caused by wireless interference or being too far away from the router. Please try again or move the printer closer to the router and try again.",
+    "0500_402B": "Router connection failed due to incorrect password. Please check the password and try again.",
+    "0500_402C": "Failed to obtain IP address, which may be caused by wireless interference resulting in data transmission failure or the DHCP address pool of the router being full. Please move the printer closer to...",
+    "0500_402D": "System exception",
+    "0500_402E": "System does not support the file system currently used by the USB flash drive. Please replace or format the USB flash drive to FAT32.",
+    "0500_402F": "The MicroSD card sector data is damaged. Please use the SD card repair tool to repair or format it. If it still cannot be identified, please replace the MicroSD card.",
+    "0500_4030": "The device is currently upgrading. Please try again when it is idle.",
+    "0500_4031": "The accessory firmware does not match the printer. Please update it on the 'Firmware' page.",
+    "0500_4033": "The AMS firmware does not match the printer. Please update it on the 'Firmware' page.",
+    "0500_4034": "The Laser Module firmware does not match the printer. Please update it on the 'Firmware' page.",
+    "0500_4035": "The BirdsEye Camera is malfunctioning. Please try restarting the device. If the issue persists after multiple restarts, check the camera connection status or contact customer support.",
+    "0500_4037": "Your sliced file is not compatible with current printer model. This file can't be printed on this printer.",
+    "0500_4038": "The nozzle diameter in sliced file is not consistent with the current nozzle setting. This file can't be printed.",
+    "0500_4039": "The current task does not allow the installation of the laser/cutting module, and the task has been halted.",
+    "0500_403A": "The current temperature is too low. In order to protect you and your printer, printing tasks, moving an axis and other operations are disabled. Please move the printer to an environment above 10 de...",
+    "0500_403B": "Laser/cutting tasks cannot be initiated on the machine at the moment. Please use the computer software to start the task.",
+    "0500_403C": "The current nozzle setting does not match the slicing file. Continuing to print may affect print quality. It is recommended to re-slice before starting the print.",
+    "0500_403D": "The toolhead module is not set up. Please set it up before initiating the task.",
+    "0500_403E": "The current tool head does not support initialization.",
+    "0500_403F": "Failed to download print job; please check your network connection.",
+    "0500_4040": "The printer has reached its power limit. Please connect a dedicated power adapter to this AMS to enable drying.",
+    "0500_4041": "The AMS drying cannot be started during printing.",
+    "0500_4042": "Due to power limitations, starting AMS drying will pause current operations such as nozzle heating and fan running. Do you want to proceed with drying?",
+    "0500_4043": "Due to power limitations, only one AMS is allowed to use the device's power for drying.",
+    "0500_4044": "BirdsEye Camera malfunction: please contact customer support.",
+    "0500_4045": "Hotend check in progress. This operation is temporarily unavailable. Please wait.",
+    "0500_4050": "Error detected on the print board.",
+    "0500_4052": "Error detected on the hot end.",
+    "0500_4054": "Error detected on the mat.",
+    "0500_405D": "Laser module Serial Number error: unable to calibrate or make project.",
+    "0500_4065": "The task requires a Laser Platform, but the current one is a Cutting Platform. Please replace it, measure the material thickness in the software, and then restart the task.",
+    "0500_4070": "The laser or cutter module is connected, so the device cannot initiate a 3D printing task.",
+    "0500_4075": "No Laser Platform was detected, which may affect thickness measurement accuracy. Please place the laser platform correctly and ensure the rear markers are not blocked, then restart the thickness me...",
+    "0500_4076": "Please place the Laser Platform correctly and ensure the rear markers are not blocked, then restart the thickness measurement in the software before initiating the task.",
+    "0500_4097": "The device cannot detect the Laser Module. Please reconnect the module cable or restart the printer.",
+    "0500_4098": "The device cannot detect AMS A. Please reconnect the AMS cable or restart the printer.",
+    "0500_4099": "The firmware of Cutting Module does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
+    "0500_409A": "The firmware of the Air Pump does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
+    "0500_409B": "The firmware of the Laser Module does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
+    "0500_409D": "The firmware of AMS A does not match the printer; the device cannot continue working. Please upgrade it on the 'Firmware' page.",
+    "0500_409E": "The device cannot detect the Cutting Module. Please reconnect the module cable or restart the printer.",
+    "0500_409F": "The device cannot detect the Air Pump.  Please reconnect the module cable or restart the printer.",
+    "0500_40A0": "The Rotary Attachment module is not detected. Please reconnect the cable or restart the printer.",
+    "0500_40A1": "The Auto Fire Extinguishing System is not detected.  Please reconnect the module cable or restart the printer.",
+    "0500_40A3": "AMS(or AMS lite) A communication is abnormal. Please reconnect the module cable or restart the printer.",
+    "0500_40A4": "The current firmware only supports 1 AMS Lite. Please remove all AMS units before reconnecting the supported AMS Lite device.",
+    "0500_40A5": "The current firmware only supports AMS/AMS 2 Pro/AMS HT, with a maximum of 4 units. Please remove all AMS units before reconnecting the supported one.",
+    "0500_8013": "The print file is not available. Please check to see if the storage media has been removed.",
+    "0500_8036": "Your sliced file is not consistent with the current printer model. Continue?",
+    "0500_803C": "The current nozzle setting does not match the slicing file. Continuing to print may affect print quality. It is recommended to re-slice before starting the print.",
+    "0500_8040": "Toolhead front cover is detached. Moving the toolhead may damage the printer. Do you want to continue?",
+    "0500_8041": "The filament in hotend is too cold. Extrusion may damage the extruder. Still feeding in/out the filament?",
+    "0500_8048": "The module on the toolhead is not calibrated. Please cancel the task to perform calibration or switch to a calibrated module.",
+    "0500_8051": "Detected build plate is not the same as the Gcode file. Please adjust slicer settings or use the correct plate.",
+    "0500_8053": "Nozzle mismatch was detected during printing. Please initiate the print after re-slicing, or continue printing after replacing with the correct nozzle. Caution: the hotend temperature is high.",
+    "0500_8055": "Laser module is installed, but a Cutting Platform is detected. Please place a Laser Platform and perform laser calibration.",
+    "0500_8056": "Cutting module is installed, but the laser platform is detected. Please place the cutting platform for calibration.",
+    "0500_8058": "Please place the light grip cutting mat correctly and ensure the marker is exposed.",
+    "0500_8059": "Cutting platform base is not correctly aligned. Please ensure that the four corners of the platform are aligned with the heatbed.",
+    "0500_805A": "Please place the cutting mat on cutting protection base.",
+    "0500_805B": "The cutting mat type is unknown; please replace it with the correct cutting mat.",
+    "0500_805C": "The grip cutting mat type does not match; please place a LightGrip cutting mat.",
+    "0500_805E": "Cutting module Serial Number error: unable to calibrate or make project.",
+    "0500_8060": "The current module on toolhead does not meet requirements. Please replace the module as per the on-screen instructions.",
+    "0500_8061": "No print plate detected. Please make sure it is placed correctly.",
+    "0500_8062": "The print plate marker was not detected. Please confirm the print plate is correctly positioned on the heatbed with all four corners aligned, and the marker is visible. If strong light is shining o...",
+    "0500_8063": "The platform is not detected during calibration; please make sure the Laser Platform is properly placed.",
+    "0500_8064": "Please place the Laser Platform correctly and ensure the rear markers are not blocked for laser calibration.",
+    "0500_8066": "The task requires a Cutting Platform, but the current one is a Laser Platform. Please replace it with a Cutting Platform (Cutting Protection Base + LightGrip cutting mat).",
+    "0500_8067": "Please place a LightGrip cutting mat on the cutting protection base.",
+    "0500_8068": "Please place the strong grip cutting mat correctly and ensure the marker is exposed.",
+    "0500_8069": "Unable to recognize the left and right hotends. They might be third party hotends, or the hotend marks may be dirty. Please manually set the hotend types.",
+    "0500_806A": "Unable to recognize the left and right hotends. They might be third party hotends, or the hotend marks may be dirty. Please set hotend types on printer screen before next print.",
+    "0500_806B": "Quick-release Lever is not locked. Please press down the external toolhead module to ensure it is properly seated, then push down the level to lock it in place.",
+    "0500_806C": "Please place the cutting platform correctly and ensure the marker is exposed.",
+    "0500_806D": "Material not detected. Please confirm placement and continue.",
+    "0500_806E": "Foreign objects detected on heatbed; please check and clean up the heatbed.",
+    "0500_806F": "The grip cutting mat type does not match; please place a StrongGrip cutting mat.",
+    "0500_8071": "No cutting platform was detected. Please confirm that it has been correctly placed.",
+    "0500_8072": "Live View camera is blocked",
+    "0500_8073": "Heatbed limit block is obstructed or contaminated. Please clean and ensure the limit block is visible, otherwise platform position offset detection may be inaccurate.",
+    "0500_8074": "The Laser Platform is offset. Please ensure that the four corners of the platform are aligned with the heatbed, and the marker is not obstructed.",
+    "0500_8077": "The visual marker was not detected. Please ensure the paper is properly placed.",
+    "0500_8078": "Current material does not match the sliced file settings. Please load the correct material and ensure the QR code on the material is not damaged or dirty.",
+    "0500_8079": "Please place the Laser Test Material (350g paperboard) and position support strips underneath to prevent material warping.",
+    "0500_807A": "The foreign object detection function is not working. You can continue the task or check the assistant for troubleshooting.",
+    "0500_807B": "Please place the cutting platform (cutting protection base + LightGrip cutting mat).",
+    "0500_807C": "Please place the cutting platform (cutting protection base + StrongGrip cutting mat).",
+    "0500_807D": "This task requires a Cutting Platform, but the current one is a Laser Platform. Please replace it with a Cutting Platform (Cutting Protection Base + StrongGrip Cutting Mat).",
+    "0500_807E": "Please place a StrongGrip cutting mat on the cutting protection base.",
+    "0500_8080": "The left and right hotends are not installed.",
+    "0500_8081": "The left and right hotends are not installed.",
+    "0500_8082": "Please remove the protective film on the Opaque Glossy Acrylic before processing",
+    "0500_8083": "Material is not allowed in Mounting Calibration. Please remove the material from the platform.",
+    "0500_8084": "The Live View Camera is dirty; please clean it and continue.",
+    "0500_8085": "Toolhead camera is obstructed",
+    "0500_8086": "Toolhead Camera is dirty, which affects the AI function; please clean the lens surface.",
+    "0500_8087": "BirdsEye camera is obstructed",
+    "0500_8088": "The Birdseye Camera is dirty",
+    "0500_8089": "Task paused due to Presence Check failed. Please check the printer to continue.",
+    "0500_808A": "The BirdsEye Camera is installed offset. Please refer to the assistant to reinstall it.",
+    "0500_808B": "The BirdsEye Camera setup failed. Please remove all objects and the mat on the heatbed to ensure the heatbed markers are visible. Meanwhile, please ensure the BirdsEye Camera is installed correctly...",
+    "0500_808C": "Detected build plate offset. Please align the build plate with the heatbed, and then continue.",
+    "0500_808D": "The Cutting Module offset calibration failed, which may result in inaccurate cuts. Please ensure the cutting material is properly positioned and check whether the cutting blade tip is worn.",
+    "0500_808E": "BirdsEye Camera initialization failed. The toolhead camera did not detect the Heatbed features. Please clean the Heatbed, remove all objects and pads, and ensure the bed markings are visible. Check...",
+    "0500_808F": "Nozzle camera lens is dirty, affecting AI monitoring. Clean the lens with a non-woven cloth and a small amount of alcohol. Beware of hotend heat; wait for it to cool before handling.",
+    "0500_8090": "Please attach the 80g White Printing Paper to the center area of the platform.",
+    "0500_8091": "The Cutting Module offset calibration failed, which may result in inaccurate cuts. Please ensure the 80g white printer paper(letter paper thickness) is properly positioned and check whether the cut...",
+    "0500_8092": "Toolhead Camera initialization failed. This print can still continue, but some AI functions will be disabled. If you encounter this issue again after restarting, please contact customer support.",
+    "0500_8093": "The nozzle silicone sleeve is not installed; there is a risk of temperature control failure. Please install it correctly and try again.",
+    "0500_80A0": "The visual encoder board was not detected. Please check if the board is properly placed and aligned at all four corners, and ensure the positioning markings are clear and free from wear.",
+    "0500_C010": "MicroSD Card read/write exception: please reinsert or replace the MicroSD Card.",
+    "0500_C032": "Laser/Cutting module connected to the toolhead. The drying process has been automatically stopped.",
+    "0500_C036": "This is a printing task. Please detach the Laser/Cutting Module from the Toolhead.",
+    "0500_C07F": "Device is busy and cannot perform this operation. To proceed, please pause or stop the current task.",
+    "0501_4017": "Binding failed. Please retry or restart the printer and retry.",
+    "0501_4018": "Binding configuration information parsing failed; please try again.",
+    "0501_4019": "The printer has already been bound. Please unbind it and try again.",
+    "0501_401A": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the pri...",
+    "0501_401B": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+    "0501_401C": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer support.",
+    "0501_401D": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
+    "0501_401E": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+    "0501_401F": "Authorization timed out. Please make sure that your phone or PC has access to the internet, and ensure that the Bambu Studio/Bambu Handy APP is running in the foreground during the binding operation.",
+    "0501_4020": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
+    "0501_4021": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
+    "0501_4022": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+    "0501_4023": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
+    "0501_4024": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the pri...",
+    "0501_4025": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+    "0501_4026": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer support.",
+    "0501_4027": "Cloud access failed; this may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.",
+    "0501_4028": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer support.",
+    "0501_4029": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer support.",
+    "0501_4031": "Device discovery binding is in progress, and the QR code cannot be displayed on the screen. You can wait for the binding to finish or abort the device discovery binding process in the APP/Studio an...",
+    "0501_4032": "QR code binding is in progress, so device discovery binding cannot be performed. You can scan the QR code on the screen for binding or exit the QR code display page on screen and try device discove...",
+    "0501_4033": "Your APP region does not match with your printer; please download the APP in the corresponding region and register your account again.",
+    "0501_4034": "The slicing progress has not been updated for a long time, and the printing task has exited. Please confirm the parameters and reinitiate printing.",
+    "0501_4035": "The device is in the process of binding and cannot respond to new binding requests.",
+    "0501_4038": "The regional settings do not match the printer; please check the printer's regional settings.",
+    "0501_4039": "Device login has expired; please try to bind again.",
+    "0501_4098": "The device cannot detect AMS B. Please reconnect the AMS cable or restart the printer.",
+    "0501_409D": "The firmware of AMS B does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
+    "0501_40A3": "AMS(or AMS lite) B communication is abnormal. Please reconnect the module cable or restart the printer.",
+    "0502_4001": "Current filament will be used in this print job. Settings cannot be changed.",
+    "0502_4002": "Please go to “Settings > Calibration” to run the Motion Accuracy Enhancement Calibration before turning on Motion Accuracy Enhancement mode.",
+    "0502_4003": "The printer is currently printing and the motion accuracy enhancement feature cannot be turned on or off.",
+    "0502_4004": "Some features are not supported by the current device. Please check the Studio feature settings or update the firmware to the latest version.",
+    "0502_4005": "The AMS has not been calibrated yet, so printing cannot be initiated.",
+    "0502_4006": "Unknown module detected; please try updating the firmware to the latest version.",
+    "0502_400D": "Failed to start a new task: filament loading/unloading not completed.",
+    "0502_400E": "Failed to start a new task: The nozzle cold pull was not completed.",
+    "0502_4013": "This device is not compatible with the 40W laser module. Please replace it with a 10W laser module or remove it.",
+    "0502_4098": "The device cannot detect AMS C. Please reconnect the AMS cable or restart the printer.",
+    "0502_409D": "The firmware of AMS C does not match the printer; the device cannot continue working. Please upgrade it on the 'Firmware' page.",
+    "0502_40A3": "AMS(or AMS lite) C communication is abnormal. Please reconnect the module cable or restart the printer.",
+    "0502_C00F": "The device is busy and cannot perform nozzle identification.",
+    "0502_C010": "Due to printer power limitations, printing, calibration, controls and other actions cannot be performed during AMS drying. Please stop the drying process before proceeding with any other operation.",
+    "0502_C011": "Currently in 2D production mode. Please continue the operation on the printer",
+    "0502_C012": "The task cannot be paused.",
+    "0502_C014": "The AMS Remaining Filament Estimation is enabled by default and cannot be disabled.",
+    "0502_C024": "The flow dynamic calibration records have exceeded the storage limit. Please delete some historical records in the slicer software before adding new calibration data.",
+    "0503_4098": "The device cannot detect AMS D. Please reconnect the AMS cable or restart the printer.",
+    "0503_409D": "The firmware of AMS D does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
+    "0503_40A3": "AMS(or AMS lite) D communication is abnormal. Please reconnect the module cable or restart the printer.",
+    "0580_4096": "The device cannot detect AMS-HT A. Please reconnect the AMS-HT cable or restart the printer.",
+    "0580_409C": "The firmware of AMS-HT A does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
+    "0580_40A2": "AMS-HT A communication is abnormal. Please reconnect the module cable or restart the printer.",
+    "0581_4096": "The device cannot detect AMS-HT B. Please reconnect the AMS-HT cable or restart the printer.",
+    "0581_409C": "The firmware of AMS-HT B does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
+    "0581_40A2": "AMS-HT B communication is abnormal. Please reconnect the module cable or restart the printer.",
+    "0582_4096": "The device cannot detect AMS-HT C. Please reconnect the AMS-HT cable or restart the printer.",
+    "0582_409C": "The firmware of AMS-HT C does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
+    "0582_40A2": "AMS-HT C communication is abnormal. Please reconnect the module cable or restart the printer.",
+    "0583_4096": "The device cannot detect AMS-HT D. Please reconnect the AMS-HT cable or restart the printer.",
+    "0583_409C": "The firmware of AMS-HT D does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
+    "0583_40A2": "AMS-HT D communication is abnormal. Please reconnect the module cable or restart the printer.",
+    "0584_4096": "The device cannot detect AMS-HT F. Please reconnect the AMS-HT cable or restart the printer.",
+    "0584_409C": "The firmware of AMS-HT E does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
+    "0584_40A2": "AMS-HT E communication is abnormal. Please reconnect the module cable or restart the printer.",
+    "0585_4096": "The device cannot detect AMS-HT E. Please reconnect the AMS-HT cable or restart the printer.",
+    "0585_409C": "The firmware of AMS-HT F does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
+    "0585_40A2": "AMS-HT F communication is abnormal. Please reconnect the module cable or restart the printer.",
+    "0586_4096": "The device cannot detect AMS-HT G. Please reconnect the AMS-HT cable or restart the printer.",
+    "0586_409C": "The firmware of AMS-HT G does not match the printer; the device cannot continue working. Please update it on the 'Firmware' page.",
+    "0586_40A2": "AMS-HT G communication is abnormal. Please reconnect the module cable or restart the printer.",
+    "0587_4096": "The device cannot detect AMS-HT H. Please reconnect the AMS-HT cable or restart the printer.",
+    "0587_409C": "The firmware of AMS-HT H does not match the printer; the device cannot continue working. Please upgrade it on the 'Firmware' page.",
+    "0587_40A2": "AMS-HT H communication is abnormal. Please reconnect the module cable or restart the printer.",
+    "05FE_8053": "The left nozzle is not matched with slicing file. Please initiate the print after re-slicing, or continue printing after replacing with the correct nozzle. Caution: the hotend temperature is high.",
+    "05FE_8069": "Unable to recognize the left hotend. It might be a third party hotend, or the hotend mark may be dirty. Please manually set the hotend type.",
+    "05FE_806A": "Unable to recognize the left hotend. It might be a third party hotend, or the hotend mark may be dirty. Please set hotend type on printer screen before next print.",
+    "05FE_8080": "The left hotend is not installed.",
+    "05FE_8081": "The left hotend is not installed.",
+    "05FF_8053": "The right nozzle is not matched with slicing file. Please initiate the print after re-slicing, or continue printing after replacing with the correct nozzle. Caution: the hotend temperature is high.",
+    "05FF_8069": "Unable to recognize the right hotend. It might be a third party hotend, or the hotend mark may be dirty. Please manually set the hotend type.",
+    "05FF_806A": "Unable to recognize the right hotend. It might be a third party hotend, or the hotend mark may be dirty. Please set hotend type on printer screen before next print.",
+    "05FF_8080": "The right hotend is not installed.",
+    "05FF_8081": "The right hotend is not installed.",
+    "0700_4001": "The AMS has been disabled for a print, but it still has filament loaded. Please unload the AMS filament and switch to the spool holder filament for printing.",
+    "0700_4025": "Failed to read the filament information.",
+    "0700_8001": "Failed to cut the filament. Please check the cutter.",
+    "0700_8002": "The cutter is stuck. Please make sure the cutter handle is out.",
+    "0700_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "0700_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "0700_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "0700_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "0700_8007": "Extruding filament failed. The extruder might be clogged.",
+    "0700_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS A to the extruder is properly connected.",
+    "0700_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "0700_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+    "0700_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "0700_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "0700_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "0700_8017": "AMS A is drying. Please stop drying process before loading/unloading material.",
+    "0700_8021": "AMS setup failed; please refer to the assistant.",
+    "0700_8023": "AMS A cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "0700_C069": "An error occurred during AMS A drying. Please go to Assistant for more details.",
+    "0700_C06A": "AMS A is reading RFID. Unable to start drying. Please try again later.",
+    "0700_C06B": "AMS A is changing filament. Unable to start drying. Please try again later.",
+    "0700_C06C": "AMS A is in Feed Assist Mode. Unable to start drying. Please try again later.",
+    "0700_C06D": "AMS A is assisting in filament insertion. Unable to start drying. Please try again later.",
+    "0700_C06E": "AMS A motor is performing self-test. Unable to start drying. Please try again later.",
+    "0701_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
+    "0701_4025": "Failed to read the filament information.",
+    "0701_8001": "Failed to cut the filament. Please check the cutter.",
+    "0701_8002": "The cutter is stuck. Please make sure the cutter handle is out.",
+    "0701_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "0701_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "0701_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "0701_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "0701_8007": "Extruding filament failed. The extruder might be clogged.",
+    "0701_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS B to the extruder is properly connected.",
+    "0701_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "0701_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+    "0701_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "0701_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "0701_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "0701_8017": "AMS B is drying. Please stop drying process before loading/unloading material.",
+    "0701_8021": "AMS setup failed; please refer to the assistant.",
+    "0701_8023": "AMS B cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "0701_C069": "An error occurred during AMS B drying. Please go to Assistant for more details.",
+    "0701_C06A": "AMS B is reading RFID. Unable to start drying. Please try again later.",
+    "0701_C06B": "AMS B is changing filament. Unable to start drying. Please try again later.",
+    "0701_C06C": "AMS B is in Feed Assist Mode. Unable to start drying. Please try again later.",
+    "0701_C06D": "AMS B is assisting in filament insertion. Unable to start drying. Please try again later.",
+    "0701_C06E": "AMS B motor is performing self-test. Unable to start drying. Please try again later.",
+    "0702_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
+    "0702_4025": "Failed to read the filament information.",
+    "0702_8001": "Failed to cut the filament. Please check the cutter.",
+    "0702_8002": "The cutter is stuck. Please make sure the cutter handle is out.",
+    "0702_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "0702_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "0702_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "0702_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "0702_8007": "Extruding filament failed. The extruder might be clogged.",
+    "0702_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS C to the extruder is properly connected.",
+    "0702_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "0702_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+    "0702_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "0702_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "0702_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "0702_8017": "AMS C is drying. Please stop drying process before loading/unloading material.",
+    "0702_8021": "AMS setup failed; please refer to the assistant.",
+    "0702_8023": "AMS C cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "0702_C069": "An error occurred during AMS C drying. Please go to Assistant for more details.",
+    "0702_C06A": "AMS C is reading RFID. Unable to start drying. Please try again later.",
+    "0702_C06B": "AMS C is changing filament. Unable to start drying. Please try again later.",
+    "0702_C06C": "AMS C is in Feed Assist Mode. Unable to start drying. Please try again later.",
+    "0702_C06D": "AMS C is assisting in filament insertion. Unable to start drying. Please try again later.",
+    "0702_C06E": "AMS C motor is performing self-test. Unable to start drying. Please try again later.",
+    "0703_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
+    "0703_4025": "Failed to read the filament information.",
+    "0703_8001": "Failed to cut the filament. Please check the cutter.",
+    "0703_8002": "The cutter is stuck. Please make sure the cutter handle is out.",
+    "0703_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "0703_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "0703_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "0703_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "0703_8007": "Extruding filament failed. The extruder might be clogged.",
+    "0703_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS D to the extruder is properly connected.",
+    "0703_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "0703_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+    "0703_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "0703_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "0703_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "0703_8017": "AMS D is drying. Please stop drying process before loading/unloading material.",
+    "0703_8021": "AMS setup failed; please refer to the assistant.",
+    "0703_8023": "AMS D cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "0703_C069": "An error occurred during AMS D drying. Please go to Assistant for more details.",
+    "0703_C06A": "AMS D is reading RFID. Unable to start drying. Please try again later.",
+    "0703_C06B": "AMS D is changing filament. Unable to start drying. Please try again later.",
+    "0703_C06C": "AMS D is in Feed Assist Mode. Unable to start drying. Please try again later.",
+    "0703_C06D": "AMS D is assisting in filament insertion. Unable to start drying. Please try again later.",
+    "0703_C06E": "AMS D motor is performing self-test. Unable to start drying. Please try again later.",
+    "0704_4025": "Failed to read the filament information.",
+    "0704_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "0704_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "0704_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "0704_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "0704_8007": "Extruding filament failed. The extruder might be clogged.",
+    "0704_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS E to the extruder is properly connected.",
+    "0704_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "0704_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+    "0704_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "0704_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "0704_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "0704_8021": "AMS setup failed; please refer to the assistant.",
+    "0704_8023": "AMS E cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "0705_4025": "Failed to read the filament information.",
+    "0705_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "0705_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "0705_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "0705_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "0705_8007": "Extruding filament failed. The extruder might be clogged.",
+    "0705_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS F to the extruder is properly connected.",
+    "0705_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "0705_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+    "0705_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "0705_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "0705_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "0705_8021": "AMS setup failed; please refer to the assistant.",
+    "0705_8023": "AMS F cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "0706_4025": "Failed to read the filament information.",
+    "0706_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "0706_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "0706_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "0706_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "0706_8007": "Extruding filament failed. The extruder might be clogged.",
+    "0706_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS G to the extruder is properly connected.",
+    "0706_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "0706_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+    "0706_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "0706_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "0706_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "0706_8021": "AMS setup failed; please refer to the assistant.",
+    "0706_8023": "AMS G cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "0707_4025": "Failed to read the filament information.",
+    "0707_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "0707_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "0707_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "0707_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "0707_8007": "Extruding filament failed. The extruder might be clogged.",
+    "0707_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS H to the extruder is properly connected.",
+    "0707_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "0707_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.",
+    "0707_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "0707_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "0707_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "0707_8021": "AMS setup failed; please refer to the assistant.",
+    "0707_8023": "AMS H cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "07FE_8001": "Failed to cut the filament of the left extruder. Please check the cutter.",
+    "07FE_8002": "The cutter of the left extruder is stuck. Please pull out the cutter handle.",
+    "07FE_8003": "Please pull out the filament on the spool holder  of the left extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are ab...",
+    "07FE_8004": "Failed to pull back the filament from the left extruder. Please check whether the filament is stuck inside the extruder.",
+    "07FE_8005": "Failed to feed the filament outside the AMS. Please clip the end of the filament flat and check to see if the spool is stuck.",
+    "07FE_8006": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
+    "07FE_8007": "Please observe the nozzle of the left extruder. If the filament has been extruded, select 'Continue'; if it has not, please push the filament forward slightly, and then select 'Retry'.",
+    "07FE_8010": "Check if the left external filament spool or filament is stuck.",
+    "07FE_8011": "The external filament connected to the left extruder has run out; please load a new filament.",
+    "07FE_8012": "Failed to get mapping table; please select 'Resume' to retry.",
+    "07FE_8013": "Timeout purging old filament of the left extruder: Please check if the filament is stuck or the extruder is clogged.",
+    "07FE_8020": "Extruder change failed; please refer to the assistant.",
+    "07FE_8021": "AMS setup failed; please refer to the assistant.",
+    "07FE_8024": "Extruder position calibration failed; please refer to the assistant.",
+    "07FE_8025": "Cold pull timed out. Please promptly operate or check whether the filament is broken inside the extruder, and click the Assistant for details.",
+    "07FE_8030": "The filament specified in the slicer has been used up. Printing is paused. Please go to the machine to replace the material and resume printing.",
+    "07FE_C003": "Please pull out the filament on the spool holder of the left extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube i...",
+    "07FE_C006": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
+    "07FE_C008": "Please pull out the filament on the spool holder of the left extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube i...",
+    "07FE_C009": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
+    "07FE_C00A": "Please observe the nozzle of the left extruder. If the filament has been extruded, select 'Continue'; if not, please push the filament forward slightly and then select 'Retry'.",
+    "07FE_C010": "Insert the filament (over 30cm long) until it stops. You might see slight smoke during flushing. After insertion, close the front door and top cover.",
+    "07FE_C011": "Please manually and slowly pull out the filament from the extruder. Then click “Continue”.",
+    "07FE_C012": "Press the black PTFE tube coupler and unplug the PTFE tube. After completing the operation, click 'Continue.'",
+    "07FF_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.",
+    "07FF_8001": "Failed to cut the filament of the right extruder. Please check the cutter.",
+    "07FF_8002": "The cutter is stuck. Please make sure the cutter handle is out.",
+    "07FF_8003": "Please pull out the filament on the spool holder  of the right extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are a...",
+    "07FF_8004": "Failed to pull back the filament from the right extruder. Please check whether the filament is stuck inside the extruder.",
+    "07FF_8005": "Failed to feed the filament outside the AMS. Please clip the end of the filament flat and check to see if the spool is stuck.",
+    "07FF_8006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+    "07FF_8007": "Please observe the nozzle of the right extruder. If the filament has been extruded, select 'Continue'; if it has not, please push the filament forward slightly, and then select 'Retry'.",
+    "07FF_8010": "Check if the external filament spool or filament is stuck.",
+    "07FF_8011": "External filament has run out; please load a new filament.",
+    "07FF_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "07FF_8013": "Timeout purging old filament of the right extruder: Please check if the filament is stuck or the extruder is clogged.",
+    "07FF_8020": "Extruder change failed; please refer to the assistant.",
+    "07FF_8021": "AMS setup failed; please refer to the assistant.",
+    "07FF_8024": "Extruder position calibration failed; please refer to the assistant.",
+    "07FF_8025": "Cold pull timed out. Please promptly operate or check whether the filament is broken inside the extruder, and click the Assistant for details.",
+    "07FF_8030": "The filament specified in the slicer has been used up. Printing is paused. Please go to the machine to replace the material and resume printing.",
+    "07FF_C003": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube ...",
+    "07FF_C006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+    "07FF_C008": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube ...",
+    "07FF_C009": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+    "07FF_C00A": "Please observe the nozzle of the right extruder. If the filament has been extruded, select 'Continue'; if not, please push the filament forward slightly and then select 'Retry'.",
+    "07FF_C010": "Insert the filament (over 30cm long) until it stops. You might see slight smoke during flushing. After insertion, close the front door and top cover.",
+    "07FF_C011": "Hold the driven wheel bracket, slowly pull the filament from the extruder, then press 'Continue'.",
+    "07FF_C012": "Press the black PTFE tube coupler and unplug the PTFE tube. After completing the operation, click 'Continue.'",
+    "0C00_4020": "The setup of BirdsEye Camera failed. Please clear all objects and remove the mat. Make sure the marker is not obstructed. Meanwhile, clean both the BirdsEye Camera and Toolhead Camera, and remove a...",
+    "0C00_4021": "The setup of BirdsEye Camera failed; please reboot the printer.",
+    "0C00_4022": "The setup of BirdsEye Camera failed.  Please check if the laser module is working properly.",
+    "0C00_4024": "The Birdseye Camera is installed offset. Please refer to the assistant to reinstall it.",
+    "0C00_4025": "The Birdseye Camera is dirty. Please clean it and restart the process.",
+    "0C00_4026": "The Live View Camera initialization failed; please reboot the printer.",
+    "0C00_4027": "The Live View Camera calibration failed. Please refer to the assistant for details and recalibrate the camera after processing.",
+    "0C00_4029": "Material not detected. Please confirm placement and continue.",
+    "0C00_402A": "The visual marker was not detected. Please re-paste the paper in the correct position.",
+    "0C00_402C": "Device data link error. Please reboot the printer",
+    "0C00_402D": "The toolhead camera is not working properly; please reboot the device.",
+    "0C00_403D": "The vision encoder plate was not detected. Please confirm it is correctly positioned on the heatbed.",
+    "0C00_403E": "The high-precision nozzle offset calibration has failed, possibly due to a damaged pattern or the similarity of the colors of the two selected filaments. Please clear the printed pattern and replac...",
+    "0C00_4041": "Toolhead camera calibration failed. Please ensure the Calibration Marker on the heatbed or Height Calibration Marker on the homing area is clean and undamaged, then re-run the calibration process.",
+    "0C00_8001": "First layer defects were detected. If the defects are acceptable, select 'Resume' to resume the print job.",
+    "0C00_8005": "Purged filament has piled up in the waste chute, which may cause a tool head collision.",
+    "0C00_8009": "Build plate localization marker was not found.",
+    "0C00_800B": "The heatbed marker was not detected. Please clear all objects and remove the mat. Make sure the marker is not obstructed.",
+    "0C00_8015": "Objects detected on the platform; please clean them up in a timely manner.",
+    "0C00_8016": "The foreign object detection function is not working. You can continue the task or check assistant for solutions.",
+    "0C00_8017": "Foreign objects detected on the platform; please clean them up on time.",
+    "0C00_8018": "The foreign object detection function is not working. You can continue the task or view the assistant for troubleshooting.",
+    "0C00_8033": "Quick-release Lever is not locked. Please push it down to secure.",
+    "0C00_8034": "Liveview Camera initialization failed. This print can still continue, but some AI functions will be disabled. If you encounter this issue again after restarting, please contact customer support.",
+    "0C00_803F": "AI detected nozzle clumping. Please check the nozzle condition. Refer to assistant for solutions.",
+    "0C00_8040": "AI detected air-printing defect. Please check the hotend extrusion status. Refer to assistant for solutions.",
+    "0C00_8042": "The AI print monitor has detected a spaghetti defect. Please check the print and take the necessary action.",
+    "0C00_8043": "AI detected nozzle clumping. Please check the nozzle condition. Refer to assistant for solutions.",
+    "0C00_C003": "Possible defects were detected in the first layer.",
+    "0C00_C004": "Possible spaghetti failure was detected.",
+    "0C00_C006": "Purged filament may have piled up in the waste chute.",
+    "1000_C001": "High bed temperature may lead to filament clogging in the nozzle. You may open the chamber door.",
+    "1000_C002": "Printing CF material with stainless steel may cause nozzle damage.",
+    "1000_C003": "Enabling Timelapse in traditional mode may cause defects; please activate this feature as needed.",
+    "1001_4001": "Timelapse is not supported as Spiral Vase mode is enabled in slicing presets.",
+    "1001_4002": "Timelapse is not supported as the Print sequence is set to 'By object'.",
+    "1001_8003": "The time-lapse mode is set to Traditional in the slicing file. This may cause surface defects. Would you like to enable it?",
+    "1001_8004": "Prime Tower is not enabled and time-lapse mode is set to Smooth in slicing file. This may cause surface defects. Would you like to enable it?",
+    "1200_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
+    "1200_8001": "Cutting the filament failed. Please check to see if the cutter is stuck. Refer to the Assistant for solutions.",
+    "1200_8002": "The cutter is stuck. Please pull out the cutter handle.",
+    "1200_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder.",
+    "1200_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
+    "1200_8005": "The filament is not inserted. Please insert the filament.",
+    "1200_8006": "Unable to feed filament into the extruder. This could be due to tangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.",
+    "1200_8007": "Failed to extrude the filament. This might be caused by clogged extruder or stuck filament. Refer to the Assistant for solutions.",
+    "1200_8010": "Filament or spool may be stuck.",
+    "1200_8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
+    "1200_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "1200_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
+    "1200_8014": "The filament location in the toolhead was not found. Refer to the Assistant for solutions.",
+    "1200_8015": "Failed to pull out the filament from the toolhead. Please check if the filament is stuck, or if it is broken inside the extruder or PTFE tube.",
+    "1200_8016": "The extruder is not extruding normally. Refer to the Assistant for troubleshooting. There may be defects in this layer, but you may resume if the defects are acceptable.",
+    "1201_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
+    "1201_8001": "Failed to cut the filament. Please check the cutter.",
+    "1201_8002": "The cutter is stuck. Please pull out the cutter handle.",
+    "1201_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder.",
+    "1201_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
+    "1201_8005": "Failed to feed the filament. Please load the filament and then select 'Retry'.",
+    "1201_8006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck.",
+    "1201_8007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS.",
+    "1201_8010": "Please check if the spool or filament is stuck.",
+    "1201_8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
+    "1201_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "1201_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
+    "1201_8014": "Failed to check the filament location in the tool head; please refer to the HMS.",
+    "1201_8015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or the filament is broken inside the extruder.",
+    "1201_8016": "The extruder is not extruding normally; please refer to the HMS. After trouble shooting, if the defects are acceptable, please resume printing.",
+    "1202_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
+    "1202_8001": "Failed to cut the filament. Please check the cutter.",
+    "1202_8002": "The cutter is stuck. Please pull out the cutter handle.",
+    "1202_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder.",
+    "1202_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
+    "1202_8005": "The filament is not inserted. Please insert the filament.",
+    "1202_8006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck.",
+    "1202_8007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS.",
+    "1202_8010": "Please check if the spool or filament is stuck.",
+    "1202_8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
+    "1202_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "1202_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
+    "1202_8014": "Failed to check the filament location in the tool head; please refer to the HMS.",
+    "1202_8015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or is broken inside the extruder.",
+    "1202_8016": "The extruder is not extruding normally; please refer to the HMS. After trouble shooting, if the defects are acceptable, please resume printing.",
+    "1203_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
+    "1203_8001": "Failed to cut the filament. Please check the cutter.",
+    "1203_8002": "The cutter is stuck. Please pull out the cutter handle.",
+    "1203_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder.",
+    "1203_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
+    "1203_8005": "The filament is not inserted. Please insert the filament.",
+    "1203_8006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck.",
+    "1203_8007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS.",
+    "1203_8010": "Please check if the spool or filament is stuck.",
+    "1203_8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
+    "1203_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "1203_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
+    "1203_8014": "Failed to check the filament location in the tool head; please refer to the HMS.",
+    "1203_8015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or is broken inside the extruder.",
+    "1203_8016": "The extruder is not extruding normally; please refer to the HMS. After trouble shooting, if the defects are acceptable, please resume printing.",
+    "12FF_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.",
+    "12FF_8001": "Failed to cut the filament. Please check the cutter.",
+    "12FF_8002": "The cutter is stuck. Please pull out the cutter handle.",
+    "12FF_8003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube if you are about to us...",
+    "12FF_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck.",
+    "12FF_8005": "The filament is not inserted. Please insert the filament.",
+    "12FF_8006": "Please feed filament into the PTFE tube until it can not be pushed any farther.",
+    "12FF_8007": "Check nozzle. Select 'Done' if filament was extruded, otherwise push filament forward slightly and select 'Retry.'",
+    "12FF_8010": "Please check if the filament or the spool is stuck.",
+    "12FF_8011": "AMS filament has run out. Please insert a new filament into the same AMS slot.",
+    "12FF_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "12FF_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged.",
+    "12FF_C003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder or PTFE Tube. (Connect a PTFE tube if you are about to us...",
+    "12FF_C006": "Please feed filament into the PTFE tube until it can not be pushed any farther.",
+    "1800_4025": "Failed to read the filament information.",
+    "1800_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "1800_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "1800_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "1800_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "1800_8007": "Extruding filament failed. The extruder might be clogged.",
+    "1800_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT A to the extruder is properly connected.",
+    "1800_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "1800_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+    "1800_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "1800_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "1800_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "1800_8017": "AMS-HT A is drying. Please stop drying process before loading/unloading material.",
+    "1800_8021": "AMS setup failed; please refer to the assistant.",
+    "1800_8023": "AMS-HT A cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "1800_C069": "An error occurred during AMS-HT A drying. Please go to Assistant for more details.",
+    "1800_C06A": "AMS-HT A is reading RFID. Unable to start drying. Please try again later.",
+    "1800_C06B": "AMS-HT A is changing filament. Unable to start drying. Please try again later.",
+    "1800_C06C": "AMS-HT A is in Feed Assist Mode. Unable to start drying. Please try again later.",
+    "1800_C06D": "AMS-HT A is assisting in filament insertion. Unable to start drying. Please try again later.",
+    "1800_C06E": "AMS-HT A motor is performing self-test. Unable to start drying. Please try again later.",
+    "1801_4025": "Failed to read the filament information.",
+    "1801_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "1801_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "1801_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "1801_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "1801_8007": "Extruding filament failed. The extruder might be clogged.",
+    "1801_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT B to the extruder is properly connected.",
+    "1801_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "1801_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+    "1801_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "1801_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "1801_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "1801_8017": "AMS-HT B is drying. Please stop drying process before loading/unloading material.",
+    "1801_8021": "AMS setup failed; please refer to the assistant.",
+    "1801_8023": "AMS-HT B cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "1801_C069": "An error occurred during AMS-HT B drying. Please go to Assistant for more details.",
+    "1801_C06A": "AMS-HT B is reading RFID. Unable to start drying. Please try again later.",
+    "1801_C06B": "AMS-HT B is changing filament. Unable to start drying. Please try again later.",
+    "1801_C06C": "AMS-HT B is in Feed Assist Mode. Unable to start drying. Please try again later.",
+    "1801_C06D": "AMS-HT B is assisting in filament insertion. Unable to start drying. Please try again later.",
+    "1801_C06E": "AMS-HT B motor is performing self-test. Unable to start drying. Please try again later.",
+    "1802_4025": "Failed to read the filament information.",
+    "1802_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "1802_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "1802_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "1802_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "1802_8007": "Extruding filament failed. The extruder might be clogged.",
+    "1802_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT C to the extruder is properly connected.",
+    "1802_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "1802_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+    "1802_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "1802_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "1802_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "1802_8017": "AMS-HT C is drying. Please stop drying process before loading/unloading material.",
+    "1802_8021": "AMS setup failed; please refer to the assistant.",
+    "1802_8023": "AMS-HT C cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "1802_C069": "An error occurred during AMS-HT C drying. Please go to Assistant for more details.",
+    "1802_C06A": "AMS-HT C is reading RFID. Unable to start drying. Please try again later.",
+    "1802_C06B": "AMS-HT C is changing filament. Unable to start drying. Please try again later.",
+    "1802_C06C": "AMS-HT C is in Feed Assist Mode. Unable to start drying. Please try again later.",
+    "1802_C06D": "AMS-HT C is assisting in filament insertion. Unable to start drying. Please try again later.",
+    "1802_C06E": "AMS-HT C motor is performing self-test. Unable to start drying. Please try again later.",
+    "1803_4025": "Failed to read the filament information.",
+    "1803_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "1803_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "1803_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "1803_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "1803_8007": "Extruding filament failed. The extruder might be clogged.",
+    "1803_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT D to the extruder is properly connected.",
+    "1803_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "1803_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+    "1803_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "1803_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "1803_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "1803_8017": "AMS-HT D is drying. Please stop drying process before loading/unloading material.",
+    "1803_8021": "AMS setup failed; please refer to the assistant.",
+    "1803_8023": "AMS-HT D cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "1803_C069": "An error occurred during AMS-HT D drying. Please go to Assistant for more details.",
+    "1803_C06A": "AMS-HT D is reading RFID. Unable to start drying. Please try again later.",
+    "1803_C06B": "AMS-HT D is changing filament. Unable to start drying. Please try again later.",
+    "1803_C06C": "AMS-HT D is in Feed Assist Mode. Unable to start drying. Please try again later.",
+    "1803_C06D": "AMS-HT D is assisting in filament insertion. Unable to start drying. Please try again later.",
+    "1803_C06E": "AMS-HT D motor is performing self-test. Unable to start drying. Please try again later.",
+    "1804_4025": "Failed to read the filament information.",
+    "1804_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "1804_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "1804_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "1804_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "1804_8007": "Extruding filament failed. The extruder might be clogged.",
+    "1804_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT E to the extruder is properly connected.",
+    "1804_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "1804_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+    "1804_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "1804_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "1804_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "1804_8021": "AMS setup failed; please refer to the assistant.",
+    "1804_8023": "AMS-HT E cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "1804_C069": "An error occurred during AMS-HT E drying. Please go to Assistant for more details.",
+    "1804_C06A": "AMS-HT E is reading RFID. Unable to start drying. Please try again later.",
+    "1804_C06B": "AMS-HT E is changing filament. Unable to start drying. Please try again later.",
+    "1804_C06C": "AMS-HT E is in Feed Assist Mode. Unable to start drying. Please try again later.",
+    "1804_C06D": "AMS-HT E is assisting in filament insertion. Unable to start drying. Please try again later.",
+    "1804_C06E": "AMS-HT E motor is performing self-test. Unable to start drying. Please try again later.",
+    "1805_4025": "Failed to read the filament information.",
+    "1805_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "1805_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "1805_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "1805_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "1805_8007": "Extruding filament failed. The extruder might be clogged.",
+    "1805_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT F to the extruder is properly connected.",
+    "1805_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "1805_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+    "1805_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "1805_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "1805_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "1805_8021": "AMS setup failed; please refer to the assistant.",
+    "1805_8023": "AMS-HT F cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "1805_C069": "An error occurred during AMS-HT F drying. Please go to Assistant for more details.",
+    "1805_C06A": "AMS-HT F is reading RFID. Unable to start drying. Please try again later.",
+    "1805_C06B": "AMS-HT F is changing filament. Unable to start drying. Please try again later.",
+    "1805_C06C": "AMS-HT F is in Feed Assist Mode. Unable to start drying. Please try again later.",
+    "1805_C06D": "AMS-HT F is assisting in filament insertion. Unable to start drying. Please try again later.",
+    "1805_C06E": "AMS-HT F motor is performing self-test. Unable to start drying. Please try again later.",
+    "1806_4025": "Failed to read the filament information.",
+    "1806_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "1806_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "1806_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "1806_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "1806_8007": "Extruding filament failed. The extruder might be clogged.",
+    "1806_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT G to the extruder is properly connected.",
+    "1806_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "1806_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+    "1806_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "1806_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "1806_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "1806_8021": "AMS setup failed; please refer to the assistant.",
+    "1806_8023": "AMS-HT G cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "1806_C069": "An error occurred during AMS-HT G drying. Please go to Assistant for more details.",
+    "1806_C06A": "AMS-HT G is reading RFID. Unable to start drying. Please try again later.",
+    "1806_C06B": "AMS-HT G is changing filament. Unable to start drying. Please try again later.",
+    "1806_C06C": "AMS-HT G is in Feed Assist Mode. Unable to start drying. Please try again later.",
+    "1806_C06D": "AMS-HT G is assisting in filament insertion. Unable to start drying. Please try again later.",
+    "1806_C06E": "AMS-HT G motor is performing self-test. Unable to start drying. Please try again later.",
+    "1807_4025": "Failed to read the filament information.",
+    "1807_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.",
+    "1807_8004": "AMS-HT failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.",
+    "1807_8005": "The AMS-HT failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.",
+    "1807_8006": "Unable to feed filament into the extruder. The AMS may be mismatched with the extruder. You can rerun the AMS Setup. This could also be due to an entangled filament or a stuck spool. If not, please...",
+    "1807_8007": "Extruding filament failed. The extruder might be clogged.",
+    "1807_800A": "PTFE tube disconnection detected. Please check if the PTFE tube from AMS-HT H to the extruder is properly connected.",
+    "1807_8010": "The AMS-HT assist motor is overloaded. This could be due to entangled filament or a stuck spool.",
+    "1807_8011": "AMS-HT filament ran out. Please insert a new filament into the same AMS-HT slot.",
+    "1807_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "1807_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged.",
+    "1807_8016": "The extruder is not extruding normally; please refer to the Assistant. After trouble shooting. If the defects are acceptable, please resume.",
+    "1807_8021": "AMS setup failed; please refer to the assistant.",
+    "1807_8023": "AMS-HT H cooling failed. The ambient temperature may be too high. Please operate the device in a suitable environment.",
+    "1807_C069": "An error occurred during AMS-HT H drying. Please go to Assistant for more details.",
+    "1807_C06A": "AMS-HT H is reading RFID. Unable to start drying. Please try again later.",
+    "1807_C06B": "AMS-HT H is changing filament. Unable to start drying. Please try again later.",
+    "1807_C06C": "AMS-HT H is in Feed Assist Mode. Unable to start drying. Please try again later.",
+    "1807_C06D": "AMS-HT H is assisting in filament insertion. Unable to start drying. Please try again later.",
+    "1807_C06E": "AMS-HT H motor is performing self-test. Unable to start drying. Please try again later.",
+    "18FE_8001": "Failed to cut the filament of the left extruder. Please check the cutter.",
+    "18FE_8002": "The cutter of the left extruder is stuck. Please pull out the cutter handle.",
+    "18FE_8003": "Please pull out the filament on the spool holder  of the left extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are ab...",
+    "18FE_8004": "Failed to pull back the filament from the left extruder. Please check whether the filament is stuck inside the extruder.",
+    "18FE_8005": "Failed to feed the filament outside the AMS-HT. Please clip the end of the filament flat and check to see if the spool is stuck.",
+    "18FE_8006": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
+    "18FE_8007": "Please observe the nozzle of the left extruder. If the filament has been extruded, select 'Continue'; if it has not, please push the filament forward slightly, and then select 'Retry'.",
+    "18FE_8011": "The external filament connected to the left extruder has run out; please load a new filament.",
+    "18FE_8012": "Failed to get mapping table; please select 'Resume' to retry.",
+    "18FE_8013": "Timeout purging old filament of the left extruder: Please check if the filament is stuck or the extruder is clogged.",
+    "18FE_8020": "Extruder change failed; please refer to the assistant.",
+    "18FE_8021": "AMS setup failed; please refer to the assistant.",
+    "18FE_8024": "Extruder position calibration failed; please refer to the assistant.",
+    "18FE_C003": "Please pull out the filament on the spool holder of the left extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube i...",
+    "18FE_C006": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
+    "18FE_C008": "Please pull out the filament on the spool holder of the left extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube i...",
+    "18FE_C009": "Please feed filament into the PTFE tube of the left extruder until it can not be pushed any farther.",
+    "18FE_C00A": "Please observe the nozzle of the left extruder. If the filament has been extruded, select 'Continue'; if not, please push the filament forward slightly and then select 'Retry'.",
+    "18FF_8001": "Failed to cut the filament of the right extruder. Please check the cutter.",
+    "18FF_8002": "The cutter of the right extruder is stuck. Please pull out the cutter handle.",
+    "18FF_8003": "Please pull out the filament on the spool holder  of the right extruder. If this message persists, please check to see if there is filament broken in the extruder. (Connect a PTFE tube if you are a...",
+    "18FF_8004": "Failed to pull back the filament from the right extruder. Please check whether the filament is stuck inside the extruder.",
+    "18FF_8005": "Failed to feed the filament outside the AMS-HT. Please clip the end of the filament flat and check to see if the spool is stuck.",
+    "18FF_8006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+    "18FF_8007": "Please observe the nozzle of the right extruder. If the filament has been extruded, select 'Continue'; if it has not, please push the filament forward slightly, and then select 'Retry'.",
+    "18FF_8011": "The external filament connected to the right extruder has run out; please load a new filament.",
+    "18FF_8012": "Failed to get AMS mapping table; please select 'Resume' to retry.",
+    "18FF_8013": "Timeout purging old filament of the right extruder: Please check if the filament is stuck or the extruder is clogged.",
+    "18FF_8020": "Extruder change failed; please refer to the assistant.",
+    "18FF_8021": "AMS setup failed; please refer to the assistant.",
+    "18FF_8024": "Extruder position calibration failed; please refer to the assistant.",
+    "18FF_C003": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube ...",
+    "18FF_C006": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+    "18FF_C008": "Please pull out the filament on the spool holder of the right extruder. If this message persists, please check to see if there is filament broken in the extruder or PTFE tube. (Connect a PTFE tube ...",
+    "18FF_C009": "Please feed filament into the PTFE tube of the right extruder until it can not be pushed any farther.",
+    "18FF_C00A": "Please observe the nozzle of the right extruder. If the filament has been extruded, select 'Continue'; if not, please push the filament forward slightly and then select 'Retry'.",
+}
+
+
+def get_error_description(error_code: str) -> str | None:
+    """Get human-readable description for an HMS error code.
+
+    Args:
+        error_code: Error code in format "XXXX_YYYY" (e.g., "0300_400C")
+
+    Returns:
+        Human-readable description or None if not found
+    """
+    return HMS_ERROR_DESCRIPTIONS.get(error_code.upper())

+ 28 - 3
backend/app/services/notification_service.py

@@ -252,15 +252,18 @@ class NotificationService:
 
         url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
 
-        # Check if message contains URLs (which have underscores that break Markdown)
-        # If so, don't use parse_mode to avoid parsing errors
+        # Check if message contains characters that break Markdown parsing
+        # URLs and error codes with underscores cause issues
         has_url = "http://" in message or "https://" in message
+        # Check for underscores outside of the bold title (odd number of _ breaks markdown)
+        body_part = message.split("\n", 1)[1] if "\n" in message else ""
+        has_problematic_underscore = "_" in body_part
 
         data = {
             "chat_id": chat_id,
             "text": message,
         }
-        if not has_url:
+        if not has_url and not has_problematic_underscore:
             data["parse_mode"] = "Markdown"
 
         client = await self._get_client()
@@ -750,6 +753,28 @@ class NotificationService:
         title, message = await self._build_message_from_template(db, "printer_error", variables)
         await self._send_to_providers(providers, title, message, db, "printer_error", printer_id, printer_name)
 
+    async def on_plate_not_empty(
+        self,
+        printer_id: int,
+        printer_name: str,
+        db: AsyncSession,
+        difference_percent: float | None = None,
+    ):
+        """Handle plate not empty event - objects detected on build plate before print."""
+        providers = await self._get_providers_for_event(db, "on_plate_not_empty", printer_id)
+        if not providers:
+            return
+
+        variables = {
+            "printer": printer_name,
+            "difference_percent": f"{difference_percent:.1f}" if difference_percent else "N/A",
+        }
+
+        title, message = await self._build_message_from_template(db, "plate_not_empty", variables)
+        await self._send_to_providers(
+            providers, title, message, db, "plate_not_empty", printer_id, printer_name, force_immediate=True
+        )
+
     async def on_filament_low(
         self,
         printer_id: int,

+ 23 - 19
backend/app/services/plate_detection.py

@@ -20,8 +20,12 @@ except ImportError:
     OPENCV_AVAILABLE = False
     logger.info("OpenCV not available - plate detection feature disabled")
 
-# Directory to store calibration reference images
-CALIBRATION_DIR = Path(__file__).parent.parent.parent.parent / "data" / "plate_calibration"
+
+def _get_calibration_dir() -> Path:
+    """Get the calibration directory from settings (ensures persistence in Docker)."""
+    from backend.app.core.config import settings
+
+    return settings.plate_calibration_dir
 
 
 class PlateDetectionResult:
@@ -93,8 +97,8 @@ class PlateDetector:
 
     def _get_metadata_path(self, printer_id: int) -> Path:
         """Get the path to the metadata JSON file for a printer."""
-        CALIBRATION_DIR.mkdir(parents=True, exist_ok=True)
-        return CALIBRATION_DIR / f"printer_{printer_id}_metadata.json"
+        _get_calibration_dir().mkdir(parents=True, exist_ok=True)
+        return _get_calibration_dir() / f"printer_{printer_id}_metadata.json"
 
     def _load_metadata(self, printer_id: int) -> dict:
         """Load metadata for a printer's references."""
@@ -119,35 +123,35 @@ class PlateDetector:
 
     def _get_reference_paths(self, printer_id: int) -> list[Path]:
         """Get all existing reference image paths for a printer."""
-        CALIBRATION_DIR.mkdir(parents=True, exist_ok=True)
+        _get_calibration_dir().mkdir(parents=True, exist_ok=True)
         paths = []
         for i in range(self.MAX_REFERENCES):
-            path = CALIBRATION_DIR / f"printer_{printer_id}_ref_{i}.jpg"
+            path = _get_calibration_dir() / f"printer_{printer_id}_ref_{i}.jpg"
             if path.exists():
                 paths.append(path)
         return paths
 
     def _get_next_reference_slot(self, printer_id: int) -> Path:
         """Get the path for the next reference image slot (cycles through slots)."""
-        CALIBRATION_DIR.mkdir(parents=True, exist_ok=True)
+        _get_calibration_dir().mkdir(parents=True, exist_ok=True)
         # Find first empty slot, or use oldest (slot 0) and shift others
         for i in range(self.MAX_REFERENCES):
-            path = CALIBRATION_DIR / f"printer_{printer_id}_ref_{i}.jpg"
+            path = _get_calibration_dir() / f"printer_{printer_id}_ref_{i}.jpg"
             if not path.exists():
                 return path
         # All slots full - return slot 0 (will be overwritten, but we rotate first)
-        return CALIBRATION_DIR / f"printer_{printer_id}_ref_0.jpg"
+        return _get_calibration_dir() / f"printer_{printer_id}_ref_0.jpg"
 
     def _rotate_references(self, printer_id: int) -> None:
         """Rotate references: delete oldest (0), shift others down."""
         # Delete slot 0
-        slot0 = CALIBRATION_DIR / f"printer_{printer_id}_ref_0.jpg"
+        slot0 = _get_calibration_dir() / f"printer_{printer_id}_ref_0.jpg"
         if slot0.exists():
             slot0.unlink()
         # Shift others down
         for i in range(1, self.MAX_REFERENCES):
-            old_path = CALIBRATION_DIR / f"printer_{printer_id}_ref_{i}.jpg"
-            new_path = CALIBRATION_DIR / f"printer_{printer_id}_ref_{i - 1}.jpg"
+            old_path = _get_calibration_dir() / f"printer_{printer_id}_ref_{i}.jpg"
+            new_path = _get_calibration_dir() / f"printer_{printer_id}_ref_{i - 1}.jpg"
             if old_path.exists():
                 old_path.rename(new_path)
 
@@ -172,7 +176,7 @@ class PlateDetector:
         result = []
 
         for i in range(self.MAX_REFERENCES):
-            path = CALIBRATION_DIR / f"printer_{printer_id}_ref_{i}.jpg"
+            path = _get_calibration_dir() / f"printer_{printer_id}_ref_{i}.jpg"
             if path.exists():
                 ref_meta = refs.get(str(i), {})
                 result.append(
@@ -191,7 +195,7 @@ class PlateDetector:
         if index < 0 or index >= self.MAX_REFERENCES:
             return False
 
-        path = CALIBRATION_DIR / f"printer_{printer_id}_ref_{index}.jpg"
+        path = _get_calibration_dir() / f"printer_{printer_id}_ref_{index}.jpg"
         if not path.exists():
             return False
 
@@ -210,7 +214,7 @@ class PlateDetector:
         if index < 0 or index >= self.MAX_REFERENCES:
             return False
 
-        path = CALIBRATION_DIR / f"printer_{printer_id}_ref_{index}.jpg"
+        path = _get_calibration_dir() / f"printer_{printer_id}_ref_{index}.jpg"
         if not path.exists():
             return False
 
@@ -227,8 +231,8 @@ class PlateDetector:
 
         # Shift remaining references down to fill the gap
         for i in range(index + 1, self.MAX_REFERENCES):
-            old_img = CALIBRATION_DIR / f"printer_{printer_id}_ref_{i}.jpg"
-            new_img = CALIBRATION_DIR / f"printer_{printer_id}_ref_{i - 1}.jpg"
+            old_img = _get_calibration_dir() / f"printer_{printer_id}_ref_{i}.jpg"
+            new_img = _get_calibration_dir() / f"printer_{printer_id}_ref_{i - 1}.jpg"
             if old_img.exists():
                 old_img.rename(new_img)
                 # Also shift metadata
@@ -245,7 +249,7 @@ class PlateDetector:
 
         Returns JPEG bytes or None if not found.
         """
-        path = CALIBRATION_DIR / f"printer_{printer_id}_ref_{index}.jpg"
+        path = _get_calibration_dir() / f"printer_{printer_id}_ref_{index}.jpg"
         if not path.exists():
             return None
 
@@ -333,7 +337,7 @@ class PlateDetector:
 
             # Save to next available slot
             slot_index = num_existing
-            reference_path = CALIBRATION_DIR / f"printer_{printer_id}_ref_{slot_index}.jpg"
+            reference_path = _get_calibration_dir() / f"printer_{printer_id}_ref_{slot_index}.jpg"
             cv2.imwrite(str(reference_path), frame, [cv2.IMWRITE_JPEG_QUALITY, 95])
 
             # Save metadata

+ 76 - 0
backend/tests/unit/services/test_hms_errors.py

@@ -0,0 +1,76 @@
+"""Tests for HMS error code translations."""
+
+import pytest
+
+from backend.app.services.hms_errors import HMS_ERROR_DESCRIPTIONS, get_error_description
+
+
+class TestHMSErrorDescriptions:
+    """Tests for the HMS error descriptions dictionary."""
+
+    def test_dictionary_is_not_empty(self):
+        """Verify the error descriptions dictionary has entries."""
+        assert len(HMS_ERROR_DESCRIPTIONS) > 0
+
+    def test_dictionary_has_expected_count(self):
+        """Verify we have the expected number of error codes."""
+        # Should have 853 error codes from the frontend
+        assert len(HMS_ERROR_DESCRIPTIONS) == 853
+
+    def test_all_keys_are_valid_format(self):
+        """Verify all keys follow the XXXX_YYYY format."""
+        import re
+
+        pattern = re.compile(r"^[0-9A-F]{4}_[0-9A-F]{4}$")
+        for code in HMS_ERROR_DESCRIPTIONS:
+            assert pattern.match(code), f"Invalid error code format: {code}"
+
+    def test_all_values_are_non_empty_strings(self):
+        """Verify all descriptions are non-empty strings."""
+        for code, description in HMS_ERROR_DESCRIPTIONS.items():
+            assert isinstance(description, str), f"Description for {code} is not a string"
+            assert len(description) > 0, f"Description for {code} is empty"
+
+
+class TestGetErrorDescription:
+    """Tests for the get_error_description function."""
+
+    def test_returns_description_for_known_code(self):
+        """Verify known error codes return their descriptions."""
+        # 0300_400C = "The task was canceled."
+        result = get_error_description("0300_400C")
+        assert result == "The task was canceled."
+
+    def test_returns_description_for_ams_error(self):
+        """Verify AMS error codes return their descriptions."""
+        # 0700_8010 = AMS assist motor overloaded
+        result = get_error_description("0700_8010")
+        assert "AMS assist motor" in result
+
+    def test_returns_none_for_unknown_code(self):
+        """Verify unknown error codes return None."""
+        result = get_error_description("XXXX_YYYY")
+        assert result is None
+
+    def test_handles_lowercase_input(self):
+        """Verify function handles lowercase input."""
+        result = get_error_description("0300_400c")
+        assert result == "The task was canceled."
+
+    def test_handles_mixed_case_input(self):
+        """Verify function handles mixed case input."""
+        result = get_error_description("0300_400C")
+        assert result == "The task was canceled."
+
+    def test_common_error_codes_have_descriptions(self):
+        """Verify common error codes have descriptions."""
+        common_codes = [
+            "0300_4000",  # Z axis homing failed
+            "0300_4006",  # Nozzle clogged
+            "0300_8004",  # Filament ran out
+            "0500_4001",  # Failed to connect to Bambu Cloud
+            "0700_8010",  # AMS assist motor overloaded
+        ]
+        for code in common_codes:
+            result = get_error_description(code)
+            assert result is not None, f"Missing description for common code: {code}"

+ 227 - 0
backend/tests/unit/services/test_notification_service.py

@@ -965,3 +965,230 @@ class TestNotificationTemplates:
             assert "Test" in result
         except KeyError:
             pytest.fail("Template should handle missing variables gracefully")
+
+
+class TestPrinterErrorNotifications:
+    """Tests for HMS error (printer error) notifications."""
+
+    @pytest.fixture
+    def service(self):
+        return NotificationService()
+
+    @pytest.fixture
+    def mock_provider(self):
+        """Create a mock notification provider with error notifications enabled."""
+        provider = MagicMock()
+        provider.id = 1
+        provider.name = "Test Provider"
+        provider.provider_type = "webhook"
+        provider.enabled = True
+        provider.config = json.dumps({"webhook_url": "http://test.local/webhook"})
+        provider.on_printer_error = True  # Enable error notifications
+        provider.quiet_hours_enabled = False
+        provider.daily_digest_enabled = False
+        provider.printer_id = None
+        return provider
+
+    @pytest.fixture
+    def mock_db(self):
+        """Create a mock database session."""
+        db = AsyncMock()
+        db.commit = AsyncMock()
+        return db
+
+    @pytest.mark.asyncio
+    async def test_on_printer_error_sends_notification(self, service, mock_provider, mock_db):
+        """Verify HMS error notification is sent when triggered."""
+        with (
+            patch.object(service, "_get_providers_for_event", new_callable=AsyncMock) as mock_get,
+            patch.object(service, "_send_to_providers", new_callable=AsyncMock) as mock_send,
+            patch.object(service, "_build_message_from_template", new_callable=AsyncMock) as mock_build,
+        ):
+            mock_get.return_value = [mock_provider]
+            mock_build.return_value = ("Printer Error", "AMS/Filament Error: 0700_8010")
+
+            await service.on_printer_error(
+                printer_id=1,
+                printer_name="Test Printer",
+                error_type="AMS/Filament Error",
+                db=mock_db,
+                error_detail="Error code: 0700_8010",
+            )
+
+            mock_get.assert_called_once()
+            mock_send.assert_called_once()
+
+    @pytest.mark.asyncio
+    async def test_on_printer_error_skipped_when_disabled(self, service, mock_provider, mock_db):
+        """CRITICAL: Verify error notifications respect toggle setting."""
+        mock_provider.on_printer_error = False
+
+        with (
+            patch.object(service, "_get_providers_for_event", new_callable=AsyncMock) as mock_get,
+            patch.object(service, "_send_to_providers", new_callable=AsyncMock) as mock_send,
+        ):
+            # Provider with toggle disabled won't be returned
+            mock_get.return_value = []
+
+            await service.on_printer_error(
+                printer_id=1,
+                printer_name="Test",
+                error_type="AMS Error",
+                db=mock_db,
+                error_detail="Test error",
+            )
+
+            mock_send.assert_not_called()
+
+    @pytest.mark.asyncio
+    async def test_on_printer_error_includes_error_detail(self, service, mock_provider, mock_db):
+        """Verify error details are passed to template variables."""
+        captured_variables = {}
+
+        async def capture_build(db, event_type, variables):
+            captured_variables.update(variables)
+            return ("Test", "Test")
+
+        with (
+            patch.object(service, "_get_providers_for_event", new_callable=AsyncMock) as mock_get,
+            patch.object(service, "_send_to_providers", new_callable=AsyncMock),
+            patch.object(service, "_build_message_from_template", side_effect=capture_build),
+        ):
+            mock_get.return_value = [mock_provider]
+
+            await service.on_printer_error(
+                printer_id=1,
+                printer_name="X1 Carbon",
+                error_type="AMS/Filament Error",
+                db=mock_db,
+                error_detail="Error code: 0700_8010",
+            )
+
+            assert captured_variables["printer"] == "X1 Carbon"
+            assert captured_variables["error_type"] == "AMS/Filament Error"
+            assert captured_variables["error_detail"] == "Error code: 0700_8010"
+
+    @pytest.mark.asyncio
+    async def test_on_printer_error_fallback_when_no_detail(self, service, mock_provider, mock_db):
+        """Verify fallback message when error_detail is None."""
+        captured_variables = {}
+
+        async def capture_build(db, event_type, variables):
+            captured_variables.update(variables)
+            return ("Test", "Test")
+
+        with (
+            patch.object(service, "_get_providers_for_event", new_callable=AsyncMock) as mock_get,
+            patch.object(service, "_send_to_providers", new_callable=AsyncMock),
+            patch.object(service, "_build_message_from_template", side_effect=capture_build),
+        ):
+            mock_get.return_value = [mock_provider]
+
+            await service.on_printer_error(
+                printer_id=1,
+                printer_name="Test Printer",
+                error_type="Unknown Error",
+                db=mock_db,
+                error_detail=None,  # No detail provided
+            )
+
+            assert captured_variables["error_detail"] == "No details available"
+
+
+class TestPlateNotEmptyNotifications:
+    """Tests for plate not empty (build plate detection) notifications."""
+
+    @pytest.fixture
+    def service(self):
+        return NotificationService()
+
+    @pytest.fixture
+    def mock_provider(self):
+        """Create a mock notification provider with plate detection enabled."""
+        provider = MagicMock()
+        provider.id = 1
+        provider.name = "Test Provider"
+        provider.provider_type = "webhook"
+        provider.enabled = True
+        provider.config = json.dumps({"webhook_url": "http://test.local/webhook"})
+        provider.on_plate_not_empty = True
+        provider.quiet_hours_enabled = False
+        provider.daily_digest_enabled = False
+        provider.printer_id = None
+        return provider
+
+    @pytest.fixture
+    def mock_db(self):
+        """Create a mock database session."""
+        db = AsyncMock()
+        db.commit = AsyncMock()
+        return db
+
+    @pytest.mark.asyncio
+    async def test_on_plate_not_empty_sends_notification(self, service, mock_provider, mock_db):
+        """Verify plate not empty notification is sent when triggered."""
+        with (
+            patch.object(service, "_get_providers_for_event", new_callable=AsyncMock) as mock_get,
+            patch.object(service, "_send_to_providers", new_callable=AsyncMock) as mock_send,
+            patch.object(service, "_build_message_from_template", new_callable=AsyncMock) as mock_build,
+        ):
+            mock_get.return_value = [mock_provider]
+            mock_build.return_value = ("Plate Not Empty", "Objects detected on build plate")
+
+            await service.on_plate_not_empty(
+                printer_id=1,
+                printer_name="Test Printer",
+                db=mock_db,
+                difference_percent=5.2,
+            )
+
+            mock_get.assert_called_once()
+            mock_send.assert_called_once()
+            # Verify force_immediate is True (critical alert)
+            call_kwargs = mock_send.call_args[1]
+            assert call_kwargs.get("force_immediate") is True
+
+    @pytest.mark.asyncio
+    async def test_on_plate_not_empty_skipped_when_disabled(self, service, mock_provider, mock_db):
+        """Verify notification is skipped when toggle is disabled."""
+        mock_provider.on_plate_not_empty = False
+
+        with (
+            patch.object(service, "_get_providers_for_event", new_callable=AsyncMock) as mock_get,
+            patch.object(service, "_send_to_providers", new_callable=AsyncMock) as mock_send,
+        ):
+            mock_get.return_value = []
+
+            await service.on_plate_not_empty(
+                printer_id=1,
+                printer_name="Test",
+                db=mock_db,
+            )
+
+            mock_send.assert_not_called()
+
+    @pytest.mark.asyncio
+    async def test_on_plate_not_empty_includes_difference_percent(self, service, mock_provider, mock_db):
+        """Verify difference percentage is passed to template variables."""
+        captured_variables = {}
+
+        async def capture_build(db, event_type, variables):
+            captured_variables.update(variables)
+            return ("Test", "Test")
+
+        with (
+            patch.object(service, "_get_providers_for_event", new_callable=AsyncMock) as mock_get,
+            patch.object(service, "_send_to_providers", new_callable=AsyncMock),
+            patch.object(service, "_build_message_from_template", side_effect=capture_build),
+        ):
+            mock_get.return_value = [mock_provider]
+
+            await service.on_plate_not_empty(
+                printer_id=1,
+                printer_name="X1 Carbon",
+                db=mock_db,
+                difference_percent=3.5,
+            )
+
+            assert captured_variables["printer"] == "X1 Carbon"
+            assert captured_variables["difference_percent"] == "3.5"

+ 256 - 14
frontend/src/__tests__/components/Dashboard.test.tsx

@@ -1,24 +1,266 @@
 /**
  * Tests for the Dashboard component.
- * Note: Dashboard component may be named differently or have different structure.
- * These tests verify basic rendering if the component exists.
+ * Tests drag-and-drop widget management, visibility toggles, and layout persistence.
  */
 
-import { describe, it, expect, beforeEach } from 'vitest';
-import { http, HttpResponse } from 'msw';
-import { server } from '../mocks/server';
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import { screen, fireEvent, waitFor } from '@testing-library/react';
+import { render } from '../utils';
+import { Dashboard, type DashboardWidget } from '../../components/Dashboard';
 
-// Skip these tests as Dashboard component structure may differ
-describe.skip('Dashboard', () => {
+const mockWidgets: DashboardWidget[] = [
+  {
+    id: 'widget-1',
+    title: 'Widget One',
+    component: <div>Widget One Content</div>,
+    defaultVisible: true,
+    defaultSize: 2,
+  },
+  {
+    id: 'widget-2',
+    title: 'Widget Two',
+    component: <div>Widget Two Content</div>,
+    defaultVisible: true,
+    defaultSize: 4,
+  },
+  {
+    id: 'widget-3',
+    title: 'Widget Three',
+    component: <div>Widget Three Content</div>,
+    defaultVisible: false, // Hidden by default
+    defaultSize: 1,
+  },
+];
+
+// Create a working localStorage mock for these tests
+const localStorageData: Record<string, string> = {};
+const localStorageMock = {
+  getItem: vi.fn((key: string) => localStorageData[key] || null),
+  setItem: vi.fn((key: string, value: string) => {
+    localStorageData[key] = value;
+  }),
+  removeItem: vi.fn((key: string) => {
+    delete localStorageData[key];
+  }),
+  clear: vi.fn(() => {
+    Object.keys(localStorageData).forEach((key) => delete localStorageData[key]);
+  }),
+};
+
+describe('Dashboard', () => {
   beforeEach(() => {
-    server.use(
-      http.get('/api/v1/printers/', () => {
-        return HttpResponse.json([]);
-      })
-    );
+    // Clear localStorage data and mocks before each test
+    Object.keys(localStorageData).forEach((key) => delete localStorageData[key]);
+    vi.clearAllMocks();
+    Object.defineProperty(window, 'localStorage', { value: localStorageMock, writable: true });
+  });
+
+  describe('rendering', () => {
+    it('renders visible widgets', () => {
+      render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
+
+      expect(screen.getByText('Widget One')).toBeInTheDocument();
+      expect(screen.getByText('Widget Two')).toBeInTheDocument();
+      expect(screen.getByText('Widget One Content')).toBeInTheDocument();
+      expect(screen.getByText('Widget Two Content')).toBeInTheDocument();
+    });
+
+    it('does not render hidden widgets', () => {
+      render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
+
+      // Widget Three is hidden by default
+      expect(screen.queryByText('Widget Three')).not.toBeInTheDocument();
+      expect(screen.queryByText('Widget Three Content')).not.toBeInTheDocument();
+    });
+
+    it('renders Reset Layout button when controls are shown', () => {
+      render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
+
+      expect(screen.getByText('Reset Layout')).toBeInTheDocument();
+    });
+
+    it('hides controls when hideControls is true', () => {
+      render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" hideControls />);
+
+      expect(screen.queryByText('Reset Layout')).not.toBeInTheDocument();
+    });
+
+    it('shows hidden count button when widgets are hidden', () => {
+      render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
+
+      // Widget Three is hidden by default
+      expect(screen.getByText('1 Hidden')).toBeInTheDocument();
+    });
+  });
+
+  describe('visibility toggle', () => {
+    it('hides a widget when hide button is clicked', async () => {
+      render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
+
+      // Find and click the hide button for Widget One
+      const hideButtons = screen.getAllByTitle('Hide widget');
+      fireEvent.click(hideButtons[0]);
+
+      await waitFor(() => {
+        expect(screen.queryByText('Widget One Content')).not.toBeInTheDocument();
+      });
+    });
+
+    it('shows hidden widgets panel when clicking hidden count button', async () => {
+      render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
+
+      const hiddenButton = screen.getByText('1 Hidden');
+      fireEvent.click(hiddenButton);
+
+      await waitFor(() => {
+        expect(screen.getByText('Hidden widgets (click to show):')).toBeInTheDocument();
+        expect(screen.getByText('Widget Three')).toBeInTheDocument();
+      });
+    });
+
+    it('shows a hidden widget when clicked in the hidden panel', async () => {
+      render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
+
+      // Open hidden panel
+      const hiddenButton = screen.getByText('1 Hidden');
+      fireEvent.click(hiddenButton);
+
+      await waitFor(() => {
+        expect(screen.getByText('Widget Three')).toBeInTheDocument();
+      });
+
+      // Click to show Widget Three
+      const showWidgetButton = screen.getByRole('button', { name: /Widget Three/i });
+      fireEvent.click(showWidgetButton);
+
+      await waitFor(() => {
+        expect(screen.getByText('Widget Three Content')).toBeInTheDocument();
+      });
+    });
   });
 
-  it('placeholder test', () => {
-    expect(true).toBe(true);
+  describe('reset layout', () => {
+    it('resets layout to default when Reset Layout is clicked', async () => {
+      render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
+
+      // Hide Widget One
+      const hideButtons = screen.getAllByTitle('Hide widget');
+      fireEvent.click(hideButtons[0]);
+
+      await waitFor(() => {
+        expect(screen.queryByText('Widget One Content')).not.toBeInTheDocument();
+      });
+
+      // Reset layout
+      const resetButton = screen.getByText('Reset Layout');
+      fireEvent.click(resetButton);
+
+      await waitFor(() => {
+        expect(screen.getByText('Widget One Content')).toBeInTheDocument();
+      });
+    });
+
+    it('calls onResetLayout callback when reset', async () => {
+      const onResetLayout = vi.fn();
+      render(
+        <Dashboard
+          widgets={mockWidgets}
+          storageKey="test-dashboard"
+          onResetLayout={onResetLayout}
+        />
+      );
+
+      const resetButton = screen.getByText('Reset Layout');
+      fireEvent.click(resetButton);
+
+      expect(onResetLayout).toHaveBeenCalled();
+    });
+  });
+
+  describe('size toggle', () => {
+    it('cycles widget size when size button is clicked', async () => {
+      render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard" />);
+
+      // Widget One starts at size 2, should cycle to 4
+      const sizeButtons = screen.getAllByTitle(/Size:/);
+      fireEvent.click(sizeButtons[0]);
+
+      // After click, size should change (verify by checking title updates)
+      await waitFor(() => {
+        // The button title should now show a different size
+        expect(screen.getAllByTitle(/Size:/)[0]).toBeInTheDocument();
+      });
+    });
+  });
+
+  describe('localStorage persistence', () => {
+    it('saves layout to localStorage when widget is hidden', async () => {
+      render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard-persist" />);
+
+      // Hide a widget to trigger a layout change
+      const hideButtons = screen.getAllByTitle('Hide widget');
+      fireEvent.click(hideButtons[0]);
+
+      await waitFor(() => {
+        // Verify setItem was called with the storage key
+        expect(localStorageMock.setItem).toHaveBeenCalled();
+        const calls = localStorageMock.setItem.mock.calls;
+        const lastCall = calls[calls.length - 1];
+        expect(lastCall[0]).toBe('test-dashboard-persist');
+        const parsed = JSON.parse(lastCall[1]);
+        expect(parsed.hidden).toContain('widget-1');
+      });
+    });
+
+    it('loads saved layout from localStorage', () => {
+      // Pre-set a layout in localStorage
+      localStorageData['test-dashboard-load'] = JSON.stringify({
+        order: ['widget-2', 'widget-1', 'widget-3'],
+        hidden: ['widget-2'],
+        sizes: { 'widget-1': 4, 'widget-2': 2, 'widget-3': 1 },
+      });
+
+      render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard-load" />);
+
+      // Widget 2 should be hidden
+      expect(screen.queryByText('Widget Two Content')).not.toBeInTheDocument();
+      // Widget 1 should be visible
+      expect(screen.getByText('Widget One Content')).toBeInTheDocument();
+    });
+  });
+
+  describe('empty state', () => {
+    it('shows empty message when all widgets are hidden', async () => {
+      // Pre-set all widgets as hidden
+      localStorageData['test-dashboard-empty'] = JSON.stringify({
+        order: ['widget-1', 'widget-2', 'widget-3'],
+        hidden: ['widget-1', 'widget-2', 'widget-3'],
+        sizes: {},
+      });
+
+      render(<Dashboard widgets={mockWidgets} storageKey="test-dashboard-empty" />);
+
+      expect(screen.getByText('All widgets are hidden.')).toBeInTheDocument();
+      // There are multiple Reset Layout buttons (one in controls, one in empty state)
+      const resetButtons = screen.getAllByRole('button', { name: 'Reset Layout' });
+      expect(resetButtons.length).toBeGreaterThan(0);
+    });
+  });
+
+  describe('custom render controls', () => {
+    it('renders custom controls when renderControls is provided', () => {
+      render(
+        <Dashboard
+          widgets={mockWidgets}
+          storageKey="test-dashboard"
+          renderControls={({ hiddenCount }) => (
+            <div data-testid="custom-controls">Hidden: {hiddenCount}</div>
+          )}
+        />
+      );
+
+      expect(screen.getByTestId('custom-controls')).toBeInTheDocument();
+      expect(screen.getByText('Hidden: 1')).toBeInTheDocument();
+    });
   });
 });

+ 398 - 11
frontend/src/__tests__/components/FileManagerModal.test.tsx

@@ -1,30 +1,417 @@
 /**
  * Tests for the FileManagerModal component.
- * Note: This component may have a different structure or name.
+ * Tests file browsing, selection, navigation, and file operations.
  */
 
 import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { screen, fireEvent, waitFor } from '@testing-library/react';
+import { render } from '../utils';
+import { FileManagerModal } from '../../components/FileManagerModal';
 import { http, HttpResponse } from 'msw';
 import { server } from '../mocks/server';
 
-// Skip these tests as FileManagerModal component structure may differ
-describe.skip('FileManagerModal', () => {
-  const _mockOnClose = vi.fn();
-  const _mockOnSelect = vi.fn();
+const mockFiles = [
+  {
+    name: 'cache',
+    path: '/cache',
+    size: 0,
+    is_directory: true,
+    mtime: '2024-01-15T10:00:00Z',
+  },
+  {
+    name: 'model',
+    path: '/model',
+    size: 0,
+    is_directory: true,
+    mtime: '2024-01-15T10:00:00Z',
+  },
+  {
+    name: 'benchy.3mf',
+    path: '/benchy.3mf',
+    size: 1024000,
+    is_directory: false,
+    mtime: '2024-01-15T10:00:00Z',
+  },
+  {
+    name: 'print_job.gcode',
+    path: '/print_job.gcode',
+    size: 2048000,
+    is_directory: false,
+    mtime: '2024-01-14T10:00:00Z',
+  },
+];
+
+const mockStorage = {
+  used_bytes: 1073741824, // 1 GB
+  free_bytes: 3221225472, // 3 GB
+};
+
+describe('FileManagerModal', () => {
+  const mockOnClose = vi.fn();
 
   beforeEach(() => {
     vi.clearAllMocks();
     server.use(
-      http.get('/api/v1/library/folders', () => {
-        return HttpResponse.json([]);
+      http.get('/api/v1/printers/:id/files', () => {
+        return HttpResponse.json({ files: mockFiles });
       }),
-      http.get('/api/v1/library/files', () => {
-        return HttpResponse.json([]);
+      http.get('/api/v1/printers/:id/storage', () => {
+        return HttpResponse.json(mockStorage);
+      }),
+      http.delete('/api/v1/printers/:id/files', () => {
+        return HttpResponse.json({ success: true });
       })
     );
   });
 
-  it('placeholder test', () => {
-    expect(true).toBe(true);
+  describe('rendering', () => {
+    it('renders the modal with header', async () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      expect(screen.getByText('File Manager')).toBeInTheDocument();
+      expect(screen.getByText('X1 Carbon')).toBeInTheDocument();
+    });
+
+    it('renders storage info', async () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      await waitFor(() => {
+        expect(screen.getByText(/Used:/)).toBeInTheDocument();
+        expect(screen.getByText(/Free:/)).toBeInTheDocument();
+      });
+    });
+
+    it('renders quick navigation buttons', () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      expect(screen.getByText('Root')).toBeInTheDocument();
+      expect(screen.getByText('Cache')).toBeInTheDocument();
+      expect(screen.getByText('Models')).toBeInTheDocument();
+      expect(screen.getByText('Timelapse')).toBeInTheDocument();
+    });
+
+    it('renders file list', async () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      await waitFor(() => {
+        expect(screen.getByText('cache')).toBeInTheDocument();
+        expect(screen.getByText('model')).toBeInTheDocument();
+        expect(screen.getByText('benchy.3mf')).toBeInTheDocument();
+        expect(screen.getByText('print_job.gcode')).toBeInTheDocument();
+      });
+    });
+
+    it('shows file sizes for files', async () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      await waitFor(() => {
+        // 1024000 bytes = 1000 KB = ~1.0 MB
+        expect(screen.getByText('1000 KB')).toBeInTheDocument();
+      });
+    });
+  });
+
+  describe('navigation', () => {
+    it('navigates into a folder when clicked', async () => {
+      server.use(
+        http.get('/api/v1/printers/:id/files', ({ request }) => {
+          const url = new URL(request.url);
+          const path = url.searchParams.get('path');
+          if (path === '/cache') {
+            return HttpResponse.json({
+              files: [
+                { name: 'temp.dat', path: '/cache/temp.dat', size: 512, is_directory: false },
+              ],
+            });
+          }
+          return HttpResponse.json({ files: mockFiles });
+        })
+      );
+
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      await waitFor(() => {
+        expect(screen.getByText('cache')).toBeInTheDocument();
+      });
+
+      // Click on cache folder
+      fireEvent.click(screen.getByText('cache'));
+
+      await waitFor(() => {
+        expect(screen.getByText('temp.dat')).toBeInTheDocument();
+      });
+    });
+
+    it('shows current path', async () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      expect(screen.getByText('/')).toBeInTheDocument();
+    });
+  });
+
+  describe('file selection', () => {
+    it('selects a file when checkbox is clicked', async () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      await waitFor(() => {
+        expect(screen.getByText('benchy.3mf')).toBeInTheDocument();
+      });
+
+      // Find and click a checkbox (files have checkboxes, directories don't)
+      const checkboxes = screen.getAllByRole('button').filter(btn =>
+        btn.querySelector('svg')?.classList.contains('lucide-square')
+      );
+
+      if (checkboxes.length > 0) {
+        fireEvent.click(checkboxes[0]);
+
+        await waitFor(() => {
+          expect(screen.getByText('1 selected')).toBeInTheDocument();
+        });
+      }
+    });
+
+    it('enables download button when files are selected', async () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      await waitFor(() => {
+        expect(screen.getByText('benchy.3mf')).toBeInTheDocument();
+      });
+
+      // Download button should be disabled initially
+      const downloadButton = screen.getByRole('button', { name: /Download/i });
+      expect(downloadButton).toBeDisabled();
+    });
+
+    it('shows Select All button when files exist', async () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      await waitFor(() => {
+        expect(screen.getByText('Select All')).toBeInTheDocument();
+      });
+    });
+  });
+
+  describe('search and filter', () => {
+    it('renders search input', () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      expect(screen.getByPlaceholderText('Filter files...')).toBeInTheDocument();
+    });
+
+    it('filters files based on search query', async () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      await waitFor(() => {
+        expect(screen.getByText('benchy.3mf')).toBeInTheDocument();
+      });
+
+      const searchInput = screen.getByPlaceholderText('Filter files...');
+      fireEvent.change(searchInput, { target: { value: 'benchy' } });
+
+      await waitFor(() => {
+        expect(screen.getByText('benchy.3mf')).toBeInTheDocument();
+        expect(screen.queryByText('print_job.gcode')).not.toBeInTheDocument();
+      });
+    });
+  });
+
+  describe('sorting', () => {
+    it('renders sort dropdown', () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      expect(screen.getByRole('combobox')).toBeInTheDocument();
+    });
+
+    it('has sort options available', () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      const sortSelect = screen.getByRole('combobox');
+      expect(sortSelect).toBeInTheDocument();
+
+      // Check that options exist
+      expect(screen.getByText('Name (A-Z)')).toBeInTheDocument();
+    });
+  });
+
+  describe('close behavior', () => {
+    it('calls onClose when X button is clicked', async () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      const closeButton = screen.getAllByRole('button').find(btn =>
+        btn.querySelector('.lucide-x')
+      );
+
+      if (closeButton) {
+        fireEvent.click(closeButton);
+        expect(mockOnClose).toHaveBeenCalled();
+      }
+    });
+
+    it('calls onClose when clicking outside the modal', () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      // Click on the backdrop
+      const backdrop = document.querySelector('.fixed.inset-0');
+      if (backdrop) {
+        fireEvent.click(backdrop);
+        expect(mockOnClose).toHaveBeenCalled();
+      }
+    });
+
+    it('calls onClose when Escape key is pressed', () => {
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      fireEvent.keyDown(window, { key: 'Escape' });
+      expect(mockOnClose).toHaveBeenCalled();
+    });
+  });
+
+  describe('empty state', () => {
+    it('shows empty message when directory has no files', async () => {
+      server.use(
+        http.get('/api/v1/printers/:id/files', () => {
+          return HttpResponse.json({ files: [] });
+        })
+      );
+
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      await waitFor(() => {
+        expect(screen.getByText('No files in this directory')).toBeInTheDocument();
+      });
+    });
+  });
+
+  describe('loading state', () => {
+    it('shows loading spinner while fetching files', () => {
+      // Delay the response to see loading state
+      server.use(
+        http.get('/api/v1/printers/:id/files', async () => {
+          await new Promise((r) => setTimeout(r, 100));
+          return HttpResponse.json({ files: mockFiles });
+        })
+      );
+
+      render(
+        <FileManagerModal
+          printerId={1}
+          printerName="X1 Carbon"
+          onClose={mockOnClose}
+        />
+      );
+
+      // The loader should be present initially
+      const loader = document.querySelector('.animate-spin');
+      expect(loader).toBeInTheDocument();
+    });
   });
 });

+ 290 - 7
frontend/src/__tests__/components/UploadModal.test.tsx

@@ -1,13 +1,296 @@
 /**
- * Tests for upload modal functionality.
- * Note: UploadModal may be integrated into other components.
+ * Tests for the UploadModal component.
+ * Tests file upload functionality with drag-and-drop support.
  */
 
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { screen, fireEvent, waitFor } from '@testing-library/react';
+import { render } from '../utils';
+import { UploadModal } from '../../components/UploadModal';
+import { http, HttpResponse } from 'msw';
+import { server } from '../mocks/server';
 
-// Skip these tests as UploadModal may be integrated into FileManagerPage
-describe.skip('UploadModal', () => {
-  it('placeholder test', () => {
-    expect(true).toBe(true);
+const mockPrinters = [
+  { id: 1, name: 'X1 Carbon', model: 'X1C', serial_number: '123' },
+  { id: 2, name: 'P1S', model: 'P1S', serial_number: '456' },
+];
+
+describe('UploadModal', () => {
+  const mockOnClose = vi.fn();
+
+  beforeEach(() => {
+    vi.clearAllMocks();
+    server.use(
+      http.get('/api/v1/printers/', () => {
+        return HttpResponse.json(mockPrinters);
+      }),
+      http.post('/api/v1/archives/upload-bulk', async () => {
+        return HttpResponse.json({
+          uploaded: 1,
+          failed: 0,
+          results: [{ id: 1, filename: 'test.3mf' }],
+          errors: [],
+        });
+      })
+    );
+  });
+
+  describe('rendering', () => {
+    it('renders the modal with title', () => {
+      render(<UploadModal onClose={mockOnClose} />);
+
+      expect(screen.getByText('Upload 3MF Files')).toBeInTheDocument();
+    });
+
+    it('renders drag and drop zone', () => {
+      render(<UploadModal onClose={mockOnClose} />);
+
+      expect(screen.getByText('Drag & drop .3mf files here')).toBeInTheDocument();
+    });
+
+    it('renders Browse Files button', () => {
+      render(<UploadModal onClose={mockOnClose} />);
+
+      expect(screen.getByRole('button', { name: 'Browse Files' })).toBeInTheDocument();
+    });
+
+    it('renders printer selection dropdown', async () => {
+      render(<UploadModal onClose={mockOnClose} />);
+
+      await waitFor(() => {
+        expect(screen.getByText('Associate with printer (optional)')).toBeInTheDocument();
+      });
+
+      const select = screen.getByRole('combobox');
+      expect(select).toBeInTheDocument();
+    });
+
+    it('renders Cancel button', () => {
+      render(<UploadModal onClose={mockOnClose} />);
+
+      expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
+    });
+
+    it('renders Upload button (disabled initially)', () => {
+      render(<UploadModal onClose={mockOnClose} />);
+
+      const uploadButton = screen.getByRole('button', { name: /Upload/i });
+      expect(uploadButton).toBeDisabled();
+    });
+  });
+
+  describe('printer selection', () => {
+    it('shows available printers in dropdown', async () => {
+      render(<UploadModal onClose={mockOnClose} />);
+
+      await waitFor(() => {
+        // Check for printer options in the select
+        expect(screen.getByRole('option', { name: 'No printer' })).toBeInTheDocument();
+        expect(screen.getByRole('option', { name: 'X1 Carbon' })).toBeInTheDocument();
+        expect(screen.getByRole('option', { name: 'P1S' })).toBeInTheDocument();
+      });
+    });
+
+    it('allows selecting a printer', async () => {
+      render(<UploadModal onClose={mockOnClose} />);
+
+      await waitFor(() => {
+        expect(screen.getByRole('option', { name: 'X1 Carbon' })).toBeInTheDocument();
+      });
+
+      const select = screen.getByRole('combobox');
+      fireEvent.change(select, { target: { value: '1' } });
+
+      expect(select).toHaveValue('1');
+    });
+  });
+
+  describe('file handling with initialFiles', () => {
+    it('shows initial files when provided', () => {
+      const initialFiles = [
+        new File(['content'], 'model.3mf', { type: 'application/3mf' }),
+      ];
+
+      render(<UploadModal onClose={mockOnClose} initialFiles={initialFiles} />);
+
+      expect(screen.getByText('model.3mf')).toBeInTheDocument();
+    });
+
+    it('enables Upload button when files are present', () => {
+      const initialFiles = [
+        new File(['content'], 'model.3mf', { type: 'application/3mf' }),
+      ];
+
+      render(<UploadModal onClose={mockOnClose} initialFiles={initialFiles} />);
+
+      const uploadButton = screen.getByRole('button', { name: /Upload/i });
+      expect(uploadButton).not.toBeDisabled();
+    });
+
+    it('shows file count in Upload button', () => {
+      const initialFiles = [
+        new File(['content'], 'model1.3mf', { type: 'application/3mf' }),
+        new File(['content'], 'model2.3mf', { type: 'application/3mf' }),
+      ];
+
+      render(<UploadModal onClose={mockOnClose} initialFiles={initialFiles} />);
+
+      expect(screen.getByRole('button', { name: /Upload \(2\)/i })).toBeInTheDocument();
+    });
+
+    it('filters out non-3mf files from initialFiles', () => {
+      const initialFiles = [
+        new File(['content'], 'model.3mf', { type: 'application/3mf' }),
+        new File(['content'], 'image.png', { type: 'image/png' }),
+        new File(['content'], 'doc.txt', { type: 'text/plain' }),
+      ];
+
+      render(<UploadModal onClose={mockOnClose} initialFiles={initialFiles} />);
+
+      expect(screen.getByText('model.3mf')).toBeInTheDocument();
+      expect(screen.queryByText('image.png')).not.toBeInTheDocument();
+      expect(screen.queryByText('doc.txt')).not.toBeInTheDocument();
+    });
+  });
+
+  describe('file removal', () => {
+    it('allows removing a file before upload', async () => {
+      const initialFiles = [
+        new File(['content'], 'model.3mf', { type: 'application/3mf' }),
+      ];
+
+      render(<UploadModal onClose={mockOnClose} initialFiles={initialFiles} />);
+
+      expect(screen.getByText('model.3mf')).toBeInTheDocument();
+
+      // Find and click the remove button (X icon next to file)
+      const fileItem = screen.getByText('model.3mf').closest('.flex');
+      const removeButton = fileItem?.querySelector('button');
+
+      if (removeButton) {
+        fireEvent.click(removeButton);
+
+        await waitFor(() => {
+          expect(screen.queryByText('model.3mf')).not.toBeInTheDocument();
+        });
+      }
+    });
+  });
+
+  describe('upload button behavior', () => {
+    it('Upload button triggers upload mutation when clicked', async () => {
+      const initialFiles = [
+        new File(['content'], 'test.3mf', { type: 'application/3mf' }),
+      ];
+
+      render(<UploadModal onClose={mockOnClose} initialFiles={initialFiles} />);
+
+      const uploadButton = screen.getByRole('button', { name: /Upload/i });
+      expect(uploadButton).not.toBeDisabled();
+
+      // Click should trigger upload (button text will change)
+      fireEvent.click(uploadButton);
+
+      // The button should show uploading state or become disabled
+      await waitFor(() => {
+        // Either showing "Uploading..." or a spinner is present
+        const hasUploadingText = screen.queryByText(/Uploading/i) !== null;
+        const hasSpinner = document.querySelector('.animate-spin') !== null;
+        expect(hasUploadingText || hasSpinner).toBe(true);
+      });
+    });
+
+    it('Upload button is disabled when no files are pending', async () => {
+      render(<UploadModal onClose={mockOnClose} initialFiles={[]} />);
+
+      const uploadButton = screen.getByRole('button', { name: /Upload/i });
+      expect(uploadButton).toBeDisabled();
+    });
+
+    it('shows correct file count in Upload button', () => {
+      const initialFiles = [
+        new File(['content'], 'file1.3mf', { type: 'application/3mf' }),
+        new File(['content'], 'file2.3mf', { type: 'application/3mf' }),
+        new File(['content'], 'file3.3mf', { type: 'application/3mf' }),
+      ];
+
+      render(<UploadModal onClose={mockOnClose} initialFiles={initialFiles} />);
+
+      expect(screen.getByRole('button', { name: /Upload \(3\)/i })).toBeInTheDocument();
+    });
+  });
+
+  describe('close behavior', () => {
+    it('calls onClose when Cancel button is clicked', () => {
+      render(<UploadModal onClose={mockOnClose} />);
+
+      const cancelButton = screen.getByRole('button', { name: 'Cancel' });
+      fireEvent.click(cancelButton);
+
+      expect(mockOnClose).toHaveBeenCalled();
+    });
+
+    it('calls onClose when X button is clicked', () => {
+      render(<UploadModal onClose={mockOnClose} />);
+
+      // Find the X button in the header
+      const buttons = screen.getAllByRole('button');
+      const closeButton = buttons.find(btn =>
+        btn.querySelector('.lucide-x')
+      );
+
+      if (closeButton) {
+        fireEvent.click(closeButton);
+        expect(mockOnClose).toHaveBeenCalled();
+      }
+    });
+
+    it('calls onClose when Escape key is pressed', () => {
+      render(<UploadModal onClose={mockOnClose} />);
+
+      fireEvent.keyDown(window, { key: 'Escape' });
+      expect(mockOnClose).toHaveBeenCalled();
+    });
+  });
+
+  describe('drag and drop', () => {
+    it('highlights drop zone on drag over', () => {
+      render(<UploadModal onClose={mockOnClose} />);
+
+      const dropZone = screen.getByText('Drag & drop .3mf files here').closest('div');
+
+      if (dropZone) {
+        fireEvent.dragOver(dropZone, {
+          dataTransfer: { files: [] },
+        });
+
+        // The drop zone should have the highlight class
+        expect(dropZone.className).toContain('border-bambu-green');
+      }
+    });
+
+    it('removes highlight on drag leave', () => {
+      render(<UploadModal onClose={mockOnClose} />);
+
+      const dropZone = screen.getByText('Drag & drop .3mf files here').closest('div');
+
+      if (dropZone) {
+        fireEvent.dragOver(dropZone, { dataTransfer: { files: [] } });
+        fireEvent.dragLeave(dropZone, { dataTransfer: { files: [] } });
+
+        // The drop zone should not have the highlight class
+        expect(dropZone.className).not.toContain('bg-bambu-green');
+      }
+    });
+  });
+
+  describe('file size display', () => {
+    it('shows file size in MB', () => {
+      const file = new File(['x'.repeat(1048576)], 'large.3mf', { type: 'application/3mf' }); // 1 MB
+
+      render(<UploadModal onClose={mockOnClose} initialFiles={[file]} />);
+
+      expect(screen.getByText('1.0 MB')).toBeInTheDocument();
+    });
   });
 });

+ 6 - 0
frontend/src/api/client.ts

@@ -1184,6 +1184,8 @@ export interface NotificationProvider {
   // AMS-HT environmental alarms
   on_ams_ht_humidity_high: boolean;
   on_ams_ht_temperature_high: boolean;
+  // Build plate detection
+  on_plate_not_empty: boolean;
   // Quiet hours
   quiet_hours_enabled: boolean;
   quiet_hours_start: string | null;
@@ -1224,6 +1226,8 @@ export interface NotificationProviderCreate {
   // AMS-HT environmental alarms
   on_ams_ht_humidity_high?: boolean;
   on_ams_ht_temperature_high?: boolean;
+  // Build plate detection
+  on_plate_not_empty?: boolean;
   // Quiet hours
   quiet_hours_enabled?: boolean;
   quiet_hours_start?: string | null;
@@ -1257,6 +1261,8 @@ export interface NotificationProviderUpdate {
   // AMS-HT environmental alarms
   on_ams_ht_humidity_high?: boolean;
   on_ams_ht_temperature_high?: boolean;
+  // Build plate detection
+  on_plate_not_empty?: boolean;
   // Quiet hours
   quiet_hours_enabled?: boolean;
   quiet_hours_start?: string | null;

+ 14 - 0
frontend/src/components/NotificationProviderCard.tsx

@@ -124,6 +124,9 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
             {provider.on_print_start && (
               <span className="px-2 py-0.5 bg-blue-500/20 text-blue-400 text-xs rounded">Start</span>
             )}
+            {provider.on_plate_not_empty && (
+              <span className="px-2 py-0.5 bg-rose-600/20 text-rose-300 text-xs rounded">Plate Check</span>
+            )}
             {provider.on_print_complete && (
               <span className="px-2 py-0.5 bg-bambu-green/20 text-bambu-green text-xs rounded">Complete</span>
             )}
@@ -254,6 +257,17 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
                   />
                 </div>
 
+                <div className="flex items-center justify-between">
+                  <div>
+                    <p className="text-sm text-white">Plate Not Empty</p>
+                    <p className="text-xs text-bambu-gray">Objects detected before print</p>
+                  </div>
+                  <Toggle
+                    checked={provider.on_plate_not_empty ?? true}
+                    onChange={(checked) => updateMutation.mutate({ on_plate_not_empty: checked })}
+                  />
+                </div>
+
                 <div className="flex items-center justify-between">
                   <p className="text-sm text-white">Print Completed</p>
                   <Toggle

+ 18 - 3
frontend/src/contexts/AuthContext.tsx

@@ -21,10 +21,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
   const [requiresSetup, setRequiresSetup] = useState(false);
   const [loading, setLoading] = useState(true);
   const hasRedirectedRef = useRef(false);
+  const mountedRef = useRef(true);
 
   const checkAuthStatus = async () => {
     try {
       const status = await api.getAuthStatus();
+      if (!mountedRef.current) return;
       setAuthEnabled(status.auth_enabled);
       setRequiresSetup(status.requires_setup);
 
@@ -33,10 +35,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
         if (token) {
           try {
             const currentUser = await api.getCurrentUser();
+            if (!mountedRef.current) return;
             setUser(currentUser);
           } catch {
             // Token invalid, clear it
             setAuthToken(null);
+            if (!mountedRef.current) return;
             setUser(null);
           }
         } else {
@@ -47,16 +51,23 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
         setUser(null);
       }
     } catch {
+      if (!mountedRef.current) return;
       setAuthEnabled(false);
       setUser(null);
     } finally {
-      setLoading(false);
+      if (mountedRef.current) {
+        setLoading(false);
+      }
     }
   };
 
   useEffect(() => {
+    mountedRef.current = true;
     // Check auth status on mount
     checkAuthStatus();
+    return () => {
+      mountedRef.current = false;
+    };
   }, []);
 
   // Separate effect to handle redirect only when setup is required
@@ -95,10 +106,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
     if (authEnabled && getAuthToken()) {
       try {
         const currentUser = await api.getCurrentUser();
-        setUser(currentUser);
+        if (mountedRef.current) {
+          setUser(currentUser);
+        }
       } catch {
         setAuthToken(null);
-        setUser(null);
+        if (mountedRef.current) {
+          setUser(null);
+        }
       }
     }
   };

+ 9 - 9
frontend/src/pages/PrintersPage.tsx

@@ -2701,17 +2701,17 @@ function PrinterCard({
               >
                 <Video className="w-4 h-4" />
               </Button>
-              {/* Split button: main part opens modal, chevron toggles */}
+              {/* Split button: main part toggles detection, chevron opens modal */}
               <div className={`inline-flex rounded-md ${printer.plate_detection_enabled ? 'ring-1 ring-green-500' : ''}`}>
                 <Button
                   variant="secondary"
                   size="sm"
-                  onClick={handleOpenPlateManagement}
-                  disabled={!status?.connected || isCheckingPlate}
-                  title="Manage plate detection calibration"
+                  onClick={handleTogglePlateDetection}
+                  disabled={!status?.connected || plateDetectionMutation.isPending}
+                  title={printer.plate_detection_enabled ? "Plate check enabled - Click to disable" : "Plate check disabled - Click to enable"}
                   className={`!rounded-r-none !border-r-0 ${printer.plate_detection_enabled ? "!border-green-500 !text-green-400 hover:!bg-green-500/20" : ""}`}
                 >
-                  {isCheckingPlate ? (
+                  {plateDetectionMutation.isPending ? (
                     <Loader2 className="w-4 h-4 animate-spin" />
                   ) : (
                     <ScanSearch className="w-4 h-4" />
@@ -2720,12 +2720,12 @@ function PrinterCard({
                 <Button
                   variant="secondary"
                   size="sm"
-                  onClick={handleTogglePlateDetection}
-                  disabled={!status?.connected || plateDetectionMutation.isPending}
-                  title={printer.plate_detection_enabled ? "Plate check enabled - Click to disable" : "Plate check disabled - Click to enable"}
+                  onClick={handleOpenPlateManagement}
+                  disabled={!status?.connected || isCheckingPlate}
+                  title="Manage plate detection calibration"
                   className={`!rounded-l-none !px-1.5 ${printer.plate_detection_enabled ? "!border-green-500 !text-green-400 hover:!bg-green-500/20" : ""}`}
                 >
-                  {plateDetectionMutation.isPending ? (
+                  {isCheckingPlate ? (
                     <Loader2 className="w-3 h-3 animate-spin" />
                   ) : (
                     <ChevronDown className="w-3 h-3" />

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-B2lCUmey.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-BI0S_RVt.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-BQIOMqJ9.css


+ 2 - 2
static/index.html

@@ -23,8 +23,8 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-BraSLW7a.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-BI0S_RVt.css">
+    <script type="module" crossorigin src="/assets/index-B2lCUmey.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-BQIOMqJ9.css">
   </head>
   <body>
     <div id="root"></div>

Некоторые файлы не были показаны из-за большого количества измененных файлов