From 6fb71de6a2814285c166355144b842e0714df855 Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 4 Jan 2026 18:41:24 +0100 Subject: [PATCH] Add firmware update helper for LAN-only printers Enables checking and uploading firmware updates for printers operating in LAN-only mode without Bambu Cloud connectivity. Features: - Automatic firmware version checking against Bambu Lab servers - Orange "Update" badge on printer cards when updates available - Firmware update modal with version info and release notes - One-click firmware upload to printer SD card via FTP - Real-time upload progress (actual bytes transferred) - Step-by-step instructions for triggering update from printer - Local firmware caching for faster re-uploads - Supports all Bambu Lab printer models New files: - backend/app/services/firmware_check.py - Version checking service - backend/app/services/firmware_update.py - Upload orchestration - backend/app/api/routes/firmware.py - REST API endpoints Also includes: - FTP upload progress callback support - 10-minute upload timeout protection - Firmware cache directory in .gitignore --- .gitignore | 3 + CHANGELOG.md | 13 + README.md | 1 + backend/app/api/routes/cloud.py | 78 + backend/app/api/routes/firmware.py | 298 + backend/app/api/routes/printers.py | 5 + backend/app/main.py | 2 + backend/app/schemas/cloud.py | 18 + backend/app/schemas/printer.py | 2 + backend/app/services/bambu_cloud.py | 30 + backend/app/services/bambu_ftp.py | 33 +- backend/app/services/bambu_mqtt.py | 56 + backend/app/services/firmware_check.py | 382 + backend/app/services/firmware_update.py | 353 + frontend/src/api/client.ts | 54 + frontend/src/pages/PrintersPage.tsx | 234 +- static/assets/index-BaxJ1N11.js | 8663 ----------------------- static/assets/index-DPwHFDrf.js | 8663 +++++++++++++++++++++++ static/assets/index-DYSmHndB.css | 1 + static/assets/index-Ds1sabci.css | 1 - static/index.html | 4 +- 21 files changed, 10220 insertions(+), 8674 deletions(-) create mode 100644 backend/app/api/routes/firmware.py create mode 100644 backend/app/services/firmware_check.py create mode 100644 backend/app/services/firmware_update.py delete mode 100644 static/assets/index-BaxJ1N11.js create mode 100644 static/assets/index-DPwHFDrf.js create mode 100644 static/assets/index-DYSmHndB.css delete mode 100644 static/assets/index-Ds1sabci.css diff --git a/.gitignore b/.gitignore index f6a98535c..124f14cd1 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ npm-debug.log* # Archive files (user data) archive/ +# Firmware cache (downloaded firmware files) +firmware/ + # Virtual printer (auto-generated certs and uploads) virtual_printer/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d2e644a3..d7739f733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to Bambuddy will be documented in this file. +## [0.1.6b7] - 2026-01-04 + +### Added +- **Firmware update helper** - Check and upload firmware updates for LAN-only printers: + - Automatic firmware update checking against Bambu Lab's servers + - Orange "Update" badge on printer cards when updates are available + - Click badge to open firmware update modal with version info and release notes + - One-click firmware upload to printer's SD card via FTP + - Real-time upload progress tracking with actual bytes transferred + - Step-by-step instructions for triggering update from printer screen + - Supports all Bambu Lab printer models (X1C, X1, X1E, P1S, P1P, P2S, A1, A1 Mini, H2D, H2C, H2S) + - Firmware files cached locally for faster re-uploads + ## [0.1.6b6] - 2026-01-04 ### Added diff --git a/README.md b/README.md index ce765eba3..5d7c7f630 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ - Interval reminders (hours/days) - Print time accuracy stats - File manager for printer storage +- Firmware update helper (LAN-only printers) diff --git a/backend/app/api/routes/cloud.py b/backend/app/api/routes/cloud.py index b285014ad..36ebf38d3 100644 --- a/backend/app/api/routes/cloud.py +++ b/backend/app/api/routes/cloud.py @@ -22,6 +22,8 @@ from backend.app.schemas.cloud import ( CloudLoginResponse, CloudTokenRequest, CloudVerifyRequest, + FirmwareUpdateInfo, + FirmwareUpdatesResponse, SlicerSetting, SlicerSettingCreate, SlicerSettingDeleteResponse, @@ -371,6 +373,82 @@ async def get_devices(db: AsyncSession = Depends(get_db)): raise HTTPException(status_code=500, detail=str(e)) +@router.get("/firmware-updates", response_model=FirmwareUpdatesResponse) +async def get_firmware_updates(db: AsyncSession = Depends(get_db)): + """ + Check for firmware updates for all bound devices. + + Returns firmware version info for each device including: + - Current installed version + - Latest available version + - Whether an update is available + - Release notes for the latest version + + Requires cloud authentication. + """ + token, _ = await get_stored_token(db) + if not token: + raise HTTPException(status_code=401, detail="Not authenticated") + + cloud = get_cloud_service() + cloud.set_token(token) + + if not cloud.is_authenticated: + raise HTTPException(status_code=401, detail="Not authenticated") + + try: + # First get list of bound devices + devices_data = await cloud.get_devices() + devices = devices_data.get("devices", []) + + updates = [] + updates_available = 0 + + # Check firmware for each device + for device in devices: + device_id = device.get("dev_id", "") + device_name = device.get("name", "Unknown") + + try: + firmware_info = await cloud.get_firmware_version(device_id) + update_available = firmware_info.get("update_available", False) + + if update_available: + updates_available += 1 + + updates.append( + FirmwareUpdateInfo( + device_id=device_id, + device_name=device_name, + current_version=firmware_info.get("current_version"), + latest_version=firmware_info.get("latest_version"), + update_available=update_available, + release_notes=firmware_info.get("release_notes"), + ) + ) + except BambuCloudError as e: + logger.warning(f"Failed to get firmware info for {device_name}: {e}") + # Still include device but with unknown firmware status + updates.append( + FirmwareUpdateInfo( + device_id=device_id, + device_name=device_name, + current_version=None, + latest_version=None, + update_available=False, + release_notes=None, + ) + ) + + return FirmwareUpdatesResponse(updates=updates, updates_available=updates_available) + + except BambuCloudAuthError: + await clear_token(db) + raise HTTPException(status_code=401, detail="Authentication expired") + except BambuCloudError as e: + raise HTTPException(status_code=500, detail=str(e)) + + @router.post("/settings") async def create_setting(request: SlicerSettingCreate, db: AsyncSession = Depends(get_db)): """ diff --git a/backend/app/api/routes/firmware.py b/backend/app/api/routes/firmware.py new file mode 100644 index 000000000..6bfd86528 --- /dev/null +++ b/backend/app/api/routes/firmware.py @@ -0,0 +1,298 @@ +""" +Firmware Update API Routes + +Check for firmware updates from Bambu Lab. +Also provides endpoints for uploading firmware to printers via SD card. +""" + +import logging + +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel, Field +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from backend.app.core.database import get_db +from backend.app.models.printer import Printer +from backend.app.services.firmware_check import get_firmware_service +from backend.app.services.firmware_update import ( + FirmwareUploadStatus, + get_firmware_update_service, + get_upload_state, +) +from backend.app.services.printer_manager import printer_manager + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/firmware", tags=["firmware"]) + + +class FirmwareUpdateInfo(BaseModel): + """Firmware update information for a printer.""" + + printer_id: int + printer_name: str + model: str | None + current_version: str | None + latest_version: str | None + update_available: bool + download_url: str | None = None + release_notes: str | None = None + + +class FirmwareUpdatesResponse(BaseModel): + """Response containing firmware updates for all printers.""" + + updates: list[FirmwareUpdateInfo] = Field(default_factory=list) + updates_available: int = Field(0, description="Number of printers with updates available") + + +class LatestFirmwareInfo(BaseModel): + """Latest firmware version info for a model.""" + + model_key: str + version: str + download_url: str + release_notes: str | None = None + + +@router.get("/updates", response_model=FirmwareUpdatesResponse) +async def check_firmware_updates( + db: AsyncSession = Depends(get_db), +): + """ + Check for firmware updates for all connected printers. + + Compares each printer's current firmware version against the latest + available version from Bambu Lab's official firmware download page. + + Note: This does not require cloud authentication - it uses public + firmware information from bambulab.com. + """ + firmware_service = get_firmware_service() + + # Get all printers from database + result = await db.execute(select(Printer).where(Printer.is_active.is_(True))) + printers = result.scalars().all() + + updates = [] + updates_available = 0 + + for printer in printers: + # Get current firmware version from MQTT state + current_version = None + mqtt_client = printer_manager.get_client(printer.id) + if mqtt_client and mqtt_client.state: + current_version = mqtt_client.state.firmware_version + + # Check for update + model = printer.model or "Unknown" + update_info = await firmware_service.check_for_update(model, current_version or "") + + if update_info["update_available"]: + updates_available += 1 + + updates.append( + FirmwareUpdateInfo( + printer_id=printer.id, + printer_name=printer.name, + model=model, + current_version=current_version, + latest_version=update_info["latest_version"], + update_available=update_info["update_available"], + download_url=update_info["download_url"], + release_notes=update_info["release_notes"], + ) + ) + + return FirmwareUpdatesResponse(updates=updates, updates_available=updates_available) + + +@router.get("/updates/{printer_id}", response_model=FirmwareUpdateInfo) +async def check_printer_firmware( + printer_id: int, + db: AsyncSession = Depends(get_db), +): + """ + Check for firmware update for a specific printer. + """ + firmware_service = get_firmware_service() + + # Get printer from database + result = await db.execute(select(Printer).where(Printer.id == printer_id)) + printer = result.scalar_one_or_none() + + if not printer: + raise HTTPException(status_code=404, detail="Printer not found") + + # Get current firmware version from MQTT state + current_version = None + mqtt_client = printer_manager.get_client(printer.id) + if mqtt_client and mqtt_client.state: + current_version = mqtt_client.state.firmware_version + + # Check for update + model = printer.model or "Unknown" + update_info = await firmware_service.check_for_update(model, current_version or "") + + return FirmwareUpdateInfo( + printer_id=printer.id, + printer_name=printer.name, + model=model, + current_version=current_version, + latest_version=update_info["latest_version"], + update_available=update_info["update_available"], + download_url=update_info["download_url"], + release_notes=update_info["release_notes"], + ) + + +@router.get("/latest", response_model=list[LatestFirmwareInfo]) +async def get_all_latest_firmware(): + """ + Get the latest firmware versions for all Bambu Lab printer models. + + This endpoint fetches the latest available firmware versions from + Bambu Lab's official firmware download page. + """ + firmware_service = get_firmware_service() + versions = await firmware_service.get_all_latest_versions() + + return [ + LatestFirmwareInfo( + model_key=key, + version=info.version, + download_url=info.download_url, + release_notes=info.release_notes, + ) + for key, info in versions.items() + ] + + +# ============================================================================ +# Firmware Upload Endpoints (for LAN-only firmware updates) +# ============================================================================ + + +class FirmwareUploadPrepareResponse(BaseModel): + """Response from firmware upload preparation check.""" + + can_proceed: bool + sd_card_present: bool + sd_card_free_space: int = Field(-1, description="Free space in bytes, -1 if unknown") + firmware_size: int = Field(0, description="Estimated firmware size in bytes") + space_sufficient: bool + update_available: bool + current_version: str | None = None + latest_version: str | None = None + firmware_filename: str | None = None + errors: list[str] = Field(default_factory=list) + + +class FirmwareUploadStatusResponse(BaseModel): + """Response containing firmware upload status.""" + + status: str # idle, preparing, downloading, uploading, complete, error + progress: int = Field(0, ge=0, le=100) + message: str = "" + error: str | None = None + firmware_filename: str | None = None + firmware_version: str | None = None + + +class FirmwareUploadStartResponse(BaseModel): + """Response when starting a firmware upload.""" + + started: bool + message: str + + +@router.get("/updates/{printer_id}/prepare", response_model=FirmwareUploadPrepareResponse) +async def prepare_firmware_upload( + printer_id: int, + db: AsyncSession = Depends(get_db), +): + """ + Check prerequisites for uploading firmware to a printer. + + This performs pre-flight checks including: + - SD card presence + - Available storage space + - Update availability + + Call this before starting a firmware upload to ensure the operation + can succeed. + """ + update_service = get_firmware_update_service() + result = await update_service.prepare_update(printer_id, db) + return FirmwareUploadPrepareResponse(**result) + + +@router.post("/updates/{printer_id}/upload", response_model=FirmwareUploadStartResponse) +async def start_firmware_upload( + printer_id: int, + db: AsyncSession = Depends(get_db), +): + """ + Start uploading firmware to a printer's SD card. + + This initiates a background process that: + 1. Downloads the firmware from Bambu Lab + 2. Uploads it to the printer's SD card via FTP + + Progress is broadcast via WebSocket with type "firmware_upload_progress". + Use GET /firmware/updates/{printer_id}/upload/status for polling fallback. + + After upload completes, the user must trigger the update from the + printer's screen (Settings > Firmware). + """ + # First check prerequisites + update_service = get_firmware_update_service() + prepare_result = await update_service.prepare_update(printer_id, db) + + if not prepare_result["can_proceed"]: + errors = prepare_result.get("errors", ["Cannot proceed with firmware upload"]) + raise HTTPException( + status_code=400, + detail="; ".join(errors), + ) + + # Start the upload + started = await update_service.start_upload(printer_id, db) + + if not started: + state = get_upload_state(printer_id) + if state.status == FirmwareUploadStatus.DOWNLOADING: + return FirmwareUploadStartResponse( + started=False, + message="Firmware upload already in progress", + ) + raise HTTPException( + status_code=500, + detail=state.error or "Failed to start firmware upload", + ) + + return FirmwareUploadStartResponse( + started=True, + message="Firmware upload started. Progress will be broadcast via WebSocket.", + ) + + +@router.get("/updates/{printer_id}/upload/status", response_model=FirmwareUploadStatusResponse) +async def get_firmware_upload_status(printer_id: int): + """ + Get the current status of a firmware upload operation. + + This is a polling fallback for clients that don't use WebSocket. + For real-time updates, connect to WebSocket and listen for + "firmware_upload_progress" messages. + """ + state = get_upload_state(printer_id) + return FirmwareUploadStatusResponse( + status=state.status.value, + progress=state.progress, + message=state.message, + error=state.error, + firmware_filename=state.firmware_filename, + firmware_version=state.firmware_version, + ) diff --git a/backend/app/api/routes/printers.py b/backend/app/api/routes/printers.py index efc289a68..6073bb115 100644 --- a/backend/app/api/routes/printers.py +++ b/backend/app/api/routes/printers.py @@ -362,6 +362,11 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db) mc_print_sub_stage=state.mc_print_sub_stage, last_ams_update=state.last_ams_update, printable_objects_count=len(state.printable_objects), + cooling_fan_speed=state.cooling_fan_speed, + big_fan1_speed=state.big_fan1_speed, + big_fan2_speed=state.big_fan2_speed, + heatbreak_fan_speed=state.heatbreak_fan_speed, + firmware_version=state.firmware_version, ) diff --git a/backend/app/main.py b/backend/app/main.py index e140a08f2..880895b4b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -59,6 +59,7 @@ from backend.app.api.routes import ( discovery, external_links, filaments, + firmware, kprofiles, maintenance, notification_templates, @@ -1814,6 +1815,7 @@ app.include_router(system.router, prefix=app_settings.api_prefix) app.include_router(websocket.router, prefix=app_settings.api_prefix) app.include_router(discovery.router, prefix=app_settings.api_prefix) app.include_router(pending_uploads.router, prefix=app_settings.api_prefix) +app.include_router(firmware.router, prefix=app_settings.api_prefix) # Serve static files (React build) diff --git a/backend/app/schemas/cloud.py b/backend/app/schemas/cloud.py index 66a663758..3d0cdd4ba 100644 --- a/backend/app/schemas/cloud.py +++ b/backend/app/schemas/cloud.py @@ -106,3 +106,21 @@ class SlicerSettingDeleteResponse(BaseModel): success: bool message: str + + +class FirmwareUpdateInfo(BaseModel): + """Firmware update information for a device.""" + + device_id: str = Field(..., description="Device ID") + device_name: str = Field(..., description="Device name") + current_version: str | None = Field(None, description="Currently installed firmware version") + latest_version: str | None = Field(None, description="Latest available firmware version") + update_available: bool = Field(False, description="Whether an update is available") + release_notes: str | None = Field(None, description="Release notes for the latest version") + + +class FirmwareUpdatesResponse(BaseModel): + """Response containing firmware updates for all devices.""" + + updates: list[FirmwareUpdateInfo] = Field(default_factory=list) + updates_available: int = Field(0, description="Total number of devices with updates available") diff --git a/backend/app/schemas/printer.py b/backend/app/schemas/printer.py index c26821452..aade85181 100644 --- a/backend/app/schemas/printer.py +++ b/backend/app/schemas/printer.py @@ -158,3 +158,5 @@ class PrinterStatus(BaseModel): big_fan1_speed: int | None = None # Auxiliary fan big_fan2_speed: int | None = None # Chamber/exhaust fan heatbreak_fan_speed: int | None = None # Hotend heatbreak fan + # Firmware version (from info.module[name="ota"].sw_ver) + firmware_version: str | None = None diff --git a/backend/app/services/bambu_cloud.py b/backend/app/services/bambu_cloud.py index 582401c16..7b3f18b40 100644 --- a/backend/app/services/bambu_cloud.py +++ b/backend/app/services/bambu_cloud.py @@ -378,6 +378,36 @@ class BambuCloudService: except httpx.RequestError as e: raise BambuCloudError(f"Request failed: {e}") + async def get_firmware_version(self, device_id: str) -> dict: + """ + Get firmware version info for a device. + + Returns dict with: + - current_version: Installed firmware version + - latest_version: Latest available firmware version + - update_available: Boolean indicating if update is available + - release_notes: Release notes for latest version + """ + if not self.is_authenticated: + raise BambuCloudAuthError("Not authenticated") + + try: + response = await self._client.get( + f"{self.base_url}/v1/iot-service/api/user/device/version", + headers=self._get_headers(), + params={"device_id": device_id}, + ) + + if response.status_code == 200: + data = response.json() + # API wraps response in 'data' field + return data.get("data", data) + + raise BambuCloudError(f"Failed to get firmware version: {response.status_code}") + + except httpx.RequestError as e: + raise BambuCloudError(f"Request failed: {e}") + async def close(self): """Close the HTTP client.""" await self._client.aclose() diff --git a/backend/app/services/bambu_ftp.py b/backend/app/services/bambu_ftp.py index 4c7138fc5..b206cedec 100644 --- a/backend/app/services/bambu_ftp.py +++ b/backend/app/services/bambu_ftp.py @@ -2,6 +2,7 @@ import asyncio import logging import socket import ssl +from collections.abc import Callable from ftplib import FTP, FTP_TLS from io import BytesIO from pathlib import Path @@ -183,8 +184,13 @@ class BambuFTPClient: pass return False - def upload_file(self, local_path: Path, remote_path: str) -> bool: - """Upload a file to the printer.""" + def upload_file( + self, + local_path: Path, + remote_path: str, + progress_callback: Callable[[int, int], None] | None = None, + ) -> bool: + """Upload a file to the printer with optional progress callback.""" if not self._ftp: logger.warning("upload_file: FTP not connected") return False @@ -192,8 +198,17 @@ class BambuFTPClient: try: file_size = local_path.stat().st_size if local_path.exists() else 0 logger.info(f"FTP uploading {local_path} ({file_size} bytes) to {remote_path}") + + uploaded = 0 + + def on_block(block: bytes): + nonlocal uploaded + uploaded += len(block) + if progress_callback: + progress_callback(uploaded, file_size) + with open(local_path, "rb") as f: - self._ftp.storbinary(f"STOR {remote_path}", f) + self._ftp.storbinary(f"STOR {remote_path}", f, callback=on_block) logger.info(f"FTP upload complete: {remote_path}") return True except Exception as e: @@ -340,8 +355,10 @@ async def upload_file_async( access_code: str, local_path: Path, remote_path: str, + timeout: float = 600.0, + progress_callback: Callable[[int, int], None] | None = None, ) -> bool: - """Async wrapper for uploading a file.""" + """Async wrapper for uploading a file with timeout and progress callback.""" loop = asyncio.get_event_loop() def _upload(): @@ -350,13 +367,17 @@ async def upload_file_async( if client.connect(): logger.info(f"FTP connected to {ip_address}") try: - return client.upload_file(local_path, remote_path) + return client.upload_file(local_path, remote_path, progress_callback) finally: client.disconnect() logger.warning(f"FTP connection failed to {ip_address}") return False - return await loop.run_in_executor(None, _upload) + try: + return await asyncio.wait_for(loop.run_in_executor(None, _upload), timeout=timeout) + except TimeoutError: + logger.warning(f"FTP upload timed out after {timeout}s for {remote_path}") + return False async def list_files_async( diff --git a/backend/app/services/bambu_mqtt.py b/backend/app/services/bambu_mqtt.py index 12f13ef91..a40c7b04f 100644 --- a/backend/app/services/bambu_mqtt.py +++ b/backend/app/services/bambu_mqtt.py @@ -158,6 +158,8 @@ class PrinterState: big_fan1_speed: int | None = None # Auxiliary fan big_fan2_speed: int | None = None # Chamber/exhaust fan heatbreak_fan_speed: int | None = None # Hotend heatbreak fan + # Firmware version info (from info.module[name="ota"].sw_ver) + firmware_version: str | None = None # Stage name mapping from BambuStudio DeviceManager.cpp @@ -323,6 +325,8 @@ class BambuMQTTClient: client.subscribe(self.topic_subscribe) # Request full status update (includes nozzle info in push_status response) self._request_push_all() + # Request firmware version info + self._request_version() # Note: get_accessories returns stale nozzle data on H2D, so we don't use it. # The correct nozzle data comes from push_status. # Prime K-profile request (Bambu printers often ignore first request) @@ -397,6 +401,12 @@ class BambuMQTTClient: logger.info(f"[{self.serial_number}] Received system data: {system_data}") self._handle_system_response(system_data) + # Handle info responses (firmware version info from get_version command) + if "info" in payload: + info_data = payload["info"] + if isinstance(info_data, dict) and info_data.get("command") == "get_version": + self._handle_version_info(info_data) + # Parse WiFi signal at top level (some printers send it here) if "wifi_signal" in payload: wifi_signal = payload["wifi_signal"] @@ -487,6 +497,39 @@ class BambuMQTTClient: # actual nozzle is 'HH01' hardened steel high-flow) logger.info(f"[{self.serial_number}] Accessories response (not used for nozzle data): {data}") + def _handle_version_info(self, data: dict): + """Handle version info response from get_version command. + + Parses firmware version from the 'ota' module in the module list. + Message format: + { + "command": "get_version", + "module": [ + {"name": "ota", "sw_ver": "01.08.05.00"}, + {"name": "rv1126", "sw_ver": "00.00.14.74"}, + ... + ] + } + """ + modules = data.get("module", []) + if not isinstance(modules, list): + return + + for module in modules: + if not isinstance(module, dict): + continue + if module.get("name") == "ota": + version = module.get("sw_ver") + if version: + old_version = self.state.firmware_version + self.state.firmware_version = version + if old_version != version: + logger.info(f"[{self.serial_number}] Firmware version: {version}") + # Trigger state change callback + if self.on_state_change: + self.on_state_change(self.state) + break + def _parse_xcam_data(self, xcam_data): """Parse xcam data for camera settings and AI detection options.""" if not isinstance(xcam_data, dict): @@ -1766,6 +1809,19 @@ class BambuMQTTClient: message = {"pushing": {"command": "pushall"}} self._client.publish(self.topic_publish, json.dumps(message), qos=1) + def _request_version(self): + """Request firmware version info from printer.""" + if self._client: + self._sequence_id += 1 + message = { + "info": { + "sequence_id": str(self._sequence_id), + "command": "get_version", + } + } + logger.debug(f"[{self.serial_number}] Requesting firmware version info") + self._client.publish(self.topic_publish, json.dumps(message), qos=1) + def request_status_update(self) -> bool: """Request a full status update from the printer (public API). diff --git a/backend/app/services/firmware_check.py b/backend/app/services/firmware_check.py new file mode 100644 index 000000000..01e144774 --- /dev/null +++ b/backend/app/services/firmware_check.py @@ -0,0 +1,382 @@ +""" +Firmware Check Service + +Checks for firmware updates by fetching from Bambu Lab's official firmware download page. +Also provides firmware download functionality for offline updates. +""" + +import logging +import re +import time +from collections.abc import Callable +from dataclasses import dataclass +from pathlib import Path + +import httpx + +from backend.app.core.config import _data_dir + +logger = logging.getLogger(__name__) + +# Bambu Lab firmware download page +BAMBU_FIRMWARE_BASE = "https://bambulab.com" +FIRMWARE_PAGE = "/en/support/firmware-download/all" + +# Cache TTL in seconds (1 hour) +CACHE_TTL = 3600 + +# Map Bambuddy model names to Bambu Lab API keys +MODEL_TO_API_KEY = { + "X1": "x1", + "X1C": "x1", + "X1-Carbon": "x1", + "X1 Carbon": "x1", + "P1P": "p1", + "P1S": "p1", + "A1": "a1", + "A1 Mini": "a1-mini", + "A1-Mini": "a1-mini", + "A1mini": "a1-mini", + "H2D": "h2d", + "H2C": "h2d", # H2C uses same firmware as H2D + "H2S": "h2s", + "P2S": "p2s", + "X1E": "x1e", + "H2D Pro": "h2d-pro", + "H2D-Pro": "h2d-pro", +} + +# Reverse mapping: API key to model codes +API_KEY_TO_DEV_MODEL = { + "x1": "BL-P001", + "p1": "C11", + "a1": "N2S", + "a1-mini": "N1", + "h2d": "O1D", + "h2s": "O1S", + "p2s": "N7", + "x1e": "C13", + "h2d-pro": "O1E", +} + + +@dataclass +class FirmwareVersion: + """Firmware version information.""" + + version: str + download_url: str + release_notes: str | None = None + release_time: str | None = None + + +class FirmwareCheckService: + """Service for checking firmware updates from Bambu Lab.""" + + def __init__(self): + self._build_id: str | None = None + self._build_id_time: float = 0 + self._version_cache: dict[str, FirmwareVersion] = {} + self._cache_time: float = 0 + self._client = httpx.AsyncClient( + timeout=30.0, + headers={ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + }, + ) + + async def _get_build_id(self) -> str | None: + """Fetch the Next.js build ID from Bambu Lab's firmware page.""" + # Use cached build ID if still valid (cache for 1 hour) + if self._build_id and (time.time() - self._build_id_time) < CACHE_TTL: + return self._build_id + + try: + response = await self._client.get(f"{BAMBU_FIRMWARE_BASE}{FIRMWARE_PAGE}") + if response.status_code == 200: + # Extract buildId from the page + match = re.search(r'"buildId":"([^"]+)"', response.text) + if match: + self._build_id = match.group(1) + self._build_id_time = time.time() + logger.info(f"Got Bambu Lab build ID: {self._build_id}") + return self._build_id + logger.warning(f"Failed to get Bambu Lab page: {response.status_code}") + except Exception as e: + logger.error(f"Error fetching Bambu Lab build ID: {e}") + + return self._build_id # Return cached value if available + + async def _fetch_firmware_versions(self, api_key: str) -> FirmwareVersion | None: + """Fetch firmware versions for a specific printer from Bambu Lab API.""" + build_id = await self._get_build_id() + if not build_id: + logger.warning("No build ID available, cannot fetch firmware versions") + return None + + try: + url = f"{BAMBU_FIRMWARE_BASE}/_next/data/{build_id}/en/support/firmware-download/{api_key}.json" + response = await self._client.get(url) + + if response.status_code == 200: + data = response.json() + page_props = data.get("pageProps", {}) + printer_map = page_props.get("printerMap", {}) + printer_data = printer_map.get(api_key, {}) + versions = printer_data.get("versions", []) + + if versions: + latest = versions[0] + return FirmwareVersion( + version=latest.get("version", ""), + download_url=latest.get("url", ""), + release_notes=latest.get("release_notes_en"), + release_time=latest.get("release_time"), + ) + else: + logger.warning(f"Failed to fetch firmware for {api_key}: {response.status_code}") + + except Exception as e: + logger.error(f"Error fetching firmware for {api_key}: {e}") + + return None + + async def get_latest_version(self, model: str) -> FirmwareVersion | None: + """ + Get the latest firmware version for a printer model. + + Args: + model: Bambuddy printer model name (e.g., "X1C", "P1S", "H2D") + + Returns: + FirmwareVersion if found, None otherwise + """ + # Normalize model name + model_upper = model.upper().replace(" ", "").replace("-", "") + + # Find the API key for this model + api_key = None + for model_name, key in MODEL_TO_API_KEY.items(): + if model_name.upper().replace(" ", "").replace("-", "") == model_upper: + api_key = key + break + + if not api_key: + # Try direct lookup with original model + api_key = MODEL_TO_API_KEY.get(model) + + if not api_key: + logger.debug(f"Unknown printer model: {model}") + return None + + # Check cache + cache_key = api_key + if cache_key in self._version_cache and (time.time() - self._cache_time) < CACHE_TTL: + return self._version_cache[cache_key] + + # Fetch from API + version = await self._fetch_firmware_versions(api_key) + if version: + self._version_cache[cache_key] = version + self._cache_time = time.time() + + return version + + async def check_for_update(self, model: str, current_version: str) -> dict: + """ + Check if a firmware update is available for a printer. + + Args: + model: Printer model name + current_version: Currently installed firmware version + + Returns: + Dict with update info: + - update_available: bool + - current_version: str + - latest_version: str or None + - download_url: str or None + - release_notes: str or None + """ + result = { + "update_available": False, + "current_version": current_version, + "latest_version": None, + "download_url": None, + "release_notes": None, + } + + if not current_version: + return result + + latest = await self.get_latest_version(model) + if not latest: + return result + + result["latest_version"] = latest.version + result["download_url"] = latest.download_url + result["release_notes"] = latest.release_notes + + # Compare versions (format: XX.XX.XX.XX) + try: + current_parts = [int(x) for x in current_version.split(".")] + latest_parts = [int(x) for x in latest.version.split(".")] + + # Pad to same length + while len(current_parts) < 4: + current_parts.append(0) + while len(latest_parts) < 4: + latest_parts.append(0) + + result["update_available"] = latest_parts > current_parts + except (ValueError, AttributeError): + logger.warning(f"Could not compare versions: {current_version} vs {latest.version}") + + return result + + async def get_all_latest_versions(self) -> dict[str, FirmwareVersion]: + """ + Fetch latest firmware versions for all known printer models. + + Returns: + Dict mapping API key to FirmwareVersion + """ + results = {} + + for api_key in API_KEY_TO_DEV_MODEL: + version = await self._fetch_firmware_versions(api_key) + if version: + results[api_key] = version + + return results + + def _get_firmware_cache_dir(self) -> Path: + """Get the firmware cache directory, creating it if needed.""" + cache_dir = _data_dir / "firmware" + cache_dir.mkdir(parents=True, exist_ok=True) + return cache_dir + + def _get_cached_firmware_path(self, model: str, version: str) -> Path: + """Get the path where a firmware file would be cached.""" + # Normalize model name for filename + model_safe = model.upper().replace(" ", "-").replace("/", "-") + version_safe = version.replace(".", "_") + filename = f"{model_safe}_{version_safe}.bin" + return self._get_firmware_cache_dir() / filename + + async def get_firmware_file_info(self, model: str) -> dict | None: + """ + Get information about the firmware file for a model. + + Returns: + Dict with download_url, version, filename, and estimated_size (if available) + """ + latest = await self.get_latest_version(model) + if not latest or not latest.download_url: + return None + + # Extract filename from URL + url_parts = latest.download_url.split("/") + filename = url_parts[-1] if url_parts else f"firmware_{model}.bin" + + return { + "download_url": latest.download_url, + "version": latest.version, + "filename": filename, + "release_notes": latest.release_notes, + } + + async def download_firmware( + self, + model: str, + progress_callback: Callable[[int, int, str], None] | None = None, + ) -> Path | None: + """ + Download firmware file for a printer model. + + Args: + model: Printer model name (e.g., "X1C", "P1S", "H2D") + progress_callback: Optional callback(bytes_downloaded, total_bytes, status_message) + + Returns: + Path to downloaded firmware file, or None on failure + """ + latest = await self.get_latest_version(model) + if not latest or not latest.download_url: + logger.warning(f"No firmware download URL available for model: {model}") + return None + + # Check if already cached + cached_path = self._get_cached_firmware_path(model, latest.version) + if cached_path.exists(): + logger.info(f"Using cached firmware: {cached_path}") + return cached_path + + # Extract original filename from URL (must preserve for SD card update) + url_parts = latest.download_url.split("/") + original_filename = url_parts[-1] if url_parts else f"firmware_{model}.bin" + + # Download to temp file first + temp_path = self._get_firmware_cache_dir() / f".downloading_{original_filename}" + + try: + logger.info(f"Downloading firmware from {latest.download_url}") + if progress_callback: + progress_callback(0, 0, "Starting download...") + + async with self._client.stream("GET", latest.download_url) as response: + if response.status_code != 200: + logger.error(f"Firmware download failed with status {response.status_code}") + return None + + total_size = int(response.headers.get("content-length", 0)) + downloaded = 0 + + with open(temp_path, "wb") as f: + async for chunk in response.aiter_bytes(chunk_size=65536): + f.write(chunk) + downloaded += len(chunk) + if progress_callback: + progress_callback(downloaded, total_size, "Downloading firmware...") + + # Also save a copy with the original filename for SD card + original_path = self._get_firmware_cache_dir() / original_filename + if original_path.exists(): + original_path.unlink() + + # Move temp to both cached path and original filename path + import shutil + + shutil.copy2(temp_path, cached_path) + temp_path.rename(original_path) + + logger.info(f"Firmware downloaded successfully: {original_path}") + if progress_callback: + progress_callback(downloaded, total_size, "Download complete") + + return original_path + + except Exception as e: + logger.error(f"Firmware download failed: {e}") + if temp_path.exists(): + try: + temp_path.unlink() + except Exception: + pass + return None + + async def close(self): + """Close the HTTP client.""" + await self._client.aclose() + + +# Singleton instance +_firmware_service: FirmwareCheckService | None = None + + +def get_firmware_service() -> FirmwareCheckService: + """Get the singleton firmware check service instance.""" + global _firmware_service + if _firmware_service is None: + _firmware_service = FirmwareCheckService() + return _firmware_service diff --git a/backend/app/services/firmware_update.py b/backend/app/services/firmware_update.py new file mode 100644 index 000000000..b66919c01 --- /dev/null +++ b/backend/app/services/firmware_update.py @@ -0,0 +1,353 @@ +""" +Firmware Update Service + +Orchestrates firmware updates for Bambu Lab printers: +1. Check prerequisites (SD card, space, update available) +2. Download firmware from Bambu Lab +3. Upload to printer's SD card via FTP +4. Notify user to trigger update from printer screen +""" + +import asyncio +import logging +from dataclasses import dataclass +from enum import Enum + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from backend.app.core.websocket import ws_manager +from backend.app.models.printer import Printer +from backend.app.services.bambu_ftp import get_storage_info_async, upload_file_async +from backend.app.services.firmware_check import get_firmware_service +from backend.app.services.printer_manager import printer_manager + +logger = logging.getLogger(__name__) + + +class FirmwareUploadStatus(str, Enum): + """Status of a firmware upload operation.""" + + IDLE = "idle" + PREPARING = "preparing" + DOWNLOADING = "downloading" + UPLOADING = "uploading" + COMPLETE = "complete" + ERROR = "error" + + +@dataclass +class FirmwareUploadState: + """State of a firmware upload operation for a printer.""" + + status: FirmwareUploadStatus = FirmwareUploadStatus.IDLE + progress: int = 0 # 0-100 + message: str = "" + error: str | None = None + firmware_filename: str | None = None + firmware_version: str | None = None + + +# Track upload state per printer +_upload_states: dict[int, FirmwareUploadState] = {} + + +def get_upload_state(printer_id: int) -> FirmwareUploadState: + """Get the current upload state for a printer.""" + if printer_id not in _upload_states: + _upload_states[printer_id] = FirmwareUploadState() + return _upload_states[printer_id] + + +def reset_upload_state(printer_id: int): + """Reset the upload state for a printer.""" + _upload_states[printer_id] = FirmwareUploadState() + + +class FirmwareUpdateService: + """Service for managing firmware updates.""" + + # Minimum free space required (100MB buffer) + MIN_FREE_SPACE_BYTES = 100 * 1024 * 1024 + + async def prepare_update( + self, + printer_id: int, + db: AsyncSession, + ) -> dict: + """ + Check prerequisites for firmware update. + + Returns: + Dict with: + - can_proceed: bool + - sd_card_present: bool + - sd_card_free_space: int (bytes, -1 if unknown) + - firmware_size: int (bytes, estimated) + - space_sufficient: bool + - update_available: bool + - current_version: str | None + - latest_version: str | None + - firmware_filename: str | None + - errors: list[str] + """ + result = { + "can_proceed": False, + "sd_card_present": False, + "sd_card_free_space": -1, + "firmware_size": 0, + "space_sufficient": False, + "update_available": False, + "current_version": None, + "latest_version": None, + "firmware_filename": None, + "errors": [], + } + + # Get printer from database + stmt = select(Printer).where(Printer.id == printer_id) + db_result = await db.execute(stmt) + printer = db_result.scalar_one_or_none() + + if not printer: + result["errors"].append("Printer not found") + return result + + # Check printer is connected + mqtt_client = printer_manager.get_client(printer_id) + if not mqtt_client or not mqtt_client.state: + result["errors"].append("Printer not connected") + return result + + state = mqtt_client.state + + # Get current firmware version + result["current_version"] = state.firmware_version + + # Check SD card + result["sd_card_present"] = state.sdcard + if not state.sdcard: + result["errors"].append("No SD card inserted in printer") + + # Get storage info via FTP + if state.sdcard: + try: + storage_info = await get_storage_info_async( + printer.ip_address, + printer.access_code, + ) + if storage_info and "free_bytes" in storage_info: + result["sd_card_free_space"] = storage_info["free_bytes"] + except Exception as e: + logger.warning(f"Could not get storage info: {e}") + + # Check for firmware update + firmware_service = get_firmware_service() + model = printer.model or "Unknown" + + if state.firmware_version: + update_info = await firmware_service.check_for_update(model, state.firmware_version) + result["update_available"] = update_info["update_available"] + result["latest_version"] = update_info["latest_version"] + else: + # If we don't know current version, just get latest + latest = await firmware_service.get_latest_version(model) + if latest: + result["latest_version"] = latest.version + result["update_available"] = True # Assume update needed + + if not result["update_available"]: + result["errors"].append("Firmware is already up to date") + + # Get firmware file info + file_info = await firmware_service.get_firmware_file_info(model) + if file_info: + result["firmware_filename"] = file_info["filename"] + # Estimate size (typical firmware is 50-150MB) + # We'll get actual size during download + result["firmware_size"] = 100 * 1024 * 1024 # 100MB estimate + + # Check space + if result["sd_card_free_space"] > 0: + # Need firmware size + buffer + required = result["firmware_size"] + self.MIN_FREE_SPACE_BYTES + result["space_sufficient"] = result["sd_card_free_space"] >= required + if not result["space_sufficient"]: + result["errors"].append( + f"Insufficient SD card space. Need {required // (1024*1024)}MB, " + f"have {result['sd_card_free_space'] // (1024*1024)}MB" + ) + elif result["sd_card_present"]: + # Couldn't determine space, assume sufficient + result["space_sufficient"] = True + + # Final check + result["can_proceed"] = ( + result["sd_card_present"] + and result["space_sufficient"] + and result["update_available"] + and len(result["errors"]) == 0 + ) + + return result + + async def start_upload( + self, + printer_id: int, + db: AsyncSession, + ) -> bool: + """ + Start the firmware upload process. + + This runs asynchronously and broadcasts progress via WebSocket. + Returns True if upload started successfully. + """ + state = get_upload_state(printer_id) + + # Check if already in progress + if state.status in (FirmwareUploadStatus.DOWNLOADING, FirmwareUploadStatus.UPLOADING): + logger.warning(f"Firmware upload already in progress for printer {printer_id}") + return False + + # Get printer + stmt = select(Printer).where(Printer.id == printer_id) + db_result = await db.execute(stmt) + printer = db_result.scalar_one_or_none() + + if not printer: + state.status = FirmwareUploadStatus.ERROR + state.error = "Printer not found" + return False + + # Get printer model + model = printer.model or "Unknown" + + # Reset state + reset_upload_state(printer_id) + state = get_upload_state(printer_id) + state.status = FirmwareUploadStatus.PREPARING + state.message = "Preparing firmware update..." + await self._broadcast_progress(printer_id, state) + + # Run the upload in background + asyncio.create_task( + self._do_upload( + printer_id=printer_id, + ip_address=printer.ip_address, + access_code=printer.access_code, + model=model, + ) + ) + + return True + + async def _do_upload( + self, + printer_id: int, + ip_address: str, + access_code: str, + model: str, + ): + """Perform the actual firmware download and upload.""" + state = get_upload_state(printer_id) + firmware_service = get_firmware_service() + + try: + # Download firmware (quick, usually cached) + state.status = FirmwareUploadStatus.DOWNLOADING + state.progress = 0 + state.message = "Preparing firmware..." + await self._broadcast_progress(printer_id, state) + + firmware_path = await firmware_service.download_firmware(model) + + if not firmware_path: + raise Exception("Failed to download firmware") + + state.firmware_filename = firmware_path.name + + # Get firmware version for state + latest = await firmware_service.get_latest_version(model) + if latest: + state.firmware_version = latest.version + + # Upload to printer (0-100% progress shown here) + state.status = FirmwareUploadStatus.UPLOADING + state.progress = 0 + state.message = f"Uploading {firmware_path.name} to printer..." + await self._broadcast_progress(printer_id, state) + + # Upload to root of SD card (where printer expects firmware) + remote_path = f"/{firmware_path.name}" + + logger.info(f"Uploading firmware to printer {printer_id}: {remote_path}") + + # Track real progress via FTP callback + loop = asyncio.get_event_loop() + last_progress = 0 + + def on_upload_progress(uploaded: int, total: int): + nonlocal last_progress + if total > 0: + progress = int((uploaded / total) * 100) + # Only broadcast every 1% to avoid flooding + if progress > last_progress: + last_progress = progress + state.progress = min(99, progress) # Cap at 99 until complete + asyncio.run_coroutine_threadsafe(self._broadcast_progress(printer_id, state), loop) + + success = await upload_file_async( + ip_address, + access_code, + firmware_path, + remote_path, + progress_callback=on_upload_progress, + ) + + if not success: + raise Exception("Failed to upload firmware to printer") + + # Complete + state.status = FirmwareUploadStatus.COMPLETE + state.progress = 100 + state.message = ( + f"Firmware {state.firmware_version or ''} uploaded successfully! " + "Please go to printer screen and trigger the update from Settings > Firmware." + ) + await self._broadcast_progress(printer_id, state) + + logger.info(f"Firmware upload complete for printer {printer_id}") + + except Exception as e: + logger.error(f"Firmware upload failed for printer {printer_id}: {e}") + state.status = FirmwareUploadStatus.ERROR + state.error = str(e) + state.message = f"Firmware upload failed: {e}" + await self._broadcast_progress(printer_id, state) + + async def _broadcast_progress(self, printer_id: int, state: FirmwareUploadState): + """Broadcast firmware upload progress via WebSocket.""" + await ws_manager.broadcast( + { + "type": "firmware_upload_progress", + "printer_id": printer_id, + "status": state.status.value, + "progress": state.progress, + "message": state.message, + "error": state.error, + "firmware_filename": state.firmware_filename, + "firmware_version": state.firmware_version, + } + ) + + +# Singleton instance +_firmware_update_service: FirmwareUpdateService | None = None + + +def get_firmware_update_service() -> FirmwareUpdateService: + """Get the singleton firmware update service instance.""" + global _firmware_update_service + if _firmware_update_service is None: + _firmware_update_service = FirmwareUpdateService() + return _firmware_update_service diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index e66875797..9efef8702 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -2526,3 +2526,57 @@ export const pendingUploadsApi = { discardAll: () => request<{ discarded: number }>('/pending-uploads/discard-all', { method: 'DELETE' }), }; + +// Firmware API Types +export interface FirmwareUpdateInfo { + printer_id: number; + printer_name: string; + model: string | null; + current_version: string | null; + latest_version: string | null; + update_available: boolean; + download_url: string | null; + release_notes: string | null; +} + +export interface FirmwareUploadPrepare { + can_proceed: boolean; + sd_card_present: boolean; + sd_card_free_space: number; + firmware_size: number; + space_sufficient: boolean; + update_available: boolean; + current_version: string | null; + latest_version: string | null; + firmware_filename: string | null; + errors: string[]; +} + +export interface FirmwareUploadStatus { + status: 'idle' | 'preparing' | 'downloading' | 'uploading' | 'complete' | 'error'; + progress: number; + message: string; + error: string | null; + firmware_filename: string | null; + firmware_version: string | null; +} + +// Firmware API +export const firmwareApi = { + checkUpdates: () => + request<{ updates: FirmwareUpdateInfo[]; updates_available: number }>('/firmware/updates'), + + checkPrinterUpdate: (printerId: number) => + request(`/firmware/updates/${printerId}`), + + prepareUpload: (printerId: number) => + request(`/firmware/updates/${printerId}/prepare`), + + startUpload: (printerId: number) => + request<{ started: boolean; message: string }>(`/firmware/updates/${printerId}/upload`, { + method: 'POST', + }), + + getUploadStatus: (printerId: number) => + request(`/firmware/updates/${printerId}/upload/status`), +}; diff --git a/frontend/src/pages/PrintersPage.tsx b/frontend/src/pages/PrintersPage.tsx index e15de0a2e..27e4fa27d 100644 --- a/frontend/src/pages/PrintersPage.tsx +++ b/frontend/src/pages/PrintersPage.tsx @@ -38,6 +38,7 @@ import { Wind, AirVent, Minus, + Download, } from 'lucide-react'; // Custom Skip Objects icon - arrow jumping over boxes @@ -53,8 +54,8 @@ const SkipObjectsIcon = ({ className }: { className?: string }) => ( ); import { useNavigate } from 'react-router-dom'; -import { api, discoveryApi } from '../api/client'; -import type { Printer, PrinterCreate, AMSUnit, DiscoveredPrinter } from '../api/client'; +import { api, discoveryApi, firmwareApi } from '../api/client'; +import type { Printer, PrinterCreate, AMSUnit, DiscoveredPrinter, FirmwareUpdateInfo, FirmwareUploadStatus } from '../api/client'; import { Card, CardContent } from '../components/Card'; import { Button } from '../components/Button'; import { ConfirmModal } from '../components/ConfirmModal'; @@ -903,6 +904,7 @@ function PrinterCard({ trayUuid: string; trayInfo: { type: string; color: string; location: string }; } | null>(null); + const [showFirmwareModal, setShowFirmwareModal] = useState(false); const { data: status } = useQuery({ queryKey: ['printerStatus', printer.id], @@ -910,6 +912,14 @@ function PrinterCard({ refetchInterval: 30000, // Fallback polling, WebSocket handles real-time }); + // Check for firmware updates (cached for 5 minutes) + const { data: firmwareInfo } = useQuery({ + queryKey: ['firmwareUpdate', printer.id], + queryFn: () => firmwareApi.checkPrinterUpdate(printer.id), + staleTime: 5 * 60 * 1000, + refetchInterval: 5 * 60 * 1000, + }); + // Collect unique tray_info_idx values for cloud filament info lookup const trayInfoIds = useMemo(() => { const ids = new Set(); @@ -1391,6 +1401,17 @@ function PrinterCard({ {queueCount} )} + {/* Firmware Update Badge */} + {firmwareInfo?.update_available && ( + + )} )} @@ -2697,6 +2718,15 @@ function PrinterCard({ /> )} + {/* Firmware Update Modal */} + {showFirmwareModal && firmwareInfo && ( + setShowFirmwareModal(false)} + /> + )} + {/* AMS Slot Menu Backdrop - closes menu when clicking outside */} {amsSlotMenu && (
void; +}) { + const queryClient = useQueryClient(); + const { showToast } = useToast(); + const [uploadStatus, setUploadStatus] = useState(null); + const [isUploading, setIsUploading] = useState(false); + const [pollInterval, setPollInterval] = useState(null); + + // Prepare check query + const { data: prepareInfo, isLoading: isPreparing } = useQuery({ + queryKey: ['firmwarePrepare', printer.id], + queryFn: () => firmwareApi.prepareUpload(printer.id), + staleTime: 30000, + }); + + // Start upload mutation + const uploadMutation = useMutation({ + mutationFn: () => firmwareApi.startUpload(printer.id), + onSuccess: () => { + setIsUploading(true); + // Start polling for status + const interval = setInterval(async () => { + try { + const status = await firmwareApi.getUploadStatus(printer.id); + setUploadStatus(status); + if (status.status === 'complete' || status.status === 'error') { + clearInterval(interval); + setPollInterval(null); + setIsUploading(false); + if (status.status === 'complete') { + showToast('Firmware uploaded! Trigger update from printer screen.', 'success'); + queryClient.invalidateQueries({ queryKey: ['firmwareUpdate', printer.id] }); + } + } + } catch { + // Ignore errors during polling + } + }, 2000); + setPollInterval(interval); + }, + onError: (error: Error) => { + showToast(`Failed to start upload: ${error.message}`, 'error'); + setIsUploading(false); + }, + }); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (pollInterval) clearInterval(pollInterval); + }; + }, [pollInterval]); + + const handleStartUpload = () => { + setUploadStatus(null); + uploadMutation.mutate(); + }; + + return ( +
+ + +
+
+ +
+
+

Firmware Update

+

+ {printer.name} +

+
+
+ + {/* Version Info */} +
+
+ Current: + {firmwareInfo.current_version || 'Unknown'} +
+
+ Latest: + {firmwareInfo.latest_version} +
+ {firmwareInfo.release_notes && ( +
+ + Release Notes + +
+ {firmwareInfo.release_notes} +
+
+ )} +
+ + {/* Status / Progress */} + {isPreparing ? ( +
+ + Checking prerequisites... +
+ ) : prepareInfo && !isUploading && !uploadStatus ? ( +
+ {prepareInfo.can_proceed ? ( +
+ + SD card ready. Click below to upload firmware. +
+ ) : ( +
+ {prepareInfo.errors.map((error, i) => ( +
+ + {error} +
+ ))} +
+ )} +
+ ) : null} + + {/* Upload Progress */} + {(isUploading || uploadStatus) && uploadStatus && ( +
+
+ {uploadStatus.status} + {uploadStatus.progress}% +
+
+
+
+

{uploadStatus.message}

+ {uploadStatus.error && ( +

{uploadStatus.error}

+ )} +
+ )} + + {/* Success Message */} + {uploadStatus?.status === 'complete' && ( +
+

+ Firmware uploaded to SD card! +

+

+ To apply the update on your printer: +

+
    +
  1. On the printer's touchscreen, go to Settings
  2. +
  3. Navigate to Firmware
  4. +
  5. Select Update from SD card
  6. +
  7. The update will take 10-20 minutes
  8. +
+
+ )} + + {/* Buttons */} +
+ + {prepareInfo?.can_proceed && !isUploading && uploadStatus?.status !== 'complete' && ( + + )} +
+ + +
+ ); +} + function EditPrinterModal({ printer, onClose, diff --git a/static/assets/index-BaxJ1N11.js b/static/assets/index-BaxJ1N11.js deleted file mode 100644 index aa13fbb38..000000000 --- a/static/assets/index-BaxJ1N11.js +++ /dev/null @@ -1,8663 +0,0 @@ -function rie(t,e){for(var n=0;nr[i]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const a of i)if(a.type==="childList")for(const s of a.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(i){const a={};return i.integrity&&(a.integrity=i.integrity),i.referrerPolicy&&(a.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?a.credentials="include":i.crossOrigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function r(i){if(i.ep)return;i.ep=!0;const a=n(i);fetch(i.href,a)}})();var Ev=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function vo(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var IA={exports:{}},px={};var AF;function iie(){if(AF)return px;AF=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function n(r,i,a){var s=null;if(a!==void 0&&(s=""+a),i.key!==void 0&&(s=""+i.key),"key"in i){a={};for(var l in i)l!=="key"&&(a[l]=i[l])}else a=i;return i=a.ref,{$$typeof:t,type:r,key:s,ref:i!==void 0?i:null,props:a}}return px.Fragment=e,px.jsx=n,px.jsxs=n,px}var TF;function aie(){return TF||(TF=1,IA.exports=iie()),IA.exports}var o=aie(),FA={exports:{}},an={};var NF;function sie(){if(NF)return an;NF=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),a=Symbol.for("react.consumer"),s=Symbol.for("react.context"),l=Symbol.for("react.forward_ref"),c=Symbol.for("react.suspense"),u=Symbol.for("react.memo"),d=Symbol.for("react.lazy"),f=Symbol.for("react.activity"),g=Symbol.iterator;function p(F){return F===null||typeof F!="object"?null:(F=g&&F[g]||F["@@iterator"],typeof F=="function"?F:null)}var x={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},v=Object.assign,b={};function y(F,$,J){this.props=F,this.context=$,this.refs=b,this.updater=J||x}y.prototype.isReactComponent={},y.prototype.setState=function(F,$){if(typeof F!="object"&&typeof F!="function"&&F!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,F,$,"setState")},y.prototype.forceUpdate=function(F){this.updater.enqueueForceUpdate(this,F,"forceUpdate")};function E(){}E.prototype=y.prototype;function w(F,$,J){this.props=F,this.context=$,this.refs=b,this.updater=J||x}var A=w.prototype=new E;A.constructor=w,v(A,y.prototype),A.isPureReactComponent=!0;var T=Array.isArray;function C(){}var N={H:null,A:null,T:null,S:null},O=Object.prototype.hasOwnProperty;function k(F,$,J){var V=J.ref;return{$$typeof:t,type:F,key:$,ref:V!==void 0?V:null,props:J}}function j(F,$){return k(F.type,$,F.props)}function z(F){return typeof F=="object"&&F!==null&&F.$$typeof===t}function I(F){var $={"=":"=0",":":"=2"};return"$"+F.replace(/[=:]/g,function(J){return $[J]})}var K=/\/+/g;function R(F,$){return typeof F=="object"&&F!==null&&F.key!=null?I(""+F.key):$.toString(36)}function q(F){switch(F.status){case"fulfilled":return F.value;case"rejected":throw F.reason;default:switch(typeof F.status=="string"?F.then(C,C):(F.status="pending",F.then(function($){F.status==="pending"&&(F.status="fulfilled",F.value=$)},function($){F.status==="pending"&&(F.status="rejected",F.reason=$)})),F.status){case"fulfilled":return F.value;case"rejected":throw F.reason}}throw F}function X(F,$,J,V,ne){var le=typeof F;(le==="undefined"||le==="boolean")&&(F=null);var ge=!1;if(F===null)ge=!0;else switch(le){case"bigint":case"string":case"number":ge=!0;break;case"object":switch(F.$$typeof){case t:case e:ge=!0;break;case d:return ge=F._init,X(ge(F._payload),$,J,V,ne)}}if(ge)return ne=ne(F),ge=V===""?"."+R(F,0):V,T(ne)?(J="",ge!=null&&(J=ge.replace(K,"$&/")+"/"),X(ne,$,J,"",function(ue){return ue})):ne!=null&&(z(ne)&&(ne=j(ne,J+(ne.key==null||F&&F.key===ne.key?"":(""+ne.key).replace(K,"$&/")+"/")+ge)),$.push(ne)),1;ge=0;var Ee=V===""?".":V+":";if(T(F))for(var pe=0;pe>>1,te=X[Z];if(0>>1;Zi(J,ee))Vi(ne,J)?(X[Z]=ne,X[V]=ee,Z=V):(X[Z]=J,X[$]=ee,Z=$);else if(Vi(ne,ee))X[Z]=ne,X[V]=ee,Z=V;else break e}}return W}function i(X,W){var ee=X.sortIndex-W.sortIndex;return ee!==0?ee:X.id-W.id}if(t.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var a=performance;t.unstable_now=function(){return a.now()}}else{var s=Date,l=s.now();t.unstable_now=function(){return s.now()-l}}var c=[],u=[],d=1,f=null,g=3,p=!1,x=!1,v=!1,b=!1,y=typeof setTimeout=="function"?setTimeout:null,E=typeof clearTimeout=="function"?clearTimeout:null,w=typeof setImmediate<"u"?setImmediate:null;function A(X){for(var W=n(u);W!==null;){if(W.callback===null)r(u);else if(W.startTime<=X)r(u),W.sortIndex=W.expirationTime,e(c,W);else break;W=n(u)}}function T(X){if(v=!1,A(X),!x)if(n(c)!==null)x=!0,C||(C=!0,I());else{var W=n(u);W!==null&&q(T,W.startTime-X)}}var C=!1,N=-1,O=5,k=-1;function j(){return b?!0:!(t.unstable_now()-kX&&j());){var Z=f.callback;if(typeof Z=="function"){f.callback=null,g=f.priorityLevel;var te=Z(f.expirationTime<=X);if(X=t.unstable_now(),typeof te=="function"){f.callback=te,A(X),W=!0;break t}f===n(c)&&r(c),A(X)}else r(c);f=n(c)}if(f!==null)W=!0;else{var F=n(u);F!==null&&q(T,F.startTime-X),W=!1}}break e}finally{f=null,g=ee,p=!1}W=void 0}}finally{W?I():C=!1}}}var I;if(typeof w=="function")I=function(){w(z)};else if(typeof MessageChannel<"u"){var K=new MessageChannel,R=K.port2;K.port1.onmessage=z,I=function(){R.postMessage(null)}}else I=function(){y(z,0)};function q(X,W){N=y(function(){X(t.unstable_now())},W)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(X){X.callback=null},t.unstable_forceFrameRate=function(X){0>X||125Z?(X.sortIndex=ee,e(u,X),n(c)===null&&X===n(u)&&(v?(E(N),N=-1):v=!0,q(T,ee-Z))):(X.sortIndex=te,e(c,X),x||p||(x=!0,C||(C=!0,I()))),X},t.unstable_shouldYield=j,t.unstable_wrapCallback=function(X){var W=g;return function(){var ee=g;g=W;try{return X.apply(this,arguments)}finally{g=ee}}}})(BA)),BA}var PF;function cie(){return PF||(PF=1,zA.exports=lie()),zA.exports}var HA={exports:{}},da={};var jF;function uie(){if(jF)return da;jF=1;var t=tg();function e(c){var u="https://react.dev/errors/"+c;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(e){console.error(e)}}return t(),HA.exports=uie(),HA.exports}var DF;function die(){if(DF)return gx;DF=1;var t=cie(),e=tg(),n=e9();function r(h){var m="https://react.dev/errors/"+h;if(1te||(h.current=Z[te],Z[te]=null,te--)}function J(h,m){te++,Z[te]=h.current,h.current=m}var V=F(null),ne=F(null),le=F(null),ge=F(null);function Ee(h,m){switch(J(le,m),J(ne,h),J(V,null),m.nodeType){case 9:case 11:h=(h=m.documentElement)&&(h=h.namespaceURI)?X5(h):0;break;default:if(h=m.tagName,m=m.namespaceURI)m=X5(m),h=Y5(m,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}$(V),J(V,h)}function pe(){$(V),$(ne),$(le)}function ue(h){h.memoizedState!==null&&J(ge,h);var m=V.current,_=Y5(m,h.type);m!==_&&(J(ne,h),J(V,_))}function ke(h){ne.current===h&&($(V),$(ne)),ge.current===h&&($(ge),dx._currentValue=ee)}var de,Re;function B(h){if(de===void 0)try{throw Error()}catch(_){var m=_.stack.trim().match(/\n( *(at )?)/);de=m&&m[1]||"",Re=-1<_.stack.indexOf(` - at`)?" ()":-1<_.stack.indexOf("@")?"@unknown:0:0":""}return` -`+de+h+Re}var H=!1;function ie(h,m){if(!h||H)return"";H=!0;var _=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var M={DetermineComponentFrameRoot:function(){try{if(m){var mt=function(){throw Error()};if(Object.defineProperty(mt.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(mt,[])}catch(ot){var it=ot}Reflect.construct(h,[],mt)}else{try{mt.call()}catch(ot){it=ot}h.call(mt.prototype)}}else{try{throw Error()}catch(ot){it=ot}(mt=h())&&typeof mt.catch=="function"&&mt.catch(function(){})}}catch(ot){if(ot&&it&&typeof ot.stack=="string")return[ot.stack,it.stack]}return[null,null]}};M.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var P=Object.getOwnPropertyDescriptor(M.DetermineComponentFrameRoot,"name");P&&P.configurable&&Object.defineProperty(M.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var D=M.DetermineComponentFrameRoot(),Y=D[0],ce=D[1];if(Y&&ce){var Ne=Y.split(` -`),nt=ce.split(` -`);for(P=M=0;MP||Ne[M]!==nt[P]){var ht=` -`+Ne[M].replace(" at new "," at ");return h.displayName&&ht.includes("")&&(ht=ht.replace("",h.displayName)),ht}while(1<=M&&0<=P);break}}}finally{H=!1,Error.prepareStackTrace=_}return(_=h?h.displayName||h.name:"")?B(_):""}function G(h,m){switch(h.tag){case 26:case 27:case 5:return B(h.type);case 16:return B("Lazy");case 13:return h.child!==m&&m!==null?B("Suspense Fallback"):B("Suspense");case 19:return B("SuspenseList");case 0:case 15:return ie(h.type,!1);case 11:return ie(h.type.render,!1);case 1:return ie(h.type,!0);case 31:return B("Activity");default:return""}}function Q(h){try{var m="",_=null;do m+=G(h,_),_=h,h=h.return;while(h);return m}catch(M){return` -Error generating stack: `+M.message+` -`+M.stack}}var oe=Object.prototype.hasOwnProperty,xe=t.unstable_scheduleCallback,Te=t.unstable_cancelCallback,Me=t.unstable_shouldYield,U=t.unstable_requestPaint,L=t.unstable_now,re=t.unstable_getCurrentPriorityLevel,ve=t.unstable_ImmediatePriority,Se=t.unstable_UserBlockingPriority,ye=t.unstable_NormalPriority,Ye=t.unstable_LowPriority,We=t.unstable_IdlePriority,Xe=t.log,ut=t.unstable_setDisableYieldValue,fe=null,ae=null;function Ve(h){if(typeof Xe=="function"&&ut(h),ae&&typeof ae.setStrictMode=="function")try{ae.setStrictMode(fe,h)}catch{}}var Be=Math.clz32?Math.clz32:me,Qe=Math.log,dt=Math.LN2;function me(h){return h>>>=0,h===0?32:31-(Qe(h)/dt|0)|0}var tt=256,be=262144,He=4194304;function Pe(h){var m=h&42;if(m!==0)return m;switch(h&-h){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return h&261888;case 262144:case 524288:case 1048576:case 2097152:return h&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return h&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return h}}function we(h,m,_){var M=h.pendingLanes;if(M===0)return 0;var P=0,D=h.suspendedLanes,Y=h.pingedLanes;h=h.warmLanes;var ce=M&134217727;return ce!==0?(M=ce&~D,M!==0?P=Pe(M):(Y&=ce,Y!==0?P=Pe(Y):_||(_=ce&~h,_!==0&&(P=Pe(_))))):(ce=M&~D,ce!==0?P=Pe(ce):Y!==0?P=Pe(Y):_||(_=M&~h,_!==0&&(P=Pe(_)))),P===0?0:m!==0&&m!==P&&(m&D)===0&&(D=P&-P,_=m&-m,D>=_||D===32&&(_&4194048)!==0)?m:P}function Ze(h,m){return(h.pendingLanes&~(h.suspendedLanes&~h.pingedLanes)&m)===0}function Fe(h,m){switch(h){case 1:case 2:case 4:case 8:case 64:return m+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return m+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function pt(){var h=He;return He<<=1,(He&62914560)===0&&(He=4194304),h}function gt(h){for(var m=[],_=0;31>_;_++)m.push(h);return m}function lt(h,m){h.pendingLanes|=m,m!==268435456&&(h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0)}function At(h,m,_,M,P,D){var Y=h.pendingLanes;h.pendingLanes=_,h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0,h.expiredLanes&=_,h.entangledLanes&=_,h.errorRecoveryDisabledLanes&=_,h.shellSuspendCounter=0;var ce=h.entanglements,Ne=h.expirationTimes,nt=h.hiddenUpdates;for(_=Y&~_;0<_;){var ht=31-Be(_),mt=1<"u")return null;try{return h.activeElement||h.body}catch{return h.body}}var qr=/[\n"\\]/g;function Vn(h){return h.replace(qr,function(m){return"\\"+m.charCodeAt(0).toString(16)+" "})}function wo(h,m,_,M,P,D,Y,ce){h.name="",Y!=null&&typeof Y!="function"&&typeof Y!="symbol"&&typeof Y!="boolean"?h.type=Y:h.removeAttribute("type"),m!=null?Y==="number"?(m===0&&h.value===""||h.value!=m)&&(h.value=""+bn(m)):h.value!==""+bn(m)&&(h.value=""+bn(m)):Y!=="submit"&&Y!=="reset"||h.removeAttribute("value"),m!=null?ji(h,Y,bn(m)):_!=null?ji(h,Y,bn(_)):M!=null&&h.removeAttribute("value"),P==null&&D!=null&&(h.defaultChecked=!!D),P!=null&&(h.checked=P&&typeof P!="function"&&typeof P!="symbol"),ce!=null&&typeof ce!="function"&&typeof ce!="symbol"&&typeof ce!="boolean"?h.name=""+bn(ce):h.removeAttribute("name")}function qn(h,m,_,M,P,D,Y,ce){if(D!=null&&typeof D!="function"&&typeof D!="symbol"&&typeof D!="boolean"&&(h.type=D),m!=null||_!=null){if(!(D!=="submit"&&D!=="reset"||m!=null)){rn(h);return}_=_!=null?""+bn(_):"",m=m!=null?""+bn(m):_,ce||m===h.value||(h.value=m),h.defaultValue=m}M=M??P,M=typeof M!="function"&&typeof M!="symbol"&&!!M,h.checked=ce?h.checked:!!M,h.defaultChecked=!!M,Y!=null&&typeof Y!="function"&&typeof Y!="symbol"&&typeof Y!="boolean"&&(h.name=Y),rn(h)}function ji(h,m,_){m==="number"&&Ur(h.ownerDocument)===h||h.defaultValue===""+_||(h.defaultValue=""+_)}function pi(h,m,_,M){if(h=h.options,m){m={};for(var P=0;P<_.length;P++)m["$"+_[P]]=!0;for(_=0;_"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),IE=!1;if(Ul)try{var Cg={};Object.defineProperty(Cg,"passive",{get:function(){IE=!0}}),window.addEventListener("test",Cg,Cg),window.removeEventListener("test",Cg,Cg)}catch{IE=!1}var ru=null,FE=null,xy=null;function YL(){if(xy)return xy;var h,m=FE,_=m.length,M,P="value"in ru?ru.value:ru.textContent,D=P.length;for(h=0;h<_&&m[h]===P[h];h++);var Y=_-h;for(M=1;M<=Y&&m[_-M]===P[D-M];M++);return xy=P.slice(h,1=jg),n4=" ",r4=!1;function i4(h,m){switch(h){case"keyup":return Ene.indexOf(m.keyCode)!==-1;case"keydown":return m.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function a4(h){return h=h.detail,typeof h=="object"&&"data"in h?h.data:null}var Rf=!1;function Tne(h,m){switch(h){case"compositionend":return a4(m);case"keypress":return m.which!==32?null:(r4=!0,n4);case"textInput":return h=m.data,h===n4&&r4?null:h;default:return null}}function Nne(h,m){if(Rf)return h==="compositionend"||!$E&&i4(h,m)?(h=YL(),xy=FE=ru=null,Rf=!1,h):null;switch(h){case"paste":return null;case"keypress":if(!(m.ctrlKey||m.altKey||m.metaKey)||m.ctrlKey&&m.altKey){if(m.char&&1=m)return{node:_,offset:m-h};h=M}e:{for(;_;){if(_.nextSibling){_=_.nextSibling;break e}_=_.parentNode}_=void 0}_=f4(_)}}function p4(h,m){return h&&m?h===m?!0:h&&h.nodeType===3?!1:m&&m.nodeType===3?p4(h,m.parentNode):"contains"in h?h.contains(m):h.compareDocumentPosition?!!(h.compareDocumentPosition(m)&16):!1:!1}function g4(h){h=h!=null&&h.ownerDocument!=null&&h.ownerDocument.defaultView!=null?h.ownerDocument.defaultView:window;for(var m=Ur(h.document);m instanceof h.HTMLIFrameElement;){try{var _=typeof m.contentWindow.location.href=="string"}catch{_=!1}if(_)h=m.contentWindow;else break;m=Ur(h.document)}return m}function WE(h){var m=h&&h.nodeName&&h.nodeName.toLowerCase();return m&&(m==="input"&&(h.type==="text"||h.type==="search"||h.type==="tel"||h.type==="url"||h.type==="password")||m==="textarea"||h.contentEditable==="true")}var Lne=Ul&&"documentMode"in document&&11>=document.documentMode,Df=null,qE=null,Lg=null,KE=!1;function x4(h,m,_){var M=_.window===_?_.document:_.nodeType===9?_:_.ownerDocument;KE||Df==null||Df!==Ur(M)||(M=Df,"selectionStart"in M&&WE(M)?M={start:M.selectionStart,end:M.selectionEnd}:(M=(M.ownerDocument&&M.ownerDocument.defaultView||window).getSelection(),M={anchorNode:M.anchorNode,anchorOffset:M.anchorOffset,focusNode:M.focusNode,focusOffset:M.focusOffset}),Lg&&Og(Lg,M)||(Lg=M,M=uv(qE,"onSelect"),0>=Y,P-=Y,Qo=1<<32-Be(m)+P|_<ln?(Nn=Ut,Ut=null):Nn=Ut.sibling;var Fn=it(Ge,Ut,et[ln],ft);if(Fn===null){Ut===null&&(Ut=Nn);break}h&&Ut&&Fn.alternate===null&&m(Ge,Ut),Oe=D(Fn,Oe,ln),In===null?Vt=Fn:In.sibling=Fn,In=Fn,Ut=Nn}if(ln===et.length)return _(Ge,Ut),kn&&Bl(Ge,ln),Vt;if(Ut===null){for(;lnln?(Nn=Ut,Ut=null):Nn=Ut.sibling;var Eu=it(Ge,Ut,Fn.value,ft);if(Eu===null){Ut===null&&(Ut=Nn);break}h&&Ut&&Eu.alternate===null&&m(Ge,Ut),Oe=D(Eu,Oe,ln),In===null?Vt=Eu:In.sibling=Eu,In=Eu,Ut=Nn}if(Fn.done)return _(Ge,Ut),kn&&Bl(Ge,ln),Vt;if(Ut===null){for(;!Fn.done;ln++,Fn=et.next())Fn=mt(Ge,Fn.value,ft),Fn!==null&&(Oe=D(Fn,Oe,ln),In===null?Vt=Fn:In.sibling=Fn,In=Fn);return kn&&Bl(Ge,ln),Vt}for(Ut=M(Ut);!Fn.done;ln++,Fn=et.next())Fn=ot(Ut,Ge,ln,Fn.value,ft),Fn!==null&&(h&&Fn.alternate!==null&&Ut.delete(Fn.key===null?ln:Fn.key),Oe=D(Fn,Oe,ln),In===null?Vt=Fn:In.sibling=Fn,In=Fn);return h&&Ut.forEach(function(nie){return m(Ge,nie)}),kn&&Bl(Ge,ln),Vt}function rr(Ge,Oe,et,ft){if(typeof et=="object"&&et!==null&&et.type===v&&et.key===null&&(et=et.props.children),typeof et=="object"&&et!==null){switch(et.$$typeof){case p:e:{for(var Vt=et.key;Oe!==null;){if(Oe.key===Vt){if(Vt=et.type,Vt===v){if(Oe.tag===7){_(Ge,Oe.sibling),ft=P(Oe,et.props.children),ft.return=Ge,Ge=ft;break e}}else if(Oe.elementType===Vt||typeof Vt=="object"&&Vt!==null&&Vt.$$typeof===O&&Kd(Vt)===Oe.type){_(Ge,Oe.sibling),ft=P(Oe,et.props),Hg(ft,et),ft.return=Ge,Ge=ft;break e}_(Ge,Oe);break}else m(Ge,Oe);Oe=Oe.sibling}et.type===v?(ft=$d(et.props.children,Ge.mode,ft,et.key),ft.return=Ge,Ge=ft):(ft=Ty(et.type,et.key,et.props,null,Ge.mode,ft),Hg(ft,et),ft.return=Ge,Ge=ft)}return Y(Ge);case x:e:{for(Vt=et.key;Oe!==null;){if(Oe.key===Vt)if(Oe.tag===4&&Oe.stateNode.containerInfo===et.containerInfo&&Oe.stateNode.implementation===et.implementation){_(Ge,Oe.sibling),ft=P(Oe,et.children||[]),ft.return=Ge,Ge=ft;break e}else{_(Ge,Oe);break}else m(Ge,Oe);Oe=Oe.sibling}ft=t2(et,Ge.mode,ft),ft.return=Ge,Ge=ft}return Y(Ge);case O:return et=Kd(et),rr(Ge,Oe,et,ft)}if(q(et))return It(Ge,Oe,et,ft);if(I(et)){if(Vt=I(et),typeof Vt!="function")throw Error(r(150));return et=Vt.call(et),Zt(Ge,Oe,et,ft)}if(typeof et.then=="function")return rr(Ge,Oe,Dy(et),ft);if(et.$$typeof===w)return rr(Ge,Oe,ky(Ge,et),ft);Oy(Ge,et)}return typeof et=="string"&&et!==""||typeof et=="number"||typeof et=="bigint"?(et=""+et,Oe!==null&&Oe.tag===6?(_(Ge,Oe.sibling),ft=P(Oe,et),ft.return=Ge,Ge=ft):(_(Ge,Oe),ft=e2(et,Ge.mode,ft),ft.return=Ge,Ge=ft),Y(Ge)):_(Ge,Oe)}return function(Ge,Oe,et,ft){try{Bg=0;var Vt=rr(Ge,Oe,et,ft);return Gf=null,Vt}catch(Ut){if(Ut===Vf||Ut===jy)throw Ut;var In=hs(29,Ut,null,Ge.mode);return In.lanes=ft,In.return=Ge,In}finally{}}}var Yd=z4(!0),B4=z4(!1),lu=!1;function f2(h){h.updateQueue={baseState:h.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function m2(h,m){h=h.updateQueue,m.updateQueue===h&&(m.updateQueue={baseState:h.baseState,firstBaseUpdate:h.firstBaseUpdate,lastBaseUpdate:h.lastBaseUpdate,shared:h.shared,callbacks:null})}function cu(h){return{lane:h,tag:0,payload:null,callback:null,next:null}}function uu(h,m,_){var M=h.updateQueue;if(M===null)return null;if(M=M.shared,(Hn&2)!==0){var P=M.pending;return P===null?m.next=m:(m.next=P.next,P.next=m),M.pending=m,m=Ay(h),M4(h,null,_),m}return Ey(h,M,m,_),Ay(h)}function $g(h,m,_){if(m=m.updateQueue,m!==null&&(m=m.shared,(_&4194048)!==0)){var M=m.lanes;M&=h.pendingLanes,_|=M,m.lanes=_,Xt(h,_)}}function p2(h,m){var _=h.updateQueue,M=h.alternate;if(M!==null&&(M=M.updateQueue,_===M)){var P=null,D=null;if(_=_.firstBaseUpdate,_!==null){do{var Y={lane:_.lane,tag:_.tag,payload:_.payload,callback:null,next:null};D===null?P=D=Y:D=D.next=Y,_=_.next}while(_!==null);D===null?P=D=m:D=D.next=m}else P=D=m;_={baseState:M.baseState,firstBaseUpdate:P,lastBaseUpdate:D,shared:M.shared,callbacks:M.callbacks},h.updateQueue=_;return}h=_.lastBaseUpdate,h===null?_.firstBaseUpdate=m:h.next=m,_.lastBaseUpdate=m}var g2=!1;function Vg(){if(g2){var h=$f;if(h!==null)throw h}}function Gg(h,m,_,M){g2=!1;var P=h.updateQueue;lu=!1;var D=P.firstBaseUpdate,Y=P.lastBaseUpdate,ce=P.shared.pending;if(ce!==null){P.shared.pending=null;var Ne=ce,nt=Ne.next;Ne.next=null,Y===null?D=nt:Y.next=nt,Y=Ne;var ht=h.alternate;ht!==null&&(ht=ht.updateQueue,ce=ht.lastBaseUpdate,ce!==Y&&(ce===null?ht.firstBaseUpdate=nt:ce.next=nt,ht.lastBaseUpdate=Ne))}if(D!==null){var mt=P.baseState;Y=0,ht=nt=Ne=null,ce=D;do{var it=ce.lane&-536870913,ot=it!==ce.lane;if(ot?(Tn&it)===it:(M&it)===it){it!==0&&it===Hf&&(g2=!0),ht!==null&&(ht=ht.next={lane:0,tag:ce.tag,payload:ce.payload,callback:null,next:null});e:{var It=h,Zt=ce;it=m;var rr=_;switch(Zt.tag){case 1:if(It=Zt.payload,typeof It=="function"){mt=It.call(rr,mt,it);break e}mt=It;break e;case 3:It.flags=It.flags&-65537|128;case 0:if(It=Zt.payload,it=typeof It=="function"?It.call(rr,mt,it):It,it==null)break e;mt=f({},mt,it);break e;case 2:lu=!0}}it=ce.callback,it!==null&&(h.flags|=64,ot&&(h.flags|=8192),ot=P.callbacks,ot===null?P.callbacks=[it]:ot.push(it))}else ot={lane:it,tag:ce.tag,payload:ce.payload,callback:ce.callback,next:null},ht===null?(nt=ht=ot,Ne=mt):ht=ht.next=ot,Y|=it;if(ce=ce.next,ce===null){if(ce=P.shared.pending,ce===null)break;ot=ce,ce=ot.next,ot.next=null,P.lastBaseUpdate=ot,P.shared.pending=null}}while(!0);ht===null&&(Ne=mt),P.baseState=Ne,P.firstBaseUpdate=nt,P.lastBaseUpdate=ht,D===null&&(P.shared.lanes=0),pu|=Y,h.lanes=Y,h.memoizedState=mt}}function H4(h,m){if(typeof h!="function")throw Error(r(191,h));h.call(m)}function $4(h,m){var _=h.callbacks;if(_!==null)for(h.callbacks=null,h=0;h<_.length;h++)H4(_[h],m)}var Wf=F(null),Ly=F(0);function V4(h,m){h=Ql,J(Ly,h),J(Wf,m),Ql=h|m.baseLanes}function x2(){J(Ly,Ql),J(Wf,Wf.current)}function b2(){Ql=Ly.current,$(Wf),$(Ly)}var fs=F(null),Bs=null;function du(h){var m=h.alternate;J(Kr,Kr.current&1),J(fs,h),Bs===null&&(m===null||Wf.current!==null||m.memoizedState!==null)&&(Bs=h)}function y2(h){J(Kr,Kr.current),J(fs,h),Bs===null&&(Bs=h)}function G4(h){h.tag===22?(J(Kr,Kr.current),J(fs,h),Bs===null&&(Bs=h)):hu()}function hu(){J(Kr,Kr.current),J(fs,fs.current)}function ms(h){$(fs),Bs===h&&(Bs=null),$(Kr)}var Kr=F(0);function Iy(h){for(var m=h;m!==null;){if(m.tag===13){var _=m.memoizedState;if(_!==null&&(_=_.dehydrated,_===null||EA(_)||AA(_)))return m}else if(m.tag===19&&(m.memoizedProps.revealOrder==="forwards"||m.memoizedProps.revealOrder==="backwards"||m.memoizedProps.revealOrder==="unstable_legacy-backwards"||m.memoizedProps.revealOrder==="together")){if((m.flags&128)!==0)return m}else if(m.child!==null){m.child.return=m,m=m.child;continue}if(m===h)break;for(;m.sibling===null;){if(m.return===null||m.return===h)return null;m=m.return}m.sibling.return=m.return,m=m.sibling}return null}var Vl=0,on=null,tr=null,ni=null,Fy=!1,qf=!1,Zd=!1,Uy=0,Wg=0,Kf=null,Wne=0;function zr(){throw Error(r(321))}function v2(h,m){if(m===null)return!1;for(var _=0;_D?D:8;var Y=X.T,ce={};X.T=ce,O2(h,!1,m,_);try{var Ne=P(),nt=X.S;if(nt!==null&&nt(ce,Ne),Ne!==null&&typeof Ne=="object"&&typeof Ne.then=="function"){var ht=Gne(Ne,M);Kg(h,m,ht,xs(h))}else Kg(h,m,M,xs(h))}catch(mt){Kg(h,m,{then:function(){},status:"rejected",reason:mt},xs())}finally{W.p=D,Y!==null&&ce.types!==null&&(Y.types=ce.types),X.T=Y}}function Zne(){}function R2(h,m,_,M){if(h.tag!==5)throw Error(r(476));var P=_I(h).queue;vI(h,P,m,ee,_===null?Zne:function(){return wI(h),_(M)})}function _I(h){var m=h.memoizedState;if(m!==null)return m;m={memoizedState:ee,baseState:ee,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Gl,lastRenderedState:ee},next:null};var _={};return m.next={memoizedState:_,baseState:_,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Gl,lastRenderedState:_},next:null},h.memoizedState=m,h=h.alternate,h!==null&&(h.memoizedState=m),m}function wI(h){var m=_I(h);m.next===null&&(m=h.alternate.memoizedState),Kg(h,m.next.queue,{},xs())}function D2(){return Yi(dx)}function SI(){return Xr().memoizedState}function MI(){return Xr().memoizedState}function Qne(h){for(var m=h.return;m!==null;){switch(m.tag){case 24:case 3:var _=xs();h=cu(_);var M=uu(m,h,_);M!==null&&(qa(M,m,_),$g(M,m,_)),m={cache:c2()},h.payload=m;return}m=m.return}}function Jne(h,m,_){var M=xs();_={lane:M,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null},Gy(h)?AI(m,_):(_=QE(h,m,_,M),_!==null&&(qa(_,h,M),TI(_,m,M)))}function EI(h,m,_){var M=xs();Kg(h,m,_,M)}function Kg(h,m,_,M){var P={lane:M,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null};if(Gy(h))AI(m,P);else{var D=h.alternate;if(h.lanes===0&&(D===null||D.lanes===0)&&(D=m.lastRenderedReducer,D!==null))try{var Y=m.lastRenderedState,ce=D(Y,_);if(P.hasEagerState=!0,P.eagerState=ce,ds(ce,Y))return Ey(h,m,P,0),fr===null&&My(),!1}catch{}finally{}if(_=QE(h,m,P,M),_!==null)return qa(_,h,M),TI(_,m,M),!0}return!1}function O2(h,m,_,M){if(M={lane:2,revertLane:fA(),gesture:null,action:M,hasEagerState:!1,eagerState:null,next:null},Gy(h)){if(m)throw Error(r(479))}else m=QE(h,_,M,2),m!==null&&qa(m,h,2)}function Gy(h){var m=h.alternate;return h===on||m!==null&&m===on}function AI(h,m){qf=Fy=!0;var _=h.pending;_===null?m.next=m:(m.next=_.next,_.next=m),h.pending=m}function TI(h,m,_){if((_&4194048)!==0){var M=m.lanes;M&=h.pendingLanes,_|=M,m.lanes=_,Xt(h,_)}}var Xg={readContext:Yi,use:By,useCallback:zr,useContext:zr,useEffect:zr,useImperativeHandle:zr,useLayoutEffect:zr,useInsertionEffect:zr,useMemo:zr,useReducer:zr,useRef:zr,useState:zr,useDebugValue:zr,useDeferredValue:zr,useTransition:zr,useSyncExternalStore:zr,useId:zr,useHostTransitionStatus:zr,useFormState:zr,useActionState:zr,useOptimistic:zr,useMemoCache:zr,useCacheRefresh:zr};Xg.useEffectEvent=zr;var NI={readContext:Yi,use:By,useCallback:function(h,m){return Ea().memoizedState=[h,m===void 0?null:m],h},useContext:Yi,useEffect:dI,useImperativeHandle:function(h,m,_){_=_!=null?_.concat([h]):null,$y(4194308,4,pI.bind(null,m,h),_)},useLayoutEffect:function(h,m){return $y(4194308,4,h,m)},useInsertionEffect:function(h,m){$y(4,2,h,m)},useMemo:function(h,m){var _=Ea();m=m===void 0?null:m;var M=h();if(Zd){Ve(!0);try{h()}finally{Ve(!1)}}return _.memoizedState=[M,m],M},useReducer:function(h,m,_){var M=Ea();if(_!==void 0){var P=_(m);if(Zd){Ve(!0);try{_(m)}finally{Ve(!1)}}}else P=m;return M.memoizedState=M.baseState=P,h={pending:null,lanes:0,dispatch:null,lastRenderedReducer:h,lastRenderedState:P},M.queue=h,h=h.dispatch=Jne.bind(null,on,h),[M.memoizedState,h]},useRef:function(h){var m=Ea();return h={current:h},m.memoizedState=h},useState:function(h){h=N2(h);var m=h.queue,_=EI.bind(null,on,m);return m.dispatch=_,[h.memoizedState,_]},useDebugValue:P2,useDeferredValue:function(h,m){var _=Ea();return j2(_,h,m)},useTransition:function(){var h=N2(!1);return h=vI.bind(null,on,h.queue,!0,!1),Ea().memoizedState=h,[!1,h]},useSyncExternalStore:function(h,m,_){var M=on,P=Ea();if(kn){if(_===void 0)throw Error(r(407));_=_()}else{if(_=m(),fr===null)throw Error(r(349));(Tn&127)!==0||X4(M,m,_)}P.memoizedState=_;var D={value:_,getSnapshot:m};return P.queue=D,dI(Z4.bind(null,M,D,h),[h]),M.flags|=2048,Xf(9,{destroy:void 0},Y4.bind(null,M,D,_,m),null),_},useId:function(){var h=Ea(),m=fr.identifierPrefix;if(kn){var _=Jo,M=Qo;_=(M&~(1<<32-Be(M)-1)).toString(32)+_,m="_"+m+"R_"+_,_=Uy++,0<_&&(m+="H"+_.toString(32)),m+="_"}else _=Wne++,m="_"+m+"r_"+_.toString(32)+"_";return h.memoizedState=m},useHostTransitionStatus:D2,useFormState:sI,useActionState:sI,useOptimistic:function(h){var m=Ea();m.memoizedState=m.baseState=h;var _={pending:null,lanes:0,dispatch:null,lastRenderedReducer:null,lastRenderedState:null};return m.queue=_,m=O2.bind(null,on,!0,_),_.dispatch=m,[h,m]},useMemoCache:E2,useCacheRefresh:function(){return Ea().memoizedState=Qne.bind(null,on)},useEffectEvent:function(h){var m=Ea(),_={impl:h};return m.memoizedState=_,function(){if((Hn&2)!==0)throw Error(r(440));return _.impl.apply(void 0,arguments)}}},L2={readContext:Yi,use:By,useCallback:xI,useContext:Yi,useEffect:k2,useImperativeHandle:gI,useInsertionEffect:fI,useLayoutEffect:mI,useMemo:bI,useReducer:Hy,useRef:uI,useState:function(){return Hy(Gl)},useDebugValue:P2,useDeferredValue:function(h,m){var _=Xr();return yI(_,tr.memoizedState,h,m)},useTransition:function(){var h=Hy(Gl)[0],m=Xr().memoizedState;return[typeof h=="boolean"?h:qg(h),m]},useSyncExternalStore:K4,useId:SI,useHostTransitionStatus:D2,useFormState:oI,useActionState:oI,useOptimistic:function(h,m){var _=Xr();return eI(_,tr,h,m)},useMemoCache:E2,useCacheRefresh:MI};L2.useEffectEvent=hI;var CI={readContext:Yi,use:By,useCallback:xI,useContext:Yi,useEffect:k2,useImperativeHandle:gI,useInsertionEffect:fI,useLayoutEffect:mI,useMemo:bI,useReducer:T2,useRef:uI,useState:function(){return T2(Gl)},useDebugValue:P2,useDeferredValue:function(h,m){var _=Xr();return tr===null?j2(_,h,m):yI(_,tr.memoizedState,h,m)},useTransition:function(){var h=T2(Gl)[0],m=Xr().memoizedState;return[typeof h=="boolean"?h:qg(h),m]},useSyncExternalStore:K4,useId:SI,useHostTransitionStatus:D2,useFormState:cI,useActionState:cI,useOptimistic:function(h,m){var _=Xr();return tr!==null?eI(_,tr,h,m):(_.baseState=h,[h,_.queue.dispatch])},useMemoCache:E2,useCacheRefresh:MI};CI.useEffectEvent=hI;function I2(h,m,_,M){m=h.memoizedState,_=_(M,m),_=_==null?m:f({},m,_),h.memoizedState=_,h.lanes===0&&(h.updateQueue.baseState=_)}var F2={enqueueSetState:function(h,m,_){h=h._reactInternals;var M=xs(),P=cu(M);P.payload=m,_!=null&&(P.callback=_),m=uu(h,P,M),m!==null&&(qa(m,h,M),$g(m,h,M))},enqueueReplaceState:function(h,m,_){h=h._reactInternals;var M=xs(),P=cu(M);P.tag=1,P.payload=m,_!=null&&(P.callback=_),m=uu(h,P,M),m!==null&&(qa(m,h,M),$g(m,h,M))},enqueueForceUpdate:function(h,m){h=h._reactInternals;var _=xs(),M=cu(_);M.tag=2,m!=null&&(M.callback=m),m=uu(h,M,_),m!==null&&(qa(m,h,_),$g(m,h,_))}};function kI(h,m,_,M,P,D,Y){return h=h.stateNode,typeof h.shouldComponentUpdate=="function"?h.shouldComponentUpdate(M,D,Y):m.prototype&&m.prototype.isPureReactComponent?!Og(_,M)||!Og(P,D):!0}function PI(h,m,_,M){h=m.state,typeof m.componentWillReceiveProps=="function"&&m.componentWillReceiveProps(_,M),typeof m.UNSAFE_componentWillReceiveProps=="function"&&m.UNSAFE_componentWillReceiveProps(_,M),m.state!==h&&F2.enqueueReplaceState(m,m.state,null)}function Qd(h,m){var _=m;if("ref"in m){_={};for(var M in m)M!=="ref"&&(_[M]=m[M])}if(h=h.defaultProps){_===m&&(_=f({},_));for(var P in h)_[P]===void 0&&(_[P]=h[P])}return _}function jI(h){Sy(h)}function RI(h){console.error(h)}function DI(h){Sy(h)}function Wy(h,m){try{var _=h.onUncaughtError;_(m.value,{componentStack:m.stack})}catch(M){setTimeout(function(){throw M})}}function OI(h,m,_){try{var M=h.onCaughtError;M(_.value,{componentStack:_.stack,errorBoundary:m.tag===1?m.stateNode:null})}catch(P){setTimeout(function(){throw P})}}function U2(h,m,_){return _=cu(_),_.tag=3,_.payload={element:null},_.callback=function(){Wy(h,m)},_}function LI(h){return h=cu(h),h.tag=3,h}function II(h,m,_,M){var P=_.type.getDerivedStateFromError;if(typeof P=="function"){var D=M.value;h.payload=function(){return P(D)},h.callback=function(){OI(m,_,M)}}var Y=_.stateNode;Y!==null&&typeof Y.componentDidCatch=="function"&&(h.callback=function(){OI(m,_,M),typeof P!="function"&&(gu===null?gu=new Set([this]):gu.add(this));var ce=M.stack;this.componentDidCatch(M.value,{componentStack:ce!==null?ce:""})})}function ere(h,m,_,M,P){if(_.flags|=32768,M!==null&&typeof M=="object"&&typeof M.then=="function"){if(m=_.alternate,m!==null&&Bf(m,_,P,!0),_=fs.current,_!==null){switch(_.tag){case 31:case 13:return Bs===null?iv():_.alternate===null&&Br===0&&(Br=3),_.flags&=-257,_.flags|=65536,_.lanes=P,M===Ry?_.flags|=16384:(m=_.updateQueue,m===null?_.updateQueue=new Set([M]):m.add(M),uA(h,M,P)),!1;case 22:return _.flags|=65536,M===Ry?_.flags|=16384:(m=_.updateQueue,m===null?(m={transitions:null,markerInstances:null,retryQueue:new Set([M])},_.updateQueue=m):(_=m.retryQueue,_===null?m.retryQueue=new Set([M]):_.add(M)),uA(h,M,P)),!1}throw Error(r(435,_.tag))}return uA(h,M,P),iv(),!1}if(kn)return m=fs.current,m!==null?((m.flags&65536)===0&&(m.flags|=256),m.flags|=65536,m.lanes=P,M!==i2&&(h=Error(r(422),{cause:M}),Fg(Is(h,_)))):(M!==i2&&(m=Error(r(423),{cause:M}),Fg(Is(m,_))),h=h.current.alternate,h.flags|=65536,P&=-P,h.lanes|=P,M=Is(M,_),P=U2(h.stateNode,M,P),p2(h,P),Br!==4&&(Br=2)),!1;var D=Error(r(520),{cause:M});if(D=Is(D,_),rx===null?rx=[D]:rx.push(D),Br!==4&&(Br=2),m===null)return!0;M=Is(M,_),_=m;do{switch(_.tag){case 3:return _.flags|=65536,h=P&-P,_.lanes|=h,h=U2(_.stateNode,M,h),p2(_,h),!1;case 1:if(m=_.type,D=_.stateNode,(_.flags&128)===0&&(typeof m.getDerivedStateFromError=="function"||D!==null&&typeof D.componentDidCatch=="function"&&(gu===null||!gu.has(D))))return _.flags|=65536,P&=-P,_.lanes|=P,P=LI(P),II(P,h,_,M),p2(_,P),!1}_=_.return}while(_!==null);return!1}var z2=Error(r(461)),ri=!1;function Zi(h,m,_,M){m.child=h===null?B4(m,null,_,M):Yd(m,h.child,_,M)}function FI(h,m,_,M,P){_=_.render;var D=m.ref;if("ref"in M){var Y={};for(var ce in M)ce!=="ref"&&(Y[ce]=M[ce])}else Y=M;return Wd(m),M=_2(h,m,_,Y,D,P),ce=w2(),h!==null&&!ri?(S2(h,m,P),Wl(h,m,P)):(kn&&ce&&n2(m),m.flags|=1,Zi(h,m,M,P),m.child)}function UI(h,m,_,M,P){if(h===null){var D=_.type;return typeof D=="function"&&!JE(D)&&D.defaultProps===void 0&&_.compare===null?(m.tag=15,m.type=D,zI(h,m,D,M,P)):(h=Ty(_.type,null,M,m,m.mode,P),h.ref=m.ref,h.return=m,m.child=h)}if(D=h.child,!K2(h,P)){var Y=D.memoizedProps;if(_=_.compare,_=_!==null?_:Og,_(Y,M)&&h.ref===m.ref)return Wl(h,m,P)}return m.flags|=1,h=zl(D,M),h.ref=m.ref,h.return=m,m.child=h}function zI(h,m,_,M,P){if(h!==null){var D=h.memoizedProps;if(Og(D,M)&&h.ref===m.ref)if(ri=!1,m.pendingProps=M=D,K2(h,P))(h.flags&131072)!==0&&(ri=!0);else return m.lanes=h.lanes,Wl(h,m,P)}return B2(h,m,_,M,P)}function BI(h,m,_,M){var P=M.children,D=h!==null?h.memoizedState:null;if(h===null&&m.stateNode===null&&(m.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),M.mode==="hidden"){if((m.flags&128)!==0){if(D=D!==null?D.baseLanes|_:_,h!==null){for(M=m.child=h.child,P=0;M!==null;)P=P|M.lanes|M.childLanes,M=M.sibling;M=P&~D}else M=0,m.child=null;return HI(h,m,D,_,M)}if((_&536870912)!==0)m.memoizedState={baseLanes:0,cachePool:null},h!==null&&Py(m,D!==null?D.cachePool:null),D!==null?V4(m,D):x2(),G4(m);else return M=m.lanes=536870912,HI(h,m,D!==null?D.baseLanes|_:_,_,M)}else D!==null?(Py(m,D.cachePool),V4(m,D),hu(),m.memoizedState=null):(h!==null&&Py(m,null),x2(),hu());return Zi(h,m,P,_),m.child}function Yg(h,m){return h!==null&&h.tag===22||m.stateNode!==null||(m.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),m.sibling}function HI(h,m,_,M,P){var D=d2();return D=D===null?null:{parent:ti._currentValue,pool:D},m.memoizedState={baseLanes:_,cachePool:D},h!==null&&Py(m,null),x2(),G4(m),h!==null&&Bf(h,m,M,!0),m.childLanes=P,null}function qy(h,m){return m=Xy({mode:m.mode,children:m.children},h.mode),m.ref=h.ref,h.child=m,m.return=h,m}function $I(h,m,_){return Yd(m,h.child,null,_),h=qy(m,m.pendingProps),h.flags|=2,ms(m),m.memoizedState=null,h}function tre(h,m,_){var M=m.pendingProps,P=(m.flags&128)!==0;if(m.flags&=-129,h===null){if(kn){if(M.mode==="hidden")return h=qy(m,M),m.lanes=536870912,Yg(null,h);if(y2(m),(h=vr)?(h=tF(h,zs),h=h!==null&&h.data==="&"?h:null,h!==null&&(m.memoizedState={dehydrated:h,treeContext:iu!==null?{id:Qo,overflow:Jo}:null,retryLane:536870912,hydrationErrors:null},_=A4(h),_.return=m,m.child=_,Xi=m,vr=null)):h=null,h===null)throw su(m);return m.lanes=536870912,null}return qy(m,M)}var D=h.memoizedState;if(D!==null){var Y=D.dehydrated;if(y2(m),P)if(m.flags&256)m.flags&=-257,m=$I(h,m,_);else if(m.memoizedState!==null)m.child=h.child,m.flags|=128,m=null;else throw Error(r(558));else if(ri||Bf(h,m,_,!1),P=(_&h.childLanes)!==0,ri||P){if(M=fr,M!==null&&(Y=En(M,_),Y!==0&&Y!==D.retryLane))throw D.retryLane=Y,Hd(h,Y),qa(M,h,Y),z2;iv(),m=$I(h,m,_)}else h=D.treeContext,vr=Hs(Y.nextSibling),Xi=m,kn=!0,au=null,zs=!1,h!==null&&C4(m,h),m=qy(m,M),m.flags|=4096;return m}return h=zl(h.child,{mode:M.mode,children:M.children}),h.ref=m.ref,m.child=h,h.return=m,h}function Ky(h,m){var _=m.ref;if(_===null)h!==null&&h.ref!==null&&(m.flags|=4194816);else{if(typeof _!="function"&&typeof _!="object")throw Error(r(284));(h===null||h.ref!==_)&&(m.flags|=4194816)}}function B2(h,m,_,M,P){return Wd(m),_=_2(h,m,_,M,void 0,P),M=w2(),h!==null&&!ri?(S2(h,m,P),Wl(h,m,P)):(kn&&M&&n2(m),m.flags|=1,Zi(h,m,_,P),m.child)}function VI(h,m,_,M,P,D){return Wd(m),m.updateQueue=null,_=q4(m,M,_,P),W4(h),M=w2(),h!==null&&!ri?(S2(h,m,D),Wl(h,m,D)):(kn&&M&&n2(m),m.flags|=1,Zi(h,m,_,D),m.child)}function GI(h,m,_,M,P){if(Wd(m),m.stateNode===null){var D=If,Y=_.contextType;typeof Y=="object"&&Y!==null&&(D=Yi(Y)),D=new _(M,D),m.memoizedState=D.state!==null&&D.state!==void 0?D.state:null,D.updater=F2,m.stateNode=D,D._reactInternals=m,D=m.stateNode,D.props=M,D.state=m.memoizedState,D.refs={},f2(m),Y=_.contextType,D.context=typeof Y=="object"&&Y!==null?Yi(Y):If,D.state=m.memoizedState,Y=_.getDerivedStateFromProps,typeof Y=="function"&&(I2(m,_,Y,M),D.state=m.memoizedState),typeof _.getDerivedStateFromProps=="function"||typeof D.getSnapshotBeforeUpdate=="function"||typeof D.UNSAFE_componentWillMount!="function"&&typeof D.componentWillMount!="function"||(Y=D.state,typeof D.componentWillMount=="function"&&D.componentWillMount(),typeof D.UNSAFE_componentWillMount=="function"&&D.UNSAFE_componentWillMount(),Y!==D.state&&F2.enqueueReplaceState(D,D.state,null),Gg(m,M,D,P),Vg(),D.state=m.memoizedState),typeof D.componentDidMount=="function"&&(m.flags|=4194308),M=!0}else if(h===null){D=m.stateNode;var ce=m.memoizedProps,Ne=Qd(_,ce);D.props=Ne;var nt=D.context,ht=_.contextType;Y=If,typeof ht=="object"&&ht!==null&&(Y=Yi(ht));var mt=_.getDerivedStateFromProps;ht=typeof mt=="function"||typeof D.getSnapshotBeforeUpdate=="function",ce=m.pendingProps!==ce,ht||typeof D.UNSAFE_componentWillReceiveProps!="function"&&typeof D.componentWillReceiveProps!="function"||(ce||nt!==Y)&&PI(m,D,M,Y),lu=!1;var it=m.memoizedState;D.state=it,Gg(m,M,D,P),Vg(),nt=m.memoizedState,ce||it!==nt||lu?(typeof mt=="function"&&(I2(m,_,mt,M),nt=m.memoizedState),(Ne=lu||kI(m,_,Ne,M,it,nt,Y))?(ht||typeof D.UNSAFE_componentWillMount!="function"&&typeof D.componentWillMount!="function"||(typeof D.componentWillMount=="function"&&D.componentWillMount(),typeof D.UNSAFE_componentWillMount=="function"&&D.UNSAFE_componentWillMount()),typeof D.componentDidMount=="function"&&(m.flags|=4194308)):(typeof D.componentDidMount=="function"&&(m.flags|=4194308),m.memoizedProps=M,m.memoizedState=nt),D.props=M,D.state=nt,D.context=Y,M=Ne):(typeof D.componentDidMount=="function"&&(m.flags|=4194308),M=!1)}else{D=m.stateNode,m2(h,m),Y=m.memoizedProps,ht=Qd(_,Y),D.props=ht,mt=m.pendingProps,it=D.context,nt=_.contextType,Ne=If,typeof nt=="object"&&nt!==null&&(Ne=Yi(nt)),ce=_.getDerivedStateFromProps,(nt=typeof ce=="function"||typeof D.getSnapshotBeforeUpdate=="function")||typeof D.UNSAFE_componentWillReceiveProps!="function"&&typeof D.componentWillReceiveProps!="function"||(Y!==mt||it!==Ne)&&PI(m,D,M,Ne),lu=!1,it=m.memoizedState,D.state=it,Gg(m,M,D,P),Vg();var ot=m.memoizedState;Y!==mt||it!==ot||lu||h!==null&&h.dependencies!==null&&Cy(h.dependencies)?(typeof ce=="function"&&(I2(m,_,ce,M),ot=m.memoizedState),(ht=lu||kI(m,_,ht,M,it,ot,Ne)||h!==null&&h.dependencies!==null&&Cy(h.dependencies))?(nt||typeof D.UNSAFE_componentWillUpdate!="function"&&typeof D.componentWillUpdate!="function"||(typeof D.componentWillUpdate=="function"&&D.componentWillUpdate(M,ot,Ne),typeof D.UNSAFE_componentWillUpdate=="function"&&D.UNSAFE_componentWillUpdate(M,ot,Ne)),typeof D.componentDidUpdate=="function"&&(m.flags|=4),typeof D.getSnapshotBeforeUpdate=="function"&&(m.flags|=1024)):(typeof D.componentDidUpdate!="function"||Y===h.memoizedProps&&it===h.memoizedState||(m.flags|=4),typeof D.getSnapshotBeforeUpdate!="function"||Y===h.memoizedProps&&it===h.memoizedState||(m.flags|=1024),m.memoizedProps=M,m.memoizedState=ot),D.props=M,D.state=ot,D.context=Ne,M=ht):(typeof D.componentDidUpdate!="function"||Y===h.memoizedProps&&it===h.memoizedState||(m.flags|=4),typeof D.getSnapshotBeforeUpdate!="function"||Y===h.memoizedProps&&it===h.memoizedState||(m.flags|=1024),M=!1)}return D=M,Ky(h,m),M=(m.flags&128)!==0,D||M?(D=m.stateNode,_=M&&typeof _.getDerivedStateFromError!="function"?null:D.render(),m.flags|=1,h!==null&&M?(m.child=Yd(m,h.child,null,P),m.child=Yd(m,null,_,P)):Zi(h,m,_,P),m.memoizedState=D.state,h=m.child):h=Wl(h,m,P),h}function WI(h,m,_,M){return Vd(),m.flags|=256,Zi(h,m,_,M),m.child}var H2={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function $2(h){return{baseLanes:h,cachePool:O4()}}function V2(h,m,_){return h=h!==null?h.childLanes&~_:0,m&&(h|=gs),h}function qI(h,m,_){var M=m.pendingProps,P=!1,D=(m.flags&128)!==0,Y;if((Y=D)||(Y=h!==null&&h.memoizedState===null?!1:(Kr.current&2)!==0),Y&&(P=!0,m.flags&=-129),Y=(m.flags&32)!==0,m.flags&=-33,h===null){if(kn){if(P?du(m):hu(),(h=vr)?(h=tF(h,zs),h=h!==null&&h.data!=="&"?h:null,h!==null&&(m.memoizedState={dehydrated:h,treeContext:iu!==null?{id:Qo,overflow:Jo}:null,retryLane:536870912,hydrationErrors:null},_=A4(h),_.return=m,m.child=_,Xi=m,vr=null)):h=null,h===null)throw su(m);return AA(h)?m.lanes=32:m.lanes=536870912,null}var ce=M.children;return M=M.fallback,P?(hu(),P=m.mode,ce=Xy({mode:"hidden",children:ce},P),M=$d(M,P,_,null),ce.return=m,M.return=m,ce.sibling=M,m.child=ce,M=m.child,M.memoizedState=$2(_),M.childLanes=V2(h,Y,_),m.memoizedState=H2,Yg(null,M)):(du(m),G2(m,ce))}var Ne=h.memoizedState;if(Ne!==null&&(ce=Ne.dehydrated,ce!==null)){if(D)m.flags&256?(du(m),m.flags&=-257,m=W2(h,m,_)):m.memoizedState!==null?(hu(),m.child=h.child,m.flags|=128,m=null):(hu(),ce=M.fallback,P=m.mode,M=Xy({mode:"visible",children:M.children},P),ce=$d(ce,P,_,null),ce.flags|=2,M.return=m,ce.return=m,M.sibling=ce,m.child=M,Yd(m,h.child,null,_),M=m.child,M.memoizedState=$2(_),M.childLanes=V2(h,Y,_),m.memoizedState=H2,m=Yg(null,M));else if(du(m),AA(ce)){if(Y=ce.nextSibling&&ce.nextSibling.dataset,Y)var nt=Y.dgst;Y=nt,M=Error(r(419)),M.stack="",M.digest=Y,Fg({value:M,source:null,stack:null}),m=W2(h,m,_)}else if(ri||Bf(h,m,_,!1),Y=(_&h.childLanes)!==0,ri||Y){if(Y=fr,Y!==null&&(M=En(Y,_),M!==0&&M!==Ne.retryLane))throw Ne.retryLane=M,Hd(h,M),qa(Y,h,M),z2;EA(ce)||iv(),m=W2(h,m,_)}else EA(ce)?(m.flags|=192,m.child=h.child,m=null):(h=Ne.treeContext,vr=Hs(ce.nextSibling),Xi=m,kn=!0,au=null,zs=!1,h!==null&&C4(m,h),m=G2(m,M.children),m.flags|=4096);return m}return P?(hu(),ce=M.fallback,P=m.mode,Ne=h.child,nt=Ne.sibling,M=zl(Ne,{mode:"hidden",children:M.children}),M.subtreeFlags=Ne.subtreeFlags&65011712,nt!==null?ce=zl(nt,ce):(ce=$d(ce,P,_,null),ce.flags|=2),ce.return=m,M.return=m,M.sibling=ce,m.child=M,Yg(null,M),M=m.child,ce=h.child.memoizedState,ce===null?ce=$2(_):(P=ce.cachePool,P!==null?(Ne=ti._currentValue,P=P.parent!==Ne?{parent:Ne,pool:Ne}:P):P=O4(),ce={baseLanes:ce.baseLanes|_,cachePool:P}),M.memoizedState=ce,M.childLanes=V2(h,Y,_),m.memoizedState=H2,Yg(h.child,M)):(du(m),_=h.child,h=_.sibling,_=zl(_,{mode:"visible",children:M.children}),_.return=m,_.sibling=null,h!==null&&(Y=m.deletions,Y===null?(m.deletions=[h],m.flags|=16):Y.push(h)),m.child=_,m.memoizedState=null,_)}function G2(h,m){return m=Xy({mode:"visible",children:m},h.mode),m.return=h,h.child=m}function Xy(h,m){return h=hs(22,h,null,m),h.lanes=0,h}function W2(h,m,_){return Yd(m,h.child,null,_),h=G2(m,m.pendingProps.children),h.flags|=2,m.memoizedState=null,h}function KI(h,m,_){h.lanes|=m;var M=h.alternate;M!==null&&(M.lanes|=m),o2(h.return,m,_)}function q2(h,m,_,M,P,D){var Y=h.memoizedState;Y===null?h.memoizedState={isBackwards:m,rendering:null,renderingStartTime:0,last:M,tail:_,tailMode:P,treeForkCount:D}:(Y.isBackwards=m,Y.rendering=null,Y.renderingStartTime=0,Y.last=M,Y.tail=_,Y.tailMode=P,Y.treeForkCount=D)}function XI(h,m,_){var M=m.pendingProps,P=M.revealOrder,D=M.tail;M=M.children;var Y=Kr.current,ce=(Y&2)!==0;if(ce?(Y=Y&1|2,m.flags|=128):Y&=1,J(Kr,Y),Zi(h,m,M,_),M=kn?Ig:0,!ce&&h!==null&&(h.flags&128)!==0)e:for(h=m.child;h!==null;){if(h.tag===13)h.memoizedState!==null&&KI(h,_,m);else if(h.tag===19)KI(h,_,m);else if(h.child!==null){h.child.return=h,h=h.child;continue}if(h===m)break e;for(;h.sibling===null;){if(h.return===null||h.return===m)break e;h=h.return}h.sibling.return=h.return,h=h.sibling}switch(P){case"forwards":for(_=m.child,P=null;_!==null;)h=_.alternate,h!==null&&Iy(h)===null&&(P=_),_=_.sibling;_=P,_===null?(P=m.child,m.child=null):(P=_.sibling,_.sibling=null),q2(m,!1,P,_,D,M);break;case"backwards":case"unstable_legacy-backwards":for(_=null,P=m.child,m.child=null;P!==null;){if(h=P.alternate,h!==null&&Iy(h)===null){m.child=P;break}h=P.sibling,P.sibling=_,_=P,P=h}q2(m,!0,_,null,D,M);break;case"together":q2(m,!1,null,null,void 0,M);break;default:m.memoizedState=null}return m.child}function Wl(h,m,_){if(h!==null&&(m.dependencies=h.dependencies),pu|=m.lanes,(_&m.childLanes)===0)if(h!==null){if(Bf(h,m,_,!1),(_&m.childLanes)===0)return null}else return null;if(h!==null&&m.child!==h.child)throw Error(r(153));if(m.child!==null){for(h=m.child,_=zl(h,h.pendingProps),m.child=_,_.return=m;h.sibling!==null;)h=h.sibling,_=_.sibling=zl(h,h.pendingProps),_.return=m;_.sibling=null}return m.child}function K2(h,m){return(h.lanes&m)!==0?!0:(h=h.dependencies,!!(h!==null&&Cy(h)))}function nre(h,m,_){switch(m.tag){case 3:Ee(m,m.stateNode.containerInfo),ou(m,ti,h.memoizedState.cache),Vd();break;case 27:case 5:ue(m);break;case 4:Ee(m,m.stateNode.containerInfo);break;case 10:ou(m,m.type,m.memoizedProps.value);break;case 31:if(m.memoizedState!==null)return m.flags|=128,y2(m),null;break;case 13:var M=m.memoizedState;if(M!==null)return M.dehydrated!==null?(du(m),m.flags|=128,null):(_&m.child.childLanes)!==0?qI(h,m,_):(du(m),h=Wl(h,m,_),h!==null?h.sibling:null);du(m);break;case 19:var P=(h.flags&128)!==0;if(M=(_&m.childLanes)!==0,M||(Bf(h,m,_,!1),M=(_&m.childLanes)!==0),P){if(M)return XI(h,m,_);m.flags|=128}if(P=m.memoizedState,P!==null&&(P.rendering=null,P.tail=null,P.lastEffect=null),J(Kr,Kr.current),M)break;return null;case 22:return m.lanes=0,BI(h,m,_,m.pendingProps);case 24:ou(m,ti,h.memoizedState.cache)}return Wl(h,m,_)}function YI(h,m,_){if(h!==null)if(h.memoizedProps!==m.pendingProps)ri=!0;else{if(!K2(h,_)&&(m.flags&128)===0)return ri=!1,nre(h,m,_);ri=(h.flags&131072)!==0}else ri=!1,kn&&(m.flags&1048576)!==0&&N4(m,Ig,m.index);switch(m.lanes=0,m.tag){case 16:e:{var M=m.pendingProps;if(h=Kd(m.elementType),m.type=h,typeof h=="function")JE(h)?(M=Qd(h,M),m.tag=1,m=GI(null,m,h,M,_)):(m.tag=0,m=B2(null,m,h,M,_));else{if(h!=null){var P=h.$$typeof;if(P===A){m.tag=11,m=FI(null,m,h,M,_);break e}else if(P===N){m.tag=14,m=UI(null,m,h,M,_);break e}}throw m=R(h)||h,Error(r(306,m,""))}}return m;case 0:return B2(h,m,m.type,m.pendingProps,_);case 1:return M=m.type,P=Qd(M,m.pendingProps),GI(h,m,M,P,_);case 3:e:{if(Ee(m,m.stateNode.containerInfo),h===null)throw Error(r(387));M=m.pendingProps;var D=m.memoizedState;P=D.element,m2(h,m),Gg(m,M,null,_);var Y=m.memoizedState;if(M=Y.cache,ou(m,ti,M),M!==D.cache&&l2(m,[ti],_,!0),Vg(),M=Y.element,D.isDehydrated)if(D={element:M,isDehydrated:!1,cache:Y.cache},m.updateQueue.baseState=D,m.memoizedState=D,m.flags&256){m=WI(h,m,M,_);break e}else if(M!==P){P=Is(Error(r(424)),m),Fg(P),m=WI(h,m,M,_);break e}else{switch(h=m.stateNode.containerInfo,h.nodeType){case 9:h=h.body;break;default:h=h.nodeName==="HTML"?h.ownerDocument.body:h}for(vr=Hs(h.firstChild),Xi=m,kn=!0,au=null,zs=!0,_=B4(m,null,M,_),m.child=_;_;)_.flags=_.flags&-3|4096,_=_.sibling}else{if(Vd(),M===P){m=Wl(h,m,_);break e}Zi(h,m,M,_)}m=m.child}return m;case 26:return Ky(h,m),h===null?(_=oF(m.type,null,m.pendingProps,null))?m.memoizedState=_:kn||(_=m.type,h=m.pendingProps,M=dv(le.current).createElement(_),M[Bn]=m,M[er]=h,Qi(M,_,h),Ie(M),m.stateNode=M):m.memoizedState=oF(m.type,h.memoizedProps,m.pendingProps,h.memoizedState),null;case 27:return ue(m),h===null&&kn&&(M=m.stateNode=iF(m.type,m.pendingProps,le.current),Xi=m,zs=!0,P=vr,vu(m.type)?(TA=P,vr=Hs(M.firstChild)):vr=P),Zi(h,m,m.pendingProps.children,_),Ky(h,m),h===null&&(m.flags|=4194304),m.child;case 5:return h===null&&kn&&((P=M=vr)&&(M=jre(M,m.type,m.pendingProps,zs),M!==null?(m.stateNode=M,Xi=m,vr=Hs(M.firstChild),zs=!1,P=!0):P=!1),P||su(m)),ue(m),P=m.type,D=m.pendingProps,Y=h!==null?h.memoizedProps:null,M=D.children,wA(P,D)?M=null:Y!==null&&wA(P,Y)&&(m.flags|=32),m.memoizedState!==null&&(P=_2(h,m,qne,null,null,_),dx._currentValue=P),Ky(h,m),Zi(h,m,M,_),m.child;case 6:return h===null&&kn&&((h=_=vr)&&(_=Rre(_,m.pendingProps,zs),_!==null?(m.stateNode=_,Xi=m,vr=null,h=!0):h=!1),h||su(m)),null;case 13:return qI(h,m,_);case 4:return Ee(m,m.stateNode.containerInfo),M=m.pendingProps,h===null?m.child=Yd(m,null,M,_):Zi(h,m,M,_),m.child;case 11:return FI(h,m,m.type,m.pendingProps,_);case 7:return Zi(h,m,m.pendingProps,_),m.child;case 8:return Zi(h,m,m.pendingProps.children,_),m.child;case 12:return Zi(h,m,m.pendingProps.children,_),m.child;case 10:return M=m.pendingProps,ou(m,m.type,M.value),Zi(h,m,M.children,_),m.child;case 9:return P=m.type._context,M=m.pendingProps.children,Wd(m),P=Yi(P),M=M(P),m.flags|=1,Zi(h,m,M,_),m.child;case 14:return UI(h,m,m.type,m.pendingProps,_);case 15:return zI(h,m,m.type,m.pendingProps,_);case 19:return XI(h,m,_);case 31:return tre(h,m,_);case 22:return BI(h,m,_,m.pendingProps);case 24:return Wd(m),M=Yi(ti),h===null?(P=d2(),P===null&&(P=fr,D=c2(),P.pooledCache=D,D.refCount++,D!==null&&(P.pooledCacheLanes|=_),P=D),m.memoizedState={parent:M,cache:P},f2(m),ou(m,ti,P)):((h.lanes&_)!==0&&(m2(h,m),Gg(m,null,null,_),Vg()),P=h.memoizedState,D=m.memoizedState,P.parent!==M?(P={parent:M,cache:M},m.memoizedState=P,m.lanes===0&&(m.memoizedState=m.updateQueue.baseState=P),ou(m,ti,M)):(M=D.cache,ou(m,ti,M),M!==P.cache&&l2(m,[ti],_,!0))),Zi(h,m,m.pendingProps.children,_),m.child;case 29:throw m.pendingProps}throw Error(r(156,m.tag))}function ql(h){h.flags|=4}function X2(h,m,_,M,P){if((m=(h.mode&32)!==0)&&(m=!1),m){if(h.flags|=16777216,(P&335544128)===P)if(h.stateNode.complete)h.flags|=8192;else if(S5())h.flags|=8192;else throw Xd=Ry,h2}else h.flags&=-16777217}function ZI(h,m){if(m.type!=="stylesheet"||(m.state.loading&4)!==0)h.flags&=-16777217;else if(h.flags|=16777216,!hF(m))if(S5())h.flags|=8192;else throw Xd=Ry,h2}function Yy(h,m){m!==null&&(h.flags|=4),h.flags&16384&&(m=h.tag!==22?pt():536870912,h.lanes|=m,Jf|=m)}function Zg(h,m){if(!kn)switch(h.tailMode){case"hidden":m=h.tail;for(var _=null;m!==null;)m.alternate!==null&&(_=m),m=m.sibling;_===null?h.tail=null:_.sibling=null;break;case"collapsed":_=h.tail;for(var M=null;_!==null;)_.alternate!==null&&(M=_),_=_.sibling;M===null?m||h.tail===null?h.tail=null:h.tail.sibling=null:M.sibling=null}}function _r(h){var m=h.alternate!==null&&h.alternate.child===h.child,_=0,M=0;if(m)for(var P=h.child;P!==null;)_|=P.lanes|P.childLanes,M|=P.subtreeFlags&65011712,M|=P.flags&65011712,P.return=h,P=P.sibling;else for(P=h.child;P!==null;)_|=P.lanes|P.childLanes,M|=P.subtreeFlags,M|=P.flags,P.return=h,P=P.sibling;return h.subtreeFlags|=M,h.childLanes=_,m}function rre(h,m,_){var M=m.pendingProps;switch(r2(m),m.tag){case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return _r(m),null;case 1:return _r(m),null;case 3:return _=m.stateNode,M=null,h!==null&&(M=h.memoizedState.cache),m.memoizedState.cache!==M&&(m.flags|=2048),$l(ti),pe(),_.pendingContext&&(_.context=_.pendingContext,_.pendingContext=null),(h===null||h.child===null)&&(zf(m)?ql(m):h===null||h.memoizedState.isDehydrated&&(m.flags&256)===0||(m.flags|=1024,a2())),_r(m),null;case 26:var P=m.type,D=m.memoizedState;return h===null?(ql(m),D!==null?(_r(m),ZI(m,D)):(_r(m),X2(m,P,null,M,_))):D?D!==h.memoizedState?(ql(m),_r(m),ZI(m,D)):(_r(m),m.flags&=-16777217):(h=h.memoizedProps,h!==M&&ql(m),_r(m),X2(m,P,h,M,_)),null;case 27:if(ke(m),_=le.current,P=m.type,h!==null&&m.stateNode!=null)h.memoizedProps!==M&&ql(m);else{if(!M){if(m.stateNode===null)throw Error(r(166));return _r(m),null}h=V.current,zf(m)?k4(m):(h=iF(P,M,_),m.stateNode=h,ql(m))}return _r(m),null;case 5:if(ke(m),P=m.type,h!==null&&m.stateNode!=null)h.memoizedProps!==M&&ql(m);else{if(!M){if(m.stateNode===null)throw Error(r(166));return _r(m),null}if(D=V.current,zf(m))k4(m);else{var Y=dv(le.current);switch(D){case 1:D=Y.createElementNS("http://www.w3.org/2000/svg",P);break;case 2:D=Y.createElementNS("http://www.w3.org/1998/Math/MathML",P);break;default:switch(P){case"svg":D=Y.createElementNS("http://www.w3.org/2000/svg",P);break;case"math":D=Y.createElementNS("http://www.w3.org/1998/Math/MathML",P);break;case"script":D=Y.createElement("div"),D.innerHTML=" - + +