diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a01c3ae5..7a0c5080d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ All notable changes to Bambuddy will be documented in this file. +## [0.1.6b4] - 2026-01-01 + +### Added +- **Camera popup window improvements** + - Auto-resize to fit video resolution on first open + - Persist window size and position to localStorage + - Restore saved window state for subsequent opens +- **Slicer protocol handler** - OS detection for correct protocol (Windows: `bambustudio://`, macOS/Linux: `bambustudioopen://`) + +### Fixed +- **Maintenance duration display** - Show weeks instead of imprecise months for better countdown precision +- **Print hours display** - Convert large hour values to readable units (e.g., 478h → 3w, 100h → 4d) +- **Printer hour counter** - Fixed runtime_seconds not incrementing during prints (first timestamp was set but never committed) + +### Changed +- **Printer cards** - Refactored AMS section for better visual grouping and spacing + +## [0.1.6b3] - 2025-12-31 + +### Added +- **Customizable Theme System** - Comprehensive theme customization with independent settings for dark and light modes: + - **Style**: Classic (clean shadows), Glow (accent-colored glow effects), Vibrant (dramatic deep shadows) + - **Background**: Neutral, Warm, Cool (light mode) + OLED, Slate, Forest (dark mode only) + - **Accent Colors**: Green, Teal, Blue, Orange, Purple, Red + - All combinations work together (e.g., Glow style + Forest background + Teal accent) + - Settings sync across devices via database + - Live preview in Settings → Appearance + +### Fixed +- **Printer hour counter** - Fixed bug in printer's hour counter display + +### Changed +- **Sidebar power switch** - Added confirmation modal to sidebar's quick power switch + ## [0.1.6b2] - 2025-12-29 ### Added diff --git a/Dockerfile b/Dockerfile index c96c85b88..a24bcf552 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,6 +37,7 @@ RUN mkdir -p /app/data /app/logs # Environment variables ENV PYTHONUNBUFFERED=1 ENV DATA_DIR=/app/data +ENV LOG_DIR=/app/logs EXPOSE 8000 diff --git a/README.md b/README.md index 63556d358..e0ac935ee 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ -**Plus:** Dark/light theme • Mobile responsive • Keyboard shortcuts • Multi-language (EN/DE) • Auto updates • Database backup/restore • System info dashboard +**Plus:** Customizable themes (style, background, accent) • Mobile responsive • Keyboard shortcuts • Multi-language (EN/DE) • Auto updates • Database backup/restore • System info dashboard --- diff --git a/backend/app/api/routes/cloud.py b/backend/app/api/routes/cloud.py index e9a5c85f6..b285014ad 100644 --- a/backend/app/api/routes/cloud.py +++ b/backend/app/api/routes/cloud.py @@ -5,33 +5,36 @@ Handles authentication and profile management with Bambu Cloud. """ import json +import logging from pathlib import Path from typing import Literal -from fastapi import APIRouter, HTTPException, Depends -from sqlalchemy.ext.asyncio import AsyncSession +from fastapi import APIRouter, Body, Depends, HTTPException from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from backend.app.core.database import get_db from backend.app.models.settings import Settings -from backend.app.services.bambu_cloud import ( - get_cloud_service, - BambuCloudError, - BambuCloudAuthError, -) from backend.app.schemas.cloud import ( - CloudLoginRequest, - CloudVerifyRequest, - CloudLoginResponse, CloudAuthStatus, - CloudTokenRequest, - SlicerSettingsResponse, - SlicerSetting, CloudDevice, + CloudLoginRequest, + CloudLoginResponse, + CloudTokenRequest, + CloudVerifyRequest, + SlicerSetting, SlicerSettingCreate, - SlicerSettingUpdate, SlicerSettingDeleteResponse, + SlicerSettingsResponse, + SlicerSettingUpdate, ) +from backend.app.services.bambu_cloud import ( + BambuCloudAuthError, + BambuCloudError, + get_cloud_service, +) + +logger = logging.getLogger(__name__) router = APIRouter(prefix="/cloud", tags=["cloud"]) @@ -43,9 +46,7 @@ CLOUD_EMAIL_KEY = "bambu_cloud_email" async def get_stored_token(db: AsyncSession) -> tuple[str | None, str | None]: """Get stored cloud token and email from database.""" - result = await db.execute( - select(Settings).where(Settings.key.in_([CLOUD_TOKEN_KEY, CLOUD_EMAIL_KEY])) - ) + result = await db.execute(select(Settings).where(Settings.key.in_([CLOUD_TOKEN_KEY, CLOUD_EMAIL_KEY]))) settings = {s.key: s.value for s in result.scalars().all()} return settings.get(CLOUD_TOKEN_KEY), settings.get(CLOUD_EMAIL_KEY) @@ -64,9 +65,7 @@ async def store_token(db: AsyncSession, token: str, email: str) -> None: async def clear_token(db: AsyncSession) -> None: """Clear stored cloud token and email.""" - result = await db.execute( - select(Settings).where(Settings.key.in_([CLOUD_TOKEN_KEY, CLOUD_EMAIL_KEY])) - ) + result = await db.execute(select(Settings).where(Settings.key.in_([CLOUD_TOKEN_KEY, CLOUD_EMAIL_KEY]))) for setting in result.scalars().all(): await db.delete(setting) await db.commit() @@ -213,14 +212,16 @@ async def get_slicer_settings( parsed = [] for s in all_settings: - parsed.append(SlicerSetting( - setting_id=s.get("setting_id", s.get("id", "")), - name=s.get("name", "Unknown"), - type=our_type, - version=s.get("version"), - user_id=s.get("user_id"), - updated_time=s.get("updated_time"), - )) + parsed.append( + SlicerSetting( + setting_id=s.get("setting_id", s.get("id", "")), + name=s.get("name", "Unknown"), + type=our_type, + version=s.get("version"), + user_id=s.get("user_id"), + updated_time=s.get("updated_time"), + ) + ) setattr(result, our_type, parsed) return result @@ -258,6 +259,80 @@ async def get_setting_detail(setting_id: str, db: AsyncSession = Depends(get_db) raise HTTPException(status_code=500, detail=str(e)) +# Cache for filament preset info (setting_id -> {name, k}) +_filament_cache: dict[str, dict] = {} +_filament_cache_time: float = 0 +FILAMENT_CACHE_TTL = 300 # 5 minutes + + +@router.post("/filament-info") +async def get_filament_info(setting_ids: list[str] = Body(...), db: AsyncSession = Depends(get_db)): + """ + Get filament preset info (name and K value) for multiple setting IDs. + + Used to enrich AMS tray tooltips with cloud preset data. + """ + import time + + logger.info(f"get_filament_info called with {len(setting_ids)} IDs: {setting_ids}") + + global _filament_cache, _filament_cache_time + + # Clear stale cache + if time.time() - _filament_cache_time > FILAMENT_CACHE_TTL: + _filament_cache = {} + _filament_cache_time = time.time() + + token, _ = await get_stored_token(db) + if not token: + logger.info("get_filament_info: Not authenticated, returning empty") + # Return empty results if not authenticated (graceful degradation) + return {} + + cloud = get_cloud_service() + cloud.set_token(token) + + if not cloud.is_authenticated: + return {} + + result = {} + for setting_id in setting_ids: + if not setting_id: + continue + + # Check cache first + if setting_id in _filament_cache: + result[setting_id] = _filament_cache[setting_id] + continue + + try: + data = await cloud.get_setting_detail(setting_id) + setting = data.get("setting", {}) + + # Extract name (e.g., "Bambu PLA Basic Jade White") + name = data.get("name", "") + + # Extract K value (pressure_advance) + k_value = setting.get("pressure_advance") + if k_value is not None: + try: + k_value = float(k_value) + except (ValueError, TypeError): + k_value = None + + info = {"name": name, "k": k_value} + _filament_cache[setting_id] = info + result[setting_id] = info + + except Exception as e: + logger.warning(f"Failed to get cloud preset {setting_id}: {e}") + # Cache the failure to avoid repeated requests + _filament_cache[setting_id] = {"name": "", "k": None} + result[setting_id] = {"name": "", "k": None} + + return result + + @router.get("/devices", response_model=list[CloudDevice]) async def get_devices(db: AsyncSession = Depends(get_db)): """ @@ -425,7 +500,7 @@ def _load_fields(preset_type: str) -> dict: if not file_path.exists(): raise HTTPException(status_code=404, detail=f"Field definitions not found for: {preset_type}") - with open(file_path, "r") as f: + with open(file_path) as f: data = json.load(f) _fields_cache[preset_type] = data diff --git a/backend/app/api/routes/printers.py b/backend/app/api/routes/printers.py index 2f12c9ddc..30ecdbf34 100644 --- a/backend/app/api/routes/printers.py +++ b/backend/app/api/routes/printers.py @@ -169,6 +169,16 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db) ams_exists = False raw_data = state.raw_data or {} + # Build K-profile lookup map: cali_idx -> k_value + # This allows looking up the calibrated K value for each AMS slot + kprofile_map: dict[int, float] = {} + for kp in state.kprofiles or []: + if kp.slot_id is not None and kp.k_value: + try: + kprofile_map[kp.slot_id] = float(kp.k_value) + except (ValueError, TypeError): + pass + if "ams" in raw_data and isinstance(raw_data["ams"], list): ams_exists = True for ams_data in raw_data["ams"]: @@ -184,6 +194,13 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db) tray_uuid = tray_data.get("tray_uuid", "") if tray_uuid in ("", "00000000000000000000000000000000"): tray_uuid = None + + # Get K value: first try tray's k field, then lookup from K-profiles + k_value = tray_data.get("k") + cali_idx = tray_data.get("cali_idx") + if k_value is None and cali_idx is not None and cali_idx in kprofile_map: + k_value = kprofile_map[cali_idx] + trays.append( AMSTray( id=tray_data.get("id", 0), @@ -193,7 +210,8 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db) tray_id_name=tray_data.get("tray_id_name"), tray_info_idx=tray_data.get("tray_info_idx"), remain=tray_data.get("remain", 0), - k=tray_data.get("k"), + k=k_value, + cali_idx=cali_idx, tag_uid=tag_uid, tray_uuid=tray_uuid, nozzle_temp_min=tray_data.get("nozzle_temp_min"), @@ -239,13 +257,23 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db) vt_tray_uuid = vt_data.get("tray_uuid", "") if vt_tray_uuid in ("", "00000000000000000000000000000000"): vt_tray_uuid = None + + # Get K value: first try tray's k field, then lookup from K-profiles + vt_k_value = vt_data.get("k") + vt_cali_idx = vt_data.get("cali_idx") + if vt_k_value is None and vt_cali_idx is not None and vt_cali_idx in kprofile_map: + vt_k_value = kprofile_map[vt_cali_idx] + vt_tray = AMSTray( id=254, # Virtual tray ID tray_color=vt_data.get("tray_color"), tray_type=vt_data.get("tray_type"), tray_sub_brands=vt_data.get("tray_sub_brands"), + tray_id_name=vt_data.get("tray_id_name"), + tray_info_idx=vt_data.get("tray_info_idx"), remain=vt_data.get("remain", 0), - k=vt_data.get("k"), + k=vt_k_value, + cali_idx=vt_cali_idx, tag_uid=vt_tag_uid, tray_uuid=vt_tray_uuid, nozzle_temp_min=vt_data.get("nozzle_temp_min"), @@ -977,7 +1005,7 @@ async def debug_simulate_print_complete( "timelapse_was_active": False, } - logger.info(f"[DEBUG] Simulating print complete for printer {printer_id}, archive {archive.id}") + logger.info(f"Simulating print complete for printer {printer_id}, archive {archive.id}") # Call the actual on_print_complete handler await on_print_complete(printer_id, data) diff --git a/backend/app/api/routes/settings.py b/backend/app/api/routes/settings.py index 50ebae51c..fc266f05f 100644 --- a/backend/app/api/routes/settings.py +++ b/backend/app/api/routes/settings.py @@ -14,10 +14,11 @@ from backend.app.core.database import get_db from backend.app.models.archive import PrintArchive from backend.app.models.external_link import ExternalLink from backend.app.models.filament import Filament -from backend.app.models.maintenance import MaintenanceType +from backend.app.models.maintenance import MaintenanceHistory, MaintenanceType, PrinterMaintenance from backend.app.models.notification import NotificationProvider from backend.app.models.notification_template import NotificationTemplate from backend.app.models.pending_upload import PendingUpload +from backend.app.models.print_queue import PrintQueueItem from backend.app.models.printer import Printer from backend.app.models.project import Project from backend.app.models.project_bom import ProjectBOMItem @@ -42,14 +43,13 @@ async def get_setting(db: AsyncSession, key: str) -> str | None: async def set_setting(db: AsyncSession, key: str, value: str) -> None: """Set a single setting value.""" - result = await db.execute(select(Settings).where(Settings.key == key)) - setting = result.scalar_one_or_none() + from sqlalchemy import func + from sqlalchemy.dialects.sqlite import insert as sqlite_insert - if setting: - setting.value = value - else: - setting = Settings(key=key, value=value) - db.add(setting) + # Use upsert (INSERT ... ON CONFLICT UPDATE) for reliability + stmt = sqlite_insert(Settings).values(key=key, value=value) + stmt = stmt.on_conflict_do_update(index_elements=["key"], set_={"value": value, "updated_at": func.now()}) + await db.execute(stmt) @router.get("/", response_model=AppSettings) @@ -180,7 +180,10 @@ async def export_backup( include_external_links: bool = Query(True, description="Include external sidebar links"), include_printers: bool = Query(False, description="Include printers (without access codes)"), include_filaments: bool = Query(False, description="Include filament inventory"), - include_maintenance: bool = Query(False, description="Include maintenance types and records"), + include_maintenance: bool = Query( + False, description="Include maintenance types, per-printer settings, and history" + ), + include_print_queue: bool = Query(False, description="Include print queue items"), include_archives: bool = Query(False, description="Include print archive metadata"), include_projects: bool = Query(False, description="Include projects with BOM items"), include_pending_uploads: bool = Query(False, description="Include pending virtual printer uploads"), @@ -202,10 +205,20 @@ async def export_backup( # Notification providers if include_notifications: + # Build printer ID to serial lookup for cross-system backup + printer_id_to_serial: dict[int, str] = {} + pr_result = await db.execute(select(Printer)) + for pr in pr_result.scalars().all(): + printer_id_to_serial[pr.id] = pr.serial_number + result = await db.execute(select(NotificationProvider)) providers = result.scalars().all() backup["notification_providers"] = [] for p in providers: + # Use printer_serial for cross-system compatibility + provider_printer_id = getattr(p, "printer_id", None) + printer_serial = printer_id_to_serial.get(provider_printer_id) if provider_printer_id else None + backup["notification_providers"].append( { "name": p.name, @@ -221,12 +234,16 @@ async def export_backup( "on_printer_error": p.on_printer_error, "on_filament_low": p.on_filament_low, "on_maintenance_due": p.on_maintenance_due, + "on_ams_humidity_high": getattr(p, "on_ams_humidity_high", False), + "on_ams_temperature_high": getattr(p, "on_ams_temperature_high", False), + "on_ams_ht_humidity_high": getattr(p, "on_ams_ht_humidity_high", False), + "on_ams_ht_temperature_high": getattr(p, "on_ams_ht_temperature_high", False), "quiet_hours_enabled": p.quiet_hours_enabled, "quiet_hours_start": p.quiet_hours_start, "quiet_hours_end": p.quiet_hours_end, "daily_digest_enabled": getattr(p, "daily_digest_enabled", False), "daily_digest_time": getattr(p, "daily_digest_time", None), - "printer_id": getattr(p, "printer_id", None), + "printer_serial": printer_serial, } ) backup["included"].append("notification_providers") @@ -253,12 +270,19 @@ async def export_backup( result = await db.execute(select(SmartPlug)) plugs = result.scalars().all() backup["smart_plugs"] = [] + + # Build printer ID to serial mapping + printer_id_to_serial: dict[int, str] = {} + pr_result = await db.execute(select(Printer)) + for pr in pr_result.scalars().all(): + printer_id_to_serial[pr.id] = pr.serial_number + for plug in plugs: backup["smart_plugs"].append( { "name": plug.name, "ip_address": plug.ip_address, - "printer_id": plug.printer_id, + "printer_serial": printer_id_to_serial.get(plug.printer_id) if plug.printer_id else None, "enabled": plug.enabled, "auto_on": plug.auto_on, "auto_off": plug.auto_off, @@ -316,6 +340,7 @@ async def export_backup( "is_active": printer.is_active, "auto_archive": printer.auto_archive, "print_hours_offset": printer.print_hours_offset, + "runtime_seconds": printer.runtime_seconds, } if include_access_codes: printer_data["access_code"] = printer.access_code @@ -368,6 +393,99 @@ async def export_backup( ) backup["included"].append("maintenance_types") + # Printer maintenance settings (per-printer custom intervals, enabled status, last performed) + result = await db.execute(select(PrinterMaintenance)) + printer_maint = result.scalars().all() + backup["printer_maintenance"] = [] + + # Build lookups for printer serial and maintenance type name + printer_id_to_serial: dict[int, str] = {} + maint_type_id_to_name: dict[int, str] = {} + pr_result = await db.execute(select(Printer)) + for pr in pr_result.scalars().all(): + printer_id_to_serial[pr.id] = pr.serial_number + for mt in types: + maint_type_id_to_name[mt.id] = mt.name + + for pm in printer_maint: + backup["printer_maintenance"].append( + { + "printer_serial": printer_id_to_serial.get(pm.printer_id), + "maintenance_type_name": maint_type_id_to_name.get(pm.maintenance_type_id), + "custom_interval_hours": pm.custom_interval_hours, + "custom_interval_type": pm.custom_interval_type, + "enabled": pm.enabled, + "last_performed_at": pm.last_performed_at.isoformat() if pm.last_performed_at else None, + "last_performed_hours": pm.last_performed_hours, + } + ) + backup["included"].append("printer_maintenance") + + # Maintenance history + result = await db.execute(select(MaintenanceHistory)) + history = result.scalars().all() + backup["maintenance_history"] = [] + + # Build printer_maintenance ID to (printer_serial, maint_type_name) mapping + pm_id_to_info: dict[int, tuple[str | None, str | None]] = {} + for pm in printer_maint: + pm_id_to_info[pm.id] = ( + printer_id_to_serial.get(pm.printer_id), + maint_type_id_to_name.get(pm.maintenance_type_id), + ) + + for mh in history: + info = pm_id_to_info.get(mh.printer_maintenance_id, (None, None)) + backup["maintenance_history"].append( + { + "printer_serial": info[0], + "maintenance_type_name": info[1], + "performed_at": mh.performed_at.isoformat() if mh.performed_at else None, + "hours_at_maintenance": mh.hours_at_maintenance, + "notes": mh.notes, + } + ) + backup["included"].append("maintenance_history") + + # Print queue + if include_print_queue: + result = await db.execute(select(PrintQueueItem)) + queue_items = result.scalars().all() + backup["print_queue"] = [] + + # Build lookups + printer_id_to_serial: dict[int, str] = {} + archive_id_to_hash: dict[int, str | None] = {} + project_id_to_name: dict[int, str] = {} + + pr_result = await db.execute(select(Printer)) + for pr in pr_result.scalars().all(): + printer_id_to_serial[pr.id] = pr.serial_number + ar_result = await db.execute(select(PrintArchive)) + for ar in ar_result.scalars().all(): + archive_id_to_hash[ar.id] = ar.content_hash + proj_result = await db.execute(select(Project)) + for proj in proj_result.scalars().all(): + project_id_to_name[proj.id] = proj.name + + for qi in queue_items: + backup["print_queue"].append( + { + "printer_serial": printer_id_to_serial.get(qi.printer_id), + "archive_hash": archive_id_to_hash.get(qi.archive_id), + "project_name": project_id_to_name.get(qi.project_id) if qi.project_id else None, + "position": qi.position, + "scheduled_time": qi.scheduled_time.isoformat() if qi.scheduled_time else None, + "require_previous_success": qi.require_previous_success, + "auto_off_after": qi.auto_off_after, + "status": qi.status, + "started_at": qi.started_at.isoformat() if qi.started_at else None, + "completed_at": qi.completed_at.isoformat() if qi.completed_at else None, + "error_message": qi.error_message, + } + ) + backup["included"].append("print_queue") + # Collect files for ZIP (icons + archives) backup_files: list[tuple[str, Path]] = [] # (zip_path, local_path) @@ -394,10 +512,18 @@ async def export_backup( for proj in proj_result.scalars().all(): project_id_to_name[proj.id] = proj.name + # Build printer ID to serial mapping for archive export + printer_id_to_serial: dict[int, str] = {} + if include_printers: + printer_result = await db.execute(select(Printer)) + for pr in printer_result.scalars().all(): + printer_id_to_serial[pr.id] = pr.serial_number + for a in archives: archive_data = { "filename": a.filename, "project_name": project_id_to_name.get(a.project_id) if a.project_id else None, + "printer_serial": printer_id_to_serial.get(a.printer_id) if a.printer_id else None, "file_size": a.file_size, "content_hash": a.content_hash, "print_name": a.print_name, @@ -485,6 +611,8 @@ async def export_backup( "priority": p.priority, "budget": p.budget, "is_template": p.is_template, + "template_source_id": p.template_source_id, + "parent_id": p.parent_id, "bom_items": [ { "name": item.name, @@ -671,10 +799,79 @@ async def import_backup( str_value = str(value) await set_setting(db, key, str_value) restored["settings"] += 1 + # Flush settings to ensure they're persisted before continuing + await db.flush() + + # Restore printers FIRST (skip or overwrite duplicates by serial_number) + # Nearly everything in the app references printers, so they must be imported first + if "printers" in backup: + for printer_data in backup["printers"]: + result = await db.execute(select(Printer).where(Printer.serial_number == printer_data["serial_number"])) + existing = result.scalar_one_or_none() + if existing: + if overwrite: + existing.name = printer_data["name"] + existing.ip_address = printer_data["ip_address"] + existing.model = printer_data.get("model") + existing.location = printer_data.get("location") + existing.nozzle_count = printer_data.get("nozzle_count", 1) + existing.auto_archive = printer_data.get("auto_archive", True) + existing.print_hours_offset = printer_data.get("print_hours_offset", 0.0) + existing.runtime_seconds = printer_data.get("runtime_seconds", 0) + + # If backup includes access_code, also update access_code and is_active + backup_access_code = printer_data.get("access_code") + if backup_access_code and backup_access_code != "CHANGE_ME": + existing.access_code = backup_access_code + is_active_val = printer_data.get("is_active", False) + if isinstance(is_active_val, str): + is_active_val = is_active_val.lower() == "true" + existing.is_active = is_active_val + + restored["printers"] += 1 + else: + skipped["printers"] += 1 + skipped_details["printers"].append(f"{printer_data['name']} ({printer_data['serial_number']})") + else: + # Use access code from backup if provided, otherwise require manual setup + access_code = printer_data.get("access_code") + has_access_code = access_code and access_code != "CHANGE_ME" + is_active_from_backup = printer_data.get("is_active", False) + # Handle bool or string "true"/"false" + if isinstance(is_active_from_backup, str): + is_active_from_backup = is_active_from_backup.lower() == "true" + + printer = Printer( + name=printer_data["name"], + serial_number=printer_data["serial_number"], + ip_address=printer_data["ip_address"], + access_code=access_code if has_access_code else "CHANGE_ME", + model=printer_data.get("model"), + location=printer_data.get("location"), + nozzle_count=printer_data.get("nozzle_count", 1), + is_active=is_active_from_backup if has_access_code else False, + auto_archive=printer_data.get("auto_archive", True), + print_hours_offset=printer_data.get("print_hours_offset", 0.0), + runtime_seconds=printer_data.get("runtime_seconds", 0), + ) + db.add(printer) + restored["printers"] += 1 + # Flush printers so other sections can look them up + await db.flush() # Restore notification providers (skip or overwrite duplicates by name) + # Build printer serial to ID lookup (printers were restored first) if "notification_providers" in backup: + printer_serial_to_id: dict[str, int] = {} + pr_result = await db.execute(select(Printer)) + for pr in pr_result.scalars().all(): + printer_serial_to_id[pr.serial_number] = pr.id + for provider_data in backup["notification_providers"]: + # Look up printer_id from serial (supports both old printer_id and new printer_serial format) + printer_serial = provider_data.get("printer_serial") + printer_id = printer_serial_to_id.get(printer_serial) if printer_serial else provider_data.get("printer_id") + result = await db.execute( select(NotificationProvider).where(NotificationProvider.name == provider_data["name"]) ) @@ -694,12 +891,16 @@ async def import_backup( existing.on_printer_error = provider_data.get("on_printer_error", False) existing.on_filament_low = provider_data.get("on_filament_low", False) existing.on_maintenance_due = provider_data.get("on_maintenance_due", False) + existing.on_ams_humidity_high = provider_data.get("on_ams_humidity_high", False) + existing.on_ams_temperature_high = provider_data.get("on_ams_temperature_high", False) + existing.on_ams_ht_humidity_high = provider_data.get("on_ams_ht_humidity_high", False) + existing.on_ams_ht_temperature_high = provider_data.get("on_ams_ht_temperature_high", False) existing.quiet_hours_enabled = provider_data.get("quiet_hours_enabled", False) existing.quiet_hours_start = provider_data.get("quiet_hours_start") existing.quiet_hours_end = provider_data.get("quiet_hours_end") existing.daily_digest_enabled = provider_data.get("daily_digest_enabled", False) existing.daily_digest_time = provider_data.get("daily_digest_time") - existing.printer_id = provider_data.get("printer_id") + existing.printer_id = printer_id restored["notification_providers"] += 1 else: skipped["notification_providers"] += 1 @@ -719,12 +920,16 @@ async def import_backup( on_printer_error=provider_data.get("on_printer_error", False), on_filament_low=provider_data.get("on_filament_low", False), on_maintenance_due=provider_data.get("on_maintenance_due", False), + on_ams_humidity_high=provider_data.get("on_ams_humidity_high", False), + on_ams_temperature_high=provider_data.get("on_ams_temperature_high", False), + on_ams_ht_humidity_high=provider_data.get("on_ams_ht_humidity_high", False), + on_ams_ht_temperature_high=provider_data.get("on_ams_ht_temperature_high", False), quiet_hours_enabled=provider_data.get("quiet_hours_enabled", False), quiet_hours_start=provider_data.get("quiet_hours_start"), quiet_hours_end=provider_data.get("quiet_hours_end"), daily_digest_enabled=provider_data.get("daily_digest_enabled", False), daily_digest_time=provider_data.get("daily_digest_time"), - printer_id=provider_data.get("printer_id"), + printer_id=printer_id, ) db.add(provider) restored["notification_providers"] += 1 @@ -754,14 +959,25 @@ async def import_backup( restored["notification_templates"] += 1 # Restore smart plugs (skip or overwrite duplicates by IP) + # Note: Smart plugs reference printers, so printers should be restored first if "smart_plugs" in backup: + # Build printer serial to ID lookup + printer_serial_to_id: dict[str, int] = {} + pr_result = await db.execute(select(Printer)) + for pr in pr_result.scalars().all(): + printer_serial_to_id[pr.serial_number] = pr.id + for plug_data in backup["smart_plugs"]: + # Look up printer_id from serial (supports both old printer_id and new printer_serial format) + printer_serial = plug_data.get("printer_serial") + printer_id = printer_serial_to_id.get(printer_serial) if printer_serial else plug_data.get("printer_id") + result = await db.execute(select(SmartPlug).where(SmartPlug.ip_address == plug_data["ip_address"])) existing = result.scalar_one_or_none() if existing: if overwrite: existing.name = plug_data["name"] - existing.printer_id = plug_data.get("printer_id") + existing.printer_id = printer_id existing.enabled = plug_data.get("enabled", True) existing.auto_on = plug_data.get("auto_on", True) existing.auto_off = plug_data.get("auto_off", True) @@ -785,7 +1001,7 @@ async def import_backup( plug = SmartPlug( name=plug_data["name"], ip_address=plug_data["ip_address"], - printer_id=plug_data.get("printer_id"), + printer_id=printer_id, enabled=plug_data.get("enabled", True), auto_on=plug_data.get("auto_on", True), auto_off=plug_data.get("auto_off", True), @@ -837,58 +1053,6 @@ async def import_backup( db.add(link) restored["external_links"] += 1 - # Restore printers (skip or overwrite duplicates by serial_number) - if "printers" in backup: - for printer_data in backup["printers"]: - result = await db.execute(select(Printer).where(Printer.serial_number == printer_data["serial_number"])) - existing = result.scalar_one_or_none() - if existing: - if overwrite: - existing.name = printer_data["name"] - existing.ip_address = printer_data["ip_address"] - existing.model = printer_data.get("model") - existing.location = printer_data.get("location") - existing.nozzle_count = printer_data.get("nozzle_count", 1) - existing.auto_archive = printer_data.get("auto_archive", True) - existing.print_hours_offset = printer_data.get("print_hours_offset", 0.0) - - # If backup includes access_code, also update access_code and is_active - backup_access_code = printer_data.get("access_code") - if backup_access_code and backup_access_code != "CHANGE_ME": - existing.access_code = backup_access_code - is_active_val = printer_data.get("is_active", False) - if isinstance(is_active_val, str): - is_active_val = is_active_val.lower() == "true" - existing.is_active = is_active_val - - restored["printers"] += 1 - else: - skipped["printers"] += 1 - skipped_details["printers"].append(f"{printer_data['name']} ({printer_data['serial_number']})") - else: - # Use access code from backup if provided, otherwise require manual setup - access_code = printer_data.get("access_code") - has_access_code = access_code and access_code != "CHANGE_ME" - is_active_from_backup = printer_data.get("is_active", False) - # Handle bool or string "true"/"false" - if isinstance(is_active_from_backup, str): - is_active_from_backup = is_active_from_backup.lower() == "true" - - printer = Printer( - name=printer_data["name"], - serial_number=printer_data["serial_number"], - ip_address=printer_data["ip_address"], - access_code=access_code if has_access_code else "CHANGE_ME", - model=printer_data.get("model"), - location=printer_data.get("location"), - nozzle_count=printer_data.get("nozzle_count", 1), - is_active=is_active_from_backup if has_access_code else False, - auto_archive=printer_data.get("auto_archive", True), - print_hours_offset=printer_data.get("print_hours_offset", 0.0), - ) - db.add(printer) - restored["printers"] += 1 - # Restore filaments (skip or overwrite duplicates by name+type+brand) if "filaments" in backup: for filament_data in backup["filaments"]: @@ -965,8 +1129,138 @@ async def import_backup( db.add(mt) restored["maintenance_types"] += 1 + # Restore printer maintenance settings (per-printer) + if "printer_maintenance" in backup: + # Build lookups + printer_serial_to_id: dict[str, int] = {} + maint_type_name_to_id: dict[str, int] = {} + + pr_result = await db.execute(select(Printer)) + for pr in pr_result.scalars().all(): + printer_serial_to_id[pr.serial_number] = pr.id + + mt_result = await db.execute(select(MaintenanceType)) + for mt in mt_result.scalars().all(): + maint_type_name_to_id[mt.name] = mt.id + + restored["printer_maintenance"] = 0 + skipped["printer_maintenance"] = 0 + skipped_details["printer_maintenance"] = [] + + for pm_data in backup["printer_maintenance"]: + printer_serial = pm_data.get("printer_serial") + maint_type_name = pm_data.get("maintenance_type_name") + + if not printer_serial or not maint_type_name: + continue + + printer_id = printer_serial_to_id.get(printer_serial) + maint_type_id = maint_type_name_to_id.get(maint_type_name) + + if not printer_id or not maint_type_id: + skipped["printer_maintenance"] += 1 + skipped_details["printer_maintenance"].append(f"{printer_serial}/{maint_type_name}") + continue + + # Check if exists + result = await db.execute( + select(PrinterMaintenance).where( + PrinterMaintenance.printer_id == printer_id, + PrinterMaintenance.maintenance_type_id == maint_type_id, + ) + ) + existing = result.scalar_one_or_none() + + if existing: + if overwrite: + existing.custom_interval_hours = pm_data.get("custom_interval_hours") + existing.custom_interval_type = pm_data.get("custom_interval_type") + existing.enabled = pm_data.get("enabled", True) + existing.last_performed_hours = pm_data.get("last_performed_hours", 0.0) + if pm_data.get("last_performed_at"): + existing.last_performed_at = datetime.fromisoformat(pm_data["last_performed_at"]) + restored["printer_maintenance"] += 1 + else: + skipped["printer_maintenance"] += 1 + skipped_details["printer_maintenance"].append(f"{printer_serial}/{maint_type_name}") + else: + pm = PrinterMaintenance( + printer_id=printer_id, + maintenance_type_id=maint_type_id, + custom_interval_hours=pm_data.get("custom_interval_hours"), + custom_interval_type=pm_data.get("custom_interval_type"), + enabled=pm_data.get("enabled", True), + last_performed_hours=pm_data.get("last_performed_hours", 0.0), + ) + if pm_data.get("last_performed_at"): + pm.last_performed_at = datetime.fromisoformat(pm_data["last_performed_at"]) + db.add(pm) + restored["printer_maintenance"] += 1 + + # Restore maintenance history + if "maintenance_history" in backup: + # Build lookups + printer_serial_to_id: dict[str, int] = {} + maint_type_name_to_id: dict[str, int] = {} + + pr_result = await db.execute(select(Printer)) + for pr in pr_result.scalars().all(): + printer_serial_to_id[pr.serial_number] = pr.id + + mt_result = await db.execute(select(MaintenanceType)) + for mt in mt_result.scalars().all(): + maint_type_name_to_id[mt.name] = mt.id + + restored["maintenance_history"] = 0 + skipped["maintenance_history"] = 0 + skipped_details["maintenance_history"] = [] + + for mh_data in backup["maintenance_history"]: + printer_serial = mh_data.get("printer_serial") + maint_type_name = mh_data.get("maintenance_type_name") + + if not printer_serial or not maint_type_name: + continue + + printer_id = printer_serial_to_id.get(printer_serial) + maint_type_id = maint_type_name_to_id.get(maint_type_name) + + if not printer_id or not maint_type_id: + skipped["maintenance_history"] += 1 + continue + + # Find the PrinterMaintenance record + result = await db.execute( + select(PrinterMaintenance).where( + PrinterMaintenance.printer_id == printer_id, + PrinterMaintenance.maintenance_type_id == maint_type_id, + ) + ) + pm = result.scalar_one_or_none() + + if not pm: + skipped["maintenance_history"] += 1 + continue + + # Create history entry (no duplicate check - history is append-only) + mh = MaintenanceHistory( + printer_maintenance_id=pm.id, + hours_at_maintenance=mh_data.get("hours_at_maintenance", 0.0), + notes=mh_data.get("notes"), + ) + if mh_data.get("performed_at"): + mh.performed_at = datetime.fromisoformat(mh_data["performed_at"]) + db.add(mh) + restored["maintenance_history"] += 1 + # Restore archives (skip duplicates by content_hash - overwrite not supported for archives) if "archives" in backup: + # Build printer serial to ID mapping + printer_serial_to_id: dict[str, int] = {} + printer_result = await db.execute(select(Printer)) + for pr in printer_result.scalars().all(): + printer_serial_to_id[pr.serial_number] = pr.id + for archive_data in backup["archives"]: # Skip if no content_hash or already exists content_hash = archive_data.get("content_hash") @@ -981,11 +1275,16 @@ async def import_backup( # Only restore if file exists (from ZIP extraction) file_path = archive_data.get("file_path") if file_path and (base_dir / file_path).exists(): + # Look up printer_id from serial + printer_serial = archive_data.get("printer_serial") + printer_id = printer_serial_to_id.get(printer_serial) if printer_serial else None + archive = PrintArchive( filename=archive_data["filename"], file_path=file_path, file_size=archive_data.get("file_size", 0), content_hash=content_hash, + printer_id=printer_id, thumbnail_path=archive_data.get("thumbnail_path"), timelapse_path=archive_data.get("timelapse_path"), source_3mf_path=archive_data.get("source_3mf_path"), @@ -1032,6 +1331,8 @@ async def import_backup( existing.priority = project_data.get("priority", "normal") existing.budget = project_data.get("budget") existing.is_template = project_data.get("is_template", False) + existing.template_source_id = project_data.get("template_source_id") + existing.parent_id = project_data.get("parent_id") existing.attachments = project_data.get("attachments") if project_data.get("due_date"): existing.due_date = datetime.fromisoformat(project_data["due_date"]) @@ -1069,6 +1370,8 @@ async def import_backup( priority=project_data.get("priority", "normal"), budget=project_data.get("budget"), is_template=project_data.get("is_template", False), + template_source_id=project_data.get("template_source_id"), + parent_id=project_data.get("parent_id"), attachments=project_data.get("attachments"), ) if project_data.get("due_date"): @@ -1113,6 +1416,68 @@ async def import_backup( if archive: archive.project_id = project_name_to_id[project_name] + # Restore print queue (must be after archives and projects) + if "print_queue" in backup: + # Build lookups + printer_serial_to_id: dict[str, int] = {} + archive_hash_to_id: dict[str, int] = {} + project_name_to_id: dict[str, int] = {} + + pr_result = await db.execute(select(Printer)) + for pr in pr_result.scalars().all(): + printer_serial_to_id[pr.serial_number] = pr.id + + ar_result = await db.execute(select(PrintArchive)) + for ar in ar_result.scalars().all(): + if ar.content_hash: + archive_hash_to_id[ar.content_hash] = ar.id + + proj_result = await db.execute(select(Project)) + for proj in proj_result.scalars().all(): + project_name_to_id[proj.name] = proj.id + + restored["print_queue"] = 0 + skipped["print_queue"] = 0 + skipped_details["print_queue"] = [] + + for qi_data in backup["print_queue"]: + printer_serial = qi_data.get("printer_serial") + archive_hash = qi_data.get("archive_hash") + + if not printer_serial or not archive_hash: + skipped["print_queue"] += 1 + continue + + printer_id = printer_serial_to_id.get(printer_serial) + archive_id = archive_hash_to_id.get(archive_hash) + + if not printer_id or not archive_id: + skipped["print_queue"] += 1 + skipped_details["print_queue"].append(f"{printer_serial}/{archive_hash[:8] if archive_hash else 'N/A'}") + continue + + project_name = qi_data.get("project_name") + project_id = project_name_to_id.get(project_name) if project_name else None + + qi = PrintQueueItem( + printer_id=printer_id, + archive_id=archive_id, + project_id=project_id, + position=qi_data.get("position", 0), + require_previous_success=qi_data.get("require_previous_success", False), + auto_off_after=qi_data.get("auto_off_after", False), + status=qi_data.get("status", "pending"), + error_message=qi_data.get("error_message"), + ) + if qi_data.get("scheduled_time"): + qi.scheduled_time = datetime.fromisoformat(qi_data["scheduled_time"]) + if qi_data.get("started_at"): + qi.started_at = datetime.fromisoformat(qi_data["started_at"]) + if qi_data.get("completed_at"): + qi.completed_at = datetime.fromisoformat(qi_data["completed_at"]) + db.add(qi) + restored["print_queue"] += 1 + # Restore pending uploads (skip duplicates by filename) if "pending_uploads" in backup: # Ensure the pending uploads directory exists diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 9b3e47f00..a4bfe3db9 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,4 +1,5 @@ import logging +import os from pathlib import Path from pydantic_settings import BaseSettings @@ -7,14 +8,23 @@ from pydantic_settings import BaseSettings APP_VERSION = "0.1.6b3" GITHUB_REPO = "maziggy/bambuddy" -# Base directory for path calculations -_base_dir = Path(__file__).resolve().parent.parent.parent.parent +# App directory - where the application is installed (for static files) +_app_dir = Path(__file__).resolve().parent.parent.parent.parent + +# Data directory - for persistent data (database, archives) +# Use DATA_DIR env var if set (Docker), otherwise use project root (local dev) +_data_dir_env = os.environ.get("DATA_DIR") +_data_dir = Path(_data_dir_env) if _data_dir_env else _app_dir + +# Log directory - use LOG_DIR env var if set, otherwise use app_dir/logs +_log_dir_env = os.environ.get("LOG_DIR") +_log_dir = Path(_log_dir_env) if _log_dir_env else _app_dir / "logs" def _migrate_database() -> Path: """Migrate database from old name to new name if needed.""" - old_db = _base_dir / "bambutrack.db" - new_db = _base_dir / "bambuddy.db" + old_db = _data_dir / "bambutrack.db" + new_db = _data_dir / "bambuddy.db" # If old database exists and new one doesn't, rename it if old_db.exists() and not new_db.exists(): @@ -40,10 +50,10 @@ class Settings(BaseSettings): debug: bool = False # Default to production mode # Paths - base_dir: Path = _base_dir - archive_dir: Path = base_dir / "archive" - static_dir: Path = base_dir / "static" - log_dir: Path = base_dir / "logs" + base_dir: Path = _data_dir # For backwards compatibility + archive_dir: Path = _data_dir / "archive" + static_dir: Path = _app_dir / "static" # Static files are part of app, not data + log_dir: Path = _log_dir database_url: str = f"sqlite+aiosqlite:///{_db_path}" # Logging diff --git a/backend/app/main.py b/backend/app/main.py index e7c68d428..64e5d87c4 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1466,6 +1466,8 @@ async def track_printer_runtime(): now = datetime.now() updated_count = 0 + needs_commit = False + for printer in printers: # Get current state from printer manager state = printer_manager.get_status(printer.id) @@ -1481,15 +1483,22 @@ async def track_printer_runtime(): if elapsed > 0 and elapsed < RUNTIME_TRACKING_INTERVAL * 2: printer.runtime_seconds += int(elapsed) updated_count += 1 + needs_commit = True + else: + # First time seeing printer active - need to commit to save timestamp + needs_commit = True printer.last_runtime_update = now else: # Printer is idle/offline - clear last_runtime_update - printer.last_runtime_update = None + if printer.last_runtime_update is not None: + printer.last_runtime_update = None + needs_commit = True - if updated_count > 0: + if needs_commit: await db.commit() - logger.debug(f"Updated runtime for {updated_count} printer(s)") + if updated_count > 0: + logger.debug(f"Updated runtime for {updated_count} printer(s)") except asyncio.CancelledError: logger.info("Runtime tracking cancelled") diff --git a/backend/app/schemas/printer.py b/backend/app/schemas/printer.py index bd29ecf8f..f46065646 100644 --- a/backend/app/schemas/printer.py +++ b/backend/app/schemas/printer.py @@ -1,4 +1,5 @@ from datetime import datetime + from pydantic import BaseModel, Field @@ -54,7 +55,8 @@ class AMSTray(BaseModel): tray_id_name: str | None = None # Bambu filament ID like "A00-Y2" (can decode to color) tray_info_idx: str | None = None # Filament preset ID like "GFA00" remain: int = 0 - k: float | None = None # Pressure advance value + k: float | None = None # Pressure advance value (from tray or K-profile lookup) + cali_idx: int | None = None # Calibration index for K-profile lookup tag_uid: str | None = None # RFID tag UID (any tag) tray_uuid: str | None = None # Bambu Lab spool UUID (32-char hex) nozzle_temp_min: int | None = None # Min nozzle temperature @@ -76,6 +78,7 @@ class NozzleInfoResponse(BaseModel): class PrintOptionsResponse(BaseModel): """AI detection and print options from xcam data.""" + # Core AI detectors spaghetti_detector: bool = False print_halt: bool = False diff --git a/backend/app/schemas/settings.py b/backend/app/schemas/settings.py index a1e167162..295ab61e9 100644 --- a/backend/app/schemas/settings.py +++ b/backend/app/schemas/settings.py @@ -56,6 +56,18 @@ class AppSettings(BaseModel): virtual_printer_access_code: str = Field(default="", description="Access code for virtual printer authentication") virtual_printer_mode: str = Field(default="immediate", description="Archive mode: 'immediate' or 'queue'") + # Dark mode theme settings + dark_style: str = Field(default="classic", description="Dark mode style: classic, glow, vibrant") + dark_background: str = Field( + default="neutral", description="Dark mode background: neutral, warm, cool, oled, slate, forest" + ) + dark_accent: str = Field(default="green", description="Dark mode accent: green, teal, blue, orange, purple, red") + + # Light mode theme settings + light_style: str = Field(default="classic", description="Light mode style: classic, glow, vibrant") + light_background: str = Field(default="neutral", description="Light mode background: neutral, warm, cool") + light_accent: str = Field(default="green", description="Light mode accent: green, teal, blue, orange, purple, red") + class AppSettingsUpdate(BaseModel): """Schema for updating settings (all fields optional).""" @@ -84,3 +96,9 @@ class AppSettingsUpdate(BaseModel): virtual_printer_enabled: bool | None = None virtual_printer_access_code: str | None = None virtual_printer_mode: str | None = None + dark_style: str | None = None + dark_background: str | None = None + dark_accent: str | None = None + light_style: str | None = None + light_background: str | None = None + light_accent: str | None = None diff --git a/backend/app/services/bambu_mqtt.py b/backend/app/services/bambu_mqtt.py index 777d05407..358698138 100644 --- a/backend/app/services/bambu_mqtt.py +++ b/backend/app/services/bambu_mqtt.py @@ -733,9 +733,9 @@ class BambuMQTTClient: parsed_tray_now = raw_tray_now if raw_tray_now is not None else 255 # H2D dual-nozzle printers report only slot number (0-3), not global tray ID - # Use pending_tray_target from our load command tracking for disambiguation + # Use active_extruder + ams_extruder_map to determine which AMS the slot belongs to if parsed_tray_now >= 0 and parsed_tray_now <= 3: - # Check if we have a pending target that matches this slot + # First, check if we have a pending target that matches this slot pending_target = self.state.pending_tray_target if pending_target is not None: pending_slot = pending_target % 4 @@ -758,28 +758,45 @@ class BambuMQTTClient: # Clear pending target since it's stale self.state.pending_tray_target = None else: - # No pending target - check if we already have a resolved global ID - # that matches this slot (from a previous successful disambiguation) - current_tray = self.state.tray_now - if current_tray > 3 and current_tray != 255 and (current_tray % 4) == parsed_tray_now: - # Current tray_now is already a valid global ID that matches this slot - # Keep it (don't overwrite with raw slot number) - logger.debug( - f"[{self.serial_number}] H2D tray_now: keeping existing global ID {current_tray} " - f"(matches incoming slot {parsed_tray_now})" + # No pending target - use active_extruder + ams_extruder_map to disambiguate + # Find which AMS is connected to the active extruder + active_ext = self.state.active_extruder # 0=right, 1=left + ams_map = self.state.ams_extruder_map # {ams_id: extruder_id} + + # Find the AMS connected to the active extruder + active_ams_id = None + for ams_id_str, ext_id in ams_map.items(): + if ext_id == active_ext: + try: + active_ams_id = int(ams_id_str) + except ValueError: + pass + break + + if active_ams_id is not None: + # Calculate global tray ID using the active AMS + global_tray_id = active_ams_id * 4 + parsed_tray_now + logger.info( + f"[{self.serial_number}] H2D tray_now disambiguation: " + f"slot {parsed_tray_now} + active_extruder {active_ext} -> AMS {active_ams_id} -> global ID {global_tray_id}" ) + self.state.tray_now = global_tray_id else: - # No pending target and no valid existing global ID - # For H2D with multiple AMS units, we can't reliably determine which AMS - # the slot belongs to without a pending_tray_target from our load command. - # Use slot number as-is - this may be incorrect for multi-AMS setups, - # but it's better than guessing wrong based on unreliable heuristics. - # The user can load filament via our API to get correct tracking. - logger.warning( - f"[{self.serial_number}] H2D tray_now: no pending target, " - f"using slot {parsed_tray_now} as global ID (may be incorrect for multi-AMS)" - ) - self.state.tray_now = parsed_tray_now + # No AMS found for active extruder - check if we already have a resolved global ID + current_tray = self.state.tray_now + if current_tray > 3 and current_tray != 255 and (current_tray % 4) == parsed_tray_now: + # Current tray_now is already a valid global ID that matches this slot + logger.debug( + f"[{self.serial_number}] H2D tray_now: keeping existing global ID {current_tray} " + f"(matches incoming slot {parsed_tray_now})" + ) + else: + # Fallback: use slot as-is + logger.warning( + f"[{self.serial_number}] H2D tray_now: no ams_extruder_map for active_extruder {active_ext}, " + f"using slot {parsed_tray_now} as global ID (may be incorrect for multi-AMS)" + ) + self.state.tray_now = parsed_tray_now else: # tray_now > 3 means it's already a global ID, or 255 means unloaded # Note: Do NOT clear pending_tray_target on tray_now=255 here. @@ -806,15 +823,6 @@ class BambuMQTTClient: # Extract ams_extruder_map from each AMS unit's info field # According to OpenBambuAPI: info field bit 8 indicates which extruder (0=right, 1=left) - # Log AMS unit fields once to discover available fields - if not hasattr(self, "_ams_fields_logged") and ams_list: - first_unit = ams_list[0] - logger.info(f"[{self.serial_number}] AMS unit fields: {sorted(first_unit.keys())}") - for ams_unit in ams_list: - ams_id = ams_unit.get("id") - unit_info = {k: v for k, v in ams_unit.items() if k != "tray"} - logger.info(f"[{self.serial_number}] AMS {ams_id} data: {unit_info}") - self._ams_fields_logged = True ams_extruder_map = {} for ams_unit in ams_list: diff --git a/backend/app/services/printer_manager.py b/backend/app/services/printer_manager.py index 27d11eb4b..f4e115c87 100644 --- a/backend/app/services/printer_manager.py +++ b/backend/app/services/printer_manager.py @@ -338,6 +338,15 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None) -> vt_tray = None raw_data = state.raw_data or {} + # Build K-profile lookup map: cali_idx -> k_value + kprofile_map: dict[int, float] = {} + for kp in state.kprofiles or []: + if kp.slot_id is not None and kp.k_value: + try: + kprofile_map[kp.slot_id] = float(kp.k_value) + except (ValueError, TypeError): + pass + if "ams" in raw_data and isinstance(raw_data["ams"], list): for ams_data in raw_data["ams"]: trays = [] @@ -348,16 +357,28 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None) -> tray_uuid = tray.get("tray_uuid") if tray_uuid in ("", "00000000000000000000000000000000"): tray_uuid = None + + # Get K value: first try tray's k field, then lookup from K-profiles + k_value = tray.get("k") + cali_idx = tray.get("cali_idx") + if k_value is None and cali_idx is not None and cali_idx in kprofile_map: + k_value = kprofile_map[cali_idx] + trays.append( { "id": tray.get("id", 0), "tray_color": tray.get("tray_color"), "tray_type": tray.get("tray_type"), "tray_sub_brands": tray.get("tray_sub_brands"), + "tray_id_name": tray.get("tray_id_name"), + "tray_info_idx": tray.get("tray_info_idx"), "remain": tray.get("remain", 0), - "k": tray.get("k"), + "k": k_value, + "cali_idx": cali_idx, "tag_uid": tag_uid, "tray_uuid": tray_uuid, + "nozzle_temp_min": tray.get("nozzle_temp_min"), + "nozzle_temp_max": tray.get("nozzle_temp_max"), } ) # Prefer humidity_raw (actual percentage) over humidity (index 1-5) @@ -396,13 +417,30 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None) -> vt_tag_uid = vt_data.get("tag_uid") if vt_tag_uid in ("", "0000000000000000"): vt_tag_uid = None + vt_tray_uuid = vt_data.get("tray_uuid") + if vt_tray_uuid in ("", "00000000000000000000000000000000"): + vt_tray_uuid = None + + # Get K value for vt_tray + vt_k_value = vt_data.get("k") + vt_cali_idx = vt_data.get("cali_idx") + if vt_k_value is None and vt_cali_idx is not None and vt_cali_idx in kprofile_map: + vt_k_value = kprofile_map[vt_cali_idx] + vt_tray = { "id": 254, "tray_color": vt_data.get("tray_color"), "tray_type": vt_data.get("tray_type"), "tray_sub_brands": vt_data.get("tray_sub_brands"), + "tray_id_name": vt_data.get("tray_id_name"), + "tray_info_idx": vt_data.get("tray_info_idx"), "remain": vt_data.get("remain", 0), + "k": vt_k_value, + "cali_idx": vt_cali_idx, "tag_uid": vt_tag_uid, + "tray_uuid": vt_tray_uuid, + "nozzle_temp_min": vt_data.get("nozzle_temp_min"), + "nozzle_temp_max": vt_data.get("nozzle_temp_max"), } # Get ams_extruder_map from raw_data (populated by MQTT handler from AMS info field) diff --git a/backend/tests/integration/test_settings_api.py b/backend/tests/integration/test_settings_api.py index 51a692dd7..5e308ebff 100644 --- a/backend/tests/integration/test_settings_api.py +++ b/backend/tests/integration/test_settings_api.py @@ -53,10 +53,7 @@ class TestSettingsAPI: # Update to opposite value new_value = not original - response = await async_client.put( - "/api/v1/settings/", - json={"auto_archive": new_value} - ) + response = await async_client.put("/api/v1/settings/", json={"auto_archive": new_value}) assert response.status_code == 200 assert response.json()["auto_archive"] == new_value @@ -65,10 +62,7 @@ class TestSettingsAPI: @pytest.mark.integration async def test_update_currency(self, async_client: AsyncClient): """Verify currency can be updated.""" - response = await async_client.put( - "/api/v1/settings/", - json={"currency": "EUR"} - ) + response = await async_client.put("/api/v1/settings/", json={"currency": "EUR"}) assert response.status_code == 200 assert response.json()["currency"] == "EUR" @@ -77,10 +71,7 @@ class TestSettingsAPI: @pytest.mark.integration async def test_update_date_format(self, async_client: AsyncClient): """Verify date format can be updated.""" - response = await async_client.put( - "/api/v1/settings/", - json={"date_format": "eu"} - ) + response = await async_client.put("/api/v1/settings/", json={"date_format": "eu"}) assert response.status_code == 200 assert response.json()["date_format"] == "eu" @@ -89,10 +80,7 @@ class TestSettingsAPI: @pytest.mark.integration async def test_update_time_format(self, async_client: AsyncClient): """Verify time format can be updated.""" - response = await async_client.put( - "/api/v1/settings/", - json={"time_format": "24h"} - ) + response = await async_client.put("/api/v1/settings/", json={"time_format": "24h"}) assert response.status_code == 200 assert response.json()["time_format"] == "24h" @@ -101,10 +89,7 @@ class TestSettingsAPI: @pytest.mark.integration async def test_update_filament_cost(self, async_client: AsyncClient): """Verify default filament cost can be updated.""" - response = await async_client.put( - "/api/v1/settings/", - json={"default_filament_cost": 30.0} - ) + response = await async_client.put("/api/v1/settings/", json={"default_filament_cost": 30.0}) assert response.status_code == 200 assert response.json()["default_filament_cost"] == 30.0 @@ -113,10 +98,7 @@ class TestSettingsAPI: @pytest.mark.integration async def test_update_energy_cost(self, async_client: AsyncClient): """Verify energy cost can be updated.""" - response = await async_client.put( - "/api/v1/settings/", - json={"energy_cost_per_kwh": 0.20} - ) + response = await async_client.put("/api/v1/settings/", json={"energy_cost_per_kwh": 0.20}) assert response.status_code == 200 assert response.json()["energy_cost_per_kwh"] == 0.20 @@ -132,7 +114,7 @@ class TestSettingsAPI: "date_format": "iso", "time_format": "12h", "save_thumbnails": False, - } + }, ) assert response.status_code == 200 @@ -152,7 +134,7 @@ class TestSettingsAPI: "spoolman_enabled": True, "spoolman_url": "http://localhost:7912", "spoolman_sync_mode": "manual", - } + }, ) assert response.status_code == 200 @@ -172,7 +154,7 @@ class TestSettingsAPI: "ams_humidity_fair": 55, "ams_temp_good": 25.0, "ams_temp_fair": 32.0, - } + }, ) assert response.status_code == 200 @@ -186,10 +168,7 @@ class TestSettingsAPI: @pytest.mark.integration async def test_update_notification_language(self, async_client: AsyncClient): """Verify notification language can be updated.""" - response = await async_client.put( - "/api/v1/settings/", - json={"notification_language": "de"} - ) + response = await async_client.put("/api/v1/settings/", json={"notification_language": "de"}) assert response.status_code == 200 assert response.json()["notification_language"] == "de" @@ -198,15 +177,37 @@ class TestSettingsAPI: # Settings persistence tests # ======================================================================== + @pytest.mark.asyncio + @pytest.mark.integration + async def test_update_theme_settings(self, async_client: AsyncClient): + """Verify theme settings can be updated.""" + response = await async_client.put( + "/api/v1/settings/", + json={ + "dark_style": "glow", + "dark_background": "forest", + "dark_accent": "teal", + "light_style": "vibrant", + "light_background": "warm", + "light_accent": "blue", + }, + ) + + assert response.status_code == 200 + result = response.json() + assert result["dark_style"] == "glow" + assert result["dark_background"] == "forest" + assert result["dark_accent"] == "teal" + assert result["light_style"] == "vibrant" + assert result["light_background"] == "warm" + assert result["light_accent"] == "blue" + @pytest.mark.asyncio @pytest.mark.integration async def test_settings_persist_after_update(self, async_client: AsyncClient): """CRITICAL: Verify settings changes persist across requests.""" # Update settings - await async_client.put( - "/api/v1/settings/", - json={"currency": "JPY", "check_updates": False} - ) + await async_client.put("/api/v1/settings/", json={"currency": "JPY", "check_updates": False}) # Verify persistence in new request response = await async_client.get("/api/v1/settings/") diff --git a/frontend/mockups/ams-redesign.html b/frontend/mockups/ams-redesign.html new file mode 100644 index 000000000..6df5e2405 --- /dev/null +++ b/frontend/mockups/ams-redesign.html @@ -0,0 +1,1299 @@ + + +
+ + +Current stacked layout vs. new 2-column grid
+- {status.vt_tray.tray_sub_brands || status.vt_tray.tray_type || 'Spool'} -
-+ Toggle between dark and light mode using the sun/moon icon in the sidebar. +
+i(he,Z)?(K[ee]=he,K[te]=J,ee=te):(K[ee]=Z,K[$]=J,ee=$);else if(tei(he,J))K[ee]=he,K[te]=J,ee=te;else break e}}return G}function i(K,G){var J=K.sortIndex-G.sortIndex;return J!==0?J:K.id-G.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,o=s.now();t.unstable_now=function(){return s.now()-o}}var c=[],u=[],d=1,f=null,g=3,p=!1,x=!1,v=!1,y=!1,b=typeof setTimeout=="function"?setTimeout:null,E=typeof clearTimeout=="function"?clearTimeout:null,w=typeof setImmediate<"u"?setImmediate:null;function T(K){for(var G=n(u);G!==null;){if(G.callback===null)r(u);else if(G.startTime<=K)r(u),G.sortIndex=G.expirationTime,e(c,G);else break;G=n(u)}}function A(K){if(v=!1,T(K),!x)if(n(c)!==null)x=!0,C||(C=!0,L());else{var G=n(u);G!==null&&V(A,G.startTime-K)}}var C=!1,N=-1,F=5,k=-1;function R(){return y?!0:!(t.unstable_now()-kK&&R());){var ee=f.callback;if(typeof ee=="function"){f.callback=null,g=f.priorityLevel;var Q=ee(f.expirationTime<=K);if(K=t.unstable_now(),typeof Q=="function"){f.callback=Q,T(K),G=!0;break t}f===n(c)&&r(c),T(K)}else r(c);f=n(c)}if(f!==null)G=!0;else{var B=n(u);B!==null&&V(A,B.startTime-K),G=!1}}break e}finally{f=null,g=J,p=!1}G=void 0}}finally{G?L():C=!1}}}var L;if(typeof w=="function")L=function(){w(I)};else if(typeof MessageChannel<"u"){var q=new MessageChannel,j=q.port2;q.port1.onmessage=I,L=function(){j.postMessage(null)}}else L=function(){b(I,0)};function V(K,G){N=b(function(){K(t.unstable_now())},G)}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(K){K.callback=null},t.unstable_forceFrameRate=function(K){0>K||125 ee?(K.sortIndex=J,e(u,K),n(c)===null&&K===n(u)&&(v?(E(N),N=-1):v=!0,V(A,J-ee))):(K.sortIndex=Q,e(c,K),x||p||(x=!0,C||(C=!0,L()))),K},t.unstable_shouldYield=R,t.unstable_wrapCallback=function(K){var G=g;return function(){var J=g;g=G;try{return K.apply(this,arguments)}finally{g=J}}}})(DT)),DT}var _F;function qre(){return _F||(_F=1,RT.exports=Wre()),RT.exports}var OT={exports:{}},oa={};var wF;function Kre(){if(wF)return oa;wF=1;var t=Zp();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(),OT.exports=Kre(),OT.exports}var MF;function Xre(){if(MF)return dx;MF=1;var t=qre(),e=Zp(),n=I9();function r(h){var m="https://react.dev/errors/"+h;if(1 Q||(h.current=ee[Q],ee[Q]=null,Q--)}function Z(h,m){Q++,ee[Q]=h.current,h.current=m}var te=B(null),he=B(null),de=B(null),ge=B(null);function Ae(h,m){switch(Z(de,m),Z(he,h),Z(te,null),m.nodeType){case 9:case 11:h=(h=m.documentElement)&&(h=h.namespaceURI)?U5(h):0;break;default:if(h=m.tagName,m=m.namespaceURI)m=U5(m),h=z5(m,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}$(te),Z(te,h)}function me(){$(te),$(he),$(de)}function ce(h){h.memoizedState!==null&&Z(ge,h);var m=te.current,_=z5(m,h.type);m!==_&&(Z(he,h),Z(te,_))}function Pe(h){he.current===h&&($(te),$(he)),ge.current===h&&($(ge),sx._currentValue=J)}var oe,Le;function z(h){if(oe===void 0)try{throw Error()}catch(_){var m=_.stack.trim().match(/\n( *(at )?)/);oe=m&&m[1]||"",Le=-1<_.stack.indexOf(` +function Zre(t,e){for(var n=0;n r[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 wv=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function bo(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var jT={exports:{}},fx={};var SF;function Qre(){if(SF)return fx;SF=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 fx.Fragment=e,fx.jsx=n,fx.jsxs=n,fx}var MF;function Jre(){return MF||(MF=1,jT.exports=Qre()),jT.exports}var o=Jre(),RT={exports:{}},tn={};var EF;function eie(){if(EF)return tn;EF=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(B){return B===null||typeof B!="object"?null:(B=g&&B[g]||B["@@iterator"],typeof B=="function"?B:null)}var x={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},v=Object.assign,y={};function b(B,G,Q){this.props=B,this.context=G,this.refs=y,this.updater=Q||x}b.prototype.isReactComponent={},b.prototype.setState=function(B,G){if(typeof B!="object"&&typeof B!="function"&&B!=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,B,G,"setState")},b.prototype.forceUpdate=function(B){this.updater.enqueueForceUpdate(this,B,"forceUpdate")};function E(){}E.prototype=b.prototype;function w(B,G,Q){this.props=B,this.context=G,this.refs=y,this.updater=Q||x}var T=w.prototype=new E;T.constructor=w,v(T,b.prototype),T.isPureReactComponent=!0;var A=Array.isArray;function C(){}var N={H:null,A:null,T:null,S:null},F=Object.prototype.hasOwnProperty;function k(B,G,Q){var te=Q.ref;return{$$typeof:t,type:B,key:G,ref:te!==void 0?te:null,props:Q}}function R(B,G){return k(B.type,G,B.props)}function I(B){return typeof B=="object"&&B!==null&&B.$$typeof===t}function L(B){var G={"=":"=0",":":"=2"};return"$"+B.replace(/[=:]/g,function(Q){return G[Q]})}var q=/\/+/g;function j(B,G){return typeof B=="object"&&B!==null&&B.key!=null?L(""+B.key):G.toString(36)}function V(B){switch(B.status){case"fulfilled":return B.value;case"rejected":throw B.reason;default:switch(typeof B.status=="string"?B.then(C,C):(B.status="pending",B.then(function(G){B.status==="pending"&&(B.status="fulfilled",B.value=G)},function(G){B.status==="pending"&&(B.status="rejected",B.reason=G)})),B.status){case"fulfilled":return B.value;case"rejected":throw B.reason}}throw B}function K(B,G,Q,te,he){var oe=typeof B;(oe==="undefined"||oe==="boolean")&&(B=null);var xe=!1;if(B===null)xe=!0;else switch(oe){case"bigint":case"string":case"number":xe=!0;break;case"object":switch(B.$$typeof){case t:case e:xe=!0;break;case d:return xe=B._init,K(xe(B._payload),G,Q,te,he)}}if(xe)return he=he(B),xe=te===""?"."+j(B,0):te,A(he)?(Q="",xe!=null&&(Q=xe.replace(q,"$&/")+"/"),K(he,G,Q,"",function(ce){return ce})):he!=null&&(I(he)&&(he=R(he,Q+(he.key==null||B&&B.key===he.key?"":(""+he.key).replace(q,"$&/")+"/")+xe)),G.push(he)),1;xe=0;var Te=te===""?".":te+":";if(A(B))for(var me=0;me >>1,Z=K[ee];if(0>>1;eei(Q,J))te i(he,Q)?(K[ee]=he,K[te]=J,ee=te):(K[ee]=Q,K[G]=J,ee=G);else if(te i(he,J))K[ee]=he,K[te]=J,ee=te;else break e}}return W}function i(K,W){var J=K.sortIndex-W.sortIndex;return J!==0?J:K.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,y=!1,b=typeof setTimeout=="function"?setTimeout:null,E=typeof clearTimeout=="function"?clearTimeout:null,w=typeof setImmediate<"u"?setImmediate:null;function T(K){for(var W=n(u);W!==null;){if(W.callback===null)r(u);else if(W.startTime<=K)r(u),W.sortIndex=W.expirationTime,e(c,W);else break;W=n(u)}}function A(K){if(v=!1,T(K),!x)if(n(c)!==null)x=!0,C||(C=!0,L());else{var W=n(u);W!==null&&V(A,W.startTime-K)}}var C=!1,N=-1,F=5,k=-1;function R(){return y?!0:!(t.unstable_now()-k K&&R());){var ee=f.callback;if(typeof ee=="function"){f.callback=null,g=f.priorityLevel;var Z=ee(f.expirationTime<=K);if(K=t.unstable_now(),typeof Z=="function"){f.callback=Z,T(K),W=!0;break t}f===n(c)&&r(c),T(K)}else r(c);f=n(c)}if(f!==null)W=!0;else{var B=n(u);B!==null&&V(A,B.startTime-K),W=!1}}break e}finally{f=null,g=J,p=!1}W=void 0}}finally{W?L():C=!1}}}var L;if(typeof w=="function")L=function(){w(I)};else if(typeof MessageChannel<"u"){var q=new MessageChannel,j=q.port2;q.port1.onmessage=I,L=function(){j.postMessage(null)}}else L=function(){b(I,0)};function V(K,W){N=b(function(){K(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(K){K.callback=null},t.unstable_forceFrameRate=function(K){0>K||125 ee?(K.sortIndex=J,e(u,K),n(c)===null&&K===n(u)&&(v?(E(N),N=-1):v=!0,V(A,J-ee))):(K.sortIndex=Z,e(c,K),x||p||(x=!0,C||(C=!0,L()))),K},t.unstable_shouldYield=R,t.unstable_wrapCallback=function(K){var W=g;return function(){var J=g;g=W;try{return K.apply(this,arguments)}finally{g=J}}}})(LT)),LT}var NF;function rie(){return NF||(NF=1,OT.exports=nie()),OT.exports}var IT={exports:{}},ca={};var CF;function iie(){if(CF)return ca;CF=1;var t=Jp();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(),IT.exports=iie(),IT.exports}var PF;function aie(){if(PF)return mx;PF=1;var t=rie(),e=Jp(),n=X9();function r(h){var m="https://react.dev/errors/"+h;if(1 Z||(h.current=ee[Z],ee[Z]=null,Z--)}function Q(h,m){Z++,ee[Z]=h.current,h.current=m}var te=B(null),he=B(null),oe=B(null),xe=B(null);function Te(h,m){switch(Q(oe,m),Q(he,h),Q(te,null),m.nodeType){case 9:case 11:h=(h=m.documentElement)&&(h=h.namespaceURI)?W5(h):0;break;default:if(h=m.tagName,m=m.namespaceURI)m=W5(m),h=q5(m,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}G(te),Q(te,h)}function me(){G(te),G(he),G(oe)}function ce(h){h.memoizedState!==null&&Q(xe,h);var m=te.current,_=q5(m,h.type);m!==_&&(Q(he,h),Q(te,_))}function Ce(h){he.current===h&&(G(te),G(he)),xe.current===h&&(G(xe),cx._currentValue=J)}var fe,Ie;function z(h){if(fe===void 0)try{throw Error()}catch(_){var m=_.stack.trim().match(/\n( *(at )?)/);fe=m&&m[1]||"",Ie=-1<_.stack.indexOf(` at`)?" ( )":-1<_.stack.indexOf("@")?"@unknown:0:0":""}return` -`+oe+h+Le}var H=!1;function ne(h,m){if(!h||H)return"";H=!0;var _=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var S={DetermineComponentFrameRoot:function(){try{if(m){var ft=function(){throw Error()};if(Object.defineProperty(ft.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(ft,[])}catch(st){var Qe=st}Reflect.construct(h,[],ft)}else{try{ft.call()}catch(st){Qe=st}h.call(ft.prototype)}}else{try{throw Error()}catch(st){Qe=st}(ft=h())&&typeof ft.catch=="function"&&ft.catch(function(){})}}catch(st){if(st&&Qe&&typeof st.stack=="string")return[st.stack,Qe.stack]}return[null,null]}};S.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var P=Object.getOwnPropertyDescriptor(S.DetermineComponentFrameRoot,"name");P&&P.configurable&&Object.defineProperty(S.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var D=S.DetermineComponentFrameRoot(),X=D[0],le=D[1];if(X&&le){var Ce=X.split(` -`),Ye=le.split(` -`);for(P=S=0;S P||Ce[S]!==Ye[P]){var ct=` -`+Ce[S].replace(" at new "," at ");return h.displayName&&ct.includes(" ")&&(ct=ct.replace(" ",h.displayName)),ct}while(1<=S&&0<=P);break}}}finally{H=!1,Error.prepareStackTrace=_}return(_=h?h.displayName||h.name:"")?z(_):""}function W(h,m){switch(h.tag){case 26:case 27:case 5:return z(h.type);case 16:return z("Lazy");case 13:return h.child!==m&&m!==null?z("Suspense Fallback"):z("Suspense");case 19:return z("SuspenseList");case 0:case 15:return ne(h.type,!1);case 11:return ne(h.type.render,!1);case 1:return ne(h.type,!0);case 31:return z("Activity");default:return""}}function Y(h){try{var m="",_=null;do m+=W(h,_),_=h,h=h.return;while(h);return m}catch(S){return` +`+fe+h+Ie}var H=!1;function re(h,m){if(!h||H)return"";H=!0;var _=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var S={DetermineComponentFrameRoot:function(){try{if(m){var ft=function(){throw Error()};if(Object.defineProperty(ft.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(ft,[])}catch(st){var tt=st}Reflect.construct(h,[],ft)}else{try{ft.call()}catch(st){tt=st}h.call(ft.prototype)}}else{try{throw Error()}catch(st){tt=st}(ft=h())&&typeof ft.catch=="function"&&ft.catch(function(){})}}catch(st){if(st&&tt&&typeof st.stack=="string")return[st.stack,tt.stack]}return[null,null]}};S.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var P=Object.getOwnPropertyDescriptor(S.DetermineComponentFrameRoot,"name");P&&P.configurable&&Object.defineProperty(S.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var D=S.DetermineComponentFrameRoot(),X=D[0],le=D[1];if(X&&le){var Ne=X.split(` +`),Je=le.split(` +`);for(P=S=0;S P||Ne[S]!==Je[P]){var ut=` +`+Ne[S].replace(" at new "," at ");return h.displayName&&ut.includes(" ")&&(ut=ut.replace(" ",h.displayName)),ut}while(1<=S&&0<=P);break}}}finally{H=!1,Error.prepareStackTrace=_}return(_=h?h.displayName||h.name:"")?z(_):""}function $(h,m){switch(h.tag){case 26:case 27:case 5:return z(h.type);case 16:return z("Lazy");case 13:return h.child!==m&&m!==null?z("Suspense Fallback"):z("Suspense");case 19:return z("SuspenseList");case 0:case 15:return re(h.type,!1);case 11:return re(h.type.render,!1);case 1:return re(h.type,!0);case 31:return z("Activity");default:return""}}function Y(h){try{var m="",_=null;do m+=$(h,_),_=h,h=h.return;while(h);return m}catch(S){return` Error generating stack: `+S.message+` -`+S.stack}}var se=Object.prototype.hasOwnProperty,be=t.unstable_scheduleCallback,Te=t.unstable_cancelCallback,Me=t.unstable_shouldYield,U=t.unstable_requestPaint,O=t.unstable_now,re=t.unstable_getCurrentPriorityLevel,ve=t.unstable_ImmediatePriority,we=t.unstable_UserBlockingPriority,ye=t.unstable_NormalPriority,rt=t.unstable_LowPriority,Ve=t.unstable_IdlePriority,tt=t.log,ut=t.unstable_setDisableYieldValue,ue=null,ae=null;function $e(h){if(typeof tt=="function"&&ut(h),ae&&typeof ae.setStrictMode=="function")try{ae.setStrictMode(ue,h)}catch{}}var Ue=Math.clz32?Math.clz32:fe,Ze=Math.log,dt=Math.LN2;function fe(h){return h>>>=0,h===0?32:31-(Ze(h)/dt|0)|0}var Je=256,xe=262144,ze=4194304;function Re(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 _e(h,m,_){var S=h.pendingLanes;if(S===0)return 0;var P=0,D=h.suspendedLanes,X=h.pingedLanes;h=h.warmLanes;var le=S&134217727;return le!==0?(S=le&~D,S!==0?P=Re(S):(X&=le,X!==0?P=Re(X):_||(_=le&~h,_!==0&&(P=Re(_))))):(le=S&~D,le!==0?P=Re(le):X!==0?P=Re(X):_||(_=S&~h,_!==0&&(P=Re(_)))),P===0?0:m!==0&&m!==P&&(m&D)===0&&(D=P&-P,_=m&-m,D>=_||D===32&&(_&4194048)!==0)?m:P}function qe(h,m){return(h.pendingLanes&~(h.suspendedLanes&~h.pingedLanes)&m)===0}function lt(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 Se(){var h=ze;return ze<<=1,(ze&62914560)===0&&(ze=4194304),h}function bt(h){for(var m=[],_=0;31>_;_++)m.push(h);return m}function Gt(h,m){h.pendingLanes|=m,m!==268435456&&(h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0)}function kt(h,m,_,S,P,D){var X=h.pendingLanes;h.pendingLanes=_,h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0,h.expiredLanes&=_,h.entangledLanes&=_,h.errorRecoveryDisabledLanes&=_,h.shellSuspendCounter=0;var le=h.entanglements,Ce=h.expirationTimes,Ye=h.hiddenUpdates;for(_=X&~_;0<_;){var ct=31-Ue(_),ft=1< "u")return null;try{return h.activeElement||h.body}catch{return h.body}}var Br=/[\n"\\]/g;function Ln(h){return h.replace(Br,function(m){return"\\"+m.charCodeAt(0).toString(16)+" "})}function yo(h,m,_,S,P,D,X,le){h.name="",X!=null&&typeof X!="function"&&typeof X!="symbol"&&typeof X!="boolean"?h.type=X:h.removeAttribute("type"),m!=null?X==="number"?(m===0&&h.value===""||h.value!=m)&&(h.value=""+fn(m)):h.value!==""+fn(m)&&(h.value=""+fn(m)):X!=="submit"&&X!=="reset"||h.removeAttribute("value"),m!=null?Ti(h,X,fn(m)):_!=null?Ti(h,X,fn(_)):S!=null&&h.removeAttribute("value"),P==null&&D!=null&&(h.defaultChecked=!!D),P!=null&&(h.checked=P&&typeof P!="function"&&typeof P!="symbol"),le!=null&&typeof le!="function"&&typeof le!="symbol"&&typeof le!="boolean"?h.name=""+fn(le):h.removeAttribute("name")}function Fn(h,m,_,S,P,D,X,le){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)){Qt(h);return}_=_!=null?""+fn(_):"",m=m!=null?""+fn(m):_,le||m===h.value||(h.value=m),h.defaultValue=m}S=S??P,S=typeof S!="function"&&typeof S!="symbol"&&!!S,h.checked=le?h.checked:!!S,h.defaultChecked=!!S,X!=null&&typeof X!="function"&&typeof X!="symbol"&&typeof X!="boolean"&&(h.name=X),Qt(h)}function Ti(h,m,_){m==="number"&&Pr(h.ownerDocument)===h||h.defaultValue===""+_||(h.defaultValue=""+_)}function ci(h,m,_,S){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"),kE=!1;if(Ll)try{var Mg={};Object.defineProperty(Mg,"passive",{get:function(){kE=!0}}),window.addEventListener("test",Mg,Mg),window.removeEventListener("test",Mg,Mg)}catch{kE=!1}var tu=null,PE=null,hy=null;function zL(){if(hy)return hy;var h,m=PE,_=m.length,S,P="value"in tu?tu.value:tu.textContent,D=P.length;for(h=0;h<_&&m[h]===P[h];h++);var X=_-h;for(S=1;S<=X&&m[_-S]===P[D-S];S++);return hy=P.slice(h,1 =Ag),WL=" ",qL=!1;function KL(h,m){switch(h){case"keyup":return cne.indexOf(m.keyCode)!==-1;case"keydown":return m.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function XL(h){return h=h.detail,typeof h=="object"&&"data"in h?h.data:null}var Af=!1;function dne(h,m){switch(h){case"compositionend":return XL(m);case"keypress":return m.which!==32?null:(qL=!0,WL);case"textInput":return h=m.data,h===WL&&qL?null:h;default:return null}}function hne(h,m){if(Af)return h==="compositionend"||!LE&&KL(h,m)?(h=zL(),hy=PE=tu=null,Af=!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=S}e:{for(;_;){if(_.nextSibling){_=_.nextSibling;break e}_=_.parentNode}_=void 0}_=r4(_)}}function a4(h,m){return h&&m?h===m?!0:h&&h.nodeType===3?!1:m&&m.nodeType===3?a4(h,m.parentNode):"contains"in h?h.contains(m):h.compareDocumentPosition?!!(h.compareDocumentPosition(m)&16):!1:!1}function s4(h){h=h!=null&&h.ownerDocument!=null&&h.ownerDocument.defaultView!=null?h.ownerDocument.defaultView:window;for(var m=Pr(h.document);m instanceof h.HTMLIFrameElement;){try{var _=typeof m.contentWindow.location.href=="string"}catch{_=!1}if(_)h=m.contentWindow;else break;m=Pr(h.document)}return m}function UE(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 vne=Ll&&"documentMode"in document&&11>=document.documentMode,Nf=null,zE=null,Pg=null,BE=!1;function o4(h,m,_){var S=_.window===_?_.document:_.nodeType===9?_:_.ownerDocument;BE||Nf==null||Nf!==Pr(S)||(S=Nf,"selectionStart"in S&&UE(S)?S={start:S.selectionStart,end:S.selectionEnd}:(S=(S.ownerDocument&&S.ownerDocument.defaultView||window).getSelection(),S={anchorNode:S.anchorNode,anchorOffset:S.anchorOffset,focusNode:S.focusNode,focusOffset:S.focusOffset}),Pg&&kg(Pg,S)||(Pg=S,S=av(zE,"onSelect"),0 >=X,P-=X,Ko=1<<32-Ue(m)+P|_< rn?(_n=It,It=null):_n=It.sibling;var kn=Qe(He,It,Xe[rn],ht);if(kn===null){It===null&&(It=_n);break}h&&It&&kn.alternate===null&&m(He,It),De=D(kn,De,rn),Cn===null?zt=kn:Cn.sibling=kn,Cn=kn,It=_n}if(rn===Xe.length)return _(He,It),Mn&&Fl(He,rn),zt;if(It===null){for(;rn
rn?(_n=It,It=null):_n=It.sibling;var Su=Qe(He,It,kn.value,ht);if(Su===null){It===null&&(It=_n);break}h&&It&&Su.alternate===null&&m(He,It),De=D(Su,De,rn),Cn===null?zt=Su:Cn.sibling=Su,Cn=Su,It=_n}if(kn.done)return _(He,It),Mn&&Fl(He,rn),zt;if(It===null){for(;!kn.done;rn++,kn=Xe.next())kn=ft(He,kn.value,ht),kn!==null&&(De=D(kn,De,rn),Cn===null?zt=kn:Cn.sibling=kn,Cn=kn);return Mn&&Fl(He,rn),zt}for(It=S(It);!kn.done;rn++,kn=Xe.next())kn=st(It,He,rn,kn.value,ht),kn!==null&&(h&&kn.alternate!==null&&It.delete(kn.key===null?rn:kn.key),De=D(kn,De,rn),Cn===null?zt=kn:Cn.sibling=kn,Cn=kn);return h&&It.forEach(function(zre){return m(He,zre)}),Mn&&Fl(He,rn),zt}function Kn(He,De,Xe,ht){if(typeof Xe=="object"&&Xe!==null&&Xe.type===v&&Xe.key===null&&(Xe=Xe.props.children),typeof Xe=="object"&&Xe!==null){switch(Xe.$$typeof){case p:e:{for(var zt=Xe.key;De!==null;){if(De.key===zt){if(zt=Xe.type,zt===v){if(De.tag===7){_(He,De.sibling),ht=P(De,Xe.props.children),ht.return=He,He=ht;break e}}else if(De.elementType===zt||typeof zt=="object"&&zt!==null&&zt.$$typeof===F&&Gd(zt)===De.type){_(He,De.sibling),ht=P(De,Xe.props),Ig(ht,Xe),ht.return=He,He=ht;break e}_(He,De);break}else m(He,De);De=De.sibling}Xe.type===v?(ht=zd(Xe.props.children,He.mode,ht,Xe.key),ht.return=He,He=ht):(ht=wy(Xe.type,Xe.key,Xe.props,null,He.mode,ht),Ig(ht,Xe),ht.return=He,He=ht)}return X(He);case x:e:{for(zt=Xe.key;De!==null;){if(De.key===zt)if(De.tag===4&&De.stateNode.containerInfo===Xe.containerInfo&&De.stateNode.implementation===Xe.implementation){_(He,De.sibling),ht=P(De,Xe.children||[]),ht.return=He,He=ht;break e}else{_(He,De);break}else m(He,De);De=De.sibling}ht=KE(Xe,He.mode,ht),ht.return=He,He=ht}return X(He);case F:return Xe=Gd(Xe),Kn(He,De,Xe,ht)}if(V(Xe))return Ot(He,De,Xe,ht);if(L(Xe)){if(zt=L(Xe),typeof zt!="function")throw Error(r(150));return Xe=zt.call(Xe),Wt(He,De,Xe,ht)}if(typeof Xe.then=="function")return Kn(He,De,Cy(Xe),ht);if(Xe.$$typeof===w)return Kn(He,De,Ey(He,Xe),ht);ky(He,Xe)}return typeof Xe=="string"&&Xe!==""||typeof Xe=="number"||typeof Xe=="bigint"?(Xe=""+Xe,De!==null&&De.tag===6?(_(He,De.sibling),ht=P(De,Xe),ht.return=He,He=ht):(_(He,De),ht=qE(Xe,He.mode,ht),ht.return=He,He=ht),X(He)):_(He,De)}return function(He,De,Xe,ht){try{Lg=0;var zt=Kn(He,De,Xe,ht);return Uf=null,zt}catch(It){if(It===Ff||It===Ay)throw It;var Cn=cs(29,It,null,He.mode);return Cn.lanes=ht,Cn.return=He,Cn}finally{}}}var qd=k4(!0),P4=k4(!1),su=!1;function s2(h){h.updateQueue={baseState:h.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function o2(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 ou(h){return{lane:h,tag:0,payload:null,callback:null,next:null}}function lu(h,m,_){var S=h.updateQueue;if(S===null)return null;if(S=S.shared,(Dn&2)!==0){var P=S.pending;return P===null?m.next=m:(m.next=P.next,P.next=m),S.pending=m,m=_y(h),m4(h,null,_),m}return vy(h,S,m,_),_y(h)}function Fg(h,m,_){if(m=m.updateQueue,m!==null&&(m=m.shared,(_&4194048)!==0)){var S=m.lanes;S&=h.pendingLanes,_|=S,m.lanes=_,Nn(h,_)}}function l2(h,m){var _=h.updateQueue,S=h.alternate;if(S!==null&&(S=S.updateQueue,_===S)){var P=null,D=null;if(_=_.firstBaseUpdate,_!==null){do{var X={lane:_.lane,tag:_.tag,payload:_.payload,callback:null,next:null};D===null?P=D=X:D=D.next=X,_=_.next}while(_!==null);D===null?P=D=m:D=D.next=m}else P=D=m;_={baseState:S.baseState,firstBaseUpdate:P,lastBaseUpdate:D,shared:S.shared,callbacks:S.callbacks},h.updateQueue=_;return}h=_.lastBaseUpdate,h===null?_.firstBaseUpdate=m:h.next=m,_.lastBaseUpdate=m}var c2=!1;function Ug(){if(c2){var h=If;if(h!==null)throw h}}function zg(h,m,_,S){c2=!1;var P=h.updateQueue;su=!1;var D=P.firstBaseUpdate,X=P.lastBaseUpdate,le=P.shared.pending;if(le!==null){P.shared.pending=null;var Ce=le,Ye=Ce.next;Ce.next=null,X===null?D=Ye:X.next=Ye,X=Ce;var ct=h.alternate;ct!==null&&(ct=ct.updateQueue,le=ct.lastBaseUpdate,le!==X&&(le===null?ct.firstBaseUpdate=Ye:le.next=Ye,ct.lastBaseUpdate=Ce))}if(D!==null){var ft=P.baseState;X=0,ct=Ye=Ce=null,le=D;do{var Qe=le.lane&-536870913,st=Qe!==le.lane;if(st?(vn&Qe)===Qe:(S&Qe)===Qe){Qe!==0&&Qe===Lf&&(c2=!0),ct!==null&&(ct=ct.next={lane:0,tag:le.tag,payload:le.payload,callback:null,next:null});e:{var Ot=h,Wt=le;Qe=m;var Kn=_;switch(Wt.tag){case 1:if(Ot=Wt.payload,typeof Ot=="function"){ft=Ot.call(Kn,ft,Qe);break e}ft=Ot;break e;case 3:Ot.flags=Ot.flags&-65537|128;case 0:if(Ot=Wt.payload,Qe=typeof Ot=="function"?Ot.call(Kn,ft,Qe):Ot,Qe==null)break e;ft=f({},ft,Qe);break e;case 2:su=!0}}Qe=le.callback,Qe!==null&&(h.flags|=64,st&&(h.flags|=8192),st=P.callbacks,st===null?P.callbacks=[Qe]:st.push(Qe))}else st={lane:Qe,tag:le.tag,payload:le.payload,callback:le.callback,next:null},ct===null?(Ye=ct=st,Ce=ft):ct=ct.next=st,X|=Qe;if(le=le.next,le===null){if(le=P.shared.pending,le===null)break;st=le,le=st.next,st.next=null,P.lastBaseUpdate=st,P.shared.pending=null}}while(!0);ct===null&&(Ce=ft),P.baseState=Ce,P.firstBaseUpdate=Ye,P.lastBaseUpdate=ct,D===null&&(P.shared.lanes=0),fu|=X,h.lanes=X,h.memoizedState=ft}}function j4(h,m){if(typeof h!="function")throw Error(r(191,h));h.call(m)}function R4(h,m){var _=h.callbacks;if(_!==null)for(h.callbacks=null,h=0;h<_.length;h++)j4(_[h],m)}var zf=B(null),Py=B(0);function D4(h,m){h=Xl,Z(Py,h),Z(zf,m),Xl=h|m.baseLanes}function u2(){Z(Py,Xl),Z(zf,zf.current)}function d2(){Xl=Py.current,$(zf),$(Py)}var us=B(null),Is=null;function cu(h){var m=h.alternate;Z(Hr,Hr.current&1),Z(us,h),Is===null&&(m===null||zf.current!==null||m.memoizedState!==null)&&(Is=h)}function h2(h){Z(Hr,Hr.current),Z(us,h),Is===null&&(Is=h)}function O4(h){h.tag===22?(Z(Hr,Hr.current),Z(us,h),Is===null&&(Is=h)):uu()}function uu(){Z(Hr,Hr.current),Z(us,us.current)}function ds(h){$(us),Is===h&&(Is=null),$(Hr)}var Hr=B(0);function jy(h){for(var m=h;m!==null;){if(m.tag===13){var _=m.memoizedState;if(_!==null&&(_=_.dehydrated,_===null||bT(_)||yT(_)))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 Bl=0,tn=null,Wn=null,Zr=null,Ry=!1,Bf=!1,Kd=!1,Dy=0,Bg=0,Hf=null,kne=0;function jr(){throw Error(r(321))}function f2(h,m){if(m===null)return!1;for(var _=0;_ D?D:8;var X=K.T,le={};K.T=le,N2(h,!1,m,_);try{var Ce=P(),Ye=K.S;if(Ye!==null&&Ye(le,Ce),Ce!==null&&typeof Ce=="object"&&typeof Ce.then=="function"){var ct=Cne(Ce,S);$g(h,m,ct,ms(h))}else $g(h,m,S,ms(h))}catch(ft){$g(h,m,{then:function(){},status:"rejected",reason:ft},ms())}finally{G.p=D,X!==null&&le.types!==null&&(X.types=le.types),K.T=X}}function One(){}function T2(h,m,_,S){if(h.tag!==5)throw Error(r(476));var P=dI(h).queue;uI(h,P,m,J,_===null?One:function(){return hI(h),_(S)})}function dI(h){var m=h.memoizedState;if(m!==null)return m;m={memoizedState:J,baseState:J,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Hl,lastRenderedState:J},next:null};var _={};return m.next={memoizedState:_,baseState:_,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Hl,lastRenderedState:_},next:null},h.memoizedState=m,h=h.alternate,h!==null&&(h.memoizedState=m),m}function hI(h){var m=dI(h);m.next===null&&(m=h.alternate.memoizedState),$g(h,m.next.queue,{},ms())}function A2(){return Vi(sx)}function fI(){return $r().memoizedState}function mI(){return $r().memoizedState}function Lne(h){for(var m=h.return;m!==null;){switch(m.tag){case 24:case 3:var _=ms();h=ou(_);var S=lu(m,h,_);S!==null&&(Va(S,m,_),Fg(S,m,_)),m={cache:n2()},h.payload=m;return}m=m.return}}function Ine(h,m,_){var S=ms();_={lane:S,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null},zy(h)?gI(m,_):(_=GE(h,m,_,S),_!==null&&(Va(_,h,S),xI(_,m,S)))}function pI(h,m,_){var S=ms();$g(h,m,_,S)}function $g(h,m,_,S){var P={lane:S,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null};if(zy(h))gI(m,P);else{var D=h.alternate;if(h.lanes===0&&(D===null||D.lanes===0)&&(D=m.lastRenderedReducer,D!==null))try{var X=m.lastRenderedState,le=D(X,_);if(P.hasEagerState=!0,P.eagerState=le,ls(le,X))return vy(h,m,P,0),rr===null&&yy(),!1}catch{}finally{}if(_=GE(h,m,P,S),_!==null)return Va(_,h,S),xI(_,m,S),!0}return!1}function N2(h,m,_,S){if(S={lane:2,revertLane:sT(),gesture:null,action:S,hasEagerState:!1,eagerState:null,next:null},zy(h)){if(m)throw Error(r(479))}else m=GE(h,_,S,2),m!==null&&Va(m,h,2)}function zy(h){var m=h.alternate;return h===tn||m!==null&&m===tn}function gI(h,m){Bf=Ry=!0;var _=h.pending;_===null?m.next=m:(m.next=_.next,_.next=m),h.pending=m}function xI(h,m,_){if((_&4194048)!==0){var S=m.lanes;S&=h.pendingLanes,_|=S,m.lanes=_,Nn(h,_)}}var Vg={readContext:Vi,use:Ly,useCallback:jr,useContext:jr,useEffect:jr,useImperativeHandle:jr,useLayoutEffect:jr,useInsertionEffect:jr,useMemo:jr,useReducer:jr,useRef:jr,useState:jr,useDebugValue:jr,useDeferredValue:jr,useTransition:jr,useSyncExternalStore:jr,useId:jr,useHostTransitionStatus:jr,useFormState:jr,useActionState:jr,useOptimistic:jr,useMemoCache:jr,useCacheRefresh:jr};Vg.useEffectEvent=jr;var bI={readContext:Vi,use:Ly,useCallback:function(h,m){return Sa().memoizedState=[h,m===void 0?null:m],h},useContext:Vi,useEffect:tI,useImperativeHandle:function(h,m,_){_=_!=null?_.concat([h]):null,Fy(4194308,4,aI.bind(null,m,h),_)},useLayoutEffect:function(h,m){return Fy(4194308,4,h,m)},useInsertionEffect:function(h,m){Fy(4,2,h,m)},useMemo:function(h,m){var _=Sa();m=m===void 0?null:m;var S=h();if(Kd){$e(!0);try{h()}finally{$e(!1)}}return _.memoizedState=[S,m],S},useReducer:function(h,m,_){var S=Sa();if(_!==void 0){var P=_(m);if(Kd){$e(!0);try{_(m)}finally{$e(!1)}}}else P=m;return S.memoizedState=S.baseState=P,h={pending:null,lanes:0,dispatch:null,lastRenderedReducer:h,lastRenderedState:P},S.queue=h,h=h.dispatch=Ine.bind(null,tn,h),[S.memoizedState,h]},useRef:function(h){var m=Sa();return h={current:h},m.memoizedState=h},useState:function(h){h=_2(h);var m=h.queue,_=pI.bind(null,tn,m);return m.dispatch=_,[h.memoizedState,_]},useDebugValue:M2,useDeferredValue:function(h,m){var _=Sa();return E2(_,h,m)},useTransition:function(){var h=_2(!1);return h=uI.bind(null,tn,h.queue,!0,!1),Sa().memoizedState=h,[!1,h]},useSyncExternalStore:function(h,m,_){var S=tn,P=Sa();if(Mn){if(_===void 0)throw Error(r(407));_=_()}else{if(_=m(),rr===null)throw Error(r(349));(vn&127)!==0||U4(S,m,_)}P.memoizedState=_;var D={value:_,getSnapshot:m};return P.queue=D,tI(B4.bind(null,S,D,h),[h]),S.flags|=2048,$f(9,{destroy:void 0},z4.bind(null,S,D,_,m),null),_},useId:function(){var h=Sa(),m=rr.identifierPrefix;if(Mn){var _=Xo,S=Ko;_=(S&~(1<<32-Ue(S)-1)).toString(32)+_,m="_"+m+"R_"+_,_=Dy++,0<_&&(m+="H"+_.toString(32)),m+="_"}else _=kne++,m="_"+m+"r_"+_.toString(32)+"_";return h.memoizedState=m},useHostTransitionStatus:A2,useFormState:Y4,useActionState:Y4,useOptimistic:function(h){var m=Sa();m.memoizedState=m.baseState=h;var _={pending:null,lanes:0,dispatch:null,lastRenderedReducer:null,lastRenderedState:null};return m.queue=_,m=N2.bind(null,tn,!0,_),_.dispatch=m,[h,m]},useMemoCache:b2,useCacheRefresh:function(){return Sa().memoizedState=Lne.bind(null,tn)},useEffectEvent:function(h){var m=Sa(),_={impl:h};return m.memoizedState=_,function(){if((Dn&2)!==0)throw Error(r(440));return _.impl.apply(void 0,arguments)}}},C2={readContext:Vi,use:Ly,useCallback:oI,useContext:Vi,useEffect:S2,useImperativeHandle:sI,useInsertionEffect:rI,useLayoutEffect:iI,useMemo:lI,useReducer:Iy,useRef:eI,useState:function(){return Iy(Hl)},useDebugValue:M2,useDeferredValue:function(h,m){var _=$r();return cI(_,Wn.memoizedState,h,m)},useTransition:function(){var h=Iy(Hl)[0],m=$r().memoizedState;return[typeof h=="boolean"?h:Hg(h),m]},useSyncExternalStore:F4,useId:fI,useHostTransitionStatus:A2,useFormState:Z4,useActionState:Z4,useOptimistic:function(h,m){var _=$r();return V4(_,Wn,h,m)},useMemoCache:b2,useCacheRefresh:mI};C2.useEffectEvent=nI;var yI={readContext:Vi,use:Ly,useCallback:oI,useContext:Vi,useEffect:S2,useImperativeHandle:sI,useInsertionEffect:rI,useLayoutEffect:iI,useMemo:lI,useReducer:v2,useRef:eI,useState:function(){return v2(Hl)},useDebugValue:M2,useDeferredValue:function(h,m){var _=$r();return Wn===null?E2(_,h,m):cI(_,Wn.memoizedState,h,m)},useTransition:function(){var h=v2(Hl)[0],m=$r().memoizedState;return[typeof h=="boolean"?h:Hg(h),m]},useSyncExternalStore:F4,useId:fI,useHostTransitionStatus:A2,useFormState:J4,useActionState:J4,useOptimistic:function(h,m){var _=$r();return Wn!==null?V4(_,Wn,h,m):(_.baseState=h,[h,_.queue.dispatch])},useMemoCache:b2,useCacheRefresh:mI};yI.useEffectEvent=nI;function k2(h,m,_,S){m=h.memoizedState,_=_(S,m),_=_==null?m:f({},m,_),h.memoizedState=_,h.lanes===0&&(h.updateQueue.baseState=_)}var P2={enqueueSetState:function(h,m,_){h=h._reactInternals;var S=ms(),P=ou(S);P.payload=m,_!=null&&(P.callback=_),m=lu(h,P,S),m!==null&&(Va(m,h,S),Fg(m,h,S))},enqueueReplaceState:function(h,m,_){h=h._reactInternals;var S=ms(),P=ou(S);P.tag=1,P.payload=m,_!=null&&(P.callback=_),m=lu(h,P,S),m!==null&&(Va(m,h,S),Fg(m,h,S))},enqueueForceUpdate:function(h,m){h=h._reactInternals;var _=ms(),S=ou(_);S.tag=2,m!=null&&(S.callback=m),m=lu(h,S,_),m!==null&&(Va(m,h,_),Fg(m,h,_))}};function vI(h,m,_,S,P,D,X){return h=h.stateNode,typeof h.shouldComponentUpdate=="function"?h.shouldComponentUpdate(S,D,X):m.prototype&&m.prototype.isPureReactComponent?!kg(_,S)||!kg(P,D):!0}function _I(h,m,_,S){h=m.state,typeof m.componentWillReceiveProps=="function"&&m.componentWillReceiveProps(_,S),typeof m.UNSAFE_componentWillReceiveProps=="function"&&m.UNSAFE_componentWillReceiveProps(_,S),m.state!==h&&P2.enqueueReplaceState(m,m.state,null)}function Xd(h,m){var _=m;if("ref"in m){_={};for(var S in m)S!=="ref"&&(_[S]=m[S])}if(h=h.defaultProps){_===m&&(_=f({},_));for(var P in h)_[P]===void 0&&(_[P]=h[P])}return _}function wI(h){by(h)}function SI(h){console.error(h)}function MI(h){by(h)}function By(h,m){try{var _=h.onUncaughtError;_(m.value,{componentStack:m.stack})}catch(S){setTimeout(function(){throw S})}}function EI(h,m,_){try{var S=h.onCaughtError;S(_.value,{componentStack:_.stack,errorBoundary:m.tag===1?m.stateNode:null})}catch(P){setTimeout(function(){throw P})}}function j2(h,m,_){return _=ou(_),_.tag=3,_.payload={element:null},_.callback=function(){By(h,m)},_}function TI(h){return h=ou(h),h.tag=3,h}function AI(h,m,_,S){var P=_.type.getDerivedStateFromError;if(typeof P=="function"){var D=S.value;h.payload=function(){return P(D)},h.callback=function(){EI(m,_,S)}}var X=_.stateNode;X!==null&&typeof X.componentDidCatch=="function"&&(h.callback=function(){EI(m,_,S),typeof P!="function"&&(mu===null?mu=new Set([this]):mu.add(this));var le=S.stack;this.componentDidCatch(S.value,{componentStack:le!==null?le:""})})}function Fne(h,m,_,S,P){if(_.flags|=32768,S!==null&&typeof S=="object"&&typeof S.then=="function"){if(m=_.alternate,m!==null&&Of(m,_,P,!0),_=us.current,_!==null){switch(_.tag){case 31:case 13:return Is===null?Jy():_.alternate===null&&Rr===0&&(Rr=3),_.flags&=-257,_.flags|=65536,_.lanes=P,S===Ny?_.flags|=16384:(m=_.updateQueue,m===null?_.updateQueue=new Set([S]):m.add(S),rT(h,S,P)),!1;case 22:return _.flags|=65536,S===Ny?_.flags|=16384:(m=_.updateQueue,m===null?(m={transitions:null,markerInstances:null,retryQueue:new Set([S])},_.updateQueue=m):(_=m.retryQueue,_===null?m.retryQueue=new Set([S]):_.add(S)),rT(h,S,P)),!1}throw Error(r(435,_.tag))}return rT(h,S,P),Jy(),!1}if(Mn)return m=us.current,m!==null?((m.flags&65536)===0&&(m.flags|=256),m.flags|=65536,m.lanes=P,S!==ZE&&(h=Error(r(422),{cause:S}),Rg(Rs(h,_)))):(S!==ZE&&(m=Error(r(423),{cause:S}),Rg(Rs(m,_))),h=h.current.alternate,h.flags|=65536,P&=-P,h.lanes|=P,S=Rs(S,_),P=j2(h.stateNode,S,P),l2(h,P),Rr!==4&&(Rr=2)),!1;var D=Error(r(520),{cause:S});if(D=Rs(D,_),Qg===null?Qg=[D]:Qg.push(D),Rr!==4&&(Rr=2),m===null)return!0;S=Rs(S,_),_=m;do{switch(_.tag){case 3:return _.flags|=65536,h=P&-P,_.lanes|=h,h=j2(_.stateNode,S,h),l2(_,h),!1;case 1:if(m=_.type,D=_.stateNode,(_.flags&128)===0&&(typeof m.getDerivedStateFromError=="function"||D!==null&&typeof D.componentDidCatch=="function"&&(mu===null||!mu.has(D))))return _.flags|=65536,P&=-P,_.lanes|=P,P=TI(P),AI(P,h,_,S),l2(_,P),!1}_=_.return}while(_!==null);return!1}var R2=Error(r(461)),Qr=!1;function Gi(h,m,_,S){m.child=h===null?P4(m,null,_,S):qd(m,h.child,_,S)}function NI(h,m,_,S,P){_=_.render;var D=m.ref;if("ref"in S){var X={};for(var le in S)le!=="ref"&&(X[le]=S[le])}else X=S;return $d(m),S=m2(h,m,_,X,D,P),le=p2(),h!==null&&!Qr?(g2(h,m,P),$l(h,m,P)):(Mn&&le&&XE(m),m.flags|=1,Gi(h,m,S,P),m.child)}function CI(h,m,_,S,P){if(h===null){var D=_.type;return typeof D=="function"&&!WE(D)&&D.defaultProps===void 0&&_.compare===null?(m.tag=15,m.type=D,kI(h,m,D,S,P)):(h=wy(_.type,null,S,m,m.mode,P),h.ref=m.ref,h.return=m,m.child=h)}if(D=h.child,!B2(h,P)){var X=D.memoizedProps;if(_=_.compare,_=_!==null?_:kg,_(X,S)&&h.ref===m.ref)return $l(h,m,P)}return m.flags|=1,h=Il(D,S),h.ref=m.ref,h.return=m,m.child=h}function kI(h,m,_,S,P){if(h!==null){var D=h.memoizedProps;if(kg(D,S)&&h.ref===m.ref)if(Qr=!1,m.pendingProps=S=D,B2(h,P))(h.flags&131072)!==0&&(Qr=!0);else return m.lanes=h.lanes,$l(h,m,P)}return D2(h,m,_,S,P)}function PI(h,m,_,S){var P=S.children,D=h!==null?h.memoizedState:null;if(h===null&&m.stateNode===null&&(m.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),S.mode==="hidden"){if((m.flags&128)!==0){if(D=D!==null?D.baseLanes|_:_,h!==null){for(S=m.child=h.child,P=0;S!==null;)P=P|S.lanes|S.childLanes,S=S.sibling;S=P&~D}else S=0,m.child=null;return jI(h,m,D,_,S)}if((_&536870912)!==0)m.memoizedState={baseLanes:0,cachePool:null},h!==null&&Ty(m,D!==null?D.cachePool:null),D!==null?D4(m,D):u2(),O4(m);else return S=m.lanes=536870912,jI(h,m,D!==null?D.baseLanes|_:_,_,S)}else D!==null?(Ty(m,D.cachePool),D4(m,D),uu(),m.memoizedState=null):(h!==null&&Ty(m,null),u2(),uu());return Gi(h,m,P,_),m.child}function Gg(h,m){return h!==null&&h.tag===22||m.stateNode!==null||(m.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),m.sibling}function jI(h,m,_,S,P){var D=i2();return D=D===null?null:{parent:Yr._currentValue,pool:D},m.memoizedState={baseLanes:_,cachePool:D},h!==null&&Ty(m,null),u2(),O4(m),h!==null&&Of(h,m,S,!0),m.childLanes=P,null}function Hy(h,m){return m=Vy({mode:m.mode,children:m.children},h.mode),m.ref=h.ref,h.child=m,m.return=h,m}function RI(h,m,_){return qd(m,h.child,null,_),h=Hy(m,m.pendingProps),h.flags|=2,ds(m),m.memoizedState=null,h}function Une(h,m,_){var S=m.pendingProps,P=(m.flags&128)!==0;if(m.flags&=-129,h===null){if(Mn){if(S.mode==="hidden")return h=Hy(m,S),m.lanes=536870912,Gg(null,h);if(h2(m),(h=ur)?(h=G5(h,Ls),h=h!==null&&h.data==="&"?h:null,h!==null&&(m.memoizedState={dehydrated:h,treeContext:nu!==null?{id:Ko,overflow:Xo}:null,retryLane:536870912,hydrationErrors:null},_=g4(h),_.return=m,m.child=_,$i=m,ur=null)):h=null,h===null)throw iu(m);return m.lanes=536870912,null}return Hy(m,S)}var D=h.memoizedState;if(D!==null){var X=D.dehydrated;if(h2(m),P)if(m.flags&256)m.flags&=-257,m=RI(h,m,_);else if(m.memoizedState!==null)m.child=h.child,m.flags|=128,m=null;else throw Error(r(558));else if(Qr||Of(h,m,_,!1),P=(_&h.childLanes)!==0,Qr||P){if(S=rr,S!==null&&(X=nn(S,_),X!==0&&X!==D.retryLane))throw D.retryLane=X,Ud(h,X),Va(S,h,X),R2;Jy(),m=RI(h,m,_)}else h=D.treeContext,ur=Fs(X.nextSibling),$i=m,Mn=!0,ru=null,Ls=!1,h!==null&&y4(m,h),m=Hy(m,S),m.flags|=4096;return m}return h=Il(h.child,{mode:S.mode,children:S.children}),h.ref=m.ref,m.child=h,h.return=m,h}function $y(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 D2(h,m,_,S,P){return $d(m),_=m2(h,m,_,S,void 0,P),S=p2(),h!==null&&!Qr?(g2(h,m,P),$l(h,m,P)):(Mn&&S&&XE(m),m.flags|=1,Gi(h,m,_,P),m.child)}function DI(h,m,_,S,P,D){return $d(m),m.updateQueue=null,_=I4(m,S,_,P),L4(h),S=p2(),h!==null&&!Qr?(g2(h,m,D),$l(h,m,D)):(Mn&&S&&XE(m),m.flags|=1,Gi(h,m,_,D),m.child)}function OI(h,m,_,S,P){if($d(m),m.stateNode===null){var D=Pf,X=_.contextType;typeof X=="object"&&X!==null&&(D=Vi(X)),D=new _(S,D),m.memoizedState=D.state!==null&&D.state!==void 0?D.state:null,D.updater=P2,m.stateNode=D,D._reactInternals=m,D=m.stateNode,D.props=S,D.state=m.memoizedState,D.refs={},s2(m),X=_.contextType,D.context=typeof X=="object"&&X!==null?Vi(X):Pf,D.state=m.memoizedState,X=_.getDerivedStateFromProps,typeof X=="function"&&(k2(m,_,X,S),D.state=m.memoizedState),typeof _.getDerivedStateFromProps=="function"||typeof D.getSnapshotBeforeUpdate=="function"||typeof D.UNSAFE_componentWillMount!="function"&&typeof D.componentWillMount!="function"||(X=D.state,typeof D.componentWillMount=="function"&&D.componentWillMount(),typeof D.UNSAFE_componentWillMount=="function"&&D.UNSAFE_componentWillMount(),X!==D.state&&P2.enqueueReplaceState(D,D.state,null),zg(m,S,D,P),Ug(),D.state=m.memoizedState),typeof D.componentDidMount=="function"&&(m.flags|=4194308),S=!0}else if(h===null){D=m.stateNode;var le=m.memoizedProps,Ce=Xd(_,le);D.props=Ce;var Ye=D.context,ct=_.contextType;X=Pf,typeof ct=="object"&&ct!==null&&(X=Vi(ct));var ft=_.getDerivedStateFromProps;ct=typeof ft=="function"||typeof D.getSnapshotBeforeUpdate=="function",le=m.pendingProps!==le,ct||typeof D.UNSAFE_componentWillReceiveProps!="function"&&typeof D.componentWillReceiveProps!="function"||(le||Ye!==X)&&_I(m,D,S,X),su=!1;var Qe=m.memoizedState;D.state=Qe,zg(m,S,D,P),Ug(),Ye=m.memoizedState,le||Qe!==Ye||su?(typeof ft=="function"&&(k2(m,_,ft,S),Ye=m.memoizedState),(Ce=su||vI(m,_,Ce,S,Qe,Ye,X))?(ct||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=S,m.memoizedState=Ye),D.props=S,D.state=Ye,D.context=X,S=Ce):(typeof D.componentDidMount=="function"&&(m.flags|=4194308),S=!1)}else{D=m.stateNode,o2(h,m),X=m.memoizedProps,ct=Xd(_,X),D.props=ct,ft=m.pendingProps,Qe=D.context,Ye=_.contextType,Ce=Pf,typeof Ye=="object"&&Ye!==null&&(Ce=Vi(Ye)),le=_.getDerivedStateFromProps,(Ye=typeof le=="function"||typeof D.getSnapshotBeforeUpdate=="function")||typeof D.UNSAFE_componentWillReceiveProps!="function"&&typeof D.componentWillReceiveProps!="function"||(X!==ft||Qe!==Ce)&&_I(m,D,S,Ce),su=!1,Qe=m.memoizedState,D.state=Qe,zg(m,S,D,P),Ug();var st=m.memoizedState;X!==ft||Qe!==st||su||h!==null&&h.dependencies!==null&&My(h.dependencies)?(typeof le=="function"&&(k2(m,_,le,S),st=m.memoizedState),(ct=su||vI(m,_,ct,S,Qe,st,Ce)||h!==null&&h.dependencies!==null&&My(h.dependencies))?(Ye||typeof D.UNSAFE_componentWillUpdate!="function"&&typeof D.componentWillUpdate!="function"||(typeof D.componentWillUpdate=="function"&&D.componentWillUpdate(S,st,Ce),typeof D.UNSAFE_componentWillUpdate=="function"&&D.UNSAFE_componentWillUpdate(S,st,Ce)),typeof D.componentDidUpdate=="function"&&(m.flags|=4),typeof D.getSnapshotBeforeUpdate=="function"&&(m.flags|=1024)):(typeof D.componentDidUpdate!="function"||X===h.memoizedProps&&Qe===h.memoizedState||(m.flags|=4),typeof D.getSnapshotBeforeUpdate!="function"||X===h.memoizedProps&&Qe===h.memoizedState||(m.flags|=1024),m.memoizedProps=S,m.memoizedState=st),D.props=S,D.state=st,D.context=Ce,S=ct):(typeof D.componentDidUpdate!="function"||X===h.memoizedProps&&Qe===h.memoizedState||(m.flags|=4),typeof D.getSnapshotBeforeUpdate!="function"||X===h.memoizedProps&&Qe===h.memoizedState||(m.flags|=1024),S=!1)}return D=S,$y(h,m),S=(m.flags&128)!==0,D||S?(D=m.stateNode,_=S&&typeof _.getDerivedStateFromError!="function"?null:D.render(),m.flags|=1,h!==null&&S?(m.child=qd(m,h.child,null,P),m.child=qd(m,null,_,P)):Gi(h,m,_,P),m.memoizedState=D.state,h=m.child):h=$l(h,m,P),h}function LI(h,m,_,S){return Bd(),m.flags|=256,Gi(h,m,_,S),m.child}var O2={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function L2(h){return{baseLanes:h,cachePool:E4()}}function I2(h,m,_){return h=h!==null?h.childLanes&~_:0,m&&(h|=fs),h}function II(h,m,_){var S=m.pendingProps,P=!1,D=(m.flags&128)!==0,X;if((X=D)||(X=h!==null&&h.memoizedState===null?!1:(Hr.current&2)!==0),X&&(P=!0,m.flags&=-129),X=(m.flags&32)!==0,m.flags&=-33,h===null){if(Mn){if(P?cu(m):uu(),(h=ur)?(h=G5(h,Ls),h=h!==null&&h.data!=="&"?h:null,h!==null&&(m.memoizedState={dehydrated:h,treeContext:nu!==null?{id:Ko,overflow:Xo}:null,retryLane:536870912,hydrationErrors:null},_=g4(h),_.return=m,m.child=_,$i=m,ur=null)):h=null,h===null)throw iu(m);return yT(h)?m.lanes=32:m.lanes=536870912,null}var le=S.children;return S=S.fallback,P?(uu(),P=m.mode,le=Vy({mode:"hidden",children:le},P),S=zd(S,P,_,null),le.return=m,S.return=m,le.sibling=S,m.child=le,S=m.child,S.memoizedState=L2(_),S.childLanes=I2(h,X,_),m.memoizedState=O2,Gg(null,S)):(cu(m),F2(m,le))}var Ce=h.memoizedState;if(Ce!==null&&(le=Ce.dehydrated,le!==null)){if(D)m.flags&256?(cu(m),m.flags&=-257,m=U2(h,m,_)):m.memoizedState!==null?(uu(),m.child=h.child,m.flags|=128,m=null):(uu(),le=S.fallback,P=m.mode,S=Vy({mode:"visible",children:S.children},P),le=zd(le,P,_,null),le.flags|=2,S.return=m,le.return=m,S.sibling=le,m.child=S,qd(m,h.child,null,_),S=m.child,S.memoizedState=L2(_),S.childLanes=I2(h,X,_),m.memoizedState=O2,m=Gg(null,S));else if(cu(m),yT(le)){if(X=le.nextSibling&&le.nextSibling.dataset,X)var Ye=X.dgst;X=Ye,S=Error(r(419)),S.stack="",S.digest=X,Rg({value:S,source:null,stack:null}),m=U2(h,m,_)}else if(Qr||Of(h,m,_,!1),X=(_&h.childLanes)!==0,Qr||X){if(X=rr,X!==null&&(S=nn(X,_),S!==0&&S!==Ce.retryLane))throw Ce.retryLane=S,Ud(h,S),Va(X,h,S),R2;bT(le)||Jy(),m=U2(h,m,_)}else bT(le)?(m.flags|=192,m.child=h.child,m=null):(h=Ce.treeContext,ur=Fs(le.nextSibling),$i=m,Mn=!0,ru=null,Ls=!1,h!==null&&y4(m,h),m=F2(m,S.children),m.flags|=4096);return m}return P?(uu(),le=S.fallback,P=m.mode,Ce=h.child,Ye=Ce.sibling,S=Il(Ce,{mode:"hidden",children:S.children}),S.subtreeFlags=Ce.subtreeFlags&65011712,Ye!==null?le=Il(Ye,le):(le=zd(le,P,_,null),le.flags|=2),le.return=m,S.return=m,S.sibling=le,m.child=S,Gg(null,S),S=m.child,le=h.child.memoizedState,le===null?le=L2(_):(P=le.cachePool,P!==null?(Ce=Yr._currentValue,P=P.parent!==Ce?{parent:Ce,pool:Ce}:P):P=E4(),le={baseLanes:le.baseLanes|_,cachePool:P}),S.memoizedState=le,S.childLanes=I2(h,X,_),m.memoizedState=O2,Gg(h.child,S)):(cu(m),_=h.child,h=_.sibling,_=Il(_,{mode:"visible",children:S.children}),_.return=m,_.sibling=null,h!==null&&(X=m.deletions,X===null?(m.deletions=[h],m.flags|=16):X.push(h)),m.child=_,m.memoizedState=null,_)}function F2(h,m){return m=Vy({mode:"visible",children:m},h.mode),m.return=h,h.child=m}function Vy(h,m){return h=cs(22,h,null,m),h.lanes=0,h}function U2(h,m,_){return qd(m,h.child,null,_),h=F2(m,m.pendingProps.children),h.flags|=2,m.memoizedState=null,h}function FI(h,m,_){h.lanes|=m;var S=h.alternate;S!==null&&(S.lanes|=m),e2(h.return,m,_)}function z2(h,m,_,S,P,D){var X=h.memoizedState;X===null?h.memoizedState={isBackwards:m,rendering:null,renderingStartTime:0,last:S,tail:_,tailMode:P,treeForkCount:D}:(X.isBackwards=m,X.rendering=null,X.renderingStartTime=0,X.last=S,X.tail=_,X.tailMode=P,X.treeForkCount=D)}function UI(h,m,_){var S=m.pendingProps,P=S.revealOrder,D=S.tail;S=S.children;var X=Hr.current,le=(X&2)!==0;if(le?(X=X&1|2,m.flags|=128):X&=1,Z(Hr,X),Gi(h,m,S,_),S=Mn?jg:0,!le&&h!==null&&(h.flags&128)!==0)e:for(h=m.child;h!==null;){if(h.tag===13)h.memoizedState!==null&&FI(h,_,m);else if(h.tag===19)FI(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&&jy(h)===null&&(P=_),_=_.sibling;_=P,_===null?(P=m.child,m.child=null):(P=_.sibling,_.sibling=null),z2(m,!1,P,_,D,S);break;case"backwards":case"unstable_legacy-backwards":for(_=null,P=m.child,m.child=null;P!==null;){if(h=P.alternate,h!==null&&jy(h)===null){m.child=P;break}h=P.sibling,P.sibling=_,_=P,P=h}z2(m,!0,_,null,D,S);break;case"together":z2(m,!1,null,null,void 0,S);break;default:m.memoizedState=null}return m.child}function $l(h,m,_){if(h!==null&&(m.dependencies=h.dependencies),fu|=m.lanes,(_&m.childLanes)===0)if(h!==null){if(Of(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,_=Il(h,h.pendingProps),m.child=_,_.return=m;h.sibling!==null;)h=h.sibling,_=_.sibling=Il(h,h.pendingProps),_.return=m;_.sibling=null}return m.child}function B2(h,m){return(h.lanes&m)!==0?!0:(h=h.dependencies,!!(h!==null&&My(h)))}function zne(h,m,_){switch(m.tag){case 3:Ae(m,m.stateNode.containerInfo),au(m,Yr,h.memoizedState.cache),Bd();break;case 27:case 5:ce(m);break;case 4:Ae(m,m.stateNode.containerInfo);break;case 10:au(m,m.type,m.memoizedProps.value);break;case 31:if(m.memoizedState!==null)return m.flags|=128,h2(m),null;break;case 13:var S=m.memoizedState;if(S!==null)return S.dehydrated!==null?(cu(m),m.flags|=128,null):(_&m.child.childLanes)!==0?II(h,m,_):(cu(m),h=$l(h,m,_),h!==null?h.sibling:null);cu(m);break;case 19:var P=(h.flags&128)!==0;if(S=(_&m.childLanes)!==0,S||(Of(h,m,_,!1),S=(_&m.childLanes)!==0),P){if(S)return UI(h,m,_);m.flags|=128}if(P=m.memoizedState,P!==null&&(P.rendering=null,P.tail=null,P.lastEffect=null),Z(Hr,Hr.current),S)break;return null;case 22:return m.lanes=0,PI(h,m,_,m.pendingProps);case 24:au(m,Yr,h.memoizedState.cache)}return $l(h,m,_)}function zI(h,m,_){if(h!==null)if(h.memoizedProps!==m.pendingProps)Qr=!0;else{if(!B2(h,_)&&(m.flags&128)===0)return Qr=!1,zne(h,m,_);Qr=(h.flags&131072)!==0}else Qr=!1,Mn&&(m.flags&1048576)!==0&&b4(m,jg,m.index);switch(m.lanes=0,m.tag){case 16:e:{var S=m.pendingProps;if(h=Gd(m.elementType),m.type=h,typeof h=="function")WE(h)?(S=Xd(h,S),m.tag=1,m=OI(null,m,h,S,_)):(m.tag=0,m=D2(null,m,h,S,_));else{if(h!=null){var P=h.$$typeof;if(P===T){m.tag=11,m=NI(null,m,h,S,_);break e}else if(P===N){m.tag=14,m=CI(null,m,h,S,_);break e}}throw m=j(h)||h,Error(r(306,m,""))}}return m;case 0:return D2(h,m,m.type,m.pendingProps,_);case 1:return S=m.type,P=Xd(S,m.pendingProps),OI(h,m,S,P,_);case 3:e:{if(Ae(m,m.stateNode.containerInfo),h===null)throw Error(r(387));S=m.pendingProps;var D=m.memoizedState;P=D.element,o2(h,m),zg(m,S,null,_);var X=m.memoizedState;if(S=X.cache,au(m,Yr,S),S!==D.cache&&t2(m,[Yr],_,!0),Ug(),S=X.element,D.isDehydrated)if(D={element:S,isDehydrated:!1,cache:X.cache},m.updateQueue.baseState=D,m.memoizedState=D,m.flags&256){m=LI(h,m,S,_);break e}else if(S!==P){P=Rs(Error(r(424)),m),Rg(P),m=LI(h,m,S,_);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(ur=Fs(h.firstChild),$i=m,Mn=!0,ru=null,Ls=!0,_=P4(m,null,S,_),m.child=_;_;)_.flags=_.flags&-3|4096,_=_.sibling}else{if(Bd(),S===P){m=$l(h,m,_);break e}Gi(h,m,S,_)}m=m.child}return m;case 26:return $y(h,m),h===null?(_=Z5(m.type,null,m.pendingProps,null))?m.memoizedState=_:Mn||(_=m.type,h=m.pendingProps,S=sv(de.current).createElement(_),S[wr]=m,S[zr]=h,Wi(S,_,h),Ie(S),m.stateNode=S):m.memoizedState=Z5(m.type,h.memoizedProps,m.pendingProps,h.memoizedState),null;case 27:return ce(m),h===null&&Mn&&(S=m.stateNode=K5(m.type,m.pendingProps,de.current),$i=m,Ls=!0,P=ur,bu(m.type)?(vT=P,ur=Fs(S.firstChild)):ur=P),Gi(h,m,m.pendingProps.children,_),$y(h,m),h===null&&(m.flags|=4194304),m.child;case 5:return h===null&&Mn&&((P=S=ur)&&(S=gre(S,m.type,m.pendingProps,Ls),S!==null?(m.stateNode=S,$i=m,ur=Fs(S.firstChild),Ls=!1,P=!0):P=!1),P||iu(m)),ce(m),P=m.type,D=m.pendingProps,X=h!==null?h.memoizedProps:null,S=D.children,pT(P,D)?S=null:X!==null&&pT(P,X)&&(m.flags|=32),m.memoizedState!==null&&(P=m2(h,m,Pne,null,null,_),sx._currentValue=P),$y(h,m),Gi(h,m,S,_),m.child;case 6:return h===null&&Mn&&((h=_=ur)&&(_=xre(_,m.pendingProps,Ls),_!==null?(m.stateNode=_,$i=m,ur=null,h=!0):h=!1),h||iu(m)),null;case 13:return II(h,m,_);case 4:return Ae(m,m.stateNode.containerInfo),S=m.pendingProps,h===null?m.child=qd(m,null,S,_):Gi(h,m,S,_),m.child;case 11:return NI(h,m,m.type,m.pendingProps,_);case 7:return Gi(h,m,m.pendingProps,_),m.child;case 8:return Gi(h,m,m.pendingProps.children,_),m.child;case 12:return Gi(h,m,m.pendingProps.children,_),m.child;case 10:return S=m.pendingProps,au(m,m.type,S.value),Gi(h,m,S.children,_),m.child;case 9:return P=m.type._context,S=m.pendingProps.children,$d(m),P=Vi(P),S=S(P),m.flags|=1,Gi(h,m,S,_),m.child;case 14:return CI(h,m,m.type,m.pendingProps,_);case 15:return kI(h,m,m.type,m.pendingProps,_);case 19:return UI(h,m,_);case 31:return Une(h,m,_);case 22:return PI(h,m,_,m.pendingProps);case 24:return $d(m),S=Vi(Yr),h===null?(P=i2(),P===null&&(P=rr,D=n2(),P.pooledCache=D,D.refCount++,D!==null&&(P.pooledCacheLanes|=_),P=D),m.memoizedState={parent:S,cache:P},s2(m),au(m,Yr,P)):((h.lanes&_)!==0&&(o2(h,m),zg(m,null,null,_),Ug()),P=h.memoizedState,D=m.memoizedState,P.parent!==S?(P={parent:S,cache:S},m.memoizedState=P,m.lanes===0&&(m.memoizedState=m.updateQueue.baseState=P),au(m,Yr,S)):(S=D.cache,au(m,Yr,S),S!==P.cache&&t2(m,[Yr],_,!0))),Gi(h,m,m.pendingProps.children,_),m.child;case 29:throw m.pendingProps}throw Error(r(156,m.tag))}function Vl(h){h.flags|=4}function H2(h,m,_,S,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(f5())h.flags|=8192;else throw Wd=Ny,a2}else h.flags&=-16777217}function BI(h,m){if(m.type!=="stylesheet"||(m.state.loading&4)!==0)h.flags&=-16777217;else if(h.flags|=16777216,!nF(m))if(f5())h.flags|=8192;else throw Wd=Ny,a2}function Gy(h,m){m!==null&&(h.flags|=4),h.flags&16384&&(m=h.tag!==22?Se():536870912,h.lanes|=m,qf|=m)}function Wg(h,m){if(!Mn)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 S=null;_!==null;)_.alternate!==null&&(S=_),_=_.sibling;S===null?m||h.tail===null?h.tail=null:h.tail.sibling=null:S.sibling=null}}function dr(h){var m=h.alternate!==null&&h.alternate.child===h.child,_=0,S=0;if(m)for(var P=h.child;P!==null;)_|=P.lanes|P.childLanes,S|=P.subtreeFlags&65011712,S|=P.flags&65011712,P.return=h,P=P.sibling;else for(P=h.child;P!==null;)_|=P.lanes|P.childLanes,S|=P.subtreeFlags,S|=P.flags,P.return=h,P=P.sibling;return h.subtreeFlags|=S,h.childLanes=_,m}function Bne(h,m,_){var S=m.pendingProps;switch(YE(m),m.tag){case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return dr(m),null;case 1:return dr(m),null;case 3:return _=m.stateNode,S=null,h!==null&&(S=h.memoizedState.cache),m.memoizedState.cache!==S&&(m.flags|=2048),zl(Yr),me(),_.pendingContext&&(_.context=_.pendingContext,_.pendingContext=null),(h===null||h.child===null)&&(Df(m)?Vl(m):h===null||h.memoizedState.isDehydrated&&(m.flags&256)===0||(m.flags|=1024,QE())),dr(m),null;case 26:var P=m.type,D=m.memoizedState;return h===null?(Vl(m),D!==null?(dr(m),BI(m,D)):(dr(m),H2(m,P,null,S,_))):D?D!==h.memoizedState?(Vl(m),dr(m),BI(m,D)):(dr(m),m.flags&=-16777217):(h=h.memoizedProps,h!==S&&Vl(m),dr(m),H2(m,P,h,S,_)),null;case 27:if(Pe(m),_=de.current,P=m.type,h!==null&&m.stateNode!=null)h.memoizedProps!==S&&Vl(m);else{if(!S){if(m.stateNode===null)throw Error(r(166));return dr(m),null}h=te.current,Df(m)?v4(m):(h=K5(P,S,_),m.stateNode=h,Vl(m))}return dr(m),null;case 5:if(Pe(m),P=m.type,h!==null&&m.stateNode!=null)h.memoizedProps!==S&&Vl(m);else{if(!S){if(m.stateNode===null)throw Error(r(166));return dr(m),null}if(D=te.current,Df(m))v4(m);else{var X=sv(de.current);switch(D){case 1:D=X.createElementNS("http://www.w3.org/2000/svg",P);break;case 2:D=X.createElementNS("http://www.w3.org/1998/Math/MathML",P);break;default:switch(P){case"svg":D=X.createElementNS("http://www.w3.org/2000/svg",P);break;case"math":D=X.createElementNS("http://www.w3.org/1998/Math/MathML",P);break;case"script":D=X.createElement("div"),D.innerHTML=" - + +