diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fff16a6e..f1eb3f6bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to Bambuddy will be documented in this file. ## [0.1.6] - Not released ### New Features +- **GitHub Profile Backup** - Automatically backup your Cloud profiles, K-profiles and settings to a GitHub repository: + - Configure GitHub repository URL and Personal Access Token + - Schedule backups hourly, daily, or weekly + - Manual on-demand backup trigger + - Backs up K-profiles (per-printer), cloud profiles, and app settings + - Skip unchanged commits (only creates commit when data changes) + - Real-time progress tracking during backup + - Backup history log with status and commit links + - Requires Bambu Cloud login for full profile access + - New Settings → Backup & Restore tab (local backup/restore moved here) + - Included in local backup/restore (except PAT for security) - **Plate Not Empty Notification** - Dedicated notification category for build plate detection: - New toggle in notification provider settings (enabled by default) - Sends immediately (bypasses quiet hours and digest mode) diff --git a/README.md b/README.md index b90a70b83..8c6641e3f 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ - MQTT publishing for Home Assistant, Node-RED, etc. - Bambu Cloud profile management - K-profiles (pressure advance) +- **GitHub backup** - Schedule automatic backups of cloud profiles, k profiles and settings to GitHub - External sidebar links - Webhooks & API keys - Interactive API browser with live testing diff --git a/backend/app/api/routes/github_backup.py b/backend/app/api/routes/github_backup.py new file mode 100644 index 000000000..8ac22560e --- /dev/null +++ b/backend/app/api/routes/github_backup.py @@ -0,0 +1,319 @@ +"""API routes for GitHub profile backup.""" + +import logging + +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy import delete, desc, select +from sqlalchemy.ext.asyncio import AsyncSession + +from backend.app.core.database import get_db +from backend.app.models.github_backup import GitHubBackupConfig, GitHubBackupLog +from backend.app.schemas.github_backup import ( + GitHubBackupConfigCreate, + GitHubBackupConfigResponse, + GitHubBackupConfigUpdate, + GitHubBackupLogResponse, + GitHubBackupStatus, + GitHubBackupTriggerResponse, + GitHubTestConnectionResponse, +) +from backend.app.services.github_backup import github_backup_service + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/github-backup", tags=["github-backup"]) + + +def _config_to_response(config: GitHubBackupConfig) -> dict: + """Convert config model to response dict.""" + return { + "id": config.id, + "repository_url": config.repository_url, + "has_token": bool(config.access_token), + "branch": config.branch, + "schedule_enabled": config.schedule_enabled, + "schedule_type": config.schedule_type, + "backup_kprofiles": config.backup_kprofiles, + "backup_cloud_profiles": config.backup_cloud_profiles, + "backup_settings": config.backup_settings, + "enabled": config.enabled, + "last_backup_at": config.last_backup_at, + "last_backup_status": config.last_backup_status, + "last_backup_message": config.last_backup_message, + "last_backup_commit_sha": config.last_backup_commit_sha, + "next_scheduled_run": config.next_scheduled_run, + "created_at": config.created_at, + "updated_at": config.updated_at, + } + + +@router.get("/config", response_model=GitHubBackupConfigResponse | None) +async def get_config(db: AsyncSession = Depends(get_db)): + """Get the current GitHub backup configuration.""" + result = await db.execute(select(GitHubBackupConfig).limit(1)) + config = result.scalar_one_or_none() + + if not config: + return None + + return _config_to_response(config) + + +@router.post("/config", response_model=GitHubBackupConfigResponse) +async def save_config( + config_data: GitHubBackupConfigCreate, + db: AsyncSession = Depends(get_db), +): + """Create or update GitHub backup configuration. + + Only one configuration is supported. If one exists, it will be updated. + """ + # Check for existing config + result = await db.execute(select(GitHubBackupConfig).limit(1)) + config = result.scalar_one_or_none() + + if config: + # Update existing + config.repository_url = config_data.repository_url + config.access_token = config_data.access_token + config.branch = config_data.branch + config.schedule_enabled = config_data.schedule_enabled + config.schedule_type = config_data.schedule_type.value + config.backup_kprofiles = config_data.backup_kprofiles + config.backup_cloud_profiles = config_data.backup_cloud_profiles + config.backup_settings = config_data.backup_settings + config.enabled = config_data.enabled + + # Calculate next scheduled run if enabled + if config.schedule_enabled: + config.next_scheduled_run = github_backup_service._calculate_next_run(config.schedule_type) + else: + config.next_scheduled_run = None + + logger.info(f"Updated GitHub backup config: {config.repository_url}") + else: + # Create new + config = GitHubBackupConfig( + repository_url=config_data.repository_url, + access_token=config_data.access_token, + branch=config_data.branch, + schedule_enabled=config_data.schedule_enabled, + schedule_type=config_data.schedule_type.value, + backup_kprofiles=config_data.backup_kprofiles, + backup_cloud_profiles=config_data.backup_cloud_profiles, + backup_settings=config_data.backup_settings, + enabled=config_data.enabled, + ) + + if config.schedule_enabled: + config.next_scheduled_run = github_backup_service._calculate_next_run(config.schedule_type) + + db.add(config) + logger.info(f"Created GitHub backup config: {config.repository_url}") + + await db.commit() + await db.refresh(config) + + return _config_to_response(config) + + +@router.patch("/config", response_model=GitHubBackupConfigResponse) +async def update_config( + update_data: GitHubBackupConfigUpdate, + db: AsyncSession = Depends(get_db), +): + """Partially update GitHub backup configuration.""" + result = await db.execute(select(GitHubBackupConfig).limit(1)) + config = result.scalar_one_or_none() + + if not config: + raise HTTPException(status_code=404, detail="No configuration found") + + update_dict = update_data.model_dump(exclude_unset=True) + + for key, value in update_dict.items(): + if key == "schedule_type" and value is not None: + setattr(config, key, value.value) + else: + setattr(config, key, value) + + # Recalculate next scheduled run if schedule settings changed + if "schedule_enabled" in update_dict or "schedule_type" in update_dict: + if config.schedule_enabled: + config.next_scheduled_run = github_backup_service._calculate_next_run(config.schedule_type) + else: + config.next_scheduled_run = None + + await db.commit() + await db.refresh(config) + + logger.info(f"Updated GitHub backup config: {config.repository_url}") + + return _config_to_response(config) + + +@router.delete("/config") +async def delete_config(db: AsyncSession = Depends(get_db)): + """Delete the GitHub backup configuration and all logs.""" + result = await db.execute(select(GitHubBackupConfig).limit(1)) + config = result.scalar_one_or_none() + + if not config: + raise HTTPException(status_code=404, detail="No configuration found") + + await db.delete(config) + await db.commit() + + logger.info("Deleted GitHub backup config") + + return {"message": "Configuration deleted"} + + +@router.post("/test", response_model=GitHubTestConnectionResponse) +async def test_connection( + repo_url: str = Query(..., description="GitHub repository URL"), + token: str = Query(..., description="Personal Access Token"), +): + """Test GitHub connection with provided credentials.""" + result = await github_backup_service.test_connection(repo_url, token) + return GitHubTestConnectionResponse(**result) + + +@router.post("/test-stored", response_model=GitHubTestConnectionResponse) +async def test_stored_connection(db: AsyncSession = Depends(get_db)): + """Test GitHub connection using stored configuration.""" + result = await db.execute(select(GitHubBackupConfig).limit(1)) + config = result.scalar_one_or_none() + + if not config: + raise HTTPException(status_code=404, detail="No configuration found") + + if not config.access_token: + raise HTTPException(status_code=400, detail="No access token configured") + + test_result = await github_backup_service.test_connection(config.repository_url, config.access_token) + return GitHubTestConnectionResponse(**test_result) + + +@router.post("/run", response_model=GitHubBackupTriggerResponse) +async def trigger_backup(db: AsyncSession = Depends(get_db)): + """Manually trigger a backup.""" + result = await db.execute(select(GitHubBackupConfig).limit(1)) + config = result.scalar_one_or_none() + + if not config: + raise HTTPException(status_code=404, detail="No configuration found. Configure backup first.") + + if not config.enabled: + raise HTTPException(status_code=400, detail="Backup is disabled") + + backup_result = await github_backup_service.run_backup(config.id, trigger="manual") + + return GitHubBackupTriggerResponse(**backup_result) + + +@router.get("/status", response_model=GitHubBackupStatus) +async def get_status(db: AsyncSession = Depends(get_db)): + """Get current backup status.""" + result = await db.execute(select(GitHubBackupConfig).limit(1)) + config = result.scalar_one_or_none() + + if not config: + return GitHubBackupStatus( + configured=False, + enabled=False, + is_running=False, + progress=None, + last_backup_at=None, + last_backup_status=None, + next_scheduled_run=None, + ) + + return GitHubBackupStatus( + configured=True, + enabled=config.enabled, + is_running=github_backup_service.is_running, + progress=github_backup_service.progress, + last_backup_at=config.last_backup_at, + last_backup_status=config.last_backup_status, + next_scheduled_run=config.next_scheduled_run, + ) + + +@router.get("/logs", response_model=list[GitHubBackupLogResponse]) +async def get_logs( + limit: int = Query(default=50, ge=1, le=200), + offset: int = Query(default=0, ge=0), + db: AsyncSession = Depends(get_db), +): + """Get backup logs.""" + result = await db.execute(select(GitHubBackupConfig).limit(1)) + config = result.scalar_one_or_none() + + if not config: + return [] + + logs_result = await db.execute( + select(GitHubBackupLog) + .where(GitHubBackupLog.config_id == config.id) + .order_by(desc(GitHubBackupLog.started_at)) + .offset(offset) + .limit(limit) + ) + logs = logs_result.scalars().all() + + return [ + GitHubBackupLogResponse( + id=log.id, + config_id=log.config_id, + started_at=log.started_at, + completed_at=log.completed_at, + status=log.status, + trigger=log.trigger, + commit_sha=log.commit_sha, + files_changed=log.files_changed, + error_message=log.error_message, + ) + for log in logs + ] + + +@router.delete("/logs") +async def clear_logs( + keep_last: int = Query(default=10, ge=0, le=100, description="Number of recent logs to keep"), + db: AsyncSession = Depends(get_db), +): + """Clear backup logs, optionally keeping the most recent entries.""" + result = await db.execute(select(GitHubBackupConfig).limit(1)) + config = result.scalar_one_or_none() + + if not config: + return {"deleted": 0, "message": "No configuration found"} + + if keep_last > 0: + # Get IDs to keep + keep_result = await db.execute( + select(GitHubBackupLog.id) + .where(GitHubBackupLog.config_id == config.id) + .order_by(desc(GitHubBackupLog.started_at)) + .limit(keep_last) + ) + keep_ids = [row[0] for row in keep_result.fetchall()] + + if keep_ids: + delete_result = await db.execute( + delete(GitHubBackupLog).where( + GitHubBackupLog.config_id == config.id, GitHubBackupLog.id.not_in(keep_ids) + ) + ) + else: + delete_result = await db.execute(delete(GitHubBackupLog).where(GitHubBackupLog.config_id == config.id)) + else: + delete_result = await db.execute(delete(GitHubBackupLog).where(GitHubBackupLog.config_id == config.id)) + + await db.commit() + + deleted_count = delete_result.rowcount + logger.info(f"Deleted {deleted_count} GitHub backup logs (kept {keep_last})") + + return {"deleted": deleted_count, "message": f"Deleted {deleted_count} logs"} diff --git a/backend/app/api/routes/settings.py b/backend/app/api/routes/settings.py index 1c4b4cb60..8a78cd233 100644 --- a/backend/app/api/routes/settings.py +++ b/backend/app/api/routes/settings.py @@ -15,6 +15,7 @@ from backend.app.models.api_key import APIKey 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.github_backup import GitHubBackupConfig from backend.app.models.maintenance import MaintenanceHistory, MaintenanceType, PrinterMaintenance from backend.app.models.notification import NotificationProvider from backend.app.models.notification_template import NotificationTemplate @@ -256,6 +257,7 @@ async def export_backup( include_users: bool = Query( False, description="Include users (passwords not exported - users will need new passwords)" ), + include_github_backup: bool = Query(False, description="Include GitHub backup configuration (token not exported)"), ): """Export selected data as JSON backup.""" backup: dict = { @@ -854,6 +856,24 @@ async def export_backup( ) backup["included"].append("users") + # GitHub backup configuration + if include_github_backup: + result = await db.execute(select(GitHubBackupConfig).limit(1)) + config = result.scalar_one_or_none() + if config: + backup["github_backup"] = { + "repository_url": config.repository_url, + # access_token intentionally not exported for security + "branch": config.branch, + "schedule_enabled": config.schedule_enabled, + "schedule_type": config.schedule_type, + "backup_kprofiles": config.backup_kprofiles, + "backup_cloud_profiles": config.backup_cloud_profiles, + "backup_settings": config.backup_settings, + "enabled": config.enabled, + } + backup["included"].append("github_backup") + # If there are files to include (icons or archives), create ZIP file if backup_files: zip_buffer = io.BytesIO() @@ -953,6 +973,7 @@ async def import_backup( "projects": 0, "pending_uploads": 0, "users": 0, + "github_backup": 0, } skipped = { "settings": 0, @@ -967,6 +988,7 @@ async def import_backup( "projects": 0, "pending_uploads": 0, "users": 0, + "github_backup": 0, } skipped_details = { "notification_providers": [], @@ -1966,6 +1988,42 @@ async def import_backup( restored["users"] += 1 new_users.append(f"{user_data['username']} (temp password: {temp_password})") + # Restore GitHub backup configuration (note: access_token not included for security) + if "github_backup" in backup: + github_data = backup["github_backup"] + result = await db.execute(select(GitHubBackupConfig).limit(1)) + existing = result.scalar_one_or_none() + if existing: + if overwrite: + existing.repository_url = github_data.get("repository_url", existing.repository_url) + existing.branch = github_data.get("branch", existing.branch) + existing.schedule_enabled = github_data.get("schedule_enabled", existing.schedule_enabled) + existing.schedule_type = github_data.get("schedule_type", existing.schedule_type) + existing.backup_kprofiles = github_data.get("backup_kprofiles", existing.backup_kprofiles) + existing.backup_cloud_profiles = github_data.get( + "backup_cloud_profiles", existing.backup_cloud_profiles + ) + existing.backup_settings = github_data.get("backup_settings", existing.backup_settings) + existing.enabled = github_data.get("enabled", existing.enabled) + # Note: access_token must be re-entered after restore + restored["github_backup"] += 1 + else: + skipped["github_backup"] += 1 + else: + config = GitHubBackupConfig( + repository_url=github_data.get("repository_url", ""), + access_token="", # Must be entered after restore + branch=github_data.get("branch", "main"), + schedule_enabled=github_data.get("schedule_enabled", False), + schedule_type=github_data.get("schedule_type", "daily"), + backup_kprofiles=github_data.get("backup_kprofiles", True), + backup_cloud_profiles=github_data.get("backup_cloud_profiles", True), + backup_settings=github_data.get("backup_settings", False), + enabled=False, # Disabled until token is entered + ) + db.add(config) + restored["github_backup"] += 1 + await db.commit() # If printers were in the backup (restored, updated, or skipped), reconnect all active printers diff --git a/backend/app/core/database.py b/backend/app/core/database.py index 2db6305b4..4b2506d76 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -38,6 +38,7 @@ async def init_db(): archive, external_link, filament, + github_backup, kprofile_note, library, maintenance, diff --git a/backend/app/main.py b/backend/app/main.py index fa1fc904b..d860e0dae 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -182,6 +182,7 @@ from backend.app.api.routes import ( external_links, filaments, firmware, + github_backup, kprofiles, library, maintenance, @@ -209,6 +210,7 @@ from backend.app.models.smart_plug import SmartPlug from backend.app.services.archive import ArchiveService from backend.app.services.bambu_ftp import download_file_async, get_ftp_retry_settings, with_ftp_retry from backend.app.services.bambu_mqtt import PrinterState +from backend.app.services.github_backup import github_backup_service from backend.app.services.homeassistant import homeassistant_service from backend.app.services.mqtt_relay import mqtt_relay from backend.app.services.notification_service import notification_service @@ -2383,6 +2385,9 @@ async def lifespan(app: FastAPI): # Start the notification digest scheduler notification_service.start_digest_scheduler() + # Start the GitHub backup scheduler + await github_backup_service.start_scheduler() + # Start AMS history recording start_ams_history_recording() @@ -2422,6 +2427,7 @@ async def lifespan(app: FastAPI): print_scheduler.stop() smart_plug_manager.stop_scheduler() notification_service.stop_digest_scheduler() + github_backup_service.stop_scheduler() stop_ams_history_recording() stop_runtime_tracking() printer_manager.disconnect_all() @@ -2468,6 +2474,7 @@ app.include_router(websocket.router, prefix=app_settings.api_prefix) app.include_router(discovery.router, prefix=app_settings.api_prefix) app.include_router(pending_uploads.router, prefix=app_settings.api_prefix) app.include_router(firmware.router, prefix=app_settings.api_prefix) +app.include_router(github_backup.router, prefix=app_settings.api_prefix) # Serve static files (React build) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 5c1290e82..4b4ac176e 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -2,6 +2,7 @@ from backend.app.models.ams_history import AMSSensorHistory from backend.app.models.api_key import APIKey from backend.app.models.archive import PrintArchive from backend.app.models.filament import Filament +from backend.app.models.github_backup import GitHubBackupConfig, GitHubBackupLog from backend.app.models.kprofile_note import KProfileNote from backend.app.models.library import LibraryFile, LibraryFolder from backend.app.models.maintenance import MaintenanceHistory, MaintenanceType, PrinterMaintenance @@ -33,4 +34,6 @@ __all__ = [ "LibraryFolder", "LibraryFile", "User", + "GitHubBackupConfig", + "GitHubBackupLog", ] diff --git a/backend/app/models/github_backup.py b/backend/app/models/github_backup.py new file mode 100644 index 000000000..77bbc68d7 --- /dev/null +++ b/backend/app/models/github_backup.py @@ -0,0 +1,65 @@ +"""GitHub backup configuration and log models.""" + +from datetime import datetime + +from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from backend.app.core.database import Base + + +class GitHubBackupConfig(Base): + """Configuration for GitHub profile backup.""" + + __tablename__ = "github_backup_config" + + id: Mapped[int] = mapped_column(primary_key=True) + repository_url: Mapped[str] = mapped_column(String(500)) # Full GitHub URL + access_token: Mapped[str] = mapped_column(Text) # Personal Access Token + branch: Mapped[str] = mapped_column(String(100), default="main") + + # Schedule configuration + schedule_enabled: Mapped[bool] = mapped_column(Boolean, default=False) + schedule_type: Mapped[str] = mapped_column(String(20), default="daily") # hourly/daily/weekly + schedule_cron: Mapped[str | None] = mapped_column(String(100), nullable=True) # For future cron support + + # What to backup + backup_kprofiles: Mapped[bool] = mapped_column(Boolean, default=True) + backup_cloud_profiles: Mapped[bool] = mapped_column(Boolean, default=True) + backup_settings: Mapped[bool] = mapped_column(Boolean, default=False) + + # Status tracking + enabled: Mapped[bool] = mapped_column(Boolean, default=True) + last_backup_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + last_backup_status: Mapped[str | None] = mapped_column(String(20), nullable=True) # success/failed/skipped + last_backup_message: Mapped[str | None] = mapped_column(Text, nullable=True) + last_backup_commit_sha: Mapped[str | None] = mapped_column(String(40), nullable=True) + next_scheduled_run: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + + # Timestamps + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now()) + + # Relationships + logs: Mapped[list["GitHubBackupLog"]] = relationship(back_populates="config", cascade="all, delete-orphan") + + +class GitHubBackupLog(Base): + """Log entry for GitHub backup runs.""" + + __tablename__ = "github_backup_logs" + + id: Mapped[int] = mapped_column(primary_key=True) + config_id: Mapped[int] = mapped_column(ForeignKey("github_backup_config.id", ondelete="CASCADE")) + + started_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now()) + completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + status: Mapped[str] = mapped_column(String(20)) # running/success/failed/skipped + trigger: Mapped[str] = mapped_column(String(20)) # manual/scheduled + + commit_sha: Mapped[str | None] = mapped_column(String(40), nullable=True) + files_changed: Mapped[int] = mapped_column(Integer, default=0) + error_message: Mapped[str | None] = mapped_column(Text, nullable=True) + + # Relationships + config: Mapped["GitHubBackupConfig"] = relationship(back_populates="logs") diff --git a/backend/app/schemas/github_backup.py b/backend/app/schemas/github_backup.py new file mode 100644 index 000000000..2fe8299b1 --- /dev/null +++ b/backend/app/schemas/github_backup.py @@ -0,0 +1,154 @@ +"""Pydantic schemas for GitHub backup configuration.""" + +import re +from datetime import datetime +from enum import Enum + +from pydantic import BaseModel, Field, field_validator + + +class ScheduleType(str, Enum): + """Backup schedule types.""" + + HOURLY = "hourly" + DAILY = "daily" + WEEKLY = "weekly" + + +class GitHubBackupConfigCreate(BaseModel): + """Schema for creating/updating GitHub backup config.""" + + repository_url: str = Field(..., min_length=1, max_length=500, description="GitHub repository URL") + access_token: str = Field(..., min_length=1, description="Personal Access Token") + branch: str = Field(default="main", max_length=100, description="Branch to push to") + + schedule_enabled: bool = Field(default=False, description="Enable scheduled backups") + schedule_type: ScheduleType = Field(default=ScheduleType.DAILY, description="Schedule frequency") + + backup_kprofiles: bool = Field(default=True, description="Backup K-profiles") + backup_cloud_profiles: bool = Field(default=True, description="Backup Bambu Cloud profiles") + backup_settings: bool = Field(default=False, description="Backup app settings") + + enabled: bool = Field(default=True, description="Enable backup feature") + + @field_validator("repository_url") + @classmethod + def validate_repo_url(cls, v: str) -> str: + """Validate GitHub repository URL format.""" + # Accept various GitHub URL formats + patterns = [ + r"^https://github\.com/[\w.-]+/[\w.-]+(?:\.git)?$", + r"^git@github\.com:[\w.-]+/[\w.-]+(?:\.git)?$", + ] + v = v.strip().rstrip("/") + if not any(re.match(p, v) for p in patterns): + raise ValueError("Invalid GitHub repository URL. Expected format: https://github.com/owner/repo") + return v + + +class GitHubBackupConfigUpdate(BaseModel): + """Schema for updating GitHub backup config (all fields optional).""" + + repository_url: str | None = Field(default=None, max_length=500) + access_token: str | None = Field(default=None) + branch: str | None = Field(default=None, max_length=100) + + schedule_enabled: bool | None = None + schedule_type: ScheduleType | None = None + + backup_kprofiles: bool | None = None + backup_cloud_profiles: bool | None = None + backup_settings: bool | None = None + + enabled: bool | None = None + + @field_validator("repository_url") + @classmethod + def validate_repo_url(cls, v: str | None) -> str | None: + if v is None: + return v + patterns = [ + r"^https://github\.com/[\w.-]+/[\w.-]+(?:\.git)?$", + r"^git@github\.com:[\w.-]+/[\w.-]+(?:\.git)?$", + ] + v = v.strip().rstrip("/") + if not any(re.match(p, v) for p in patterns): + raise ValueError("Invalid GitHub repository URL") + return v + + +class GitHubBackupConfigResponse(BaseModel): + """Schema for GitHub backup config API response.""" + + id: int + repository_url: str + has_token: bool = Field(description="Whether an access token is configured") + branch: str + + schedule_enabled: bool + schedule_type: str + + backup_kprofiles: bool + backup_cloud_profiles: bool + backup_settings: bool + + enabled: bool + last_backup_at: datetime | None + last_backup_status: str | None + last_backup_message: str | None + last_backup_commit_sha: str | None + next_scheduled_run: datetime | None + + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +class GitHubBackupLogResponse(BaseModel): + """Schema for backup log API response.""" + + id: int + config_id: int + started_at: datetime + completed_at: datetime | None + status: str + trigger: str + commit_sha: str | None + files_changed: int + error_message: str | None + + class Config: + from_attributes = True + + +class GitHubBackupStatus(BaseModel): + """Schema for current backup status.""" + + configured: bool = Field(description="Whether backup is configured") + enabled: bool = Field(description="Whether backup is enabled") + is_running: bool = Field(description="Whether a backup is currently running") + progress: str | None = Field(default=None, description="Current backup progress message") + last_backup_at: datetime | None + last_backup_status: str | None + next_scheduled_run: datetime | None + + +class GitHubTestConnectionResponse(BaseModel): + """Schema for test connection response.""" + + success: bool + message: str + repo_name: str | None = None + permissions: dict | None = None + + +class GitHubBackupTriggerResponse(BaseModel): + """Schema for manual backup trigger response.""" + + success: bool + message: str + log_id: int | None = None + commit_sha: str | None = None + files_changed: int = 0 diff --git a/backend/app/services/github_backup.py b/backend/app/services/github_backup.py new file mode 100644 index 000000000..d767863fe --- /dev/null +++ b/backend/app/services/github_backup.py @@ -0,0 +1,734 @@ +"""GitHub backup service for printer profiles. + +Handles scheduled and on-demand backups of K-profiles and cloud profiles to GitHub. +""" + +import asyncio +import base64 +import hashlib +import json +import logging +import re +from datetime import UTC, datetime, timedelta + +import httpx +from sqlalchemy import desc, select +from sqlalchemy.ext.asyncio import AsyncSession + +from backend.app.core.database import async_session +from backend.app.models.github_backup import GitHubBackupConfig, GitHubBackupLog +from backend.app.models.printer import Printer +from backend.app.models.settings import Settings +from backend.app.services.bambu_cloud import get_cloud_service +from backend.app.services.printer_manager import printer_manager + +logger = logging.getLogger(__name__) + +# Schedule intervals in seconds +SCHEDULE_INTERVALS = { + "hourly": 3600, + "daily": 86400, + "weekly": 604800, +} + + +class GitHubBackupService: + """Service for backing up profiles to GitHub.""" + + def __init__(self): + self._scheduler_task: asyncio.Task | None = None + self._check_interval = 60 # Check every minute for scheduled runs + self._running_backup: bool = False + self._backup_progress: str | None = None + self._http_client: httpx.AsyncClient | None = None + + async def _get_client(self) -> httpx.AsyncClient: + """Get or create HTTP client.""" + if self._http_client is None or self._http_client.is_closed: + self._http_client = httpx.AsyncClient(timeout=60.0) + return self._http_client + + async def start_scheduler(self): + """Start the background scheduler loop.""" + if self._scheduler_task is not None: + return + logger.info("Starting GitHub backup scheduler") + self._scheduler_task = asyncio.create_task(self._scheduler_loop()) + + def stop_scheduler(self): + """Stop the scheduler.""" + if self._scheduler_task: + self._scheduler_task.cancel() + self._scheduler_task = None + logger.info("Stopped GitHub backup scheduler") + + async def _scheduler_loop(self): + """Main scheduler loop - checks for due backups.""" + while True: + try: + await asyncio.sleep(self._check_interval) + await self._check_scheduled_backups() + except asyncio.CancelledError: + break + except Exception as e: + logger.error(f"Error in GitHub backup scheduler: {e}") + await asyncio.sleep(60) + + async def _check_scheduled_backups(self): + """Check if any scheduled backups are due.""" + async with async_session() as db: + result = await db.execute( + select(GitHubBackupConfig).where( + GitHubBackupConfig.enabled == True, # noqa: E712 + GitHubBackupConfig.schedule_enabled == True, # noqa: E712 + ) + ) + configs = result.scalars().all() + + now = datetime.now(UTC) + for config in configs: + if config.next_scheduled_run and config.next_scheduled_run <= now: + logger.info(f"Running scheduled backup for config {config.id}") + await self.run_backup(config.id, trigger="scheduled") + + def _calculate_next_run(self, schedule_type: str, from_time: datetime | None = None) -> datetime: + """Calculate the next scheduled run time.""" + now = from_time or datetime.now(UTC) + interval = SCHEDULE_INTERVALS.get(schedule_type, SCHEDULE_INTERVALS["daily"]) + return now + timedelta(seconds=interval) + + async def test_connection(self, repo_url: str, token: str) -> dict: + """Test GitHub connection and permissions. + + Args: + repo_url: GitHub repository URL + token: Personal Access Token + + Returns: + dict with success, message, repo_name, permissions + """ + try: + owner, repo = self._parse_repo_url(repo_url) + client = await self._get_client() + + # Test API access + response = await client.get( + f"https://api.github.com/repos/{owner}/{repo}", + headers={ + "Authorization": f"token {token}", + "Accept": "application/vnd.github.v3+json", + "User-Agent": "Bambuddy-Backup", + }, + ) + + if response.status_code == 401: + return {"success": False, "message": "Invalid access token", "repo_name": None, "permissions": None} + + if response.status_code == 404: + return { + "success": False, + "message": "Repository not found. Check URL and token permissions.", + "repo_name": None, + "permissions": None, + } + + if response.status_code != 200: + return { + "success": False, + "message": f"GitHub API error: {response.status_code}", + "repo_name": None, + "permissions": None, + } + + data = response.json() + permissions = data.get("permissions", {}) + + # Check for push permission + if not permissions.get("push", False): + return { + "success": False, + "message": "Token does not have push permission to this repository", + "repo_name": data.get("full_name"), + "permissions": permissions, + } + + return { + "success": True, + "message": "Connection successful", + "repo_name": data.get("full_name"), + "permissions": permissions, + } + + except Exception as e: + logger.error(f"GitHub connection test failed: {e}") + return {"success": False, "message": str(e), "repo_name": None, "permissions": None} + + def _parse_repo_url(self, url: str) -> tuple[str, str]: + """Parse owner and repo from GitHub URL.""" + # Handle HTTPS URLs + match = re.match(r"https://github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$", url) + if match: + return match.group(1), match.group(2) + + # Handle SSH URLs + match = re.match(r"git@github\.com:([^/]+)/([^/]+?)(?:\.git)?$", url) + if match: + return match.group(1), match.group(2) + + raise ValueError(f"Invalid GitHub URL: {url}") + + async def run_backup(self, config_id: int, trigger: str = "manual") -> dict: + """Run a backup operation. + + Args: + config_id: ID of the backup configuration + trigger: "manual" or "scheduled" + + Returns: + dict with success, message, log_id, commit_sha, files_changed + """ + if self._running_backup: + return {"success": False, "message": "A backup is already running", "log_id": None} + + self._running_backup = True + log_id = None + + try: + async with async_session() as db: + # Get config + result = await db.execute(select(GitHubBackupConfig).where(GitHubBackupConfig.id == config_id)) + config = result.scalar_one_or_none() + + if not config: + return {"success": False, "message": "Configuration not found", "log_id": None} + + if not config.enabled: + return {"success": False, "message": "Backup is disabled", "log_id": None} + + # Create log entry + log = GitHubBackupLog(config_id=config_id, status="running", trigger=trigger) + db.add(log) + await db.commit() + await db.refresh(log) + log_id = log.id + + try: + # Collect backup data + self._backup_progress = "Collecting profiles..." + backup_data = await self._collect_backup_data(db, config) + + if not backup_data: + # No data to backup + log.status = "skipped" + log.completed_at = datetime.now(UTC) + log.error_message = "No data to backup" + config.last_backup_at = datetime.now(UTC) + config.last_backup_status = "skipped" + config.last_backup_message = "No data to backup" + if config.schedule_enabled: + config.next_scheduled_run = self._calculate_next_run(config.schedule_type) + await db.commit() + return { + "success": True, + "message": "No data to backup", + "log_id": log_id, + "commit_sha": None, + "files_changed": 0, + } + + # Push to GitHub + self._backup_progress = "Pushing to GitHub..." + push_result = await self._push_to_github(config, backup_data) + + # Update log and config + log.status = push_result["status"] + log.completed_at = datetime.now(UTC) + log.commit_sha = push_result.get("commit_sha") + log.files_changed = push_result.get("files_changed", 0) + log.error_message = push_result.get("error") + + config.last_backup_at = datetime.now(UTC) + config.last_backup_status = push_result["status"] + config.last_backup_message = push_result.get("message", "") + config.last_backup_commit_sha = push_result.get("commit_sha") + + if config.schedule_enabled: + config.next_scheduled_run = self._calculate_next_run(config.schedule_type) + + await db.commit() + + return { + "success": push_result["status"] in ("success", "skipped"), + "message": push_result.get("message", "Backup completed"), + "log_id": log_id, + "commit_sha": push_result.get("commit_sha"), + "files_changed": push_result.get("files_changed", 0), + } + + except Exception as e: + logger.error(f"Backup failed: {e}") + log.status = "failed" + log.completed_at = datetime.now(UTC) + log.error_message = str(e) + + config.last_backup_at = datetime.now(UTC) + config.last_backup_status = "failed" + config.last_backup_message = str(e) + + if config.schedule_enabled: + config.next_scheduled_run = self._calculate_next_run(config.schedule_type) + + await db.commit() + return { + "success": False, + "message": str(e), + "log_id": log_id, + "commit_sha": None, + "files_changed": 0, + } + + finally: + self._running_backup = False + self._backup_progress = None + + async def _collect_backup_data(self, db: AsyncSession, config: GitHubBackupConfig) -> dict: + """Collect data to backup based on config settings. + + Returns dict with structure: + { + "backup_metadata.json": {...}, + "kprofiles/{serial}/{nozzle}.json": {...}, + "cloud_profiles/filament.json": [...], + "cloud_profiles/printer.json": [...], + "cloud_profiles/process.json": [...], + "settings/app_settings.json": {...}, + } + """ + files: dict[str, dict | list] = {} + now = datetime.now(UTC) + + # Metadata file + metadata = { + "version": "1.0", + "backup_type": "bambuddy_profiles", + "created_at": now.isoformat(), + "contents": { + "kprofiles": config.backup_kprofiles, + "cloud_profiles": config.backup_cloud_profiles, + "settings": config.backup_settings, + }, + } + files["backup_metadata.json"] = metadata + + # Collect K-profiles from all connected printers + if config.backup_kprofiles: + self._backup_progress = "Collecting K-profiles from printers..." + await self._collect_kprofiles(db, files) + + # Collect cloud profiles + if config.backup_cloud_profiles: + self._backup_progress = "Collecting cloud profiles from Bambu Cloud..." + await self._collect_cloud_profiles(db, files) + + # Collect app settings + if config.backup_settings: + self._backup_progress = "Collecting app settings..." + await self._collect_settings(db, files) + + return files + + async def _collect_kprofiles(self, db: AsyncSession, files: dict): + """Collect K-profiles from all connected printers.""" + result = await db.execute(select(Printer).where(Printer.is_active == True)) # noqa: E712 + printers = result.scalars().all() + + nozzle_diameters = ["0.2", "0.4", "0.6", "0.8"] + + for printer in printers: + client = printer_manager.get_client(printer.id) + if not client or not client.state.connected: + continue + + serial = printer.serial_number + printer_profiles = {} + + for nozzle in nozzle_diameters: + try: + profiles = await client.get_kprofiles(nozzle_diameter=nozzle) + if profiles: + profile_data = { + "version": "1.0", + "printer_name": printer.name, + "printer_serial": serial, + "nozzle_diameter": nozzle, + "exported_at": datetime.now(UTC).isoformat(), + "profiles": [ + { + "slot_id": p.slot_id, + "name": p.name, + "k_value": p.k_value, + "filament_id": p.filament_id, + "nozzle_id": p.nozzle_id, + "extruder_id": p.extruder_id, + "setting_id": p.setting_id, + "n_coef": p.n_coef, + } + for p in profiles + ], + } + files[f"kprofiles/{serial}/{nozzle}.json"] = profile_data + printer_profiles[nozzle] = len(profiles) + except Exception as e: + logger.warning(f"Failed to get K-profiles for printer {serial} nozzle {nozzle}: {e}") + + if printer_profiles: + logger.info(f"Collected K-profiles for {serial}: {printer_profiles}") + + async def _collect_cloud_profiles(self, db: AsyncSession, files: dict): + """Collect Bambu Cloud profiles if authenticated.""" + # Check if cloud is authenticated + cloud = get_cloud_service() + + # Try to restore token from DB + result = await db.execute(select(Settings).where(Settings.key == "bambu_cloud_token")) + setting = result.scalar_one_or_none() + if setting and setting.value: + cloud.set_token(setting.value) + + if not cloud.is_authenticated: + logger.info("Cloud not authenticated, skipping cloud profiles") + return + + try: + settings = await cloud.get_slicer_settings() + if not settings: + return + + # Separate by type + filament_settings = [] + printer_settings = [] + process_settings = [] + + for setting in settings.get("setting", []) if isinstance(settings.get("setting"), list) else []: + setting_type = setting.get("type", "") + if setting_type == "filament": + filament_settings.append(setting) + elif setting_type == "printer": + printer_settings.append(setting) + elif setting_type == "process": + process_settings.append(setting) + + if filament_settings: + files["cloud_profiles/filament.json"] = { + "version": "1.0", + "exported_at": datetime.now(UTC).isoformat(), + "profiles": filament_settings, + } + + if printer_settings: + files["cloud_profiles/printer.json"] = { + "version": "1.0", + "exported_at": datetime.now(UTC).isoformat(), + "profiles": printer_settings, + } + + if process_settings: + files["cloud_profiles/process.json"] = { + "version": "1.0", + "exported_at": datetime.now(UTC).isoformat(), + "profiles": process_settings, + } + + logger.info( + f"Collected cloud profiles: {len(filament_settings)} filament, " + f"{len(printer_settings)} printer, {len(process_settings)} process" + ) + + except Exception as e: + logger.warning(f"Failed to collect cloud profiles: {e}") + + async def _collect_settings(self, db: AsyncSession, files: dict): + """Collect app settings.""" + result = await db.execute(select(Settings)) + settings = result.scalars().all() + + # Filter out sensitive settings + sensitive_keys = {"bambu_cloud_token", "auth_secret_key"} + settings_data = {s.key: s.value for s in settings if s.key not in sensitive_keys} + + files["settings/app_settings.json"] = { + "version": "1.0", + "exported_at": datetime.now(UTC).isoformat(), + "settings": settings_data, + } + + async def _push_to_github(self, config: GitHubBackupConfig, files: dict) -> dict: + """Push files to GitHub using the GitHub API. + + Uses the Git Data API to create blobs, tree, and commit. + + Returns: + dict with status, message, commit_sha, files_changed + """ + try: + owner, repo = self._parse_repo_url(config.repository_url) + branch = config.branch + client = await self._get_client() + headers = { + "Authorization": f"token {config.access_token}", + "Accept": "application/vnd.github.v3+json", + "User-Agent": "Bambuddy-Backup", + } + + # Get current branch reference + ref_response = await client.get( + f"https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{branch}", headers=headers + ) + + if ref_response.status_code == 404: + # Branch doesn't exist, need to create it from default branch + return await self._create_branch_and_push(client, headers, owner, repo, branch, files) + + if ref_response.status_code != 200: + return { + "status": "failed", + "message": f"Failed to get branch ref: {ref_response.status_code}", + "error": ref_response.text, + } + + ref_data = ref_response.json() + current_commit_sha = ref_data["object"]["sha"] + + # Get the current tree + commit_response = await client.get( + f"https://api.github.com/repos/{owner}/{repo}/git/commits/{current_commit_sha}", headers=headers + ) + if commit_response.status_code != 200: + return {"status": "failed", "message": "Failed to get current commit"} + + current_tree_sha = commit_response.json()["tree"]["sha"] + + # Get existing files to check for changes + tree_response = await client.get( + f"https://api.github.com/repos/{owner}/{repo}/git/trees/{current_tree_sha}?recursive=1", headers=headers + ) + existing_files = {} + if tree_response.status_code == 200: + for item in tree_response.json().get("tree", []): + if item["type"] == "blob": + existing_files[item["path"]] = item["sha"] + + # Create blobs for changed files + tree_items = [] + files_changed = 0 + + for path, content in files.items(): + content_str = json.dumps(content, indent=2, default=str) + content_bytes = content_str.encode("utf-8") + content_sha = hashlib.sha1(f"blob {len(content_bytes)}\0".encode() + content_bytes).hexdigest() + + # Skip if file hasn't changed + if path in existing_files and existing_files[path] == content_sha: + continue + + # Create blob + blob_response = await client.post( + f"https://api.github.com/repos/{owner}/{repo}/git/blobs", + headers=headers, + json={"content": base64.b64encode(content_bytes).decode(), "encoding": "base64"}, + ) + + if blob_response.status_code != 201: + logger.error(f"Failed to create blob for {path}: {blob_response.text}") + continue + + blob_sha = blob_response.json()["sha"] + tree_items.append({"path": path, "mode": "100644", "type": "blob", "sha": blob_sha}) + files_changed += 1 + + if not tree_items: + return {"status": "skipped", "message": "No changes to commit", "commit_sha": None, "files_changed": 0} + + # Create new tree + tree_response = await client.post( + f"https://api.github.com/repos/{owner}/{repo}/git/trees", + headers=headers, + json={"base_tree": current_tree_sha, "tree": tree_items}, + ) + + if tree_response.status_code != 201: + return {"status": "failed", "message": f"Failed to create tree: {tree_response.text}"} + + new_tree_sha = tree_response.json()["sha"] + + # Create commit + commit_message = f"Bambuddy backup - {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}" + commit_response = await client.post( + f"https://api.github.com/repos/{owner}/{repo}/git/commits", + headers=headers, + json={"message": commit_message, "tree": new_tree_sha, "parents": [current_commit_sha]}, + ) + + if commit_response.status_code != 201: + return {"status": "failed", "message": f"Failed to create commit: {commit_response.text}"} + + new_commit_sha = commit_response.json()["sha"] + + # Update branch reference + ref_update = await client.patch( + f"https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{branch}", + headers=headers, + json={"sha": new_commit_sha}, + ) + + if ref_update.status_code != 200: + return {"status": "failed", "message": f"Failed to update branch: {ref_update.text}"} + + return { + "status": "success", + "message": f"Backup successful - {files_changed} files updated", + "commit_sha": new_commit_sha, + "files_changed": files_changed, + } + + except Exception as e: + logger.error(f"Push to GitHub failed: {e}") + return {"status": "failed", "message": str(e), "error": str(e)} + + async def _create_branch_and_push( + self, client: httpx.AsyncClient, headers: dict, owner: str, repo: str, branch: str, files: dict + ) -> dict: + """Create a new branch and push files when branch doesn't exist.""" + try: + # Get default branch + repo_response = await client.get(f"https://api.github.com/repos/{owner}/{repo}", headers=headers) + if repo_response.status_code != 200: + return {"status": "failed", "message": "Failed to get repo info"} + + default_branch = repo_response.json().get("default_branch", "main") + + # Get default branch ref + ref_response = await client.get( + f"https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{default_branch}", headers=headers + ) + if ref_response.status_code != 200: + # Empty repo - create initial commit + return await self._create_initial_commit(client, headers, owner, repo, branch, files) + + base_sha = ref_response.json()["object"]["sha"] + + # Create new branch + create_ref = await client.post( + f"https://api.github.com/repos/{owner}/{repo}/git/refs", + headers=headers, + json={"ref": f"refs/heads/{branch}", "sha": base_sha}, + ) + + if create_ref.status_code != 201: + return {"status": "failed", "message": f"Failed to create branch: {create_ref.text}"} + + # Now push to the new branch (recursive call will find the branch) + return await self._push_to_github( + type( + "Config", + (), + { + "repository_url": f"https://github.com/{owner}/{repo}", + "access_token": headers["Authorization"].replace("token ", ""), + "branch": branch, + }, + )(), + files, + ) + + except Exception as e: + return {"status": "failed", "message": str(e)} + + async def _create_initial_commit( + self, client: httpx.AsyncClient, headers: dict, owner: str, repo: str, branch: str, files: dict + ) -> dict: + """Create initial commit in an empty repository.""" + try: + # Create blobs + tree_items = [] + for path, content in files.items(): + content_str = json.dumps(content, indent=2, default=str) + blob_response = await client.post( + f"https://api.github.com/repos/{owner}/{repo}/git/blobs", + headers=headers, + json={"content": base64.b64encode(content_str.encode()).decode(), "encoding": "base64"}, + ) + if blob_response.status_code == 201: + tree_items.append( + {"path": path, "mode": "100644", "type": "blob", "sha": blob_response.json()["sha"]} + ) + + # Create tree + tree_response = await client.post( + f"https://api.github.com/repos/{owner}/{repo}/git/trees", + headers=headers, + json={"tree": tree_items}, + ) + if tree_response.status_code != 201: + return {"status": "failed", "message": "Failed to create tree"} + + tree_sha = tree_response.json()["sha"] + + # Create commit (no parents for initial) + commit_response = await client.post( + f"https://api.github.com/repos/{owner}/{repo}/git/commits", + headers=headers, + json={ + "message": f"Initial Bambuddy backup - {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}", + "tree": tree_sha, + }, + ) + if commit_response.status_code != 201: + return {"status": "failed", "message": "Failed to create commit"} + + commit_sha = commit_response.json()["sha"] + + # Create branch ref + ref_response = await client.post( + f"https://api.github.com/repos/{owner}/{repo}/git/refs", + headers=headers, + json={"ref": f"refs/heads/{branch}", "sha": commit_sha}, + ) + if ref_response.status_code != 201: + return {"status": "failed", "message": "Failed to create branch ref"} + + return { + "status": "success", + "message": f"Initial backup created - {len(files)} files", + "commit_sha": commit_sha, + "files_changed": len(files), + } + + except Exception as e: + return {"status": "failed", "message": str(e)} + + @property + def is_running(self) -> bool: + """Check if a backup is currently running.""" + return self._running_backup + + @property + def progress(self) -> str | None: + """Get current backup progress message.""" + return self._backup_progress + + async def get_logs(self, config_id: int, limit: int = 50, offset: int = 0) -> list[GitHubBackupLog]: + """Get backup logs for a configuration.""" + async with async_session() as db: + result = await db.execute( + select(GitHubBackupLog) + .where(GitHubBackupLog.config_id == config_id) + .order_by(desc(GitHubBackupLog.started_at)) + .offset(offset) + .limit(limit) + ) + return list(result.scalars().all()) + + +# Singleton instance +github_backup_service = GitHubBackupService() diff --git a/backend/tests/integration/test_github_backup_api.py b/backend/tests/integration/test_github_backup_api.py new file mode 100644 index 000000000..60488c810 --- /dev/null +++ b/backend/tests/integration/test_github_backup_api.py @@ -0,0 +1,255 @@ +"""Integration tests for GitHub Backup API endpoints.""" + +import pytest +from httpx import AsyncClient + + +class TestGitHubBackupConfigAPI: + """Integration tests for /api/v1/github-backup endpoints.""" + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_get_config_no_config(self, async_client: AsyncClient): + """Verify getting config when none exists returns null.""" + response = await async_client.get("/api/v1/github-backup/config") + assert response.status_code == 200 + assert response.json() is None + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_create_config(self, async_client: AsyncClient): + """Verify GitHub backup config can be created.""" + data = { + "repository_url": "https://github.com/test/repo", + "access_token": "ghp_testtoken123", + "branch": "main", + "schedule_enabled": False, + "schedule_type": "daily", + "backup_kprofiles": True, + "backup_cloud_profiles": True, + "backup_settings": False, + "enabled": True, + } + response = await async_client.post("/api/v1/github-backup/config", json=data) + assert response.status_code == 200 + result = response.json() + assert result["repository_url"] == "https://github.com/test/repo" + assert result["branch"] == "main" + assert result["has_token"] is True + assert result["enabled"] is True + # Token should not be exposed in response + assert "access_token" not in result + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_get_config_after_create(self, async_client: AsyncClient): + """Verify getting config after creation returns the config.""" + # Create config first + data = { + "repository_url": "https://github.com/test/getrepo", + "access_token": "ghp_testtoken456", + "branch": "develop", + "schedule_enabled": True, + "schedule_type": "weekly", + "backup_kprofiles": True, + "backup_cloud_profiles": False, + "backup_settings": True, + "enabled": True, + } + await async_client.post("/api/v1/github-backup/config", json=data) + + # Get config + response = await async_client.get("/api/v1/github-backup/config") + assert response.status_code == 200 + result = response.json() + assert result is not None + assert result["repository_url"] == "https://github.com/test/getrepo" + assert result["branch"] == "develop" + assert result["schedule_type"] == "weekly" + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_update_config_partial(self, async_client: AsyncClient): + """Verify partial update of GitHub backup config.""" + # Create config first + create_data = { + "repository_url": "https://github.com/test/update", + "access_token": "ghp_token", + "branch": "main", + "schedule_enabled": False, + "schedule_type": "daily", + "backup_kprofiles": True, + "backup_cloud_profiles": True, + "backup_settings": False, + "enabled": True, + } + await async_client.post("/api/v1/github-backup/config", json=create_data) + + # Partial update + update_data = { + "branch": "develop", + "schedule_enabled": True, + } + response = await async_client.patch("/api/v1/github-backup/config", json=update_data) + assert response.status_code == 200 + result = response.json() + assert result["branch"] == "develop" + assert result["schedule_enabled"] is True + # Original values should be preserved + assert result["repository_url"] == "https://github.com/test/update" + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_delete_config(self, async_client: AsyncClient): + """Verify GitHub backup config can be deleted.""" + # Create config first + create_data = { + "repository_url": "https://github.com/test/delete", + "access_token": "ghp_deletetoken", + "branch": "main", + "schedule_enabled": False, + "schedule_type": "daily", + "backup_kprofiles": True, + "backup_cloud_profiles": True, + "backup_settings": False, + "enabled": True, + } + await async_client.post("/api/v1/github-backup/config", json=create_data) + + # Delete + response = await async_client.delete("/api/v1/github-backup/config") + assert response.status_code == 200 + + # Verify it's deleted + get_response = await async_client.get("/api/v1/github-backup/config") + assert get_response.status_code == 200 + assert get_response.json() is None + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_delete_config_not_found(self, async_client: AsyncClient): + """Verify deleting non-existent config returns 404.""" + # Make sure no config exists + await async_client.delete("/api/v1/github-backup/config") + + # Try to delete again + response = await async_client.delete("/api/v1/github-backup/config") + assert response.status_code == 404 + + +class TestGitHubBackupStatusAPI: + """Integration tests for /api/v1/github-backup/status endpoint.""" + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_status_no_config(self, async_client: AsyncClient): + """Verify status when no config exists.""" + # Ensure no config + await async_client.delete("/api/v1/github-backup/config") + + response = await async_client.get("/api/v1/github-backup/status") + assert response.status_code == 200 + result = response.json() + assert result["configured"] is False + assert result["enabled"] is False + assert result["is_running"] is False + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_status_with_config(self, async_client: AsyncClient): + """Verify status when config exists.""" + # Create config + create_data = { + "repository_url": "https://github.com/test/status", + "access_token": "ghp_statustoken", + "branch": "main", + "schedule_enabled": True, + "schedule_type": "hourly", + "backup_kprofiles": True, + "backup_cloud_profiles": True, + "backup_settings": False, + "enabled": True, + } + await async_client.post("/api/v1/github-backup/config", json=create_data) + + response = await async_client.get("/api/v1/github-backup/status") + assert response.status_code == 200 + result = response.json() + assert result["configured"] is True + assert result["enabled"] is True + assert result["is_running"] is False + assert result["next_scheduled_run"] is not None + + +class TestGitHubBackupLogsAPI: + """Integration tests for /api/v1/github-backup/logs endpoints.""" + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_logs_no_config(self, async_client: AsyncClient): + """Verify getting logs when no config exists returns empty list.""" + # Ensure no config + await async_client.delete("/api/v1/github-backup/config") + + response = await async_client.get("/api/v1/github-backup/logs") + assert response.status_code == 200 + assert response.json() == [] + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_logs_with_config(self, async_client: AsyncClient): + """Verify getting logs with config.""" + # Create config + create_data = { + "repository_url": "https://github.com/test/logs", + "access_token": "ghp_logstoken", + "branch": "main", + "schedule_enabled": False, + "schedule_type": "daily", + "backup_kprofiles": True, + "backup_cloud_profiles": True, + "backup_settings": False, + "enabled": True, + } + await async_client.post("/api/v1/github-backup/config", json=create_data) + + response = await async_client.get("/api/v1/github-backup/logs") + assert response.status_code == 200 + # No backups run yet, so empty list + assert response.json() == [] + + +class TestGitHubBackupTriggerAPI: + """Integration tests for /api/v1/github-backup/run endpoint.""" + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_trigger_no_config(self, async_client: AsyncClient): + """Verify triggering backup without config returns 404.""" + # Ensure no config + await async_client.delete("/api/v1/github-backup/config") + + response = await async_client.post("/api/v1/github-backup/run") + assert response.status_code == 404 + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_trigger_disabled_config(self, async_client: AsyncClient): + """Verify triggering backup with disabled config returns 400.""" + # Create disabled config + create_data = { + "repository_url": "https://github.com/test/trigger", + "access_token": "ghp_triggertoken", + "branch": "main", + "schedule_enabled": False, + "schedule_type": "daily", + "backup_kprofiles": True, + "backup_cloud_profiles": True, + "backup_settings": False, + "enabled": False, # Disabled + } + await async_client.post("/api/v1/github-backup/config", json=create_data) + + response = await async_client.post("/api/v1/github-backup/run") + assert response.status_code == 400 + assert "disabled" in response.json()["detail"].lower() diff --git a/frontend/src/__tests__/api/githubBackupApi.test.ts b/frontend/src/__tests__/api/githubBackupApi.test.ts new file mode 100644 index 000000000..851ba074b --- /dev/null +++ b/frontend/src/__tests__/api/githubBackupApi.test.ts @@ -0,0 +1,283 @@ +/** + * Tests for the GitHub Backup API client functions. + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import type { + GitHubBackupConfig, + GitHubBackupStatus, + GitHubBackupLog, +} from '../../api/client'; + +// Mock API base URL +const API_BASE = 'http://localhost:5000/api/v1'; + +// Create MSW server +const server = setupServer(); + +beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' })); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +describe('GitHub Backup API Types', () => { + it('GitHubBackupConfig has correct shape', () => { + const config: GitHubBackupConfig = { + id: 1, + repository_url: 'https://github.com/test/repo', + has_token: true, + branch: 'main', + schedule_enabled: true, + schedule_type: 'daily', + backup_kprofiles: true, + backup_cloud_profiles: true, + backup_settings: false, + enabled: true, + last_backup_at: '2026-01-27T10:00:00Z', + last_backup_status: 'success', + last_backup_message: null, + last_backup_commit_sha: 'abc123', + next_scheduled_run: '2026-01-28T00:00:00Z', + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-27T10:00:00Z', + }; + + expect(config.id).toBe(1); + expect(config.has_token).toBe(true); + expect(config.schedule_type).toBe('daily'); + }); + + it('GitHubBackupStatus has correct shape', () => { + const status: GitHubBackupStatus = { + configured: true, + enabled: true, + is_running: false, + progress: null, + last_backup_at: '2026-01-27T10:00:00Z', + last_backup_status: 'success', + next_scheduled_run: '2026-01-28T00:00:00Z', + }; + + expect(status.configured).toBe(true); + expect(status.is_running).toBe(false); + }); + + it('GitHubBackupStatus can have progress', () => { + const status: GitHubBackupStatus = { + configured: true, + enabled: true, + is_running: true, + progress: 'Pushing to GitHub...', + last_backup_at: null, + last_backup_status: null, + next_scheduled_run: null, + }; + + expect(status.is_running).toBe(true); + expect(status.progress).toBe('Pushing to GitHub...'); + }); + + it('GitHubBackupLog has correct shape', () => { + const log: GitHubBackupLog = { + id: 1, + config_id: 1, + started_at: '2026-01-27T10:00:00Z', + completed_at: '2026-01-27T10:01:00Z', + status: 'success', + trigger: 'manual', + commit_sha: 'abc123', + files_changed: 5, + error_message: null, + }; + + expect(log.status).toBe('success'); + expect(log.trigger).toBe('manual'); + expect(log.files_changed).toBe(5); + }); + + it('GitHubBackupLog can have error', () => { + const log: GitHubBackupLog = { + id: 2, + config_id: 1, + started_at: '2026-01-27T10:00:00Z', + completed_at: '2026-01-27T10:00:30Z', + status: 'failed', + trigger: 'scheduled', + commit_sha: null, + files_changed: 0, + error_message: 'Authentication failed', + }; + + expect(log.status).toBe('failed'); + expect(log.error_message).toBe('Authentication failed'); + expect(log.commit_sha).toBeNull(); + }); +}); + +describe('GitHub Backup API Endpoints', () => { + it('GET /github-backup/config returns null when not configured', async () => { + server.use( + http.get(`${API_BASE}/github-backup/config`, () => { + return HttpResponse.json(null); + }) + ); + + const response = await fetch(`${API_BASE}/github-backup/config`); + const data = await response.json(); + expect(data).toBeNull(); + }); + + it('GET /github-backup/config returns config when exists', async () => { + const mockConfig: GitHubBackupConfig = { + id: 1, + repository_url: 'https://github.com/test/repo', + has_token: true, + branch: 'main', + schedule_enabled: false, + schedule_type: 'daily', + backup_kprofiles: true, + backup_cloud_profiles: true, + backup_settings: false, + enabled: true, + last_backup_at: null, + last_backup_status: null, + last_backup_message: null, + last_backup_commit_sha: null, + next_scheduled_run: null, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + }; + + server.use( + http.get(`${API_BASE}/github-backup/config`, () => { + return HttpResponse.json(mockConfig); + }) + ); + + const response = await fetch(`${API_BASE}/github-backup/config`); + const data = await response.json(); + expect(data.repository_url).toBe('https://github.com/test/repo'); + expect(data.has_token).toBe(true); + }); + + it('GET /github-backup/status returns not configured status', async () => { + const mockStatus: GitHubBackupStatus = { + configured: false, + enabled: false, + is_running: false, + progress: null, + last_backup_at: null, + last_backup_status: null, + next_scheduled_run: null, + }; + + server.use( + http.get(`${API_BASE}/github-backup/status`, () => { + return HttpResponse.json(mockStatus); + }) + ); + + const response = await fetch(`${API_BASE}/github-backup/status`); + const data = await response.json(); + expect(data.configured).toBe(false); + expect(data.enabled).toBe(false); + }); + + it('GET /github-backup/logs returns empty list when no logs', async () => { + server.use( + http.get(`${API_BASE}/github-backup/logs`, () => { + return HttpResponse.json([]); + }) + ); + + const response = await fetch(`${API_BASE}/github-backup/logs`); + const data = await response.json(); + expect(data).toEqual([]); + }); + + it('GET /github-backup/logs returns log entries', async () => { + const mockLogs: GitHubBackupLog[] = [ + { + id: 1, + config_id: 1, + started_at: '2026-01-27T10:00:00Z', + completed_at: '2026-01-27T10:01:00Z', + status: 'success', + trigger: 'manual', + commit_sha: 'abc123', + files_changed: 5, + error_message: null, + }, + ]; + + server.use( + http.get(`${API_BASE}/github-backup/logs`, () => { + return HttpResponse.json(mockLogs); + }) + ); + + const response = await fetch(`${API_BASE}/github-backup/logs`); + const data = await response.json(); + expect(data.length).toBe(1); + expect(data[0].status).toBe('success'); + }); + + it('POST /github-backup/run returns 404 when not configured', async () => { + server.use( + http.post(`${API_BASE}/github-backup/run`, () => { + return HttpResponse.json( + { detail: 'No configuration found' }, + { status: 404 } + ); + }) + ); + + const response = await fetch(`${API_BASE}/github-backup/run`, { + method: 'POST', + }); + expect(response.status).toBe(404); + }); + + it('POST /github-backup/test returns success on valid credentials', async () => { + server.use( + http.post(`${API_BASE}/github-backup/test`, () => { + return HttpResponse.json({ + success: true, + message: 'Connection successful', + repo_name: 'test/repo', + default_branch: 'main', + }); + }) + ); + + const response = await fetch( + `${API_BASE}/github-backup/test?repo_url=https://github.com/test/repo&token=ghp_test`, + { method: 'POST' } + ); + const data = await response.json(); + expect(data.success).toBe(true); + expect(data.repo_name).toBe('test/repo'); + }); + + it('POST /github-backup/test returns failure on invalid credentials', async () => { + server.use( + http.post(`${API_BASE}/github-backup/test`, () => { + return HttpResponse.json({ + success: false, + message: 'Authentication failed', + repo_name: null, + default_branch: null, + }); + }) + ); + + const response = await fetch( + `${API_BASE}/github-backup/test?repo_url=https://github.com/test/repo&token=invalid`, + { method: 'POST' } + ); + const data = await response.json(); + expect(data.success).toBe(false); + expect(data.message).toBe('Authentication failed'); + }); +}); diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 1b0617af7..6445c21a8 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1296,6 +1296,78 @@ export interface NotificationProviderUpdate { printer_id?: number | null; } +// GitHub Backup types +export type ScheduleType = 'hourly' | 'daily' | 'weekly'; + +export interface GitHubBackupConfig { + id: number; + repository_url: string; + has_token: boolean; + branch: string; + schedule_enabled: boolean; + schedule_type: ScheduleType; + backup_kprofiles: boolean; + backup_cloud_profiles: boolean; + backup_settings: boolean; + enabled: boolean; + last_backup_at: string | null; + last_backup_status: string | null; + last_backup_message: string | null; + last_backup_commit_sha: string | null; + next_scheduled_run: string | null; + created_at: string; + updated_at: string; +} + +export interface GitHubBackupConfigCreate { + repository_url: string; + access_token: string; + branch?: string; + schedule_enabled?: boolean; + schedule_type?: ScheduleType; + backup_kprofiles?: boolean; + backup_cloud_profiles?: boolean; + backup_settings?: boolean; + enabled?: boolean; +} + +export interface GitHubBackupLog { + id: number; + config_id: number; + started_at: string; + completed_at: string | null; + status: string; + trigger: string; + commit_sha: string | null; + files_changed: number; + error_message: string | null; +} + +export interface GitHubBackupStatus { + configured: boolean; + enabled: boolean; + is_running: boolean; + progress: string | null; + last_backup_at: string | null; + last_backup_status: string | null; + next_scheduled_run: string | null; +} + +export interface GitHubTestConnectionResponse { + success: boolean; + message: string; + repo_name: string | null; + permissions: Record | null; +} + +export interface GitHubBackupTriggerResponse { + success: boolean; + message: string; + log_id: number | null; + commit_sha: string | null; + files_changed: number; +} + export interface NotificationTestRequest { provider_type: ProviderType; config: Record; @@ -3038,6 +3110,46 @@ export const api = { used_meters: number; }>; }>(`/library/files/${fileId}/filament-requirements${plateId !== undefined ? `?plate_id=${plateId}` : ''}`), + + // GitHub Backup + getGitHubBackupConfig: () => + request('/github-backup/config'), + + saveGitHubBackupConfig: (config: GitHubBackupConfigCreate) => + request('/github-backup/config', { + method: 'POST', + body: JSON.stringify(config), + }), + + updateGitHubBackupConfig: (config: Partial) => + request('/github-backup/config', { + method: 'PATCH', + body: JSON.stringify(config), + }), + + deleteGitHubBackupConfig: () => + request<{ message: string }>('/github-backup/config', { method: 'DELETE' }), + + testGitHubConnection: (repoUrl: string, token: string) => + request( + `/github-backup/test?repo_url=${encodeURIComponent(repoUrl)}&token=${encodeURIComponent(token)}`, + { method: 'POST' } + ), + + testGitHubStoredConnection: () => + request('/github-backup/test-stored', { method: 'POST' }), + + triggerGitHubBackup: () => + request('/github-backup/run', { method: 'POST' }), + + getGitHubBackupStatus: () => + request('/github-backup/status'), + + getGitHubBackupLogs: (limit: number = 50) => + request(`/github-backup/logs?limit=${limit}`), + + clearGitHubBackupLogs: (keepLast: number = 10) => + request<{ deleted: number; message: string }>(`/github-backup/logs?keep_last=${keepLast}`, { method: 'DELETE' }), }; // AMS History types diff --git a/frontend/src/components/GitHubBackupSettings.tsx b/frontend/src/components/GitHubBackupSettings.tsx new file mode 100644 index 000000000..9b70207fb --- /dev/null +++ b/frontend/src/components/GitHubBackupSettings.tsx @@ -0,0 +1,746 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + Github, + Play, + Clock, + CheckCircle, + XCircle, + Loader2, + Eye, + EyeOff, + ExternalLink, + RefreshCw, + Download, + Upload, + Database, + History, + SkipForward, + AlertTriangle, + Trash2, +} from 'lucide-react'; +import { api } from '../api/client'; +import type { + GitHubBackupConfig, + GitHubBackupConfigCreate, + GitHubBackupLog, + GitHubBackupStatus, + GitHubBackupTriggerResponse, + ScheduleType, + CloudAuthStatus, +} from '../api/client'; +import { Card, CardContent, CardHeader } from './Card'; +import { Button } from './Button'; +import { Toggle } from './Toggle'; +import { BackupModal } from './BackupModal'; +import { RestoreModal } from './RestoreModal'; +import { useToast } from '../contexts/ToastContext'; + +interface StatusBadgeProps { + status: string | null; +} + +function StatusBadge({ status }: StatusBadgeProps) { + if (!status) return null; + + const styles: Record = { + success: 'bg-green-500/20 text-green-400', + failed: 'bg-red-500/20 text-red-400', + skipped: 'bg-yellow-500/20 text-yellow-400', + running: 'bg-blue-500/20 text-blue-400', + }; + + const icons: Record = { + success: , + failed: , + skipped: , + running: , + }; + + return ( + + {icons[status]} + {status.charAt(0).toUpperCase() + status.slice(1)} + + ); +} + +function formatDateTime(dateStr: string | null): string { + if (!dateStr) return '-'; + const date = new Date(dateStr); + return date.toLocaleString(); +} + +function formatRelativeTime(dateStr: string | null): string { + if (!dateStr) return '-'; + const date = new Date(dateStr); + const now = new Date(); + const diffMs = date.getTime() - now.getTime(); + const diffMins = Math.round(diffMs / 60000); + + if (diffMins < 0) { + const absMins = Math.abs(diffMins); + if (absMins < 60) return `${absMins}m ago`; + const hours = Math.floor(absMins / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; + } else { + if (diffMins < 60) return `in ${diffMins}m`; + const hours = Math.floor(diffMins / 60); + if (hours < 24) return `in ${hours}h`; + const days = Math.floor(hours / 24); + return `in ${days}d`; + } +} + +export function GitHubBackupSettings() { + const queryClient = useQueryClient(); + const { showToast } = useToast(); + + // Local state for form + const [repoUrl, setRepoUrl] = useState(''); + const [accessToken, setAccessToken] = useState(''); + const [branch, setBranch] = useState('main'); + const [showToken, setShowToken] = useState(false); + const [scheduleEnabled, setScheduleEnabled] = useState(false); + const [scheduleType, setScheduleType] = useState('daily'); + const [backupKProfiles, setBackupKProfiles] = useState(true); + const [backupCloudProfiles, setBackupCloudProfiles] = useState(true); + const [backupSettings, setBackupSettings] = useState(false); + const [enabled, setEnabled] = useState(true); + + // Local backup modals + const [showBackupModal, setShowBackupModal] = useState(false); + const [showRestoreModal, setShowRestoreModal] = useState(false); + + // Test connection state + const [testLoading, setTestLoading] = useState(false); + const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); + + // Auto-save debounce + const autoSaveTimerRef = useRef(null); + const isInitializedRef = useRef(false); + + // Queries + const { data: config, isLoading: configLoading } = useQuery({ + queryKey: ['github-backup-config'], + queryFn: api.getGitHubBackupConfig, + }); + + const { data: status } = useQuery({ + queryKey: ['github-backup-status'], + queryFn: api.getGitHubBackupStatus, + refetchInterval: (query) => query.state.data?.is_running ? 500 : 10000, // Poll fast during backup + }); + + const { data: logs } = useQuery({ + queryKey: ['github-backup-logs'], + queryFn: () => api.getGitHubBackupLogs(20), + }); + + const { data: cloudStatus } = useQuery({ + queryKey: ['cloud-status'], + queryFn: api.getCloudStatus, + }); + + // Initialize form from config + useEffect(() => { + if (config) { + setRepoUrl(config.repository_url); + setBranch(config.branch); + setScheduleEnabled(config.schedule_enabled); + setScheduleType(config.schedule_type); + setBackupKProfiles(config.backup_kprofiles); + setBackupCloudProfiles(config.backup_cloud_profiles); + setBackupSettings(config.backup_settings); + setEnabled(config.enabled); + setAccessToken(''); // Don't show stored token + // Mark as initialized after a tick to avoid auto-save on initial load + setTimeout(() => { isInitializedRef.current = true; }, 100); + } + }, [config]); + + // Auto-save function for existing configs + const autoSave = useCallback(async (includeToken: boolean = false) => { + if (!config?.has_token) return; // Only auto-save if config already exists + + try { + if (includeToken && accessToken) { + // Full save with new token + await api.saveGitHubBackupConfig({ + repository_url: repoUrl, + access_token: accessToken, + branch, + schedule_enabled: scheduleEnabled, + schedule_type: scheduleType, + backup_kprofiles: backupKProfiles, + backup_cloud_profiles: backupCloudProfiles, + backup_settings: backupSettings, + enabled, + }); + setAccessToken(''); // Clear after save + showToast('Token updated'); + } else { + // Update without token + await api.updateGitHubBackupConfig({ + repository_url: repoUrl, + branch, + schedule_enabled: scheduleEnabled, + schedule_type: scheduleType, + backup_kprofiles: backupKProfiles, + backup_cloud_profiles: backupCloudProfiles, + backup_settings: backupSettings, + enabled, + }); + showToast('Settings saved'); + } + queryClient.invalidateQueries({ queryKey: ['github-backup-config'] }); + queryClient.invalidateQueries({ queryKey: ['github-backup-status'] }); + } catch (error) { + showToast(`Failed to save: ${(error as Error).message}`, 'error'); + } + }, [config?.has_token, repoUrl, accessToken, branch, scheduleEnabled, scheduleType, backupKProfiles, backupCloudProfiles, backupSettings, enabled, queryClient, showToast]); + + // Auto-save effect for existing configs (debounced) + useEffect(() => { + if (!isInitializedRef.current || !config?.has_token) return; + + if (autoSaveTimerRef.current) { + clearTimeout(autoSaveTimerRef.current); + } + + autoSaveTimerRef.current = setTimeout(() => { + autoSave(false); + }, 500); + + return () => { + if (autoSaveTimerRef.current) { + clearTimeout(autoSaveTimerRef.current); + } + }; + }, [repoUrl, branch, scheduleEnabled, scheduleType, backupKProfiles, backupCloudProfiles, backupSettings, enabled, autoSave, config?.has_token]); + + // Auto-save token when it changes (with longer debounce) + useEffect(() => { + if (!isInitializedRef.current || !config?.has_token || !accessToken) return; + + if (autoSaveTimerRef.current) { + clearTimeout(autoSaveTimerRef.current); + } + + autoSaveTimerRef.current = setTimeout(() => { + autoSave(true); + }, 1000); + + return () => { + if (autoSaveTimerRef.current) { + clearTimeout(autoSaveTimerRef.current); + } + }; + }, [accessToken, autoSave, config?.has_token]); + + // Mutations + const saveConfigMutation = useMutation({ + mutationFn: (data: GitHubBackupConfigCreate) => api.saveGitHubBackupConfig(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['github-backup-config'] }); + queryClient.invalidateQueries({ queryKey: ['github-backup-status'] }); + showToast('GitHub backup enabled'); + setAccessToken(''); + isInitializedRef.current = true; + }, + onError: (error: Error) => { + showToast(`Failed to save: ${error.message}`, 'error'); + }, + }); + + const triggerBackupMutation = useMutation({ + mutationFn: api.triggerGitHubBackup, + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey: ['github-backup-status'] }); + queryClient.invalidateQueries({ queryKey: ['github-backup-logs'] }); + if (result.success) { + if (result.files_changed > 0) { + showToast(`Backup complete - ${result.files_changed} files updated`); + } else { + showToast('Backup skipped - no changes'); + } + } else { + showToast(`Backup failed: ${result.message}`, 'error'); + } + }, + onError: (error: Error) => { + showToast(`Backup failed: ${error.message}`, 'error'); + }, + }); + + const clearLogsMutation = useMutation<{ deleted: number; message: string }, Error>({ + mutationFn: () => api.clearGitHubBackupLogs(10), + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey: ['github-backup-logs'] }); + showToast(`Cleared ${result.deleted} logs`); + }, + onError: (error: Error) => { + showToast(`Failed to clear logs: ${error.message}`, 'error'); + }, + }); + + const handleTestConnection = async () => { + setTestLoading(true); + setTestResult(null); + try { + let result; + // If user entered a new token, test with those credentials + if (accessToken) { + if (!repoUrl) { + showToast('Enter repository URL', 'error'); + setTestLoading(false); + return; + } + result = await api.testGitHubConnection(repoUrl, accessToken); + } else if (config?.has_token) { + // Use stored credentials + result = await api.testGitHubStoredConnection(); + } else { + showToast('Enter repository URL and access token', 'error'); + setTestLoading(false); + return; + } + setTestResult({ success: result.success, message: result.message }); + } catch (error) { + setTestResult({ success: false, message: (error as Error).message }); + } finally { + setTestLoading(false); + } + }; + + // Initial setup save (only for new configs) + const handleInitialSetup = () => { + if (!repoUrl) { + showToast('Repository URL is required', 'error'); + return; + } + if (!accessToken) { + showToast('Access token is required', 'error'); + return; + } + + saveConfigMutation.mutate({ + repository_url: repoUrl, + access_token: accessToken, + branch, + schedule_enabled: scheduleEnabled, + schedule_type: scheduleType, + backup_kprofiles: backupKProfiles, + backup_cloud_profiles: backupCloudProfiles, + backup_settings: backupSettings, + enabled, + }); + }; + + if (configLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Left Column - GitHub Backup */} +
+ + +
+
+ +

GitHub Backup

+
+ {config && cloudStatus?.is_authenticated && ( +
+ Enabled + +
+ )} +
+
+ + {/* Bambu Cloud required message */} + {!cloudStatus?.is_authenticated ? ( +
+ +

+ Bambu Cloud login required. Sign in under Profiles → Cloud Profiles to enable GitHub backup. +

+
+ ) : ( + <> +

+ Automatically sync your profiles to a private GitHub repository for backup and version history. +

+ + {/* Repository URL */} +
+ + { setRepoUrl(e.target.value); setTestResult(null); }} + placeholder="https://github.com/username/bambuddy-backup" + className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" + /> +
+ + {/* Access Token */} +
+ +
+ { setAccessToken(e.target.value); setTestResult(null); }} + placeholder={config?.has_token ? 'Enter new token to update' : 'ghp_xxxxxxxxxxxx'} + className="w-full px-3 py-2 pr-10 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" + /> + +
+

+ Fine-grained token with Contents read/write permission +

+
+ + {/* Branch - inline with schedule */} +
+
+ + setBranch(e.target.value)} + placeholder="main" + className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" + /> +
+
+ + +
+
+ + {/* What to backup */} +
+ +
+ + + +
+
+ + {/* Test + Status + Actions */} +
+ {/* Status line */} + {status?.configured && ( +
+
+ {status.last_backup_at ? ( + <> + Last backup: {formatRelativeTime(status.last_backup_at)} + + + ) : ( + No backups yet + )} +
+ {status.next_scheduled_run && ( + + + Next: {formatRelativeTime(status.next_scheduled_run)} + + )} +
+ )} + + {/* Test result */} + {testResult && ( +
+ {testResult.success ? : } + {testResult.message} +
+ )} + + {/* Action buttons */} +
+ {status?.configured ? ( + <> + {(triggerBackupMutation.isPending || status.is_running) ? ( +
+ + {status.progress || 'Starting backup...'} +
+ ) : ( + <> + + + + )} + + ) : ( + <> + + + + )} +
+
+ + )} +
+
+ + {/* Backup History - only show if configured and has logs */} + {logs && logs.length > 0 && ( + + +
+
+ +

History

+
+ +
+
+ +
+ + + + + + + + + + {logs.slice(0, 10).map((log) => ( + + + + + + ))} + +
DateStatusCommit
{formatDateTime(log.started_at)} + {log.commit_sha ? ( + + {log.commit_sha.substring(0, 7)} + + + ) : ( + - + )} +
+
+
+
+ )} +
+ + {/* Right Column - Local Backup */} +
+ + +
+ +

Local Backup

+
+
+ +

+ Export or import your Bambuddy data as a local file for manual backup or migration. +

+ +
+
+

Export Data

+

+ Download all settings, printers, and profiles +

+
+ +
+ +
+
+

Import Backup

+

+ Restore from a previous export file +

+
+ +
+
+
+
+ + {/* Modals */} + {showBackupModal && ( + setShowBackupModal(false)} + onExport={async (categories) => { + setShowBackupModal(false); + try { + const { blob, filename } = await api.exportBackup(categories); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + showToast('Backup downloaded successfully'); + } catch { + showToast('Failed to create backup', 'error'); + } + }} + /> + )} + + {showRestoreModal && ( + setShowRestoreModal(false)} + onRestore={async (file, overwrite) => { + return await api.importBackup(file, overwrite); + }} + onSuccess={() => { + setShowRestoreModal(false); + showToast('Backup restored successfully'); + queryClient.invalidateQueries(); + }} + /> + )} +
+ ); +} diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index e66c0ea15..492ffa7e4 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -1,11 +1,11 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Upload, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, X, Shield, Printer, Cylinder, Wifi, Home, Video, Users, Lock, Unlock } from 'lucide-react'; +import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, X, Shield, Printer, Cylinder, Wifi, Home, Video, Users, Lock, Unlock } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { api } from '../api/client'; import { useAuth } from '../contexts/AuthContext'; import { formatDateOnly } from '../utils/date'; -import type { AppSettings, AppSettingsUpdate, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus } from '../api/client'; +import type { AppSettings, AppSettingsUpdate, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus, GitHubBackupStatus, CloudAuthStatus } from '../api/client'; import { Card, CardContent, CardHeader } from '../components/Card'; import { Button } from '../components/Button'; import { SmartPlugCard } from '../components/SmartPlugCard'; @@ -15,11 +15,10 @@ import { AddNotificationModal } from '../components/AddNotificationModal'; import { NotificationTemplateEditor } from '../components/NotificationTemplateEditor'; import { NotificationLogViewer } from '../components/NotificationLogViewer'; import { ConfirmModal } from '../components/ConfirmModal'; -import { BackupModal } from '../components/BackupModal'; -import { RestoreModal } from '../components/RestoreModal'; import { SpoolmanSettings } from '../components/SpoolmanSettings'; import { ExternalLinksSettings } from '../components/ExternalLinksSettings'; import { VirtualPrinterSettings } from '../components/VirtualPrinterSettings'; +import { GitHubBackupSettings } from '../components/GitHubBackupSettings'; import { APIBrowser } from '../components/APIBrowser'; import { virtualPrinterApi } from '../api/client'; import { defaultNavItems, getDefaultView, setDefaultView } from '../components/Layout'; @@ -29,7 +28,7 @@ import { useTheme, type ThemeStyle, type DarkBackground, type LightBackground, t import { useState, useEffect, useRef, useCallback } from 'react'; import { Palette } from 'lucide-react'; -const validTabs = ['general', 'network', 'plugs', 'notifications', 'filament', 'apikeys', 'virtual-printer', 'users'] as const; +const validTabs = ['general', 'network', 'plugs', 'notifications', 'filament', 'apikeys', 'virtual-printer', 'users', 'backup'] as const; type TabType = typeof validTabs[number]; export function SettingsPage() { @@ -37,7 +36,7 @@ export function SettingsPage() { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const { t, i18n } = useTranslation(); - const { showToast, showPersistentToast, dismissToast } = useToast(); + const { showToast } = useToast(); const { authEnabled, user, refreshAuth } = useAuth(); const { mode, @@ -85,8 +84,6 @@ export function SettingsPage() { const [showClearLogsConfirm, setShowClearLogsConfirm] = useState(false); const [showClearStorageConfirm, setShowClearStorageConfirm] = useState(false); const [showBulkPlugConfirm, setShowBulkPlugConfirm] = useState<'on' | 'off' | null>(null); - const [showBackupModal, setShowBackupModal] = useState(false); - const [showRestoreModal, setShowRestoreModal] = useState(false); const [showReleaseNotes, setShowReleaseNotes] = useState(false); const [showDisableAuthConfirm, setShowDisableAuthConfirm] = useState(false); @@ -254,6 +251,18 @@ export function SettingsPage() { refetchInterval: activeTab === 'network' ? 5000 : false, // Poll every 5s when on Network tab }); + // GitHub backup status for Backup tab indicator + const { data: githubBackupStatus } = useQuery({ + queryKey: ['github-backup-status'], + queryFn: api.getGitHubBackupStatus, + }); + + // Cloud auth status for Backup tab indicator + const { data: cloudAuthStatus } = useQuery({ + queryKey: ['cloud-status'], + queryFn: api.getCloudStatus, + }); + const applyUpdateMutation = useMutation({ mutationFn: api.applyUpdate, onSuccess: (data) => { @@ -694,6 +703,18 @@ export function SettingsPage() { )} + {/* General Tab */} @@ -1390,58 +1411,22 @@ export function SettingsPage() {

Data Management

- {/* Backup/Restore */}
-

Backup Data

+

Clear Notification Logs

- Export settings, providers, printers, and more + Delete notification logs older than 30 days

-
-
-

Restore Backup

-

- Import settings from a backup file with duplicate handling options -

-
- -
- -
-
-
-

Clear Notification Logs

-

- Delete notification logs older than 30 days -

-
- -
-

Reset UI Preferences

@@ -1458,6 +1443,22 @@ export function SettingsPage() { Reset
+
+
+

Backup & Restore

+

+ Export/import settings and configure GitHub backup +

+
+ +
@@ -2877,73 +2878,6 @@ export function SettingsPage() { /> )} - {/* Backup Modal */} - {showBackupModal && ( - setShowBackupModal(false)} - onExport={async (categories) => { - setShowBackupModal(false); - const toastId = 'backup-progress'; - const includesArchives = categories.archives; - - // Show persistent loading toast for archive backups (can be large) - if (includesArchives) { - showPersistentToast(toastId, t('backup.preparing', { defaultValue: 'Preparing backup...' }), 'loading'); - } - - try { - const { blob, filename } = await api.exportBackup(categories); - - // Dismiss loading toast before download starts - if (includesArchives) { - dismissToast(toastId); - } - - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - a.click(); - URL.revokeObjectURL(url); - showToast(t('backup.downloaded', { defaultValue: 'Backup downloaded' }), 'success'); - } catch { - // Dismiss loading toast on error - if (includesArchives) { - dismissToast(toastId); - } - showToast(t('backup.failed', { defaultValue: 'Failed to create backup' }), 'error'); - } - }} - /> - )} - - {/* Restore Modal */} - {showRestoreModal && ( - setShowRestoreModal(false)} - onRestore={async (file, overwrite) => { - return await api.importBackup(file, overwrite); - }} - onSuccess={() => { - // Reset local settings to force re-sync from restored data - setLocalSettings(null); - isInitialLoadRef.current = true; - // Use resetQueries to clear cached data completely - // This ensures fresh data is fetched, not stale cache - queryClient.resetQueries({ queryKey: ['settings'] }); - // Invalidate other queries that may have changed - queryClient.invalidateQueries({ queryKey: ['notification-providers'] }); - queryClient.invalidateQueries({ queryKey: ['notification-templates'] }); - queryClient.invalidateQueries({ queryKey: ['smart-plugs'] }); - queryClient.invalidateQueries({ queryKey: ['external-links'] }); - queryClient.invalidateQueries({ queryKey: ['printers'] }); - queryClient.invalidateQueries({ queryKey: ['filaments'] }); - queryClient.invalidateQueries({ queryKey: ['maintenance-types'] }); - queryClient.invalidateQueries({ queryKey: ['api-keys'] }); - }} - /> - )} - {/* Release Notes Modal */} {showReleaseNotes && updateCheck?.release_notes && (
)} + {/* Backup Tab */} + {activeTab === 'backup' && ( + + )} + {/* Disable Authentication Confirmation Modal */} {showDisableAuthConfirm && ( :not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}.gap-x-6{column-gap:calc(var(--spacing)*6)}.gap-y-1{row-gap:calc(var(--spacing)*1)}.gap-y-3{row-gap:calc(var(--spacing)*3)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-bambu-dark-tertiary>:not(:last-child)),:where(.divide-bambu-dark-tertiary\/30>:not(:last-child)){border-color:var(--color-bambu-dark-tertiary)}@supports (color:color-mix(in lab,red,red)){:where(.divide-bambu-dark-tertiary\/30>:not(:last-child)){border-color:color-mix(in oklab,var(--color-bambu-dark-tertiary)30%,transparent)}}:where(.divide-bambu-gray\/20>:not(:last-child)){border-color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){:where(.divide-bambu-gray\/20>:not(:last-child)){border-color:color-mix(in oklab,var(--color-bambu-gray)20%,transparent)}}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-t-xl{border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl)}.\!rounded-l-none{border-top-left-radius:0!important;border-bottom-left-radius:0!important}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-l-lg{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.rounded-tl{border-top-left-radius:.25rem}.\!rounded-r-none{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-r-lg{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-\[5px\]{border-top-style:var(--tw-border-style);border-top-width:5px}.border-t-\[6px\]{border-top-style:var(--tw-border-style);border-top-width:6px}.\!border-r-0{border-right-style:var(--tw-border-style)!important;border-right-width:0!important}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-r-\[5px\]{border-right-style:var(--tw-border-style);border-right-width:5px}.border-r-\[6px\]{border-right-style:var(--tw-border-style);border-right-width:6px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-b-\[6px\]{border-bottom-style:var(--tw-border-style);border-bottom-width:6px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-l-\[5px\]{border-left-style:var(--tw-border-style);border-left-width:5px}.border-l-\[6px\]{border-left-style:var(--tw-border-style);border-left-width:6px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.\!border-green-500{border-color:var(--color-green-500)!important}.border-amber-500\/20{border-color:#f99c0033}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/20{border-color:color-mix(in oklab,var(--color-amber-500)20%,transparent)}}.border-amber-500\/30{border-color:#f99c004d}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/30{border-color:color-mix(in oklab,var(--color-amber-500)30%,transparent)}}.border-bambu-dark-tertiary,.border-bambu-dark-tertiary\/30{border-color:var(--color-bambu-dark-tertiary)}@supports (color:color-mix(in lab,red,red)){.border-bambu-dark-tertiary\/30{border-color:color-mix(in oklab,var(--color-bambu-dark-tertiary)30%,transparent)}}.border-bambu-gray,.border-bambu-gray\/30{border-color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){.border-bambu-gray\/30{border-color:color-mix(in oklab,var(--color-bambu-gray)30%,transparent)}}.border-bambu-gray\/50{border-color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){.border-bambu-gray\/50{border-color:color-mix(in oklab,var(--color-bambu-gray)50%,transparent)}}.border-bambu-green,.border-bambu-green\/20{border-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.border-bambu-green\/20{border-color:color-mix(in oklab,var(--color-bambu-green)20%,transparent)}}.border-bambu-green\/30{border-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.border-bambu-green\/30{border-color:color-mix(in oklab,var(--color-bambu-green)30%,transparent)}}.border-bambu-green\/50{border-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.border-bambu-green\/50{border-color:color-mix(in oklab,var(--color-bambu-green)50%,transparent)}}.border-blue-400\/20{border-color:#54a2ff33}@supports (color:color-mix(in lab,red,red)){.border-blue-400\/20{border-color:color-mix(in oklab,var(--color-blue-400)20%,transparent)}}.border-blue-500\/20{border-color:#3080ff33}@supports (color:color-mix(in lab,red,red)){.border-blue-500\/20{border-color:color-mix(in oklab,var(--color-blue-500)20%,transparent)}}.border-blue-500\/30{border-color:#3080ff4d}@supports (color:color-mix(in lab,red,red)){.border-blue-500\/30{border-color:color-mix(in oklab,var(--color-blue-500)30%,transparent)}}.border-blue-500\/50{border-color:#3080ff80}@supports (color:color-mix(in lab,red,red)){.border-blue-500\/50{border-color:color-mix(in oklab,var(--color-blue-500)50%,transparent)}}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-400\/20{border-color:#99a1af33}@supports (color:color-mix(in lab,red,red)){.border-gray-400\/20{border-color:color-mix(in oklab,var(--color-gray-400)20%,transparent)}}.border-gray-500\/20{border-color:#6a728233}@supports (color:color-mix(in lab,red,red)){.border-gray-500\/20{border-color:color-mix(in oklab,var(--color-gray-500)20%,transparent)}}.border-gray-700{border-color:var(--color-gray-700)}.border-green-300{border-color:var(--color-green-300)}.border-green-500\/30{border-color:#00c7584d}@supports (color:color-mix(in lab,red,red)){.border-green-500\/30{border-color:color-mix(in oklab,var(--color-green-500)30%,transparent)}}.border-green-500\/50{border-color:#00c75880}@supports (color:color-mix(in lab,red,red)){.border-green-500\/50{border-color:color-mix(in oklab,var(--color-green-500)50%,transparent)}}.border-orange-400\/20{border-color:#ff8b1a33}@supports (color:color-mix(in lab,red,red)){.border-orange-400\/20{border-color:color-mix(in oklab,var(--color-orange-400)20%,transparent)}}.border-orange-400\/50{border-color:#ff8b1a80}@supports (color:color-mix(in lab,red,red)){.border-orange-400\/50{border-color:color-mix(in oklab,var(--color-orange-400)50%,transparent)}}.border-orange-500\/20{border-color:#fe6e0033}@supports (color:color-mix(in lab,red,red)){.border-orange-500\/20{border-color:color-mix(in oklab,var(--color-orange-500)20%,transparent)}}.border-orange-500\/30{border-color:#fe6e004d}@supports (color:color-mix(in lab,red,red)){.border-orange-500\/30{border-color:color-mix(in oklab,var(--color-orange-500)30%,transparent)}}.border-purple-500\/20{border-color:#ac4bff33}@supports (color:color-mix(in lab,red,red)){.border-purple-500\/20{border-color:color-mix(in oklab,var(--color-purple-500)20%,transparent)}}.border-red-300{border-color:var(--color-red-300)}.border-red-500{border-color:var(--color-red-500)}.border-red-500\/20{border-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){.border-red-500\/20{border-color:color-mix(in oklab,var(--color-red-500)20%,transparent)}}.border-red-500\/30{border-color:#fb2c364d}@supports (color:color-mix(in lab,red,red)){.border-red-500\/30{border-color:color-mix(in oklab,var(--color-red-500)30%,transparent)}}.border-red-500\/50{border-color:#fb2c3680}@supports (color:color-mix(in lab,red,red)){.border-red-500\/50{border-color:color-mix(in oklab,var(--color-red-500)50%,transparent)}}.border-status-error\/20{border-color:var(--color-status-error)}@supports (color:color-mix(in lab,red,red)){.border-status-error\/20{border-color:color-mix(in oklab,var(--color-status-error)20%,transparent)}}.border-status-ok,.border-status-ok\/20{border-color:var(--color-status-ok)}@supports (color:color-mix(in lab,red,red)){.border-status-ok\/20{border-color:color-mix(in oklab,var(--color-status-ok)20%,transparent)}}.border-status-warning\/20{border-color:var(--color-status-warning)}@supports (color:color-mix(in lab,red,red)){.border-status-warning\/20{border-color:color-mix(in oklab,var(--color-status-warning)20%,transparent)}}.border-transparent{border-color:#0000}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.border-white\/10{border-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.border-white\/20{border-color:#fff3}@supports (color:color-mix(in lab,red,red)){.border-white\/20{border-color:color-mix(in oklab,var(--color-white)20%,transparent)}}.border-white\/30{border-color:#ffffff4d}@supports (color:color-mix(in lab,red,red)){.border-white\/30{border-color:color-mix(in oklab,var(--color-white)30%,transparent)}}.border-yellow-400\/50{border-color:#fac80080}@supports (color:color-mix(in lab,red,red)){.border-yellow-400\/50{border-color:color-mix(in oklab,var(--color-yellow-400)50%,transparent)}}.border-yellow-500{border-color:var(--color-yellow-500)}.border-yellow-500\/20{border-color:#edb20033}@supports (color:color-mix(in lab,red,red)){.border-yellow-500\/20{border-color:color-mix(in oklab,var(--color-yellow-500)20%,transparent)}}.border-yellow-500\/30{border-color:#edb2004d}@supports (color:color-mix(in lab,red,red)){.border-yellow-500\/30{border-color:color-mix(in oklab,var(--color-yellow-500)30%,transparent)}}.border-yellow-500\/50{border-color:#edb20080}@supports (color:color-mix(in lab,red,red)){.border-yellow-500\/50{border-color:color-mix(in oklab,var(--color-yellow-500)50%,transparent)}}.border-t-bambu-dark-tertiary{border-top-color:var(--color-bambu-dark-tertiary)}.border-t-transparent{border-top-color:#0000}.border-r-transparent{border-right-color:#0000}.border-b-bambu-dark-tertiary{border-bottom-color:var(--color-bambu-dark-tertiary)}.border-l-transparent{border-left-color:#0000}.border-l-yellow-500{border-left-color:var(--color-yellow-500)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-amber-500\/5{background-color:#f99c000d}@supports (color:color-mix(in lab,red,red)){.bg-amber-500\/5{background-color:color-mix(in oklab,var(--color-amber-500)5%,transparent)}}.bg-amber-500\/10{background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.bg-amber-500\/10{background-color:color-mix(in oklab,var(--color-amber-500)10%,transparent)}}.bg-amber-500\/20{background-color:#f99c0033}@supports (color:color-mix(in lab,red,red)){.bg-amber-500\/20{background-color:color-mix(in oklab,var(--color-amber-500)20%,transparent)}}.bg-amber-600\/20{background-color:#dd740033}@supports (color:color-mix(in lab,red,red)){.bg-amber-600\/20{background-color:color-mix(in oklab,var(--color-amber-600)20%,transparent)}}.bg-bambu-dark{background-color:var(--color-bambu-dark)}.bg-bambu-dark-secondary,.bg-bambu-dark-secondary\/30{background-color:var(--color-bambu-dark-secondary)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-dark-secondary\/30{background-color:color-mix(in oklab,var(--color-bambu-dark-secondary)30%,transparent)}}.bg-bambu-dark-secondary\/50{background-color:var(--color-bambu-dark-secondary)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-dark-secondary\/50{background-color:color-mix(in oklab,var(--color-bambu-dark-secondary)50%,transparent)}}.bg-bambu-dark-secondary\/90{background-color:var(--color-bambu-dark-secondary)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-dark-secondary\/90{background-color:color-mix(in oklab,var(--color-bambu-dark-secondary)90%,transparent)}}.bg-bambu-dark-secondary\/95{background-color:var(--color-bambu-dark-secondary)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-dark-secondary\/95{background-color:color-mix(in oklab,var(--color-bambu-dark-secondary)95%,transparent)}}.bg-bambu-dark-tertiary,.bg-bambu-dark-tertiary\/30{background-color:var(--color-bambu-dark-tertiary)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-dark-tertiary\/30{background-color:color-mix(in oklab,var(--color-bambu-dark-tertiary)30%,transparent)}}.bg-bambu-dark-tertiary\/50{background-color:var(--color-bambu-dark-tertiary)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-dark-tertiary\/50{background-color:color-mix(in oklab,var(--color-bambu-dark-tertiary)50%,transparent)}}.bg-bambu-dark-tertiary\/80{background-color:var(--color-bambu-dark-tertiary)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-dark-tertiary\/80{background-color:color-mix(in oklab,var(--color-bambu-dark-tertiary)80%,transparent)}}.bg-bambu-dark\/20{background-color:var(--color-bambu-dark)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-dark\/20{background-color:color-mix(in oklab,var(--color-bambu-dark)20%,transparent)}}.bg-bambu-dark\/30{background-color:var(--color-bambu-dark)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-dark\/30{background-color:color-mix(in oklab,var(--color-bambu-dark)30%,transparent)}}.bg-bambu-dark\/50{background-color:var(--color-bambu-dark)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-dark\/50{background-color:color-mix(in oklab,var(--color-bambu-dark)50%,transparent)}}.bg-bambu-dark\/80{background-color:var(--color-bambu-dark)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-dark\/80{background-color:color-mix(in oklab,var(--color-bambu-dark)80%,transparent)}}.bg-bambu-dark\/90{background-color:var(--color-bambu-dark)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-dark\/90{background-color:color-mix(in oklab,var(--color-bambu-dark)90%,transparent)}}.bg-bambu-gray\/10{background-color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-gray\/10{background-color:color-mix(in oklab,var(--color-bambu-gray)10%,transparent)}}.bg-bambu-gray\/20{background-color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-gray\/20{background-color:color-mix(in oklab,var(--color-bambu-gray)20%,transparent)}}.bg-bambu-gray\/30{background-color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-gray\/30{background-color:color-mix(in oklab,var(--color-bambu-gray)30%,transparent)}}.bg-bambu-gray\/90{background-color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-gray\/90{background-color:color-mix(in oklab,var(--color-bambu-gray)90%,transparent)}}.bg-bambu-green,.bg-bambu-green\/5{background-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-green\/5{background-color:color-mix(in oklab,var(--color-bambu-green)5%,transparent)}}.bg-bambu-green\/10{background-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-green\/10{background-color:color-mix(in oklab,var(--color-bambu-green)10%,transparent)}}.bg-bambu-green\/20{background-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-green\/20{background-color:color-mix(in oklab,var(--color-bambu-green)20%,transparent)}}.bg-bambu-green\/30{background-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-green\/30{background-color:color-mix(in oklab,var(--color-bambu-green)30%,transparent)}}.bg-bambu-green\/50{background-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-green\/50{background-color:color-mix(in oklab,var(--color-bambu-green)50%,transparent)}}.bg-bambu-green\/75{background-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-green\/75{background-color:color-mix(in oklab,var(--color-bambu-green)75%,transparent)}}.bg-bambu-green\/90{background-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.bg-bambu-green\/90{background-color:color-mix(in oklab,var(--color-bambu-green)90%,transparent)}}.bg-black{background-color:var(--color-black)}.bg-black\/30{background-color:#0000004d}@supports (color:color-mix(in lab,red,red)){.bg-black\/30{background-color:color-mix(in oklab,var(--color-black)30%,transparent)}}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black)40%,transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black)60%,transparent)}}.bg-black\/70{background-color:#000000b3}@supports (color:color-mix(in lab,red,red)){.bg-black\/70{background-color:color-mix(in oklab,var(--color-black)70%,transparent)}}.bg-black\/80{background-color:#000c}@supports (color:color-mix(in lab,red,red)){.bg-black\/80{background-color:color-mix(in oklab,var(--color-black)80%,transparent)}}.bg-black\/90{background-color:#000000e6}@supports (color:color-mix(in lab,red,red)){.bg-black\/90{background-color:color-mix(in oklab,var(--color-black)90%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-400\/10{background-color:#54a2ff1a}@supports (color:color-mix(in lab,red,red)){.bg-blue-400\/10{background-color:color-mix(in oklab,var(--color-blue-400)10%,transparent)}}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-500\/10{background-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/10{background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.bg-blue-500\/20{background-color:#3080ff33}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/20{background-color:color-mix(in oklab,var(--color-blue-500)20%,transparent)}}.bg-blue-500\/30{background-color:#3080ff4d}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/30{background-color:color-mix(in oklab,var(--color-blue-500)30%,transparent)}}.bg-blue-500\/90{background-color:#3080ffe6}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/90{background-color:color-mix(in oklab,var(--color-blue-500)90%,transparent)}}.bg-blue-600{background-color:var(--color-blue-600)}.bg-blue-600\/20{background-color:#155dfc33}@supports (color:color-mix(in lab,red,red)){.bg-blue-600\/20{background-color:color-mix(in oklab,var(--color-blue-600)20%,transparent)}}.bg-cyan-500\/10{background-color:#00b7d71a}@supports (color:color-mix(in lab,red,red)){.bg-cyan-500\/10{background-color:color-mix(in oklab,var(--color-cyan-500)10%,transparent)}}.bg-cyan-500\/20{background-color:#00b7d733}@supports (color:color-mix(in lab,red,red)){.bg-cyan-500\/20{background-color:color-mix(in oklab,var(--color-cyan-500)20%,transparent)}}.bg-cyan-600\/20{background-color:#0092b533}@supports (color:color-mix(in lab,red,red)){.bg-cyan-600\/20{background-color:color-mix(in oklab,var(--color-cyan-600)20%,transparent)}}.bg-emerald-500\/20{background-color:#00bb7f33}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/20{background-color:color-mix(in oklab,var(--color-emerald-500)20%,transparent)}}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-400{background-color:var(--color-gray-400)}.bg-gray-400\/10{background-color:#99a1af1a}@supports (color:color-mix(in lab,red,red)){.bg-gray-400\/10{background-color:color-mix(in oklab,var(--color-gray-400)10%,transparent)}}.bg-gray-500{background-color:var(--color-gray-500)}.bg-gray-500\/20{background-color:#6a728233}@supports (color:color-mix(in lab,red,red)){.bg-gray-500\/20{background-color:color-mix(in oklab,var(--color-gray-500)20%,transparent)}}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-400{background-color:var(--color-green-400)}.bg-green-500{background-color:var(--color-green-500)}.bg-green-500\/10{background-color:#00c7581a}@supports (color:color-mix(in lab,red,red)){.bg-green-500\/10{background-color:color-mix(in oklab,var(--color-green-500)10%,transparent)}}.bg-green-500\/20{background-color:#00c75833}@supports (color:color-mix(in lab,red,red)){.bg-green-500\/20{background-color:color-mix(in oklab,var(--color-green-500)20%,transparent)}}.bg-indigo-500\/20{background-color:#625fff33}@supports (color:color-mix(in lab,red,red)){.bg-indigo-500\/20{background-color:color-mix(in oklab,var(--color-indigo-500)20%,transparent)}}.bg-orange-400\/10{background-color:#ff8b1a1a}@supports (color:color-mix(in lab,red,red)){.bg-orange-400\/10{background-color:color-mix(in oklab,var(--color-orange-400)10%,transparent)}}.bg-orange-500{background-color:var(--color-orange-500)}.bg-orange-500\/10{background-color:#fe6e001a}@supports (color:color-mix(in lab,red,red)){.bg-orange-500\/10{background-color:color-mix(in oklab,var(--color-orange-500)10%,transparent)}}.bg-orange-500\/20{background-color:#fe6e0033}@supports (color:color-mix(in lab,red,red)){.bg-orange-500\/20{background-color:color-mix(in oklab,var(--color-orange-500)20%,transparent)}}.bg-orange-600{background-color:var(--color-orange-600)}.bg-orange-600\/20{background-color:#f0510033}@supports (color:color-mix(in lab,red,red)){.bg-orange-600\/20{background-color:color-mix(in oklab,var(--color-orange-600)20%,transparent)}}.bg-purple-500\/10{background-color:#ac4bff1a}@supports (color:color-mix(in lab,red,red)){.bg-purple-500\/10{background-color:color-mix(in oklab,var(--color-purple-500)10%,transparent)}}.bg-purple-500\/20{background-color:#ac4bff33}@supports (color:color-mix(in lab,red,red)){.bg-purple-500\/20{background-color:color-mix(in oklab,var(--color-purple-500)20%,transparent)}}.bg-purple-500\/80{background-color:#ac4bffcc}@supports (color:color-mix(in lab,red,red)){.bg-purple-500\/80{background-color:color-mix(in oklab,var(--color-purple-500)80%,transparent)}}.bg-purple-500\/90{background-color:#ac4bffe6}@supports (color:color-mix(in lab,red,red)){.bg-purple-500\/90{background-color:color-mix(in oklab,var(--color-purple-500)90%,transparent)}}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-400{background-color:var(--color-red-400)}.bg-red-500{background-color:var(--color-red-500)}.bg-red-500\/5{background-color:#fb2c360d}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/5{background-color:color-mix(in oklab,var(--color-red-500)5%,transparent)}}.bg-red-500\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/10{background-color:color-mix(in oklab,var(--color-red-500)10%,transparent)}}.bg-red-500\/15{background-color:#fb2c3626}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/15{background-color:color-mix(in oklab,var(--color-red-500)15%,transparent)}}.bg-red-500\/20{background-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/20{background-color:color-mix(in oklab,var(--color-red-500)20%,transparent)}}.bg-red-500\/30{background-color:#fb2c364d}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/30{background-color:color-mix(in oklab,var(--color-red-500)30%,transparent)}}.bg-red-500\/40{background-color:#fb2c3666}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/40{background-color:color-mix(in oklab,var(--color-red-500)40%,transparent)}}.bg-red-500\/80{background-color:#fb2c36cc}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/80{background-color:color-mix(in oklab,var(--color-red-500)80%,transparent)}}.bg-red-600{background-color:var(--color-red-600)}.bg-rose-500\/20{background-color:#ff235733}@supports (color:color-mix(in lab,red,red)){.bg-rose-500\/20{background-color:color-mix(in oklab,var(--color-rose-500)20%,transparent)}}.bg-rose-600\/20{background-color:#e7004433}@supports (color:color-mix(in lab,red,red)){.bg-rose-600\/20{background-color:color-mix(in oklab,var(--color-rose-600)20%,transparent)}}.bg-status-error,.bg-status-error\/10{background-color:var(--color-status-error)}@supports (color:color-mix(in lab,red,red)){.bg-status-error\/10{background-color:color-mix(in oklab,var(--color-status-error)10%,transparent)}}.bg-status-error\/20{background-color:var(--color-status-error)}@supports (color:color-mix(in lab,red,red)){.bg-status-error\/20{background-color:color-mix(in oklab,var(--color-status-error)20%,transparent)}}.bg-status-error\/80{background-color:var(--color-status-error)}@supports (color:color-mix(in lab,red,red)){.bg-status-error\/80{background-color:color-mix(in oklab,var(--color-status-error)80%,transparent)}}.bg-status-ok,.bg-status-ok\/10{background-color:var(--color-status-ok)}@supports (color:color-mix(in lab,red,red)){.bg-status-ok\/10{background-color:color-mix(in oklab,var(--color-status-ok)10%,transparent)}}.bg-status-ok\/20{background-color:var(--color-status-ok)}@supports (color:color-mix(in lab,red,red)){.bg-status-ok\/20{background-color:color-mix(in oklab,var(--color-status-ok)20%,transparent)}}.bg-status-warning\/10{background-color:var(--color-status-warning)}@supports (color:color-mix(in lab,red,red)){.bg-status-warning\/10{background-color:color-mix(in oklab,var(--color-status-warning)10%,transparent)}}.bg-status-warning\/20{background-color:var(--color-status-warning)}@supports (color:color-mix(in lab,red,red)){.bg-status-warning\/20{background-color:color-mix(in oklab,var(--color-status-warning)20%,transparent)}}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-white\/50{background-color:#ffffff80}@supports (color:color-mix(in lab,red,red)){.bg-white\/50{background-color:color-mix(in oklab,var(--color-white)50%,transparent)}}.bg-white\/90{background-color:#ffffffe6}@supports (color:color-mix(in lab,red,red)){.bg-white\/90{background-color:color-mix(in oklab,var(--color-white)90%,transparent)}}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-400\/20{background-color:#fac80033}@supports (color:color-mix(in lab,red,red)){.bg-yellow-400\/20{background-color:color-mix(in oklab,var(--color-yellow-400)20%,transparent)}}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-yellow-500\/5{background-color:#edb2000d}@supports (color:color-mix(in lab,red,red)){.bg-yellow-500\/5{background-color:color-mix(in oklab,var(--color-yellow-500)5%,transparent)}}.bg-yellow-500\/10{background-color:#edb2001a}@supports (color:color-mix(in lab,red,red)){.bg-yellow-500\/10{background-color:color-mix(in oklab,var(--color-yellow-500)10%,transparent)}}.bg-yellow-500\/20{background-color:#edb20033}@supports (color:color-mix(in lab,red,red)){.bg-yellow-500\/20{background-color:color-mix(in oklab,var(--color-yellow-500)20%,transparent)}}.bg-gradient-to-b{--tw-gradient-position:to bottom in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-t{--tw-gradient-position:to top in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-bambu-green\/10{--tw-gradient-from:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.from-bambu-green\/10{--tw-gradient-from:color-mix(in oklab,var(--color-bambu-green)10%,transparent)}}.from-bambu-green\/10{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-black\/80{--tw-gradient-from:#000c}@supports (color:color-mix(in lab,red,red)){.from-black\/80{--tw-gradient-from:color-mix(in oklab,var(--color-black)80%,transparent)}}.from-black\/80{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-500{--tw-gradient-from:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-500\/5{--tw-gradient-from:#3080ff0d}@supports (color:color-mix(in lab,red,red)){.from-blue-500\/5{--tw-gradient-from:color-mix(in oklab,var(--color-blue-500)5%,transparent)}}.from-blue-500\/5{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-500\/10{--tw-gradient-from:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.from-blue-500\/10{--tw-gradient-from:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.from-blue-500\/10{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-500\/10{--tw-gradient-from:#6a72821a}@supports (color:color-mix(in lab,red,red)){.from-gray-500\/10{--tw-gradient-from:color-mix(in oklab,var(--color-gray-500)10%,transparent)}}.from-gray-500\/10{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-white\/10{--tw-gradient-from:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.from-white\/10{--tw-gradient-from:color-mix(in oklab,var(--color-white)10%,transparent)}}.from-white\/10{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-500\/10{--tw-gradient-from:#edb2001a}@supports (color:color-mix(in lab,red,red)){.from-yellow-500\/10{--tw-gradient-from:color-mix(in oklab,var(--color-yellow-500)10%,transparent)}}.from-yellow-500\/10{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-bambu-dark-secondary{--tw-gradient-to:var(--color-bambu-dark-secondary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-400{--tw-gradient-to:var(--color-blue-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.bg-cover{background-size:cover}.bg-center{background-position:50%}.fill-yellow-400{fill:var(--color-yellow-400)}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.object-left{object-position:left}.p-0{padding:calc(var(--spacing)*0)}.p-0\.5{padding:calc(var(--spacing)*.5)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-2\.5{padding:calc(var(--spacing)*2.5)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-5{padding:calc(var(--spacing)*5)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.p-12{padding:calc(var(--spacing)*12)}.\!px-1\.5{padding-inline:calc(var(--spacing)*1.5)!important}.\!px-3{padding-inline:calc(var(--spacing)*3)!important}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.py-8{padding-block:calc(var(--spacing)*8)}.py-12{padding-block:calc(var(--spacing)*12)}.py-16{padding-block:calc(var(--spacing)*16)}.py-20{padding-block:calc(var(--spacing)*20)}.py-24{padding-block:calc(var(--spacing)*24)}.pt-0{padding-top:calc(var(--spacing)*0)}.pt-1{padding-top:calc(var(--spacing)*1)}.pt-2{padding-top:calc(var(--spacing)*2)}.pt-3{padding-top:calc(var(--spacing)*3)}.pt-4{padding-top:calc(var(--spacing)*4)}.pr-1{padding-right:calc(var(--spacing)*1)}.pr-2{padding-right:calc(var(--spacing)*2)}.pr-3{padding-right:calc(var(--spacing)*3)}.pr-4{padding-right:calc(var(--spacing)*4)}.pr-6{padding-right:calc(var(--spacing)*6)}.pr-8{padding-right:calc(var(--spacing)*8)}.pr-10{padding-right:calc(var(--spacing)*10)}.pb-1{padding-bottom:calc(var(--spacing)*1)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.pl-2{padding-left:calc(var(--spacing)*2)}.pl-4{padding-left:calc(var(--spacing)*4)}.pl-6{padding-left:calc(var(--spacing)*6)}.pl-8{padding-left:calc(var(--spacing)*8)}.pl-9{padding-left:calc(var(--spacing)*9)}.pl-10{padding-left:calc(var(--spacing)*10)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.\!text-green-400{color:var(--color-green-400)!important}.text-amber-200{color:var(--color-amber-200)}.text-amber-300{color:var(--color-amber-300)}.text-amber-300\/60{color:#ffd23699}@supports (color:color-mix(in lab,red,red)){.text-amber-300\/60{color:color-mix(in oklab,var(--color-amber-300)60%,transparent)}}.text-amber-300\/70{color:#ffd236b3}@supports (color:color-mix(in lab,red,red)){.text-amber-300\/70{color:color-mix(in oklab,var(--color-amber-300)70%,transparent)}}.text-amber-300\/80{color:#ffd236cc}@supports (color:color-mix(in lab,red,red)){.text-amber-300\/80{color:color-mix(in oklab,var(--color-amber-300)80%,transparent)}}.text-amber-400{color:var(--color-amber-400)}.text-amber-400\/70{color:#fcbb00b3}@supports (color:color-mix(in lab,red,red)){.text-amber-400\/70{color:color-mix(in oklab,var(--color-amber-400)70%,transparent)}}.text-amber-500{color:var(--color-amber-500)}.text-amber-500\/80{color:#f99c00cc}@supports (color:color-mix(in lab,red,red)){.text-amber-500\/80{color:color-mix(in oklab,var(--color-amber-500)80%,transparent)}}.text-amber-600{color:var(--color-amber-600)}.text-bambu-dark-tertiary{color:var(--color-bambu-dark-tertiary)}.text-bambu-gray{color:var(--color-bambu-gray)}.text-bambu-gray-dark{color:var(--color-bambu-gray-dark)}.text-bambu-gray-light{color:var(--color-bambu-gray-light)}.text-bambu-gray\/30{color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){.text-bambu-gray\/30{color:color-mix(in oklab,var(--color-bambu-gray)30%,transparent)}}.text-bambu-gray\/50{color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){.text-bambu-gray\/50{color:color-mix(in oklab,var(--color-bambu-gray)50%,transparent)}}.text-bambu-gray\/60{color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){.text-bambu-gray\/60{color:color-mix(in oklab,var(--color-bambu-gray)60%,transparent)}}.text-bambu-gray\/70{color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){.text-bambu-gray\/70{color:color-mix(in oklab,var(--color-bambu-gray)70%,transparent)}}.text-bambu-gray\/80{color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){.text-bambu-gray\/80{color:color-mix(in oklab,var(--color-bambu-gray)80%,transparent)}}.text-bambu-green,.text-bambu-green\/70{color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.text-bambu-green\/70{color:color-mix(in oklab,var(--color-bambu-green)70%,transparent)}}.text-black{color:var(--color-black)}.text-black\/80{color:#000c}@supports (color:color-mix(in lab,red,red)){.text-black\/80{color:color-mix(in oklab,var(--color-black)80%,transparent)}}.text-blue-300{color:var(--color-blue-300)}.text-blue-300\/70{color:#90c5ffb3}@supports (color:color-mix(in lab,red,red)){.text-blue-300\/70{color:color-mix(in oklab,var(--color-blue-300)70%,transparent)}}.text-blue-300\/80{color:#90c5ffcc}@supports (color:color-mix(in lab,red,red)){.text-blue-300\/80{color:color-mix(in oklab,var(--color-blue-300)80%,transparent)}}.text-blue-400{color:var(--color-blue-400)}.text-blue-500{color:var(--color-blue-500)}.text-blue-500\/70{color:#3080ffb3}@supports (color:color-mix(in lab,red,red)){.text-blue-500\/70{color:color-mix(in oklab,var(--color-blue-500)70%,transparent)}}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-cyan-300{color:var(--color-cyan-300)}.text-cyan-400{color:var(--color-cyan-400)}.text-emerald-400{color:var(--color-emerald-400)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-900{color:var(--color-gray-900)}.text-green-300{color:var(--color-green-300)}.text-green-400{color:var(--color-green-400)}.text-green-500{color:var(--color-green-500)}.text-green-500\/60{color:#00c75899}@supports (color:color-mix(in lab,red,red)){.text-green-500\/60{color:color-mix(in oklab,var(--color-green-500)60%,transparent)}}.text-green-600{color:var(--color-green-600)}.text-indigo-400{color:var(--color-indigo-400)}.text-orange-200{color:var(--color-orange-200)}.text-orange-300{color:var(--color-orange-300)}.text-orange-400{color:var(--color-orange-400)}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-purple-300{color:var(--color-purple-300)}.text-purple-400{color:var(--color-purple-400)}.text-purple-400\/80{color:#c07effcc}@supports (color:color-mix(in lab,red,red)){.text-purple-400\/80{color:color-mix(in oklab,var(--color-purple-400)80%,transparent)}}.text-red-300{color:var(--color-red-300)}.text-red-300\/80{color:#ffa3a3cc}@supports (color:color-mix(in lab,red,red)){.text-red-300\/80{color:color-mix(in oklab,var(--color-red-300)80%,transparent)}}.text-red-400{color:var(--color-red-400)}.text-red-400\/60{color:#ff656899}@supports (color:color-mix(in lab,red,red)){.text-red-400\/60{color:color-mix(in oklab,var(--color-red-400)60%,transparent)}}.text-red-400\/70{color:#ff6568b3}@supports (color:color-mix(in lab,red,red)){.text-red-400\/70{color:color-mix(in oklab,var(--color-red-400)70%,transparent)}}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-rose-300{color:var(--color-rose-300)}.text-rose-400{color:var(--color-rose-400)}.text-status-error{color:var(--color-status-error)}.text-status-ok{color:var(--color-status-ok)}.text-status-warning{color:var(--color-status-warning)}.text-white{color:var(--color-white)}.text-white\/60{color:#fff9}@supports (color:color-mix(in lab,red,red)){.text-white\/60{color:color-mix(in oklab,var(--color-white)60%,transparent)}}.text-white\/80{color:#fffc}@supports (color:color-mix(in lab,red,red)){.text-white\/80{color:color-mix(in oklab,var(--color-white)80%,transparent)}}.text-white\/90{color:#ffffffe6}@supports (color:color-mix(in lab,red,red)){.text-white\/90{color:color-mix(in oklab,var(--color-white)90%,transparent)}}.text-yellow-200{color:var(--color-yellow-200)}.text-yellow-400{color:var(--color-yellow-400)}.text-yellow-500{color:var(--color-yellow-500)}.text-yellow-500\/70{color:#edb200b3}@supports (color:color-mix(in lab,red,red)){.text-yellow-500\/70{color:color-mix(in oklab,var(--color-yellow-500)70%,transparent)}}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-700{color:var(--color-yellow-700)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.line-through{text-decoration-line:line-through}.underline{text-decoration-line:underline}.placeholder-bambu-gray::placeholder{color:var(--color-bambu-gray)}.placeholder-bambu-gray-dark::placeholder{color:var(--color-bambu-gray-dark)}.placeholder-bambu-gray\/50::placeholder{color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){.placeholder-bambu-gray\/50::placeholder{color:color-mix(in oklab,var(--color-bambu-gray)50%,transparent)}}.accent-bambu-green{accent-color:var(--color-bambu-green)}.accent-green-500{accent-color:var(--color-green-500)}.opacity-0{opacity:0}.opacity-20{opacity:.2}.opacity-30{opacity:.3}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.opacity-100{opacity:1}.opacity-\[0\.65\]{opacity:.65}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-bambu-green\/20{--tw-shadow-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.shadow-bambu-green\/20{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-bambu-green)20%,transparent)var(--tw-shadow-alpha),transparent)}}.shadow-bambu-green\/25{--tw-shadow-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.shadow-bambu-green\/25{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-bambu-green)25%,transparent)var(--tw-shadow-alpha),transparent)}}.ring-bambu-green{--tw-ring-color:var(--color-bambu-green)}.ring-blue-400\/50{--tw-ring-color:#54a2ff80}@supports (color:color-mix(in lab,red,red)){.ring-blue-400\/50{--tw-ring-color:color-mix(in oklab,var(--color-blue-400)50%,transparent)}}.ring-green-500{--tw-ring-color:var(--color-green-500)}.ring-white{--tw-ring-color:var(--color-white)}.ring-offset-1{--tw-ring-offset-width:1px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-2{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-bambu-dark{--tw-ring-offset-color:var(--color-bambu-dark)}.ring-offset-bambu-dark-secondary{--tw-ring-offset-color:var(--color-bambu-dark-secondary)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.invert{--tw-invert:invert(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.select-none{-webkit-user-select:none;user-select:none}@media(hover:hover){.group-hover\:bg-bambu-green\/20:is(:where(.group):hover *){background-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.group-hover\:bg-bambu-green\/20:is(:where(.group):hover *){background-color:color-mix(in oklab,var(--color-bambu-green)20%,transparent)}}.group-hover\:text-bambu-gray:is(:where(.group):hover *){color:var(--color-bambu-gray)}.group-hover\:text-bambu-green:is(:where(.group):hover *){color:var(--color-bambu-green)}.group-hover\:opacity-50:is(:where(.group):hover *){opacity:.5}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.group-hover\/thumb\:block:is(:where(.group\/thumb):hover *){display:block}}.peer-checked\:bg-bambu-green:is(:where(.peer):checked~*){background-color:var(--color-bambu-green)}.peer-focus\:outline-none:is(:where(.peer):focus~*){--tw-outline-style:none;outline-style:none}.placeholder\:text-bambu-gray::placeholder{color:var(--color-bambu-gray)}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:top-0:before{content:var(--tw-content);top:calc(var(--spacing)*0)}.before\:right-0:before{content:var(--tw-content);right:calc(var(--spacing)*0)}.before\:left-0:before{content:var(--tw-content);left:calc(var(--spacing)*0)}.before\:h-0\.5:before{content:var(--tw-content);height:calc(var(--spacing)*.5)}.before\:bg-bambu-green:before{content:var(--tw-content);background-color:var(--color-bambu-green)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:top-\[2px\]:after{content:var(--tw-content);top:2px}.after\:left-\[2px\]:after{content:var(--tw-content);left:2px}.after\:h-4:after{content:var(--tw-content);height:calc(var(--spacing)*4)}.after\:h-5:after{content:var(--tw-content);height:calc(var(--spacing)*5)}.after\:w-4:after{content:var(--tw-content);width:calc(var(--spacing)*4)}.after\:w-5:after{content:var(--tw-content);width:calc(var(--spacing)*5)}.after\:rounded-full:after{content:var(--tw-content);border-radius:3.40282e38px}.after\:border:after{content:var(--tw-content);border-style:var(--tw-border-style);border-width:1px}.after\:border-gray-300:after{content:var(--tw-content);border-color:var(--color-gray-300)}.after\:bg-white:after{content:var(--tw-content);background-color:var(--color-white)}.after\:transition-all:after{content:var(--tw-content);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.peer-checked\:after\:translate-x-full:is(:where(.peer):checked~*):after{content:var(--tw-content);--tw-translate-x:100%;translate:var(--tw-translate-x)var(--tw-translate-y)}.peer-checked\:after\:border-white:is(:where(.peer):checked~*):after{content:var(--tw-content);border-color:var(--color-white)}.last\:border-0:last-child{border-style:var(--tw-border-style);border-width:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media(hover:hover){.hover\:border-bambu-dark-tertiary\/80:hover{border-color:var(--color-bambu-dark-tertiary)}@supports (color:color-mix(in lab,red,red)){.hover\:border-bambu-dark-tertiary\/80:hover{border-color:color-mix(in oklab,var(--color-bambu-dark-tertiary)80%,transparent)}}.hover\:border-bambu-gray:hover{border-color:var(--color-bambu-gray)}.hover\:border-bambu-gray-dark:hover{border-color:var(--color-bambu-gray-dark)}.hover\:border-bambu-green:hover,.hover\:border-bambu-green\/50:hover{border-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.hover\:border-bambu-green\/50:hover{border-color:color-mix(in oklab,var(--color-bambu-green)50%,transparent)}}.hover\:border-white\/40:hover{border-color:#fff6}@supports (color:color-mix(in lab,red,red)){.hover\:border-white\/40:hover{border-color:color-mix(in oklab,var(--color-white)40%,transparent)}}.hover\:\!bg-green-500\/20:hover{background-color:#00c75833!important}@supports (color:color-mix(in lab,red,red)){.hover\:\!bg-green-500\/20:hover{background-color:color-mix(in oklab,var(--color-green-500)20%,transparent)!important}}.hover\:bg-amber-500\/30:hover{background-color:#f99c004d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-amber-500\/30:hover{background-color:color-mix(in oklab,var(--color-amber-500)30%,transparent)}}.hover\:bg-bambu-dark:hover{background-color:var(--color-bambu-dark)}.hover\:bg-bambu-dark-secondary:hover,.hover\:bg-bambu-dark-secondary\/50:hover{background-color:var(--color-bambu-dark-secondary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-bambu-dark-secondary\/50:hover{background-color:color-mix(in oklab,var(--color-bambu-dark-secondary)50%,transparent)}}.hover\:bg-bambu-dark-tertiary:hover,.hover\:bg-bambu-dark-tertiary\/30:hover{background-color:var(--color-bambu-dark-tertiary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-bambu-dark-tertiary\/30:hover{background-color:color-mix(in oklab,var(--color-bambu-dark-tertiary)30%,transparent)}}.hover\:bg-bambu-dark-tertiary\/50:hover{background-color:var(--color-bambu-dark-tertiary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-bambu-dark-tertiary\/50:hover{background-color:color-mix(in oklab,var(--color-bambu-dark-tertiary)50%,transparent)}}.hover\:bg-bambu-dark-tertiary\/70:hover{background-color:var(--color-bambu-dark-tertiary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-bambu-dark-tertiary\/70:hover{background-color:color-mix(in oklab,var(--color-bambu-dark-tertiary)70%,transparent)}}.hover\:bg-bambu-dark-tertiary\/80:hover{background-color:var(--color-bambu-dark-tertiary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-bambu-dark-tertiary\/80:hover{background-color:color-mix(in oklab,var(--color-bambu-dark-tertiary)80%,transparent)}}.hover\:bg-bambu-dark\/50:hover{background-color:var(--color-bambu-dark)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-bambu-dark\/50:hover{background-color:color-mix(in oklab,var(--color-bambu-dark)50%,transparent)}}.hover\:bg-bambu-gray-dark:hover{background-color:var(--color-bambu-gray-dark)}.hover\:bg-bambu-green:hover{background-color:var(--color-bambu-green)}.hover\:bg-bambu-green-dark:hover{background-color:var(--color-bambu-green-dark)}.hover\:bg-bambu-green-light:hover{background-color:var(--color-bambu-green-light)}.hover\:bg-bambu-green\/10:hover{background-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-bambu-green\/10:hover{background-color:color-mix(in oklab,var(--color-bambu-green)10%,transparent)}}.hover\:bg-bambu-green\/20:hover{background-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-bambu-green\/20:hover{background-color:color-mix(in oklab,var(--color-bambu-green)20%,transparent)}}.hover\:bg-bambu-green\/30:hover{background-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-bambu-green\/30:hover{background-color:color-mix(in oklab,var(--color-bambu-green)30%,transparent)}}.hover\:bg-bambu-green\/80:hover{background-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-bambu-green\/80:hover{background-color:color-mix(in oklab,var(--color-bambu-green)80%,transparent)}}.hover\:bg-black\/40:hover{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.hover\:bg-black\/40:hover{background-color:color-mix(in oklab,var(--color-black)40%,transparent)}}.hover\:bg-black\/70:hover{background-color:#000000b3}@supports (color:color-mix(in lab,red,red)){.hover\:bg-black\/70:hover{background-color:color-mix(in oklab,var(--color-black)70%,transparent)}}.hover\:bg-black\/80:hover{background-color:#000c}@supports (color:color-mix(in lab,red,red)){.hover\:bg-black\/80:hover{background-color:color-mix(in oklab,var(--color-black)80%,transparent)}}.hover\:bg-blue-500\/30:hover{background-color:#3080ff4d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-blue-500\/30:hover{background-color:color-mix(in oklab,var(--color-blue-500)30%,transparent)}}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-orange-400\/20:hover{background-color:#ff8b1a33}@supports (color:color-mix(in lab,red,red)){.hover\:bg-orange-400\/20:hover{background-color:color-mix(in oklab,var(--color-orange-400)20%,transparent)}}.hover\:bg-red-200:hover{background-color:var(--color-red-200)}.hover\:bg-red-400\/10:hover{background-color:#ff65681a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-red-400\/10:hover{background-color:color-mix(in oklab,var(--color-red-400)10%,transparent)}}.hover\:bg-red-500\/10:hover{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-red-500\/10:hover{background-color:color-mix(in oklab,var(--color-red-500)10%,transparent)}}.hover\:bg-red-500\/20:hover{background-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){.hover\:bg-red-500\/20:hover{background-color:color-mix(in oklab,var(--color-red-500)20%,transparent)}}.hover\:bg-red-500\/30:hover{background-color:#fb2c364d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-red-500\/30:hover{background-color:color-mix(in oklab,var(--color-red-500)30%,transparent)}}.hover\:bg-red-600:hover{background-color:var(--color-red-600)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.hover\:bg-yellow-500\/30:hover{background-color:#edb2004d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-yellow-500\/30:hover{background-color:color-mix(in oklab,var(--color-yellow-500)30%,transparent)}}.hover\:bg-yellow-600:hover{background-color:var(--color-yellow-600)}.hover\:text-amber-300:hover{color:var(--color-amber-300)}.hover\:text-bambu-gray:hover{color:var(--color-bambu-gray)}.hover\:text-bambu-green:hover{color:var(--color-bambu-green)}.hover\:text-bambu-green-light:hover{color:var(--color-bambu-green-light)}.hover\:text-bambu-green\/70:hover{color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.hover\:text-bambu-green\/70:hover{color:color-mix(in oklab,var(--color-bambu-green)70%,transparent)}}.hover\:text-bambu-green\/80:hover{color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.hover\:text-bambu-green\/80:hover{color:color-mix(in oklab,var(--color-bambu-green)80%,transparent)}}.hover\:text-blue-300:hover{color:var(--color-blue-300)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-orange-300:hover{color:var(--color-orange-300)}.hover\:text-red-300:hover{color:var(--color-red-300)}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:text-white:hover{color:var(--color-white)}.hover\:text-white\/80:hover{color:#fffc}@supports (color:color-mix(in lab,red,red)){.hover\:text-white\/80:hover{color:color-mix(in oklab,var(--color-white)80%,transparent)}}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-70:hover{opacity:.7}.hover\:opacity-80:hover{opacity:.8}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-bambu-green\/5:hover{--tw-shadow-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.hover\:shadow-bambu-green\/5:hover{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-bambu-green)5%,transparent)var(--tw-shadow-alpha),transparent)}}.hover\:shadow-bambu-green\/30:hover{--tw-shadow-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.hover\:shadow-bambu-green\/30:hover{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-bambu-green)30%,transparent)var(--tw-shadow-alpha),transparent)}}}.focus\:border-bambu-green:focus{border-color:var(--color-bambu-green)}.focus\:border-red-500:focus{border-color:var(--color-red-500)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-bambu-gray:focus{--tw-ring-color:var(--color-bambu-gray)}.focus\:ring-bambu-green:focus,.focus\:ring-bambu-green\/50:focus{--tw-ring-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.focus\:ring-bambu-green\/50:focus{--tw-ring-color:color-mix(in oklab,var(--color-bambu-green)50%,transparent)}}.focus\:ring-red-500:focus{--tw-ring-color:var(--color-red-500)}.focus\:ring-offset-0:focus{--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:ring-offset-bambu-dark:focus{--tw-ring-offset-color:var(--color-bambu-dark)}.focus\:ring-offset-bambu-dark-secondary:focus{--tw-ring-offset-color:var(--color-bambu-dark-secondary)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:cursor-grabbing:active{cursor:grabbing}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-50:disabled{opacity:.5}@media(hover:hover){.disabled\:hover\:bg-bambu-green:disabled:hover{background-color:var(--color-bambu-green)}}@media(min-width:40rem){.sm\:inline{display:inline}.sm\:h-4{height:calc(var(--spacing)*4)}.sm\:w-4{width:calc(var(--spacing)*4)}.sm\:w-auto{width:auto}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:p-1\.5{padding:calc(var(--spacing)*1.5)}}@media(min-width:48rem){.md\:top-\[2px\]{top:2px}.md\:left-\[2px\]{left:2px}.md\:mx-0{margin-inline:calc(var(--spacing)*0)}.md\:block{display:block}.md\:inline{display:inline}.md\:h-4{height:calc(var(--spacing)*4)}.md\:h-5{height:calc(var(--spacing)*5)}.md\:h-8{height:calc(var(--spacing)*8)}.md\:min-h-0{min-height:calc(var(--spacing)*0)}.md\:w-4{width:calc(var(--spacing)*4)}.md\:w-8{width:calc(var(--spacing)*8)}.md\:w-9{width:calc(var(--spacing)*9)}.md\:min-w-\[200px\]{min-width:200px}.md\:flex-1{flex:1}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:flex-wrap{flex-wrap:wrap}.md\:items-center{align-items:center}.md\:gap-4{gap:calc(var(--spacing)*4)}.md\:p-8{padding:calc(var(--spacing)*8)}.md\:px-0{padding-inline:calc(var(--spacing)*0)}.md\:py-2{padding-block:calc(var(--spacing)*2)}.md\:pb-0{padding-bottom:calc(var(--spacing)*0)}.md\:opacity-0{opacity:0}@media(hover:hover){.md\:group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}}@media(min-width:64rem){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:w-80{width:calc(var(--spacing)*80)}.lg\:w-\[480px\]{width:480px}.lg\:max-w-md{max-width:var(--container-md)}.lg\:max-w-sm{max-width:var(--container-sm)}.lg\:max-w-xl{max-width:var(--container-xl)}.lg\:flex-shrink-0{flex-shrink:0}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:gap-8{gap:calc(var(--spacing)*8)}.lg\:p-8{padding:calc(var(--spacing)*8)}}@media(min-width:80rem){.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media(min-width:96rem){.\32xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}.dark\:border-bambu-dark-tertiary:where(.dark,.dark *),.dark\:border-bambu-dark-tertiary\/50:where(.dark,.dark *){border-color:var(--color-bambu-dark-tertiary)}@supports (color:color-mix(in lab,red,red)){.dark\:border-bambu-dark-tertiary\/50:where(.dark,.dark *){border-color:color-mix(in oklab,var(--color-bambu-dark-tertiary)50%,transparent)}}.dark\:border-bambu-green\/40:where(.dark,.dark *){border-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.dark\:border-bambu-green\/40:where(.dark,.dark *){border-color:color-mix(in oklab,var(--color-bambu-green)40%,transparent)}}.dark\:border-gray-600:where(.dark,.dark *){border-color:var(--color-gray-600)}.dark\:border-red-500\/30:where(.dark,.dark *){border-color:#fb2c364d}@supports (color:color-mix(in lab,red,red)){.dark\:border-red-500\/30:where(.dark,.dark *){border-color:color-mix(in oklab,var(--color-red-500)30%,transparent)}}.dark\:border-red-500\/40:where(.dark,.dark *){border-color:#fb2c3666}@supports (color:color-mix(in lab,red,red)){.dark\:border-red-500\/40:where(.dark,.dark *){border-color:color-mix(in oklab,var(--color-red-500)40%,transparent)}}.dark\:bg-amber-500\/10:where(.dark,.dark *){background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.dark\:bg-amber-500\/10:where(.dark,.dark *){background-color:color-mix(in oklab,var(--color-amber-500)10%,transparent)}}.dark\:bg-bambu-dark:where(.dark,.dark *){background-color:var(--color-bambu-dark)}.dark\:bg-bambu-dark-secondary:where(.dark,.dark *){background-color:var(--color-bambu-dark-secondary)}.dark\:bg-bambu-green\/20:where(.dark,.dark *){background-color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-bambu-green\/20:where(.dark,.dark *){background-color:color-mix(in oklab,var(--color-bambu-green)20%,transparent)}}.dark\:bg-black\/80:where(.dark,.dark *){background-color:#000c}@supports (color:color-mix(in lab,red,red)){.dark\:bg-black\/80:where(.dark,.dark *){background-color:color-mix(in oklab,var(--color-black)80%,transparent)}}.dark\:bg-blue-500\/10:where(.dark,.dark *){background-color:#3080ff1a}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-500\/10:where(.dark,.dark *){background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.dark\:bg-blue-500\/20:where(.dark,.dark *){background-color:#3080ff33}@supports (color:color-mix(in lab,red,red)){.dark\:bg-blue-500\/20:where(.dark,.dark *){background-color:color-mix(in oklab,var(--color-blue-500)20%,transparent)}}.dark\:bg-gray-500\/40:where(.dark,.dark *){background-color:#6a728266}@supports (color:color-mix(in lab,red,red)){.dark\:bg-gray-500\/40:where(.dark,.dark *){background-color:color-mix(in oklab,var(--color-gray-500)40%,transparent)}}.dark\:bg-gray-900:where(.dark,.dark *){background-color:var(--color-gray-900)}.dark\:bg-red-500\/10:where(.dark,.dark *){background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.dark\:bg-red-500\/10:where(.dark,.dark *){background-color:color-mix(in oklab,var(--color-red-500)10%,transparent)}}.dark\:bg-red-500\/20:where(.dark,.dark *){background-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){.dark\:bg-red-500\/20:where(.dark,.dark *){background-color:color-mix(in oklab,var(--color-red-500)20%,transparent)}}.dark\:text-amber-400:where(.dark,.dark *){color:var(--color-amber-400)}.dark\:text-bambu-gray:where(.dark,.dark *),.dark\:text-bambu-gray\/30:where(.dark,.dark *){color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){.dark\:text-bambu-gray\/30:where(.dark,.dark *){color:color-mix(in oklab,var(--color-bambu-gray)30%,transparent)}}.dark\:text-bambu-gray\/50:where(.dark,.dark *){color:var(--color-bambu-gray)}@supports (color:color-mix(in lab,red,red)){.dark\:text-bambu-gray\/50:where(.dark,.dark *){color:color-mix(in oklab,var(--color-bambu-gray)50%,transparent)}}.dark\:text-bambu-green:where(.dark,.dark *),.dark\:text-bambu-green\/60:where(.dark,.dark *){color:var(--color-bambu-green)}@supports (color:color-mix(in lab,red,red)){.dark\:text-bambu-green\/60:where(.dark,.dark *){color:color-mix(in oklab,var(--color-bambu-green)60%,transparent)}}.dark\:text-blue-200:where(.dark,.dark *){color:var(--color-blue-200)}.dark\:text-blue-200\/80:where(.dark,.dark *){color:#bedbffcc}@supports (color:color-mix(in lab,red,red)){.dark\:text-blue-200\/80:where(.dark,.dark *){color:color-mix(in oklab,var(--color-blue-200)80%,transparent)}}.dark\:text-blue-300:where(.dark,.dark *){color:var(--color-blue-300)}.dark\:text-blue-300\/60:where(.dark,.dark *){color:#90c5ff99}@supports (color:color-mix(in lab,red,red)){.dark\:text-blue-300\/60:where(.dark,.dark *){color:color-mix(in oklab,var(--color-blue-300)60%,transparent)}}.dark\:text-blue-400:where(.dark,.dark *){color:var(--color-blue-400)}.dark\:text-orange-200:where(.dark,.dark *){color:var(--color-orange-200)}.dark\:text-orange-400:where(.dark,.dark *){color:var(--color-orange-400)}.dark\:text-red-400:where(.dark,.dark *){color:var(--color-red-400)}.dark\:text-red-400\/70:where(.dark,.dark *){color:#ff6568b3}@supports (color:color-mix(in lab,red,red)){.dark\:text-red-400\/70:where(.dark,.dark *){color:color-mix(in oklab,var(--color-red-400)70%,transparent)}}.dark\:text-white:where(.dark,.dark *){color:var(--color-white)}.dark\:text-yellow-200:where(.dark,.dark *){color:var(--color-yellow-200)}.dark\:text-yellow-200\/70:where(.dark,.dark *){color:#fff085b3}@supports (color:color-mix(in lab,red,red)){.dark\:text-yellow-200\/70:where(.dark,.dark *){color:color-mix(in oklab,var(--color-yellow-200)70%,transparent)}}@media(hover:hover){.dark\:hover\:bg-bambu-dark-tertiary:where(.dark,.dark *):hover{background-color:var(--color-bambu-dark-tertiary)}.dark\:hover\:bg-bambu-dark\/50:where(.dark,.dark *):hover{background-color:var(--color-bambu-dark)}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-bambu-dark\/50:where(.dark,.dark *):hover{background-color:color-mix(in oklab,var(--color-bambu-dark)50%,transparent)}}.dark\:hover\:bg-red-500\/30:where(.dark,.dark *):hover{background-color:#fb2c364d}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-red-500\/30:where(.dark,.dark *):hover{background-color:color-mix(in oklab,var(--color-red-500)30%,transparent)}}.dark\:hover\:text-white:where(.dark,.dark *):hover{color:var(--color-white)}}.\[\&\:\:-webkit-slider-thumb\]\:h-3::-webkit-slider-thumb{height:calc(var(--spacing)*3)}.\[\&\:\:-webkit-slider-thumb\]\:w-3::-webkit-slider-thumb{width:calc(var(--spacing)*3)}.\[\&\:\:-webkit-slider-thumb\]\:cursor-pointer::-webkit-slider-thumb{cursor:pointer}.\[\&\:\:-webkit-slider-thumb\]\:appearance-none::-webkit-slider-thumb{appearance:none}.\[\&\:\:-webkit-slider-thumb\]\:rounded-full::-webkit-slider-thumb{border-radius:3.40282e38px}.\[\&\:\:-webkit-slider-thumb\]\:bg-bambu-green::-webkit-slider-thumb{background-color:var(--color-bambu-green)}}:root{--accent:#00ae42;--accent-light:#00c64d;--accent-dark:#009438;--status-ok:#22c55e;--status-error:#ef4444;--status-warning:#f59e0b;--bg-primary:#f5f5f5;--bg-secondary:#fff;--bg-tertiary:#e5e5e5;--text-primary:#1a1a1a;--text-secondary:#4a4a4a;--text-muted:#6b6b6b;--text-tertiary:gray;--border-color:#d4d4d4;--card-shadow:0 2px 8px #00000014;--glow-color:transparent;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:Inter,system-ui,sans-serif;font-weight:400;line-height:1.5}.dark{--bg-primary:#1a1a1a;--bg-secondary:#2d2d2d;--bg-tertiary:#3d3d3d;--text-primary:#fff;--text-secondary:#a0a0a0;--text-muted:gray;--text-tertiary:#4a4a4a;--border-color:#3d3d3d;--card-shadow:0 4px 16px #0006}.bg-warm{--bg-primary:#faf8f5;--bg-secondary:#fffefa;--bg-tertiary:#e8e4dd;--text-primary:#2d2a26;--text-secondary:#5c5750;--text-muted:#7a756c;--text-tertiary:#9a9590;--border-color:#d8d4cc}.bg-cool{--bg-primary:#f0f4f8;--bg-secondary:#fff;--bg-tertiary:#dce4ec;--text-primary:#1a2530;--text-secondary:#4a5568;--text-muted:#6b7a8a;--text-tertiary:#8a9aaa;--border-color:#c8d4e0}.dark.bg-neutral{--bg-primary:#1a1a1a;--bg-secondary:#2d2d2d;--bg-tertiary:#3d3d3d;--text-primary:#fff;--text-secondary:#a0a0a0;--text-muted:gray;--text-tertiary:#4a4a4a;--border-color:#3d3d3d}.dark.bg-warm{--bg-primary:#1c1a18;--bg-secondary:#2e2a26;--bg-tertiary:#3e3a36;--text-primary:#f5f0ea;--text-secondary:#b0a898;--text-muted:#8a8278;--text-tertiary:#5a5248;--border-color:#3e3a36}.dark.bg-cool{--bg-primary:#181c20;--bg-secondary:#262c32;--bg-tertiary:#363e46;--text-primary:#f0f4f8;--text-secondary:#98a8b8;--text-muted:#788898;--text-tertiary:#4a5a6a;--border-color:#363e46}.dark.bg-oled{--bg-primary:#000;--bg-secondary:#141414;--bg-tertiary:#1f1f1f;--text-primary:#fff;--text-secondary:#a0a0a0;--text-muted:#707070;--text-tertiary:#404040;--border-color:#2a2a2a}.dark.bg-slate{--bg-primary:#0f172a;--bg-secondary:#1e293b;--bg-tertiary:#334155;--text-primary:#f1f5f9;--text-secondary:#94a3b8;--text-muted:#64748b;--text-tertiary:#475569;--border-color:#334155}.dark.bg-forest{--bg-primary:#121a16;--bg-secondary:#1c2a22;--bg-tertiary:#2a3d30;--text-primary:#e8f5ec;--text-secondary:#8aa894;--text-muted:#6a8874;--text-tertiary:#4a6854;--border-color:#2a3d30}.style-glow{--card-shadow:0 2px 8px #00000014,0 0 25px var(--accent)}@supports (color:color-mix(in lab,red,red)){.style-glow{--card-shadow:0 2px 8px #00000014,0 0 25px color-mix(in srgb,var(--accent)12%,transparent)}}.dark.style-glow{--card-shadow:0 4px 20px #00000080,0 0 40px var(--accent)}@supports (color:color-mix(in lab,red,red)){.dark.style-glow{--card-shadow:0 4px 20px #00000080,0 0 40px color-mix(in srgb,var(--accent)15%,transparent)}}.style-vibrant{--card-shadow:0 8px 30px #00000026,0 2px 8px #0000001a}.dark.style-vibrant{--card-shadow:0 10px 40px #0009,0 4px 12px #0006}.accent-green{--accent:#00ae42;--accent-light:#00c64d;--accent-dark:#009438}.accent-teal{--accent:#14b8a6;--accent-light:#2dd4bf;--accent-dark:#0d9488}.accent-blue{--accent:#3b82f6;--accent-light:#60a5fa;--accent-dark:#2563eb}.accent-orange{--accent:#f97316;--accent-light:#fb923c;--accent-dark:#ea580c}.accent-purple{--accent:#8b5cf6;--accent-light:#a78bfa;--accent-dark:#7c3aed}.accent-red{--accent:#ef4444;--accent-light:#f87171;--accent-dark:#dc2626}body{background-color:var(--bg-primary);color:var(--text-primary);min-height:100vh;margin:0;transition:background-color .2s,color .2s}#root{min-height:100vh}.text-white{color:var(--text-primary)}.bg-bambu-dark,.bg-bambu-dark-secondary,.bg-bambu-dark-tertiary,.border-bambu-dark-tertiary{transition:background-color .2s,border-color .2s}@keyframes slide-in{0%{opacity:0;transform:translate(100%)}to{opacity:1;transform:translate(0)}}.animate-slide-in{animation:.2s ease-out slide-in}.icon-theme{opacity:.5}.dark .icon-theme{filter:invert();opacity:.4}.icon-green{filter:invert(48%)sepia(89%)saturate(459%)hue-rotate(93deg)brightness(95%)contrast(92%);opacity:1}.icon-heating{filter:invert(50%)sepia()saturate(1000%)hue-rotate(360deg)brightness()contrast();opacity:1}.dark .jogpad-theme{filter:brightness(.35)contrast(1.2)}.ams-empty-slot{background:repeating-linear-gradient(45deg,#444,#444 2px,#222 2px 4px)}.dark .ams-empty-slot{background:repeating-linear-gradient(45deg,#555,#555 2px,#333 2px 4px)}.touch-manipulation{touch-action:manipulation}.safe-area-bottom{padding-bottom:env(safe-area-inset-bottom,0)}.safe-area-top{padding-top:env(safe-area-inset-top,0)}.scrollbar-hide{-ms-overflow-style:none;scrollbar-width:none}.scrollbar-hide::-webkit-scrollbar{display:none}@keyframes slide-in-left{0%{transform:translate(-100%)}to{transform:translate(0)}}.animate-slide-in-left{animation:.3s ease-out slide-in-left}.card-shadow{box-shadow:var(--card-shadow)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:""}}} diff --git a/static/assets/index-BpwPeQpb.js b/static/assets/index-BpwPeQpb.js new file mode 100644 index 000000000..a1445eef0 --- /dev/null +++ b/static/assets/index-BpwPeQpb.js @@ -0,0 +1,8729 @@ +function Jse(t,e){for(var n=0;nr[i]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const a of i)if(a.type==="childList")for(const o of a.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).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 f_=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function ml(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var VA={exports:{}},ib={};var ZU;function eoe(){if(ZU)return ib;ZU=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function n(r,i,a){var o=null;if(a!==void 0&&(o=""+a),i.key!==void 0&&(o=""+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:o,ref:i!==void 0?i:null,props:a}}return ib.Fragment=e,ib.jsx=n,ib.jsxs=n,ib}var QU;function toe(){return QU||(QU=1,VA.exports=eoe()),VA.exports}var s=toe(),GA={exports:{}},gn={};var JU;function noe(){if(JU)return gn;JU=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"),o=Symbol.for("react.context"),l=Symbol.for("react.forward_ref"),c=Symbol.for("react.suspense"),u=Symbol.for("react.memo"),d=Symbol.for("react.lazy"),f=Symbol.for("react.activity"),m=Symbol.iterator;function p(z){return z===null||typeof z!="object"?null:(z=m&&z[m]||z["@@iterator"],typeof z=="function"?z:null)}var b={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},y=Object.assign,x={};function v(z,V,ne){this.props=z,this.context=V,this.refs=x,this.updater=ne||b}v.prototype.isReactComponent={},v.prototype.setState=function(z,V){if(typeof z!="object"&&typeof z!="function"&&z!=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,z,V,"setState")},v.prototype.forceUpdate=function(z){this.updater.enqueueForceUpdate(this,z,"forceUpdate")};function S(){}S.prototype=v.prototype;function M(z,V,ne){this.props=z,this.context=V,this.refs=x,this.updater=ne||b}var A=M.prototype=new S;A.constructor=M,y(A,v.prototype),A.isPureReactComponent=!0;var T=Array.isArray;function k(){}var E={H:null,A:null,T:null,S:null},D=Object.prototype.hasOwnProperty;function C(z,V,ne){var ae=ne.ref;return{$$typeof:t,type:z,key:V,ref:ae!==void 0?ae:null,props:ne}}function R(z,V){return C(z.type,V,z.props)}function U(z){return typeof z=="object"&&z!==null&&z.$$typeof===t}function O(z){var V={"=":"=0",":":"=2"};return"$"+z.replace(/[=:]/g,function(ne){return V[ne]})}var q=/\/+/g;function j(z,V){return typeof z=="object"&&z!==null&&z.key!=null?O(""+z.key):V.toString(36)}function W(z){switch(z.status){case"fulfilled":return z.value;case"rejected":throw z.reason;default:switch(typeof z.status=="string"?z.then(k,k):(z.status="pending",z.then(function(V){z.status==="pending"&&(z.status="fulfilled",z.value=V)},function(V){z.status==="pending"&&(z.status="rejected",z.reason=V)})),z.status){case"fulfilled":return z.value;case"rejected":throw z.reason}}throw z}function K(z,V,ne,ae,he){var pe=typeof z;(pe==="undefined"||pe==="boolean")&&(z=null);var ie=!1;if(z===null)ie=!0;else switch(pe){case"bigint":case"string":case"number":ie=!0;break;case"object":switch(z.$$typeof){case t:case e:ie=!0;break;case d:return ie=z._init,K(ie(z._payload),V,ne,ae,he)}}if(ie)return he=he(z),ie=ae===""?"."+j(z,0):ae,T(he)?(ne="",ie!=null&&(ne=ie.replace(q,"$&/")+"/"),K(he,V,ne,"",function(Ne){return Ne})):he!=null&&(U(he)&&(he=R(he,ne+(he.key==null||z&&z.key===he.key?"":(""+he.key).replace(q,"$&/")+"/")+ie)),V.push(he)),1;ie=0;var Ee=ae===""?".":ae+":";if(T(z))for(var xe=0;xe>>1,J=K[te];if(0>>1;tei(ne,Q))aei(he,ne)?(K[te]=he,K[ae]=Q,te=ae):(K[te]=ne,K[V]=Q,te=V);else if(aei(he,Q))K[te]=he,K[ae]=Q,te=ae;else break e}}return G}function i(K,G){var Q=K.sortIndex-G.sortIndex;return Q!==0?Q: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 o=Date,l=o.now();t.unstable_now=function(){return o.now()-l}}var c=[],u=[],d=1,f=null,m=3,p=!1,b=!1,y=!1,x=!1,v=typeof setTimeout=="function"?setTimeout:null,S=typeof clearTimeout=="function"?clearTimeout:null,M=typeof setImmediate<"u"?setImmediate:null;function A(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 T(K){if(y=!1,A(K),!b)if(n(c)!==null)b=!0,k||(k=!0,O());else{var G=n(u);G!==null&&W(T,G.startTime-K)}}var k=!1,E=-1,D=5,C=-1;function R(){return x?!0:!(t.unstable_now()-CK&&R());){var te=f.callback;if(typeof te=="function"){f.callback=null,m=f.priorityLevel;var J=te(f.expirationTime<=K);if(K=t.unstable_now(),typeof J=="function"){f.callback=J,A(K),G=!0;break t}f===n(c)&&r(c),A(K)}else r(c);f=n(c)}if(f!==null)G=!0;else{var z=n(u);z!==null&&W(T,z.startTime-K),G=!1}}break e}finally{f=null,m=Q,p=!1}G=void 0}}finally{G?O():k=!1}}}var O;if(typeof M=="function")O=function(){M(U)};else if(typeof MessageChannel<"u"){var q=new MessageChannel,j=q.port2;q.port1.onmessage=U,O=function(){j.postMessage(null)}}else O=function(){v(U,0)};function W(K,G){E=v(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||125te?(K.sortIndex=Q,e(u,K),n(c)===null&&K===n(u)&&(y?(S(E),E=-1):y=!0,W(T,Q-te))):(K.sortIndex=J,e(c,K),b||p||(b=!0,k||(k=!0,O()))),K},t.unstable_shouldYield=R,t.unstable_wrapCallback=function(K){var G=m;return function(){var Q=m;m=G;try{return K.apply(this,arguments)}finally{m=Q}}}})(KA)),KA}var nz;function aoe(){return nz||(nz=1,qA.exports=ioe()),qA.exports}var XA={exports:{}},ka={};var rz;function soe(){if(rz)return ka;rz=1;var t=Gg();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(),XA.exports=soe(),XA.exports}var az;function ooe(){if(az)return ab;az=1;var t=aoe(),e=Gg(),n=bq();function r(h){var g="https://react.dev/errors/"+h;if(1J||(h.current=te[J],te[J]=null,J--)}function ne(h,g){J++,te[J]=h.current,h.current=g}var ae=z(null),he=z(null),pe=z(null),ie=z(null);function Ee(h,g){switch(ne(pe,g),ne(he,h),ne(ae,null),g.nodeType){case 9:case 11:h=(h=g.documentElement)&&(h=h.namespaceURI)?vU(h):0;break;default:if(h=g.tagName,g=g.namespaceURI)g=vU(g),h=_U(g,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}V(ae),ne(ae,h)}function xe(){V(ae),V(he),V(pe)}function Ne(h){h.memoizedState!==null&&ne(ie,h);var g=ae.current,_=_U(g,h.type);g!==_&&(ne(he,h),ne(ae,_))}function _e(h){he.current===h&&(V(ae),V(he)),ie.current===h&&(V(ie),eb._currentValue=Q)}var le,Ve;function H(h){if(le===void 0)try{throw Error()}catch(_){var g=_.stack.trim().match(/\n( *(at )?)/);le=g&&g[1]||"",Ve=-1<_.stack.indexOf(` + at`)?" ()":-1<_.stack.indexOf("@")?"@unknown:0:0":""}return` +`+le+h+Ve}var ee=!1;function X(h,g){if(!h||ee)return"";ee=!0;var _=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var N={DetermineComponentFrameRoot:function(){try{if(g){var vt=function(){throw Error()};if(Object.defineProperty(vt.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(vt,[])}catch(ht){var ot=ht}Reflect.construct(h,[],vt)}else{try{vt.call()}catch(ht){ot=ht}h.call(vt.prototype)}}else{try{throw Error()}catch(ht){ot=ht}(vt=h())&&typeof vt.catch=="function"&&vt.catch(function(){})}}catch(ht){if(ht&&ot&&typeof ht.stack=="string")return[ht.stack,ot.stack]}return[null,null]}};N.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var P=Object.getOwnPropertyDescriptor(N.DetermineComponentFrameRoot,"name");P&&P.configurable&&Object.defineProperty(N.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var F=N.DetermineComponentFrameRoot(),Z=F[0],ge=F[1];if(Z&&ge){var Oe=Z.split(` +`),it=ge.split(` +`);for(P=N=0;NP||Oe[N]!==it[P]){var xt=` +`+Oe[N].replace(" at new "," at ");return h.displayName&&xt.includes("")&&(xt=xt.replace("",h.displayName)),xt}while(1<=N&&0<=P);break}}}finally{ee=!1,Error.prepareStackTrace=_}return(_=h?h.displayName||h.name:"")?H(_):""}function B(h,g){switch(h.tag){case 26:case 27:case 5:return H(h.type);case 16:return H("Lazy");case 13:return h.child!==g&&g!==null?H("Suspense Fallback"):H("Suspense");case 19:return H("SuspenseList");case 0:case 15:return X(h.type,!1);case 11:return X(h.type.render,!1);case 1:return X(h.type,!0);case 31:return H("Activity");default:return""}}function I(h){try{var g="",_=null;do g+=B(h,_),_=h,h=h.return;while(h);return g}catch(N){return` +Error generating stack: `+N.message+` +`+N.stack}}var ue=Object.prototype.hasOwnProperty,Ae=t.unstable_scheduleCallback,Ie=t.unstable_cancelCallback,ce=t.unstable_shouldYield,$=t.unstable_requestPaint,L=t.unstable_now,Y=t.unstable_getCurrentPriorityLevel,be=t.unstable_ImmediatePriority,Se=t.unstable_UserBlockingPriority,se=t.unstable_NormalPriority,Pe=t.unstable_LowPriority,Re=t.unstable_IdlePriority,Xe=t.log,et=t.unstable_setDisableYieldValue,Je=null,oe=null;function ye(h){if(typeof Xe=="function"&&et(h),oe&&typeof oe.setStrictMode=="function")try{oe.setStrictMode(Je,h)}catch{}}var je=Math.clz32?Math.clz32:re,$e=Math.log,ft=Math.LN2;function re(h){return h>>>=0,h===0?32:31-($e(h)/ft|0)|0}var ke=256,ve=262144,Fe=4194304;function Ue(h){var g=h&42;if(g!==0)return g;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 Te(h,g,_){var N=h.pendingLanes;if(N===0)return 0;var P=0,F=h.suspendedLanes,Z=h.pingedLanes;h=h.warmLanes;var ge=N&134217727;return ge!==0?(N=ge&~F,N!==0?P=Ue(N):(Z&=ge,Z!==0?P=Ue(Z):_||(_=ge&~h,_!==0&&(P=Ue(_))))):(ge=N&~F,ge!==0?P=Ue(ge):Z!==0?P=Ue(Z):_||(_=N&~h,_!==0&&(P=Ue(_)))),P===0?0:g!==0&&g!==P&&(g&F)===0&&(F=P&-P,_=g&-g,F>=_||F===32&&(_&4194048)!==0)?g:P}function qe(h,g){return(h.pendingLanes&~(h.suspendedLanes&~h.pingedLanes)&g)===0}function bt(h,g){switch(h){case 1:case 2:case 4:case 8:case 64:return g+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return g+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Pt(){var h=Fe;return Fe<<=1,(Fe&62914560)===0&&(Fe=4194304),h}function Ot(h){for(var g=[],_=0;31>_;_++)g.push(h);return g}function ln(h,g){h.pendingLanes|=g,g!==268435456&&(h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0)}function Gt(h,g,_,N,P,F){var Z=h.pendingLanes;h.pendingLanes=_,h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0,h.expiredLanes&=_,h.entangledLanes&=_,h.errorRecoveryDisabledLanes&=_,h.shellSuspendCounter=0;var ge=h.entanglements,Oe=h.expirationTimes,it=h.hiddenUpdates;for(_=Z&~_;0<_;){var xt=31-je(_),vt=1<"u")return null;try{return h.activeElement||h.body}catch{return h.body}}var Bt=/[\n"\\]/g;function At(h){return h.replace(Bt,function(g){return"\\"+g.charCodeAt(0).toString(16)+" "})}function nn(h,g,_,N,P,F,Z,ge){h.name="",Z!=null&&typeof Z!="function"&&typeof Z!="symbol"&&typeof Z!="boolean"?h.type=Z:h.removeAttribute("type"),g!=null?Z==="number"?(g===0&&h.value===""||h.value!=g)&&(h.value=""+me(g)):h.value!==""+me(g)&&(h.value=""+me(g)):Z!=="submit"&&Z!=="reset"||h.removeAttribute("value"),g!=null?Lr(h,Z,me(g)):_!=null?Lr(h,Z,me(_)):N!=null&&h.removeAttribute("value"),P==null&&F!=null&&(h.defaultChecked=!!F),P!=null&&(h.checked=P&&typeof P!="function"&&typeof P!="symbol"),ge!=null&&typeof ge!="function"&&typeof ge!="symbol"&&typeof ge!="boolean"?h.name=""+me(ge):h.removeAttribute("name")}function Zr(h,g,_,N,P,F,Z,ge){if(F!=null&&typeof F!="function"&&typeof F!="symbol"&&typeof F!="boolean"&&(h.type=F),g!=null||_!=null){if(!(F!=="submit"&&F!=="reset"||g!=null)){jt(h);return}_=_!=null?""+me(_):"",g=g!=null?""+me(g):_,ge||g===h.value||(h.value=g),h.defaultValue=g}N=N??P,N=typeof N!="function"&&typeof N!="symbol"&&!!N,h.checked=ge?h.checked:!!N,h.defaultChecked=!!N,Z!=null&&typeof Z!="function"&&typeof Z!="symbol"&&typeof Z!="boolean"&&(h.name=Z),jt(h)}function Lr(h,g,_){g==="number"&&Rt(h.ownerDocument)===h||h.defaultValue===""+_||(h.defaultValue=""+_)}function ca(h,g,_,N){if(h=h.options,g){g={};for(var P=0;P<_.length;P++)g["$"+_[P]]=!0;for(_=0;_"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),VE=!1;if(mc)try{var bx={};Object.defineProperty(bx,"passive",{get:function(){VE=!0}}),window.addEventListener("test",bx,bx),window.removeEventListener("test",bx,bx)}catch{VE=!1}var ju=null,GE=null,av=null;function _I(){if(av)return av;var h,g=GE,_=g.length,N,P="value"in ju?ju.value:ju.textContent,F=P.length;for(h=0;h<_&&g[h]===P[h];h++);var Z=_-h;for(N=1;N<=Z&&g[_-N]===P[F-N];N++);return av=P.slice(h,1=_x),AI=" ",TI=!1;function CI(h,g){switch(h){case"keyup":return _ae.indexOf(g.keyCode)!==-1;case"keydown":return g.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function kI(h){return h=h.detail,typeof h=="object"&&"data"in h?h.data:null}var vm=!1;function Sae(h,g){switch(h){case"compositionend":return kI(g);case"keypress":return g.which!==32?null:(TI=!0,AI);case"textInput":return h=g.data,h===AI&&TI?null:h;default:return null}}function Mae(h,g){if(vm)return h==="compositionend"||!YE&&CI(h,g)?(h=_I(),av=GE=ju=null,vm=!1,h):null;switch(h){case"paste":return null;case"keypress":if(!(g.ctrlKey||g.altKey||g.metaKey)||g.ctrlKey&&g.altKey){if(g.char&&1=g)return{node:_,offset:g-h};h=N}e:{for(;_;){if(_.nextSibling){_=_.nextSibling;break e}_=_.parentNode}_=void 0}_=FI(_)}}function zI(h,g){return h&&g?h===g?!0:h&&h.nodeType===3?!1:g&&g.nodeType===3?zI(h,g.parentNode):"contains"in h?h.contains(g):h.compareDocumentPosition?!!(h.compareDocumentPosition(g)&16):!1:!1}function BI(h){h=h!=null&&h.ownerDocument!=null&&h.ownerDocument.defaultView!=null?h.ownerDocument.defaultView:window;for(var g=Rt(h.document);g instanceof h.HTMLIFrameElement;){try{var _=typeof g.contentWindow.location.href=="string"}catch{_=!1}if(_)h=g.contentWindow;else break;g=Rt(h.document)}return g}function JE(h){var g=h&&h.nodeName&&h.nodeName.toLowerCase();return g&&(g==="input"&&(h.type==="text"||h.type==="search"||h.type==="tel"||h.type==="url"||h.type==="password")||g==="textarea"||h.contentEditable==="true")}var Pae=mc&&"documentMode"in document&&11>=document.documentMode,_m=null,e2=null,Nx=null,t2=!1;function $I(h,g,_){var N=_.window===_?_.document:_.nodeType===9?_:_.ownerDocument;t2||_m==null||_m!==Rt(N)||(N=_m,"selectionStart"in N&&JE(N)?N={start:N.selectionStart,end:N.selectionEnd}:(N=(N.ownerDocument&&N.ownerDocument.defaultView||window).getSelection(),N={anchorNode:N.anchorNode,anchorOffset:N.anchorOffset,focusNode:N.focusNode,focusOffset:N.focusOffset}),Nx&&Mx(Nx,N)||(Nx=N,N=Qv(e2,"onSelect"),0>=Z,P-=Z,vl=1<<32-je(g)+P|_<yn?(Dn=Qt,Qt=null):Dn=Qt.sibling;var qn=ot(Qe,Qt,rt[yn],yt);if(qn===null){Qt===null&&(Qt=Dn);break}h&&Qt&&qn.alternate===null&&g(Qe,Qt),ze=F(qn,ze,yn),Wn===null?en=qn:Wn.sibling=qn,Wn=qn,Qt=Dn}if(yn===rt.length)return _(Qe,Qt),In&&gc(Qe,yn),en;if(Qt===null){for(;ynyn?(Dn=Qt,Qt=null):Dn=Qt.sibling;var Ju=ot(Qe,Qt,qn.value,yt);if(Ju===null){Qt===null&&(Qt=Dn);break}h&&Qt&&Ju.alternate===null&&g(Qe,Qt),ze=F(Ju,ze,yn),Wn===null?en=Ju:Wn.sibling=Ju,Wn=Ju,Qt=Dn}if(qn.done)return _(Qe,Qt),In&&gc(Qe,yn),en;if(Qt===null){for(;!qn.done;yn++,qn=rt.next())qn=vt(Qe,qn.value,yt),qn!==null&&(ze=F(qn,ze,yn),Wn===null?en=qn:Wn.sibling=qn,Wn=qn);return In&&gc(Qe,yn),en}for(Qt=N(Qt);!qn.done;yn++,qn=rt.next())qn=ht(Qt,Qe,yn,qn.value,yt),qn!==null&&(h&&qn.alternate!==null&&Qt.delete(qn.key===null?yn:qn.key),ze=F(qn,ze,yn),Wn===null?en=qn:Wn.sibling=qn,Wn=qn);return h&&Qt.forEach(function(Qse){return g(Qe,Qse)}),In&&gc(Qe,yn),en}function dr(Qe,ze,rt,yt){if(typeof rt=="object"&&rt!==null&&rt.type===y&&rt.key===null&&(rt=rt.props.children),typeof rt=="object"&&rt!==null){switch(rt.$$typeof){case p:e:{for(var en=rt.key;ze!==null;){if(ze.key===en){if(en=rt.type,en===y){if(ze.tag===7){_(Qe,ze.sibling),yt=P(ze,rt.props.children),yt.return=Qe,Qe=yt;break e}}else if(ze.elementType===en||typeof en=="object"&&en!==null&&en.$$typeof===D&&jh(en)===ze.type){_(Qe,ze.sibling),yt=P(ze,rt.props),jx(yt,rt),yt.return=Qe,Qe=yt;break e}_(Qe,ze);break}else g(Qe,ze);ze=ze.sibling}rt.type===y?(yt=Eh(rt.props.children,Qe.mode,yt,rt.key),yt.return=Qe,Qe=yt):(yt=pv(rt.type,rt.key,rt.props,null,Qe.mode,yt),jx(yt,rt),yt.return=Qe,Qe=yt)}return Z(Qe);case b:e:{for(en=rt.key;ze!==null;){if(ze.key===en)if(ze.tag===4&&ze.stateNode.containerInfo===rt.containerInfo&&ze.stateNode.implementation===rt.implementation){_(Qe,ze.sibling),yt=P(ze,rt.children||[]),yt.return=Qe,Qe=yt;break e}else{_(Qe,ze);break}else g(Qe,ze);ze=ze.sibling}yt=l2(rt,Qe.mode,yt),yt.return=Qe,Qe=yt}return Z(Qe);case D:return rt=jh(rt),dr(Qe,ze,rt,yt)}if(W(rt))return Xt(Qe,ze,rt,yt);if(O(rt)){if(en=O(rt),typeof en!="function")throw Error(r(150));return rt=en.call(rt),sn(Qe,ze,rt,yt)}if(typeof rt.then=="function")return dr(Qe,ze,wv(rt),yt);if(rt.$$typeof===M)return dr(Qe,ze,bv(Qe,rt),yt);Sv(Qe,rt)}return typeof rt=="string"&&rt!==""||typeof rt=="number"||typeof rt=="bigint"?(rt=""+rt,ze!==null&&ze.tag===6?(_(Qe,ze.sibling),yt=P(ze,rt),yt.return=Qe,Qe=yt):(_(Qe,ze),yt=o2(rt,Qe.mode,yt),yt.return=Qe,Qe=yt),Z(Qe)):_(Qe,ze)}return function(Qe,ze,rt,yt){try{kx=0;var en=dr(Qe,ze,rt,yt);return Pm=null,en}catch(Qt){if(Qt===jm||Qt===vv)throw Qt;var Wn=ks(29,Qt,null,Qe.mode);return Wn.lanes=yt,Wn.return=Qe,Wn}}}var Rh=dF(!0),hF=dF(!1),Lu=!1;function v2(h){h.updateQueue={baseState:h.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function _2(h,g){h=h.updateQueue,g.updateQueue===h&&(g.updateQueue={baseState:h.baseState,firstBaseUpdate:h.firstBaseUpdate,lastBaseUpdate:h.lastBaseUpdate,shared:h.shared,callbacks:null})}function Iu(h){return{lane:h,tag:0,payload:null,callback:null,next:null}}function Fu(h,g,_){var N=h.updateQueue;if(N===null)return null;if(N=N.shared,(Qn&2)!==0){var P=N.pending;return P===null?g.next=g:(g.next=P.next,P.next=g),N.pending=g,g=mv(h),XI(h,null,_),g}return fv(h,N,g,_),mv(h)}function Px(h,g,_){if(g=g.updateQueue,g!==null&&(g=g.shared,(_&4194048)!==0)){var N=g.lanes;N&=h.pendingLanes,_|=N,g.lanes=_,gt(h,_)}}function w2(h,g){var _=h.updateQueue,N=h.alternate;if(N!==null&&(N=N.updateQueue,_===N)){var P=null,F=null;if(_=_.firstBaseUpdate,_!==null){do{var Z={lane:_.lane,tag:_.tag,payload:_.payload,callback:null,next:null};F===null?P=F=Z:F=F.next=Z,_=_.next}while(_!==null);F===null?P=F=g:F=F.next=g}else P=F=g;_={baseState:N.baseState,firstBaseUpdate:P,lastBaseUpdate:F,shared:N.shared,callbacks:N.callbacks},h.updateQueue=_;return}h=_.lastBaseUpdate,h===null?_.firstBaseUpdate=g:h.next=g,_.lastBaseUpdate=g}var S2=!1;function Rx(){if(S2){var h=km;if(h!==null)throw h}}function Dx(h,g,_,N){S2=!1;var P=h.updateQueue;Lu=!1;var F=P.firstBaseUpdate,Z=P.lastBaseUpdate,ge=P.shared.pending;if(ge!==null){P.shared.pending=null;var Oe=ge,it=Oe.next;Oe.next=null,Z===null?F=it:Z.next=it,Z=Oe;var xt=h.alternate;xt!==null&&(xt=xt.updateQueue,ge=xt.lastBaseUpdate,ge!==Z&&(ge===null?xt.firstBaseUpdate=it:ge.next=it,xt.lastBaseUpdate=Oe))}if(F!==null){var vt=P.baseState;Z=0,xt=it=Oe=null,ge=F;do{var ot=ge.lane&-536870913,ht=ot!==ge.lane;if(ht?(Rn&ot)===ot:(N&ot)===ot){ot!==0&&ot===Cm&&(S2=!0),xt!==null&&(xt=xt.next={lane:0,tag:ge.tag,payload:ge.payload,callback:null,next:null});e:{var Xt=h,sn=ge;ot=g;var dr=_;switch(sn.tag){case 1:if(Xt=sn.payload,typeof Xt=="function"){vt=Xt.call(dr,vt,ot);break e}vt=Xt;break e;case 3:Xt.flags=Xt.flags&-65537|128;case 0:if(Xt=sn.payload,ot=typeof Xt=="function"?Xt.call(dr,vt,ot):Xt,ot==null)break e;vt=f({},vt,ot);break e;case 2:Lu=!0}}ot=ge.callback,ot!==null&&(h.flags|=64,ht&&(h.flags|=8192),ht=P.callbacks,ht===null?P.callbacks=[ot]:ht.push(ot))}else ht={lane:ot,tag:ge.tag,payload:ge.payload,callback:ge.callback,next:null},xt===null?(it=xt=ht,Oe=vt):xt=xt.next=ht,Z|=ot;if(ge=ge.next,ge===null){if(ge=P.shared.pending,ge===null)break;ht=ge,ge=ht.next,ht.next=null,P.lastBaseUpdate=ht,P.shared.pending=null}}while(!0);xt===null&&(Oe=vt),P.baseState=Oe,P.firstBaseUpdate=it,P.lastBaseUpdate=xt,F===null&&(P.shared.lanes=0),Hu|=Z,h.lanes=Z,h.memoizedState=vt}}function fF(h,g){if(typeof h!="function")throw Error(r(191,h));h.call(g)}function mF(h,g){var _=h.callbacks;if(_!==null)for(h.callbacks=null,h=0;h<_.length;h++)fF(_[h],g)}var Rm=z(null),Mv=z(0);function pF(h,g){h=Ac,ne(Mv,h),ne(Rm,g),Ac=h|g.baseLanes}function M2(){ne(Mv,Ac),ne(Rm,Rm.current)}function N2(){Ac=Mv.current,V(Rm),V(Mv)}var js=z(null),oo=null;function Uu(h){var g=h.alternate;ne(hi,hi.current&1),ne(js,h),oo===null&&(g===null||Rm.current!==null||g.memoizedState!==null)&&(oo=h)}function E2(h){ne(hi,hi.current),ne(js,h),oo===null&&(oo=h)}function gF(h){h.tag===22?(ne(hi,hi.current),ne(js,h),oo===null&&(oo=h)):zu()}function zu(){ne(hi,hi.current),ne(js,js.current)}function Ps(h){V(js),oo===h&&(oo=null),V(hi)}var hi=z(0);function Nv(h){for(var g=h;g!==null;){if(g.tag===13){var _=g.memoizedState;if(_!==null&&(_=_.dehydrated,_===null||PA(_)||RA(_)))return g}else if(g.tag===19&&(g.memoizedProps.revealOrder==="forwards"||g.memoizedProps.revealOrder==="backwards"||g.memoizedProps.revealOrder==="unstable_legacy-backwards"||g.memoizedProps.revealOrder==="together")){if((g.flags&128)!==0)return g}else if(g.child!==null){g.child.return=g,g=g.child;continue}if(g===h)break;for(;g.sibling===null;){if(g.return===null||g.return===h)return null;g=g.return}g.sibling.return=g.return,g=g.sibling}return null}var yc=0,bn=null,cr=null,yi=null,Ev=!1,Dm=!1,Dh=!1,Av=0,Ox=0,Om=null,$ae=0;function Qr(){throw Error(r(321))}function A2(h,g){if(g===null)return!1;for(var _=0;_F?F:8;var Z=K.T,ge={};K.T=ge,$2(h,!1,g,_);try{var Oe=P(),it=K.S;if(it!==null&&it(ge,Oe),Oe!==null&&typeof Oe=="object"&&typeof Oe.then=="function"){var xt=Bae(Oe,N);Ix(h,g,xt,Os(h))}else Ix(h,g,N,Os(h))}catch(vt){Ix(h,g,{then:function(){},status:"rejected",reason:vt},Os())}finally{G.p=F,Z!==null&&ge.types!==null&&(Z.types=ge.types),K.T=Z}}function qae(){}function z2(h,g,_,N){if(h.tag!==5)throw Error(r(476));var P=WF(h).queue;GF(h,P,g,Q,_===null?qae:function(){return qF(h),_(N)})}function WF(h){var g=h.memoizedState;if(g!==null)return g;g={memoizedState:Q,baseState:Q,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:vc,lastRenderedState:Q},next:null};var _={};return g.next={memoizedState:_,baseState:_,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:vc,lastRenderedState:_},next:null},h.memoizedState=g,h=h.alternate,h!==null&&(h.memoizedState=g),g}function qF(h){var g=WF(h);g.next===null&&(g=h.alternate.memoizedState),Ix(h,g.next.queue,{},Os())}function B2(){return da(eb)}function KF(){return fi().memoizedState}function XF(){return fi().memoizedState}function Kae(h){for(var g=h.return;g!==null;){switch(g.tag){case 24:case 3:var _=Os();h=Iu(_);var N=Fu(g,h,_);N!==null&&(fs(N,g,_),Px(N,g,_)),g={cache:g2()},h.payload=g;return}g=g.return}}function Xae(h,g,_){var N=Os();_={lane:N,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null},Rv(h)?ZF(g,_):(_=a2(h,g,_,N),_!==null&&(fs(_,h,N),QF(_,g,N)))}function YF(h,g,_){var N=Os();Ix(h,g,_,N)}function Ix(h,g,_,N){var P={lane:N,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null};if(Rv(h))ZF(g,P);else{var F=h.alternate;if(h.lanes===0&&(F===null||F.lanes===0)&&(F=g.lastRenderedReducer,F!==null))try{var Z=g.lastRenderedState,ge=F(Z,_);if(P.hasEagerState=!0,P.eagerState=ge,Cs(ge,Z))return fv(h,g,P,0),_r===null&&hv(),!1}catch{}if(_=a2(h,g,P,N),_!==null)return fs(_,h,N),QF(_,g,N),!0}return!1}function $2(h,g,_,N){if(N={lane:2,revertLane:vA(),gesture:null,action:N,hasEagerState:!1,eagerState:null,next:null},Rv(h)){if(g)throw Error(r(479))}else g=a2(h,_,N,2),g!==null&&fs(g,h,2)}function Rv(h){var g=h.alternate;return h===bn||g!==null&&g===bn}function ZF(h,g){Dm=Ev=!0;var _=h.pending;_===null?g.next=g:(g.next=_.next,_.next=g),h.pending=g}function QF(h,g,_){if((_&4194048)!==0){var N=g.lanes;N&=h.pendingLanes,_|=N,g.lanes=_,gt(h,_)}}var Fx={readContext:da,use:Cv,useCallback:Qr,useContext:Qr,useEffect:Qr,useImperativeHandle:Qr,useLayoutEffect:Qr,useInsertionEffect:Qr,useMemo:Qr,useReducer:Qr,useRef:Qr,useState:Qr,useDebugValue:Qr,useDeferredValue:Qr,useTransition:Qr,useSyncExternalStore:Qr,useId:Qr,useHostTransitionStatus:Qr,useFormState:Qr,useActionState:Qr,useOptimistic:Qr,useMemoCache:Qr,useCacheRefresh:Qr};Fx.useEffectEvent=Qr;var JF={readContext:da,use:Cv,useCallback:function(h,g){return Ga().memoizedState=[h,g===void 0?null:g],h},useContext:da,useEffect:LF,useImperativeHandle:function(h,g,_){_=_!=null?_.concat([h]):null,jv(4194308,4,zF.bind(null,g,h),_)},useLayoutEffect:function(h,g){return jv(4194308,4,h,g)},useInsertionEffect:function(h,g){jv(4,2,h,g)},useMemo:function(h,g){var _=Ga();g=g===void 0?null:g;var N=h();if(Dh){ye(!0);try{h()}finally{ye(!1)}}return _.memoizedState=[N,g],N},useReducer:function(h,g,_){var N=Ga();if(_!==void 0){var P=_(g);if(Dh){ye(!0);try{_(g)}finally{ye(!1)}}}else P=g;return N.memoizedState=N.baseState=P,h={pending:null,lanes:0,dispatch:null,lastRenderedReducer:h,lastRenderedState:P},N.queue=h,h=h.dispatch=Xae.bind(null,bn,h),[N.memoizedState,h]},useRef:function(h){var g=Ga();return h={current:h},g.memoizedState=h},useState:function(h){h=O2(h);var g=h.queue,_=YF.bind(null,bn,g);return g.dispatch=_,[h.memoizedState,_]},useDebugValue:F2,useDeferredValue:function(h,g){var _=Ga();return U2(_,h,g)},useTransition:function(){var h=O2(!1);return h=GF.bind(null,bn,h.queue,!0,!1),Ga().memoizedState=h,[!1,h]},useSyncExternalStore:function(h,g,_){var N=bn,P=Ga();if(In){if(_===void 0)throw Error(r(407));_=_()}else{if(_=g(),_r===null)throw Error(r(349));(Rn&127)!==0||vF(N,g,_)}P.memoizedState=_;var F={value:_,getSnapshot:g};return P.queue=F,LF(wF.bind(null,N,F,h),[h]),N.flags|=2048,Lm(9,{destroy:void 0},_F.bind(null,N,F,_,g),null),_},useId:function(){var h=Ga(),g=_r.identifierPrefix;if(In){var _=_l,N=vl;_=(N&~(1<<32-je(N)-1)).toString(32)+_,g="_"+g+"R_"+_,_=Av++,0<_&&(g+="H"+_.toString(32)),g+="_"}else _=$ae++,g="_"+g+"r_"+_.toString(32)+"_";return h.memoizedState=g},useHostTransitionStatus:B2,useFormState:jF,useActionState:jF,useOptimistic:function(h){var g=Ga();g.memoizedState=g.baseState=h;var _={pending:null,lanes:0,dispatch:null,lastRenderedReducer:null,lastRenderedState:null};return g.queue=_,g=$2.bind(null,bn,!0,_),_.dispatch=g,[h,g]},useMemoCache:P2,useCacheRefresh:function(){return Ga().memoizedState=Kae.bind(null,bn)},useEffectEvent:function(h){var g=Ga(),_={impl:h};return g.memoizedState=_,function(){if((Qn&2)!==0)throw Error(r(440));return _.impl.apply(void 0,arguments)}}},H2={readContext:da,use:Cv,useCallback:$F,useContext:da,useEffect:I2,useImperativeHandle:BF,useInsertionEffect:FF,useLayoutEffect:UF,useMemo:HF,useReducer:kv,useRef:OF,useState:function(){return kv(vc)},useDebugValue:F2,useDeferredValue:function(h,g){var _=fi();return VF(_,cr.memoizedState,h,g)},useTransition:function(){var h=kv(vc)[0],g=fi().memoizedState;return[typeof h=="boolean"?h:Lx(h),g]},useSyncExternalStore:yF,useId:KF,useHostTransitionStatus:B2,useFormState:PF,useActionState:PF,useOptimistic:function(h,g){var _=fi();return NF(_,cr,h,g)},useMemoCache:P2,useCacheRefresh:XF};H2.useEffectEvent=IF;var e5={readContext:da,use:Cv,useCallback:$F,useContext:da,useEffect:I2,useImperativeHandle:BF,useInsertionEffect:FF,useLayoutEffect:UF,useMemo:HF,useReducer:D2,useRef:OF,useState:function(){return D2(vc)},useDebugValue:F2,useDeferredValue:function(h,g){var _=fi();return cr===null?U2(_,h,g):VF(_,cr.memoizedState,h,g)},useTransition:function(){var h=D2(vc)[0],g=fi().memoizedState;return[typeof h=="boolean"?h:Lx(h),g]},useSyncExternalStore:yF,useId:KF,useHostTransitionStatus:B2,useFormState:DF,useActionState:DF,useOptimistic:function(h,g){var _=fi();return cr!==null?NF(_,cr,h,g):(_.baseState=h,[h,_.queue.dispatch])},useMemoCache:P2,useCacheRefresh:XF};e5.useEffectEvent=IF;function V2(h,g,_,N){g=h.memoizedState,_=_(N,g),_=_==null?g:f({},g,_),h.memoizedState=_,h.lanes===0&&(h.updateQueue.baseState=_)}var G2={enqueueSetState:function(h,g,_){h=h._reactInternals;var N=Os(),P=Iu(N);P.payload=g,_!=null&&(P.callback=_),g=Fu(h,P,N),g!==null&&(fs(g,h,N),Px(g,h,N))},enqueueReplaceState:function(h,g,_){h=h._reactInternals;var N=Os(),P=Iu(N);P.tag=1,P.payload=g,_!=null&&(P.callback=_),g=Fu(h,P,N),g!==null&&(fs(g,h,N),Px(g,h,N))},enqueueForceUpdate:function(h,g){h=h._reactInternals;var _=Os(),N=Iu(_);N.tag=2,g!=null&&(N.callback=g),g=Fu(h,N,_),g!==null&&(fs(g,h,_),Px(g,h,_))}};function t5(h,g,_,N,P,F,Z){return h=h.stateNode,typeof h.shouldComponentUpdate=="function"?h.shouldComponentUpdate(N,F,Z):g.prototype&&g.prototype.isPureReactComponent?!Mx(_,N)||!Mx(P,F):!0}function n5(h,g,_,N){h=g.state,typeof g.componentWillReceiveProps=="function"&&g.componentWillReceiveProps(_,N),typeof g.UNSAFE_componentWillReceiveProps=="function"&&g.UNSAFE_componentWillReceiveProps(_,N),g.state!==h&&G2.enqueueReplaceState(g,g.state,null)}function Oh(h,g){var _=g;if("ref"in g){_={};for(var N in g)N!=="ref"&&(_[N]=g[N])}if(h=h.defaultProps){_===g&&(_=f({},_));for(var P in h)_[P]===void 0&&(_[P]=h[P])}return _}function r5(h){dv(h)}function i5(h){console.error(h)}function a5(h){dv(h)}function Dv(h,g){try{var _=h.onUncaughtError;_(g.value,{componentStack:g.stack})}catch(N){setTimeout(function(){throw N})}}function s5(h,g,_){try{var N=h.onCaughtError;N(_.value,{componentStack:_.stack,errorBoundary:g.tag===1?g.stateNode:null})}catch(P){setTimeout(function(){throw P})}}function W2(h,g,_){return _=Iu(_),_.tag=3,_.payload={element:null},_.callback=function(){Dv(h,g)},_}function o5(h){return h=Iu(h),h.tag=3,h}function l5(h,g,_,N){var P=_.type.getDerivedStateFromError;if(typeof P=="function"){var F=N.value;h.payload=function(){return P(F)},h.callback=function(){s5(g,_,N)}}var Z=_.stateNode;Z!==null&&typeof Z.componentDidCatch=="function"&&(h.callback=function(){s5(g,_,N),typeof P!="function"&&(Vu===null?Vu=new Set([this]):Vu.add(this));var ge=N.stack;this.componentDidCatch(N.value,{componentStack:ge!==null?ge:""})})}function Yae(h,g,_,N,P){if(_.flags|=32768,N!==null&&typeof N=="object"&&typeof N.then=="function"){if(g=_.alternate,g!==null&&Tm(g,_,P,!0),_=js.current,_!==null){switch(_.tag){case 31:case 13:return oo===null?Wv():_.alternate===null&&Jr===0&&(Jr=3),_.flags&=-257,_.flags|=65536,_.lanes=P,N===_v?_.flags|=16384:(g=_.updateQueue,g===null?_.updateQueue=new Set([N]):g.add(N),xA(h,N,P)),!1;case 22:return _.flags|=65536,N===_v?_.flags|=16384:(g=_.updateQueue,g===null?(g={transitions:null,markerInstances:null,retryQueue:new Set([N])},_.updateQueue=g):(_=g.retryQueue,_===null?g.retryQueue=new Set([N]):_.add(N)),xA(h,N,P)),!1}throw Error(r(435,_.tag))}return xA(h,N,P),Wv(),!1}if(In)return g=js.current,g!==null?((g.flags&65536)===0&&(g.flags|=256),g.flags|=65536,g.lanes=P,N!==d2&&(h=Error(r(422),{cause:N}),Ax(ro(h,_)))):(N!==d2&&(g=Error(r(423),{cause:N}),Ax(ro(g,_))),h=h.current.alternate,h.flags|=65536,P&=-P,h.lanes|=P,N=ro(N,_),P=W2(h.stateNode,N,P),w2(h,P),Jr!==4&&(Jr=2)),!1;var F=Error(r(520),{cause:N});if(F=ro(F,_),Wx===null?Wx=[F]:Wx.push(F),Jr!==4&&(Jr=2),g===null)return!0;N=ro(N,_),_=g;do{switch(_.tag){case 3:return _.flags|=65536,h=P&-P,_.lanes|=h,h=W2(_.stateNode,N,h),w2(_,h),!1;case 1:if(g=_.type,F=_.stateNode,(_.flags&128)===0&&(typeof g.getDerivedStateFromError=="function"||F!==null&&typeof F.componentDidCatch=="function"&&(Vu===null||!Vu.has(F))))return _.flags|=65536,P&=-P,_.lanes|=P,P=o5(P),l5(P,h,_,N),w2(_,P),!1}_=_.return}while(_!==null);return!1}var q2=Error(r(461)),vi=!1;function ha(h,g,_,N){g.child=h===null?hF(g,null,_,N):Rh(g,h.child,_,N)}function c5(h,g,_,N,P){_=_.render;var F=g.ref;if("ref"in N){var Z={};for(var ge in N)ge!=="ref"&&(Z[ge]=N[ge])}else Z=N;return Ch(g),N=T2(h,g,_,Z,F,P),ge=C2(),h!==null&&!vi?(k2(h,g,P),_c(h,g,P)):(In&&ge&&c2(g),g.flags|=1,ha(h,g,N,P),g.child)}function u5(h,g,_,N,P){if(h===null){var F=_.type;return typeof F=="function"&&!s2(F)&&F.defaultProps===void 0&&_.compare===null?(g.tag=15,g.type=F,d5(h,g,F,N,P)):(h=pv(_.type,null,N,g,g.mode,P),h.ref=g.ref,h.return=g,g.child=h)}if(F=h.child,!tA(h,P)){var Z=F.memoizedProps;if(_=_.compare,_=_!==null?_:Mx,_(Z,N)&&h.ref===g.ref)return _c(h,g,P)}return g.flags|=1,h=pc(F,N),h.ref=g.ref,h.return=g,g.child=h}function d5(h,g,_,N,P){if(h!==null){var F=h.memoizedProps;if(Mx(F,N)&&h.ref===g.ref)if(vi=!1,g.pendingProps=N=F,tA(h,P))(h.flags&131072)!==0&&(vi=!0);else return g.lanes=h.lanes,_c(h,g,P)}return K2(h,g,_,N,P)}function h5(h,g,_,N){var P=N.children,F=h!==null?h.memoizedState:null;if(h===null&&g.stateNode===null&&(g.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),N.mode==="hidden"){if((g.flags&128)!==0){if(F=F!==null?F.baseLanes|_:_,h!==null){for(N=g.child=h.child,P=0;N!==null;)P=P|N.lanes|N.childLanes,N=N.sibling;N=P&~F}else N=0,g.child=null;return f5(h,g,F,_,N)}if((_&536870912)!==0)g.memoizedState={baseLanes:0,cachePool:null},h!==null&&yv(g,F!==null?F.cachePool:null),F!==null?pF(g,F):M2(),gF(g);else return N=g.lanes=536870912,f5(h,g,F!==null?F.baseLanes|_:_,_,N)}else F!==null?(yv(g,F.cachePool),pF(g,F),zu(),g.memoizedState=null):(h!==null&&yv(g,null),M2(),zu());return ha(h,g,P,_),g.child}function Ux(h,g){return h!==null&&h.tag===22||g.stateNode!==null||(g.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),g.sibling}function f5(h,g,_,N,P){var F=b2();return F=F===null?null:{parent:bi._currentValue,pool:F},g.memoizedState={baseLanes:_,cachePool:F},h!==null&&yv(g,null),M2(),gF(g),h!==null&&Tm(h,g,N,!0),g.childLanes=P,null}function Ov(h,g){return g=Iv({mode:g.mode,children:g.children},h.mode),g.ref=h.ref,h.child=g,g.return=h,g}function m5(h,g,_){return Rh(g,h.child,null,_),h=Ov(g,g.pendingProps),h.flags|=2,Ps(g),g.memoizedState=null,h}function Zae(h,g,_){var N=g.pendingProps,P=(g.flags&128)!==0;if(g.flags&=-129,h===null){if(In){if(N.mode==="hidden")return h=Ov(g,N),g.lanes=536870912,Ux(null,h);if(E2(g),(h=kr)?(h=EU(h,so),h=h!==null&&h.data==="&"?h:null,h!==null&&(g.memoizedState={dehydrated:h,treeContext:Pu!==null?{id:vl,overflow:_l}:null,retryLane:536870912,hydrationErrors:null},_=ZI(h),_.return=g,g.child=_,ua=g,kr=null)):h=null,h===null)throw Du(g);return g.lanes=536870912,null}return Ov(g,N)}var F=h.memoizedState;if(F!==null){var Z=F.dehydrated;if(E2(g),P)if(g.flags&256)g.flags&=-257,g=m5(h,g,_);else if(g.memoizedState!==null)g.child=h.child,g.flags|=128,g=null;else throw Error(r(558));else if(vi||Tm(h,g,_,!1),P=(_&h.childLanes)!==0,vi||P){if(N=_r,N!==null&&(Z=Kt(N,_),Z!==0&&Z!==F.retryLane))throw F.retryLane=Z,Nh(h,Z),fs(N,h,Z),q2;Wv(),g=m5(h,g,_)}else h=F.treeContext,kr=lo(Z.nextSibling),ua=g,In=!0,Ru=null,so=!1,h!==null&&eF(g,h),g=Ov(g,N),g.flags|=4096;return g}return h=pc(h.child,{mode:N.mode,children:N.children}),h.ref=g.ref,g.child=h,h.return=g,h}function Lv(h,g){var _=g.ref;if(_===null)h!==null&&h.ref!==null&&(g.flags|=4194816);else{if(typeof _!="function"&&typeof _!="object")throw Error(r(284));(h===null||h.ref!==_)&&(g.flags|=4194816)}}function K2(h,g,_,N,P){return Ch(g),_=T2(h,g,_,N,void 0,P),N=C2(),h!==null&&!vi?(k2(h,g,P),_c(h,g,P)):(In&&N&&c2(g),g.flags|=1,ha(h,g,_,P),g.child)}function p5(h,g,_,N,P,F){return Ch(g),g.updateQueue=null,_=bF(g,N,_,P),xF(h),N=C2(),h!==null&&!vi?(k2(h,g,F),_c(h,g,F)):(In&&N&&c2(g),g.flags|=1,ha(h,g,_,F),g.child)}function g5(h,g,_,N,P){if(Ch(g),g.stateNode===null){var F=Mm,Z=_.contextType;typeof Z=="object"&&Z!==null&&(F=da(Z)),F=new _(N,F),g.memoizedState=F.state!==null&&F.state!==void 0?F.state:null,F.updater=G2,g.stateNode=F,F._reactInternals=g,F=g.stateNode,F.props=N,F.state=g.memoizedState,F.refs={},v2(g),Z=_.contextType,F.context=typeof Z=="object"&&Z!==null?da(Z):Mm,F.state=g.memoizedState,Z=_.getDerivedStateFromProps,typeof Z=="function"&&(V2(g,_,Z,N),F.state=g.memoizedState),typeof _.getDerivedStateFromProps=="function"||typeof F.getSnapshotBeforeUpdate=="function"||typeof F.UNSAFE_componentWillMount!="function"&&typeof F.componentWillMount!="function"||(Z=F.state,typeof F.componentWillMount=="function"&&F.componentWillMount(),typeof F.UNSAFE_componentWillMount=="function"&&F.UNSAFE_componentWillMount(),Z!==F.state&&G2.enqueueReplaceState(F,F.state,null),Dx(g,N,F,P),Rx(),F.state=g.memoizedState),typeof F.componentDidMount=="function"&&(g.flags|=4194308),N=!0}else if(h===null){F=g.stateNode;var ge=g.memoizedProps,Oe=Oh(_,ge);F.props=Oe;var it=F.context,xt=_.contextType;Z=Mm,typeof xt=="object"&&xt!==null&&(Z=da(xt));var vt=_.getDerivedStateFromProps;xt=typeof vt=="function"||typeof F.getSnapshotBeforeUpdate=="function",ge=g.pendingProps!==ge,xt||typeof F.UNSAFE_componentWillReceiveProps!="function"&&typeof F.componentWillReceiveProps!="function"||(ge||it!==Z)&&n5(g,F,N,Z),Lu=!1;var ot=g.memoizedState;F.state=ot,Dx(g,N,F,P),Rx(),it=g.memoizedState,ge||ot!==it||Lu?(typeof vt=="function"&&(V2(g,_,vt,N),it=g.memoizedState),(Oe=Lu||t5(g,_,Oe,N,ot,it,Z))?(xt||typeof F.UNSAFE_componentWillMount!="function"&&typeof F.componentWillMount!="function"||(typeof F.componentWillMount=="function"&&F.componentWillMount(),typeof F.UNSAFE_componentWillMount=="function"&&F.UNSAFE_componentWillMount()),typeof F.componentDidMount=="function"&&(g.flags|=4194308)):(typeof F.componentDidMount=="function"&&(g.flags|=4194308),g.memoizedProps=N,g.memoizedState=it),F.props=N,F.state=it,F.context=Z,N=Oe):(typeof F.componentDidMount=="function"&&(g.flags|=4194308),N=!1)}else{F=g.stateNode,_2(h,g),Z=g.memoizedProps,xt=Oh(_,Z),F.props=xt,vt=g.pendingProps,ot=F.context,it=_.contextType,Oe=Mm,typeof it=="object"&&it!==null&&(Oe=da(it)),ge=_.getDerivedStateFromProps,(it=typeof ge=="function"||typeof F.getSnapshotBeforeUpdate=="function")||typeof F.UNSAFE_componentWillReceiveProps!="function"&&typeof F.componentWillReceiveProps!="function"||(Z!==vt||ot!==Oe)&&n5(g,F,N,Oe),Lu=!1,ot=g.memoizedState,F.state=ot,Dx(g,N,F,P),Rx();var ht=g.memoizedState;Z!==vt||ot!==ht||Lu||h!==null&&h.dependencies!==null&&xv(h.dependencies)?(typeof ge=="function"&&(V2(g,_,ge,N),ht=g.memoizedState),(xt=Lu||t5(g,_,xt,N,ot,ht,Oe)||h!==null&&h.dependencies!==null&&xv(h.dependencies))?(it||typeof F.UNSAFE_componentWillUpdate!="function"&&typeof F.componentWillUpdate!="function"||(typeof F.componentWillUpdate=="function"&&F.componentWillUpdate(N,ht,Oe),typeof F.UNSAFE_componentWillUpdate=="function"&&F.UNSAFE_componentWillUpdate(N,ht,Oe)),typeof F.componentDidUpdate=="function"&&(g.flags|=4),typeof F.getSnapshotBeforeUpdate=="function"&&(g.flags|=1024)):(typeof F.componentDidUpdate!="function"||Z===h.memoizedProps&&ot===h.memoizedState||(g.flags|=4),typeof F.getSnapshotBeforeUpdate!="function"||Z===h.memoizedProps&&ot===h.memoizedState||(g.flags|=1024),g.memoizedProps=N,g.memoizedState=ht),F.props=N,F.state=ht,F.context=Oe,N=xt):(typeof F.componentDidUpdate!="function"||Z===h.memoizedProps&&ot===h.memoizedState||(g.flags|=4),typeof F.getSnapshotBeforeUpdate!="function"||Z===h.memoizedProps&&ot===h.memoizedState||(g.flags|=1024),N=!1)}return F=N,Lv(h,g),N=(g.flags&128)!==0,F||N?(F=g.stateNode,_=N&&typeof _.getDerivedStateFromError!="function"?null:F.render(),g.flags|=1,h!==null&&N?(g.child=Rh(g,h.child,null,P),g.child=Rh(g,null,_,P)):ha(h,g,_,P),g.memoizedState=F.state,h=g.child):h=_c(h,g,P),h}function x5(h,g,_,N){return Ah(),g.flags|=256,ha(h,g,_,N),g.child}var X2={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function Y2(h){return{baseLanes:h,cachePool:sF()}}function Z2(h,g,_){return h=h!==null?h.childLanes&~_:0,g&&(h|=Ds),h}function b5(h,g,_){var N=g.pendingProps,P=!1,F=(g.flags&128)!==0,Z;if((Z=F)||(Z=h!==null&&h.memoizedState===null?!1:(hi.current&2)!==0),Z&&(P=!0,g.flags&=-129),Z=(g.flags&32)!==0,g.flags&=-33,h===null){if(In){if(P?Uu(g):zu(),(h=kr)?(h=EU(h,so),h=h!==null&&h.data!=="&"?h:null,h!==null&&(g.memoizedState={dehydrated:h,treeContext:Pu!==null?{id:vl,overflow:_l}:null,retryLane:536870912,hydrationErrors:null},_=ZI(h),_.return=g,g.child=_,ua=g,kr=null)):h=null,h===null)throw Du(g);return RA(h)?g.lanes=32:g.lanes=536870912,null}var ge=N.children;return N=N.fallback,P?(zu(),P=g.mode,ge=Iv({mode:"hidden",children:ge},P),N=Eh(N,P,_,null),ge.return=g,N.return=g,ge.sibling=N,g.child=ge,N=g.child,N.memoizedState=Y2(_),N.childLanes=Z2(h,Z,_),g.memoizedState=X2,Ux(null,N)):(Uu(g),Q2(g,ge))}var Oe=h.memoizedState;if(Oe!==null&&(ge=Oe.dehydrated,ge!==null)){if(F)g.flags&256?(Uu(g),g.flags&=-257,g=J2(h,g,_)):g.memoizedState!==null?(zu(),g.child=h.child,g.flags|=128,g=null):(zu(),ge=N.fallback,P=g.mode,N=Iv({mode:"visible",children:N.children},P),ge=Eh(ge,P,_,null),ge.flags|=2,N.return=g,ge.return=g,N.sibling=ge,g.child=N,Rh(g,h.child,null,_),N=g.child,N.memoizedState=Y2(_),N.childLanes=Z2(h,Z,_),g.memoizedState=X2,g=Ux(null,N));else if(Uu(g),RA(ge)){if(Z=ge.nextSibling&&ge.nextSibling.dataset,Z)var it=Z.dgst;Z=it,N=Error(r(419)),N.stack="",N.digest=Z,Ax({value:N,source:null,stack:null}),g=J2(h,g,_)}else if(vi||Tm(h,g,_,!1),Z=(_&h.childLanes)!==0,vi||Z){if(Z=_r,Z!==null&&(N=Kt(Z,_),N!==0&&N!==Oe.retryLane))throw Oe.retryLane=N,Nh(h,N),fs(Z,h,N),q2;PA(ge)||Wv(),g=J2(h,g,_)}else PA(ge)?(g.flags|=192,g.child=h.child,g=null):(h=Oe.treeContext,kr=lo(ge.nextSibling),ua=g,In=!0,Ru=null,so=!1,h!==null&&eF(g,h),g=Q2(g,N.children),g.flags|=4096);return g}return P?(zu(),ge=N.fallback,P=g.mode,Oe=h.child,it=Oe.sibling,N=pc(Oe,{mode:"hidden",children:N.children}),N.subtreeFlags=Oe.subtreeFlags&65011712,it!==null?ge=pc(it,ge):(ge=Eh(ge,P,_,null),ge.flags|=2),ge.return=g,N.return=g,N.sibling=ge,g.child=N,Ux(null,N),N=g.child,ge=h.child.memoizedState,ge===null?ge=Y2(_):(P=ge.cachePool,P!==null?(Oe=bi._currentValue,P=P.parent!==Oe?{parent:Oe,pool:Oe}:P):P=sF(),ge={baseLanes:ge.baseLanes|_,cachePool:P}),N.memoizedState=ge,N.childLanes=Z2(h,Z,_),g.memoizedState=X2,Ux(h.child,N)):(Uu(g),_=h.child,h=_.sibling,_=pc(_,{mode:"visible",children:N.children}),_.return=g,_.sibling=null,h!==null&&(Z=g.deletions,Z===null?(g.deletions=[h],g.flags|=16):Z.push(h)),g.child=_,g.memoizedState=null,_)}function Q2(h,g){return g=Iv({mode:"visible",children:g},h.mode),g.return=h,h.child=g}function Iv(h,g){return h=ks(22,h,null,g),h.lanes=0,h}function J2(h,g,_){return Rh(g,h.child,null,_),h=Q2(g,g.pendingProps.children),h.flags|=2,g.memoizedState=null,h}function y5(h,g,_){h.lanes|=g;var N=h.alternate;N!==null&&(N.lanes|=g),m2(h.return,g,_)}function eA(h,g,_,N,P,F){var Z=h.memoizedState;Z===null?h.memoizedState={isBackwards:g,rendering:null,renderingStartTime:0,last:N,tail:_,tailMode:P,treeForkCount:F}:(Z.isBackwards=g,Z.rendering=null,Z.renderingStartTime=0,Z.last=N,Z.tail=_,Z.tailMode=P,Z.treeForkCount=F)}function v5(h,g,_){var N=g.pendingProps,P=N.revealOrder,F=N.tail;N=N.children;var Z=hi.current,ge=(Z&2)!==0;if(ge?(Z=Z&1|2,g.flags|=128):Z&=1,ne(hi,Z),ha(h,g,N,_),N=In?Ex:0,!ge&&h!==null&&(h.flags&128)!==0)e:for(h=g.child;h!==null;){if(h.tag===13)h.memoizedState!==null&&y5(h,_,g);else if(h.tag===19)y5(h,_,g);else if(h.child!==null){h.child.return=h,h=h.child;continue}if(h===g)break e;for(;h.sibling===null;){if(h.return===null||h.return===g)break e;h=h.return}h.sibling.return=h.return,h=h.sibling}switch(P){case"forwards":for(_=g.child,P=null;_!==null;)h=_.alternate,h!==null&&Nv(h)===null&&(P=_),_=_.sibling;_=P,_===null?(P=g.child,g.child=null):(P=_.sibling,_.sibling=null),eA(g,!1,P,_,F,N);break;case"backwards":case"unstable_legacy-backwards":for(_=null,P=g.child,g.child=null;P!==null;){if(h=P.alternate,h!==null&&Nv(h)===null){g.child=P;break}h=P.sibling,P.sibling=_,_=P,P=h}eA(g,!0,_,null,F,N);break;case"together":eA(g,!1,null,null,void 0,N);break;default:g.memoizedState=null}return g.child}function _c(h,g,_){if(h!==null&&(g.dependencies=h.dependencies),Hu|=g.lanes,(_&g.childLanes)===0)if(h!==null){if(Tm(h,g,_,!1),(_&g.childLanes)===0)return null}else return null;if(h!==null&&g.child!==h.child)throw Error(r(153));if(g.child!==null){for(h=g.child,_=pc(h,h.pendingProps),g.child=_,_.return=g;h.sibling!==null;)h=h.sibling,_=_.sibling=pc(h,h.pendingProps),_.return=g;_.sibling=null}return g.child}function tA(h,g){return(h.lanes&g)!==0?!0:(h=h.dependencies,!!(h!==null&&xv(h)))}function Qae(h,g,_){switch(g.tag){case 3:Ee(g,g.stateNode.containerInfo),Ou(g,bi,h.memoizedState.cache),Ah();break;case 27:case 5:Ne(g);break;case 4:Ee(g,g.stateNode.containerInfo);break;case 10:Ou(g,g.type,g.memoizedProps.value);break;case 31:if(g.memoizedState!==null)return g.flags|=128,E2(g),null;break;case 13:var N=g.memoizedState;if(N!==null)return N.dehydrated!==null?(Uu(g),g.flags|=128,null):(_&g.child.childLanes)!==0?b5(h,g,_):(Uu(g),h=_c(h,g,_),h!==null?h.sibling:null);Uu(g);break;case 19:var P=(h.flags&128)!==0;if(N=(_&g.childLanes)!==0,N||(Tm(h,g,_,!1),N=(_&g.childLanes)!==0),P){if(N)return v5(h,g,_);g.flags|=128}if(P=g.memoizedState,P!==null&&(P.rendering=null,P.tail=null,P.lastEffect=null),ne(hi,hi.current),N)break;return null;case 22:return g.lanes=0,h5(h,g,_,g.pendingProps);case 24:Ou(g,bi,h.memoizedState.cache)}return _c(h,g,_)}function _5(h,g,_){if(h!==null)if(h.memoizedProps!==g.pendingProps)vi=!0;else{if(!tA(h,_)&&(g.flags&128)===0)return vi=!1,Qae(h,g,_);vi=(h.flags&131072)!==0}else vi=!1,In&&(g.flags&1048576)!==0&&JI(g,Ex,g.index);switch(g.lanes=0,g.tag){case 16:e:{var N=g.pendingProps;if(h=jh(g.elementType),g.type=h,typeof h=="function")s2(h)?(N=Oh(h,N),g.tag=1,g=g5(null,g,h,N,_)):(g.tag=0,g=K2(null,g,h,N,_));else{if(h!=null){var P=h.$$typeof;if(P===A){g.tag=11,g=c5(null,g,h,N,_);break e}else if(P===E){g.tag=14,g=u5(null,g,h,N,_);break e}}throw g=j(h)||h,Error(r(306,g,""))}}return g;case 0:return K2(h,g,g.type,g.pendingProps,_);case 1:return N=g.type,P=Oh(N,g.pendingProps),g5(h,g,N,P,_);case 3:e:{if(Ee(g,g.stateNode.containerInfo),h===null)throw Error(r(387));N=g.pendingProps;var F=g.memoizedState;P=F.element,_2(h,g),Dx(g,N,null,_);var Z=g.memoizedState;if(N=Z.cache,Ou(g,bi,N),N!==F.cache&&p2(g,[bi],_,!0),Rx(),N=Z.element,F.isDehydrated)if(F={element:N,isDehydrated:!1,cache:Z.cache},g.updateQueue.baseState=F,g.memoizedState=F,g.flags&256){g=x5(h,g,N,_);break e}else if(N!==P){P=ro(Error(r(424)),g),Ax(P),g=x5(h,g,N,_);break e}else for(h=g.stateNode.containerInfo,h.nodeType===9?h=h.body:h=h.nodeName==="HTML"?h.ownerDocument.body:h,kr=lo(h.firstChild),ua=g,In=!0,Ru=null,so=!0,_=hF(g,null,N,_),g.child=_;_;)_.flags=_.flags&-3|4096,_=_.sibling;else{if(Ah(),N===P){g=_c(h,g,_);break e}ha(h,g,N,_)}g=g.child}return g;case 26:return Lv(h,g),h===null?(_=PU(g.type,null,g.pendingProps,null))?g.memoizedState=_:In||(_=g.type,h=g.pendingProps,N=Jv(pe.current).createElement(_),N[tn]=g,N[yr]=h,fa(N,_,h),de(N),g.stateNode=N):g.memoizedState=PU(g.type,h.memoizedProps,g.pendingProps,h.memoizedState),null;case 27:return Ne(g),h===null&&In&&(N=g.stateNode=CU(g.type,g.pendingProps,pe.current),ua=g,so=!0,P=kr,Ku(g.type)?(DA=P,kr=lo(N.firstChild)):kr=P),ha(h,g,g.pendingProps.children,_),Lv(h,g),h===null&&(g.flags|=4194304),g.child;case 5:return h===null&&In&&((P=N=kr)&&(N=Tse(N,g.type,g.pendingProps,so),N!==null?(g.stateNode=N,ua=g,kr=lo(N.firstChild),so=!1,P=!0):P=!1),P||Du(g)),Ne(g),P=g.type,F=g.pendingProps,Z=h!==null?h.memoizedProps:null,N=F.children,CA(P,F)?N=null:Z!==null&&CA(P,Z)&&(g.flags|=32),g.memoizedState!==null&&(P=T2(h,g,Hae,null,null,_),eb._currentValue=P),Lv(h,g),ha(h,g,N,_),g.child;case 6:return h===null&&In&&((h=_=kr)&&(_=Cse(_,g.pendingProps,so),_!==null?(g.stateNode=_,ua=g,kr=null,h=!0):h=!1),h||Du(g)),null;case 13:return b5(h,g,_);case 4:return Ee(g,g.stateNode.containerInfo),N=g.pendingProps,h===null?g.child=Rh(g,null,N,_):ha(h,g,N,_),g.child;case 11:return c5(h,g,g.type,g.pendingProps,_);case 7:return ha(h,g,g.pendingProps,_),g.child;case 8:return ha(h,g,g.pendingProps.children,_),g.child;case 12:return ha(h,g,g.pendingProps.children,_),g.child;case 10:return N=g.pendingProps,Ou(g,g.type,N.value),ha(h,g,N.children,_),g.child;case 9:return P=g.type._context,N=g.pendingProps.children,Ch(g),P=da(P),N=N(P),g.flags|=1,ha(h,g,N,_),g.child;case 14:return u5(h,g,g.type,g.pendingProps,_);case 15:return d5(h,g,g.type,g.pendingProps,_);case 19:return v5(h,g,_);case 31:return Zae(h,g,_);case 22:return h5(h,g,_,g.pendingProps);case 24:return Ch(g),N=da(bi),h===null?(P=b2(),P===null&&(P=_r,F=g2(),P.pooledCache=F,F.refCount++,F!==null&&(P.pooledCacheLanes|=_),P=F),g.memoizedState={parent:N,cache:P},v2(g),Ou(g,bi,P)):((h.lanes&_)!==0&&(_2(h,g),Dx(g,null,null,_),Rx()),P=h.memoizedState,F=g.memoizedState,P.parent!==N?(P={parent:N,cache:N},g.memoizedState=P,g.lanes===0&&(g.memoizedState=g.updateQueue.baseState=P),Ou(g,bi,N)):(N=F.cache,Ou(g,bi,N),N!==P.cache&&p2(g,[bi],_,!0))),ha(h,g,g.pendingProps.children,_),g.child;case 29:throw g.pendingProps}throw Error(r(156,g.tag))}function wc(h){h.flags|=4}function nA(h,g,_,N,P){if((g=(h.mode&32)!==0)&&(g=!1),g){if(h.flags|=16777216,(P&335544128)===P)if(h.stateNode.complete)h.flags|=8192;else if(K5())h.flags|=8192;else throw Ph=_v,y2}else h.flags&=-16777217}function w5(h,g){if(g.type!=="stylesheet"||(g.state.loading&4)!==0)h.flags&=-16777217;else if(h.flags|=16777216,!IU(g))if(K5())h.flags|=8192;else throw Ph=_v,y2}function Fv(h,g){g!==null&&(h.flags|=4),h.flags&16384&&(g=h.tag!==22?Pt():536870912,h.lanes|=g,zm|=g)}function zx(h,g){if(!In)switch(h.tailMode){case"hidden":g=h.tail;for(var _=null;g!==null;)g.alternate!==null&&(_=g),g=g.sibling;_===null?h.tail=null:_.sibling=null;break;case"collapsed":_=h.tail;for(var N=null;_!==null;)_.alternate!==null&&(N=_),_=_.sibling;N===null?g||h.tail===null?h.tail=null:h.tail.sibling=null:N.sibling=null}}function jr(h){var g=h.alternate!==null&&h.alternate.child===h.child,_=0,N=0;if(g)for(var P=h.child;P!==null;)_|=P.lanes|P.childLanes,N|=P.subtreeFlags&65011712,N|=P.flags&65011712,P.return=h,P=P.sibling;else for(P=h.child;P!==null;)_|=P.lanes|P.childLanes,N|=P.subtreeFlags,N|=P.flags,P.return=h,P=P.sibling;return h.subtreeFlags|=N,h.childLanes=_,g}function Jae(h,g,_){var N=g.pendingProps;switch(u2(g),g.tag){case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return jr(g),null;case 1:return jr(g),null;case 3:return _=g.stateNode,N=null,h!==null&&(N=h.memoizedState.cache),g.memoizedState.cache!==N&&(g.flags|=2048),bc(bi),xe(),_.pendingContext&&(_.context=_.pendingContext,_.pendingContext=null),(h===null||h.child===null)&&(Am(g)?wc(g):h===null||h.memoizedState.isDehydrated&&(g.flags&256)===0||(g.flags|=1024,h2())),jr(g),null;case 26:var P=g.type,F=g.memoizedState;return h===null?(wc(g),F!==null?(jr(g),w5(g,F)):(jr(g),nA(g,P,null,N,_))):F?F!==h.memoizedState?(wc(g),jr(g),w5(g,F)):(jr(g),g.flags&=-16777217):(h=h.memoizedProps,h!==N&&wc(g),jr(g),nA(g,P,h,N,_)),null;case 27:if(_e(g),_=pe.current,P=g.type,h!==null&&g.stateNode!=null)h.memoizedProps!==N&&wc(g);else{if(!N){if(g.stateNode===null)throw Error(r(166));return jr(g),null}h=ae.current,Am(g)?tF(g):(h=CU(P,N,_),g.stateNode=h,wc(g))}return jr(g),null;case 5:if(_e(g),P=g.type,h!==null&&g.stateNode!=null)h.memoizedProps!==N&&wc(g);else{if(!N){if(g.stateNode===null)throw Error(r(166));return jr(g),null}if(F=ae.current,Am(g))tF(g);else{var Z=Jv(pe.current);switch(F){case 1:F=Z.createElementNS("http://www.w3.org/2000/svg",P);break;case 2:F=Z.createElementNS("http://www.w3.org/1998/Math/MathML",P);break;default:switch(P){case"svg":F=Z.createElementNS("http://www.w3.org/2000/svg",P);break;case"math":F=Z.createElementNS("http://www.w3.org/1998/Math/MathML",P);break;case"script":F=Z.createElement("div"),F.innerHTML=" - + +