diff --git a/CHANGELOG.md b/CHANGELOG.md index 871d2b6ca..1166e1f51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file. ## [0.2.5b1] - Unreleased ### Added +- **System Health — log scanner that surfaces self-fixable issues before they become support tickets** — Complements the active Connection Diagnostic with a passive check: it scans Bambuddy's recent app log against a curated catalog of known failure signatures and reports what it finds. The catalog (`backend/app/services/log_health.py`) is a deliberate allowlist — only known-bad, actionable patterns match, so a healthy install reports nothing and noisy benign churn (the occasional MQTT reconnect after a Wi-Fi blip) is gated behind a per-signature `min_count` threshold. Six seed signatures cover the recurring "layer 8" causes from the closed-issue triage: rejected access code, FTPS :990 timeout, FTPS TLS handshake failure, flapping MQTT connection, unreachable camera (RTSPS :322), and SQLite `database is locked` contention. Each finding is deduped (`occurred N×, last seen …`), classified as *you can fix this* / *environment* / *please report this*, and carries a deep-link to the troubleshooting wiki; sample log lines are sanitized (IPs, serials, access codes redacted) before they leave the process. Exposed via `GET /system/health` and surfaced on two surfaces that share one `SystemHealthPanel` component: a System Health section on the System page (on-demand re-scan), and inline in the bug reporter when the form opens — so a setup mistake gets self-resolved instead of becoming a GitHub issue. The Add-Printer and Edit-Printer dialogs also gained a setup-time pre-flight: saving now runs the connection diagnostic and, if a check fails, warns with a "save anyway" escape hatch instead of silently saving a printer that will immediately show offline. Log-reading and redaction primitives were extracted from `routes/support.py` into a shared `backend/app/services/log_reader.py` (behaviour-preserving). 13 backend tests (`test_log_health.py`, `test_system_api.py`) and 8 frontend tests (`SystemHealthPanel`, `BugReportBubble`, `AddPrinterPreflight`, `EditPrinterPreflight`); all strings translated across the 9 locales. Backend ruff clean, full unit suite green, frontend build clean, i18n parity green. - **Event-loop stall watchdog — makes a frozen backend self-diagnose (#1486 groundwork)** — Several "container hangs after adding a printer" reports share a signature that leaves nothing to act on: the HTTP server goes silent, `/health` hangs, the process may stop responding to SIGTERM — and the logs just stop mid-stream with no traceback, because a frozen asyncio event loop cannot log anything. New `backend/app/services/loop_watchdog.py` closes that blind spot: an async heartbeat re-arms `faulthandler.dump_traceback_later()` every 10s, always 30s ahead. While the loop ticks, the timer is cancelled and re-armed before it can fire; if the loop stalls, the heartbeat can't re-arm and faulthandler's dedicated C-level timer thread — which runs independently of the frozen loop — dumps **every thread's stack to stderr**. The blocked frame then appears in `docker compose logs`, turning an un-diagnosable freeze into a one-command capture. Started in the app lifespan after migrations, stopped cleanly on shutdown; 30s threshold is well above any legitimate on-loop operation, so a trip always means a real bug. 5 unit tests in `test_loop_watchdog.py` (arms the timer, idempotent start, stop disarms + cancels, heartbeat interval below the threshold, survives a re-arm error). Backend ruff clean; full app lifespan verified via the integration suite. - **Slicer: process & filament profiles filtered by the selected printer (#1325, requested by @IndividualGhost1905)** — In the server-side Slice dialog, picking a printer profile now filters the Process and Filament dropdowns to presets compatible with that printer; presets that resolve to a different Bambu model drop into a trailing "Other printers" group instead of cluttering the main list. Matching uses the slicer's own `compatible_printers` list for imported (local) presets, and falls back to the `@BBL ` name suffix for cloud and standard presets, so all three tiers are covered. Compatibility-unknown presets (custom or untagged) are never hidden. Defaults follow suit — the pre-picked process and per-slot filament now prefer a printer-compatible preset, and switching the printer re-picks any selection left incompatible. The printer and process dropdowns also default to the preset names embedded in the source 3MF's `project_settings.config` when those presets are available, instead of always taking the first listed preset. New `frontend/src/utils/slicerPrinterMatch.ts` (11 unit tests) and `extract_embedded_presets_from_3mf` (5 unit tests); `UnifiedPreset` now carries `compatible_printers`, exposed for the local tier (`backend/app/api/routes/slicer_presets.py`); the plates endpoints return `embedded_printer` / `embedded_process`. Parity green, build clean. - **Spanish (es) translation (#1243, requested by @MiguelAngelLV)** — Bambuddy now ships a full European Spanish locale. New `frontend/src/i18n/locales/es.ts` translates all 4899 keys with placeholders, plural forms, and inline markup preserved; registered in `frontend/src/i18n/index.ts` and selectable as "Español" in the language picker. The parity checker auto-discovers the file — `frontend/scripts/check-i18n-parity.mjs` gained an `ES_COGNATES` allow-list for genuine Spanish cognates and brand/format tokens. Brings the supported-language count to 9 (en / de / es / fr / it / ja / pt-BR / zh-CN / zh-TW). Parity green, frontend build clean. diff --git a/backend/app/api/routes/support.py b/backend/app/api/routes/support.py index 1245406b0..419f0d422 100644 --- a/backend/app/api/routes/support.py +++ b/backend/app/api/routes/support.py @@ -33,6 +33,12 @@ from backend.app.models.settings import Settings from backend.app.models.smart_plug import SmartPlug from backend.app.models.user import User from backend.app.services.discovery import is_running_in_docker +from backend.app.services.log_reader import ( + LogEntry, + collect_sensitive_strings, + read_log_entries, + sanitize_log_content, +) from backend.app.services.network_utils import get_network_interfaces from backend.app.services.printer_manager import printer_manager @@ -156,15 +162,6 @@ async def toggle_debug_logging( ) -class LogEntry(BaseModel): - """A single log entry.""" - - timestamp: str - level: str - logger_name: str - message: str - - class LogsResponse(BaseModel): """Response containing log entries.""" @@ -173,107 +170,6 @@ class LogsResponse(BaseModel): filtered_count: int -# Log line regex pattern: "2024-01-15 10:30:45,123 INFO [module.name] Message here" -LOG_LINE_PATTERN = re.compile(r"^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d{3})\s+(\w+)\s+\[([^\]]+)\]\s+(.*)$") - - -def _parse_log_line(line: str) -> LogEntry | None: - """Parse a single log line into a LogEntry.""" - match = LOG_LINE_PATTERN.match(line.strip()) - if match: - return LogEntry( - timestamp=match.group(1), - level=match.group(2), - logger_name=match.group(3), - message=match.group(4), - ) - return None - - -def _read_log_entries( - limit: int = 200, - level_filter: str | None = None, - search: str | None = None, -) -> tuple[list[LogEntry], int]: - """Read and parse log entries from file with optional filtering.""" - log_file = settings.log_dir / "bambuddy.log" - if not log_file.exists(): - return [], 0 - - entries: list[LogEntry] = [] - total_lines = 0 - - try: - with open(log_file, encoding="utf-8", errors="replace") as f: - # Read all lines and process - lines = f.readlines() - total_lines = len(lines) - - # Parse lines in reverse order (newest first) - current_entry: LogEntry | None = None - multi_line_buffer: list[str] = [] - - for line in reversed(lines): - parsed = _parse_log_line(line) - if parsed: - # Found a new log entry start - if current_entry: - # Apply filters and add previous entry (without multi_line_buffer - it belongs to new entry) - should_include = True - - # Level filter - if level_filter and current_entry.level.upper() != level_filter.upper(): - should_include = False - - # Search filter (case-insensitive) - if search and should_include: - search_lower = search.lower() - if not ( - search_lower in current_entry.message.lower() - or search_lower in current_entry.logger_name.lower() - ): - should_include = False - - if should_include: - entries.append(current_entry) - - if len(entries) >= limit: - break - - # Set new entry and attach any accumulated multi-line content to it - # (in reverse order, continuation lines come before their parent entry) - current_entry = parsed - if multi_line_buffer: - current_entry.message += "\n" + "\n".join(reversed(multi_line_buffer)) - multi_line_buffer = [] - elif line.strip(): - # Continuation of multi-line log entry (will be attached to next parsed entry) - multi_line_buffer.append(line.rstrip()) - - # Don't forget the last (oldest) entry - # Note: any remaining multi_line_buffer would be orphaned lines before the first entry - if current_entry and len(entries) < limit: - should_include = True - if level_filter and current_entry.level.upper() != level_filter.upper(): - should_include = False - if search and should_include: - search_lower = search.lower() - if not ( - search_lower in current_entry.message.lower() - or search_lower in current_entry.logger_name.lower() - ): - should_include = False - if should_include: - entries.append(current_entry) - - except Exception as e: - logger.error("Error reading log file: %s", e) - return [], 0 - - # Entries are already in newest-first order - return entries, total_lines - - @router.get("/logs", response_model=LogsResponse) async def get_logs( limit: int = Query(200, ge=1, le=1000, description="Maximum number of entries to return"), @@ -282,7 +178,7 @@ async def get_logs( _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_READ), ): """Get recent application log entries with optional filtering.""" - entries, total_lines = _read_log_entries(limit=limit, level_filter=level, search=search) + entries, total_lines = read_log_entries(limit=limit, level_filter=level, search=search) return LogsResponse( entries=entries, @@ -1206,42 +1102,6 @@ async def _collect_support_info() -> dict: return info -def _sanitize_log_content(content: str, sensitive_strings: dict[str, str] | None = None) -> str: - """Remove sensitive data from log content.""" - # First, replace known sensitive values (database-aware exact matching) - # This catches printer names, usernames, and other arbitrary user-chosen strings - # that regex patterns cannot detect - if sensitive_strings: - # Sort by length descending to avoid partial matches (e.g. "My Printer 1" before "My Printer") - for value, label in sorted(sensitive_strings.items(), key=lambda x: len(x[0]), reverse=True): - if len(value) < 3: - continue # Skip very short strings to prevent over-redaction - content = re.sub(re.escape(value), label, content) - - # Replace credentials in URLs (e.g. http://user:pass@host, rtsps://bblp:code@host) - content = re.sub(r"((?:https?|rtsps?)://)[^/:@\s]+:[^/@\s]+@", r"\1[CREDENTIALS]@", content) - - # Replace email addresses - content = re.sub(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]", content) - - # Replace Bambu Lab printer serial numbers (format: 00M/01D/01S/01P/03W + alphanumeric, 12-16 chars total) - content = re.sub(r"\b0[0-3][A-Z0-9][A-Z0-9]{9,13}\b", "[SERIAL]", content, flags=re.IGNORECASE) - - # Replace IPv4 addresses (skip firmware versions like 01.09.01.00 which have leading zeros) - content = re.sub( - r"\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\b", - "[IP]", - content, - ) - - # Replace paths with usernames - content = re.sub(r"/home/[^/\s]+/", "/home/[user]/", content) - content = re.sub(r"/Users/[^/\s]+/", "/Users/[user]/", content) - content = re.sub(r"/opt/[^/\s]+/", "/opt/[user]/", content) - - return content - - def _get_log_content(max_bytes: int = 10 * 1024 * 1024, sensitive_strings: dict[str, str] | None = None) -> bytes: """Get log file content, limited to max_bytes from the end.""" log_file = settings.log_dir / "bambuddy.log" @@ -1260,35 +1120,15 @@ def _get_log_content(max_bytes: int = 10 * 1024 * 1024, sensitive_strings: dict[ content = f.read().decode("utf-8", errors="replace") # Sanitize sensitive data - content = _sanitize_log_content(content, sensitive_strings) + content = sanitize_log_content(content, sensitive_strings) return content.encode("utf-8") async def _get_recent_sanitized_logs(max_lines: int = 200) -> str: """Get recent log lines, sanitized for inclusion in bug reports.""" # Collect sensitive strings from DB for redaction - sensitive_strings: dict[str, str] = {} async with async_session() as db: - result = await db.execute(select(Printer.name, Printer.serial_number, Printer.ip_address, Printer.access_code)) - for name, serial, ip_address, access_code in result.all(): - if name: - sensitive_strings[name] = "[PRINTER]" - if serial: - sensitive_strings[serial] = "[SERIAL]" - if ip_address: - sensitive_strings[ip_address] = "[IP]" - if access_code: - sensitive_strings[access_code] = "[ACCESS_CODE]" - - result = await db.execute(select(User.username)) - for (username,) in result.all(): - if username: - sensitive_strings[username] = "[USER]" - - result = await db.execute(select(Settings.value).where(Settings.key == "bambu_cloud_email")) - cloud_email = result.scalar_one_or_none() - if cloud_email: - sensitive_strings[cloud_email] = "[EMAIL]" + sensitive_strings = await collect_sensitive_strings(db) log_file = settings.log_dir / "bambuddy.log" if not log_file.exists(): @@ -1299,7 +1139,7 @@ async def _get_recent_sanitized_logs(max_lines: int = 200) -> str: content = log_file.read_text(encoding="utf-8", errors="replace") lines = content.splitlines() recent = "\n".join(lines[-max_lines:]) - return _sanitize_log_content(recent, sensitive_strings) + return sanitize_log_content(recent, sensitive_strings) except Exception: logger.debug("Failed to read logs for bug report", exc_info=True) return "" @@ -1322,31 +1162,7 @@ async def generate_support_bundle( ) # Collect known sensitive values for log redaction - sensitive_strings: dict[str, str] = {} - - # Printer names, serial numbers, IP addresses, and access codes - result = await db.execute(select(Printer.name, Printer.serial_number, Printer.ip_address, Printer.access_code)) - for name, serial, ip_address, access_code in result.all(): - if name: - sensitive_strings[name] = "[PRINTER]" - if serial: - sensitive_strings[serial] = "[SERIAL]" - if ip_address: - sensitive_strings[ip_address] = "[IP]" - if access_code: - sensitive_strings[access_code] = "[ACCESS_CODE]" - - # Auth usernames - result = await db.execute(select(User.username)) - for (username,) in result.all(): - if username: - sensitive_strings[username] = "[USER]" - - # Bambu Cloud email - result = await db.execute(select(Settings.value).where(Settings.key == "bambu_cloud_email")) - cloud_email = result.scalar_one_or_none() - if cloud_email: - sensitive_strings[cloud_email] = "[EMAIL]" + sensitive_strings = await collect_sensitive_strings(db) # Collect support info support_info = await _collect_support_info() diff --git a/backend/app/api/routes/system.py b/backend/app/api/routes/system.py index 33733ef7e..5f9531e22 100644 --- a/backend/app/api/routes/system.py +++ b/backend/app/api/routes/system.py @@ -23,6 +23,8 @@ from backend.app.models.printer import Printer from backend.app.models.project import Project from backend.app.models.smart_plug import SmartPlug from backend.app.models.user import User +from backend.app.services.log_health import ScanResult, scan_logs +from backend.app.services.log_reader import collect_sensitive_strings from backend.app.services.printer_manager import printer_manager router = APIRouter(prefix="/system", tags=["system"]) @@ -574,3 +576,17 @@ async def get_storage_usage( """Get storage usage breakdown for Bambuddy data directories.""" max_age_seconds = max(0, min(max_age_seconds, 3600)) return await _get_storage_usage_cached(refresh=refresh, max_age_seconds=max_age_seconds) + + +@router.get("/health", response_model=ScanResult) +async def get_system_health( + db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.SYSTEM_READ), +): + """Scan the recent application log against the known-issue catalog. + + Powers the self-service triage surfaces (System page + bug reporter). + Sample lines are sanitized before they leave the process. + """ + sensitive_strings = await collect_sensitive_strings(db) + return await asyncio.to_thread(scan_logs, sensitive_strings=sensitive_strings) diff --git a/backend/app/services/log_health.py b/backend/app/services/log_health.py new file mode 100644 index 000000000..7333bb246 --- /dev/null +++ b/backend/app/services/log_health.py @@ -0,0 +1,256 @@ +"""Log-health scanner. + +Matches the recent Bambuddy app log against a curated catalog of known failure +signatures, so users can self-diagnose setup ("layer 8") issues before filing a +bug report. + +The catalog is a deliberate *allowlist*: only known-bad, actionable signatures +are matched — a healthy install produces an empty finding list. Human-readable +cause and fix text is intentionally NOT stored here; the frontend renders it +from i18n keys ``systemHealth.signature..{name,cause,fix}`` so it stays +translatable across all locales. This module only carries the machine-facing +fields (pattern, severity, category, wiki anchor). +""" + +import logging +import re +from dataclasses import dataclass + +from pydantic import BaseModel + +from backend.app.core.config import settings +from backend.app.services.log_reader import LogEntry, read_log_entries, sanitize_log_content + +logger = logging.getLogger(__name__) + +# How many recent log entries to scan by default. +DEFAULT_SCAN_LIMIT = 4000 + +# Log levels ranked so a signature can require "at least WARNING" etc. +_LEVEL_RANK = {"DEBUG": 10, "INFO": 20, "WARNING": 30, "ERROR": 40, "CRITICAL": 50} + +# Findings are ordered layer8 first (the user can act on these), then +# environment, then bug (please report). Within a group: errors before warnings. +_CATEGORY_ORDER = {"layer8": 0, "environment": 1, "bug": 2} +_SEVERITY_ORDER = {"error": 0, "warning": 1} + +# Cap the sample line length so a finding can never carry a huge folded traceback. +_SAMPLE_MAX_LEN = 400 + + +@dataclass(frozen=True) +class LogSignature: + """One curated known-issue signature. + + ``patterns`` are matched (``re.search``, case-insensitive) against the log + entry message. A signature only becomes a reported finding once it has + matched ``min_count`` times within the scan window — this gates noisy, + individually-benign symptoms (e.g. an occasional MQTT reconnect after a + Wi-Fi blip) from being surfaced as a problem. + """ + + id: str + patterns: tuple[re.Pattern[str], ...] + severity: str # "error" | "warning" + category: str # "layer8" | "environment" | "bug" + wiki_anchor: str # slug appended to the troubleshooting wiki page URL + min_level: str = "WARNING" + logger_prefix: str | None = None # only match entries from this logger tree + min_count: int = 1 + + +def _compile(*patterns: str) -> tuple[re.Pattern[str], ...]: + return tuple(re.compile(p, re.IGNORECASE) for p in patterns) + + +# --- The catalog ----------------------------------------------------------- +# Seeded from the ranked "layer 8" root causes found in the closed-issue triage +# review. Each id MUST have matching i18n keys: systemHealth.signature..* +SIGNATURES: tuple[LogSignature, ...] = ( + LogSignature( + # Wrong/mistyped access code — FTPS login is rejected (530). + id="ftp-auth-rejected", + patterns=_compile(r"FTP connection permission error"), + severity="error", + category="layer8", + wiki_anchor="wrong-access-code", + logger_prefix="backend.app.services.bambu_ftp", + ), + LogSignature( + # FTPS :990 unreachable — port blocked by a firewall, or the printer is + # off / on a different subnet. + id="ftp-connection-timeout", + patterns=_compile(r"FTP connection timed out"), + severity="warning", + category="layer8", + wiki_anchor="ftps-port-990-blocked", + logger_prefix="backend.app.services.bambu_ftp", + min_count=3, + ), + LogSignature( + # TLS negotiation to the printer's FTPS server failed. + id="ftp-ssl-error", + patterns=_compile(r"FTP SSL error connecting"), + severity="warning", + category="layer8", + wiki_anchor="ftps-tls-failure", + logger_prefix="backend.app.services.bambu_ftp", + min_count=3, + ), + LogSignature( + # MQTT connection keeps dropping — typically MQTT :8883 partially + # blocked, LAN mode unstable, or a flaky network path to the printer. + id="mqtt-connection-flapping", + patterns=_compile(r"Forcing MQTT reconnect", r"Hard reset reconnect failed"), + severity="warning", + category="layer8", + wiki_anchor="mqtt-connection-unstable", + logger_prefix="backend.app.services.bambu_mqtt", + min_count=5, + ), + LogSignature( + # Camera stream unreachable — RTSPS :322 blocked, or the printer + # camera / LAN liveview is disabled. + id="camera-connection-refused", + patterns=_compile( + r"Chamber image: connection refused", + r"Chamber image: connection timeout", + r"Camera connection test failed", + ), + severity="warning", + category="layer8", + wiki_anchor="camera-rtsps-port-322", + logger_prefix="backend.app.services.camera", + min_count=3, + ), + LogSignature( + # SQLite write contention. Surfaces inside exception tracebacks; folded + # continuation lines are part of the entry message, so this still + # matches. The fix is switching to PostgreSQL under multi-printer load. + id="database-locked", + patterns=_compile(r"database is locked"), + severity="error", + category="environment", + wiki_anchor="database-is-locked", + ), +) + + +class LogFinding(BaseModel): + """An aggregated, sanitized match of one signature against the log.""" + + signature_id: str + severity: str + category: str + wiki_anchor: str + count: int + first_seen: str + last_seen: str + sample: str + + +class ScanResult(BaseModel): + """Result of a log-health scan.""" + + findings: list[LogFinding] + scanned_entries: int + log_available: bool + summary: dict[str, int] + + +def _level_ok(entry: LogEntry, min_level: str) -> bool: + return _LEVEL_RANK.get(entry.level.upper(), 0) >= _LEVEL_RANK.get(min_level, 30) + + +def _matches(sig: LogSignature, entry: LogEntry) -> bool: + if not _level_ok(entry, sig.min_level): + return False + if sig.logger_prefix and not entry.logger_name.startswith(sig.logger_prefix): + return False + return any(p.search(entry.message) for p in sig.patterns) + + +def _sample_line(message: str) -> str: + """Take the first line of a (possibly multi-line) entry, length-capped.""" + first_line = message.splitlines()[0] if message else "" + if len(first_line) > _SAMPLE_MAX_LEN: + return first_line[:_SAMPLE_MAX_LEN] + "…" + return first_line + + +def scan_logs( + limit: int = DEFAULT_SCAN_LIMIT, + sensitive_strings: dict[str, str] | None = None, +) -> ScanResult: + """Scan the recent app log against the signature catalog. + + ``sensitive_strings`` (from :func:`log_reader.collect_sensitive_strings`) is + applied to every sample line so printer names, serials, IPs, and access + codes never leave the process. Even when it is ``None`` the regex-based + redaction passes still run. + """ + log_file = settings.log_dir / "bambuddy.log" + log_available = log_file.exists() + + entries, _total = read_log_entries(limit=limit) + + # entry_id -> accumulator. entries arrive newest-first. + agg: dict[str, dict] = {} + for entry in entries: + for sig in SIGNATURES: + if not _matches(sig, entry): + continue + acc = agg.get(sig.id) + if acc is None: + # First (== newest) occurrence encountered. + agg[sig.id] = { + "count": 1, + "sample": entry.message, + "last_seen": entry.timestamp, + "first_seen": entry.timestamp, + } + else: + acc["count"] += 1 + # Iterating newest-first, so each later hit is older. + acc["first_seen"] = entry.timestamp + + findings: list[LogFinding] = [] + for sig in SIGNATURES: + acc = agg.get(sig.id) + if acc is None or acc["count"] < sig.min_count: + continue + sample = sanitize_log_content(_sample_line(acc["sample"]), sensitive_strings) + findings.append( + LogFinding( + signature_id=sig.id, + severity=sig.severity, + category=sig.category, + wiki_anchor=sig.wiki_anchor, + count=acc["count"], + first_seen=acc["first_seen"], + last_seen=acc["last_seen"], + sample=sample, + ) + ) + + findings.sort( + key=lambda f: ( + _CATEGORY_ORDER.get(f.category, 9), + _SEVERITY_ORDER.get(f.severity, 9), + -f.count, + ) + ) + + summary = { + "total": len(findings), + "layer8": sum(1 for f in findings if f.category == "layer8"), + "environment": sum(1 for f in findings if f.category == "environment"), + "bug": sum(1 for f in findings if f.category == "bug"), + } + + return ScanResult( + findings=findings, + scanned_entries=len(entries), + log_available=log_available, + summary=summary, + ) diff --git a/backend/app/services/log_reader.py b/backend/app/services/log_reader.py new file mode 100644 index 000000000..0a8e357a6 --- /dev/null +++ b/backend/app/services/log_reader.py @@ -0,0 +1,213 @@ +"""Shared primitives for reading, parsing, and sanitizing the Bambuddy app log. + +Extracted from ``routes/support.py`` so service-layer code (e.g. the log-health +scanner in ``log_health.py``) can reuse log reading and redaction without +importing from the API layer. ``support.py`` re-imports these helpers and keeps +its own route handlers. +""" + +import logging +import re + +from pydantic import BaseModel +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from backend.app.core.config import settings +from backend.app.models.printer import Printer +from backend.app.models.settings import Settings +from backend.app.models.user import User + +logger = logging.getLogger(__name__) + +# Log line format: "2024-01-15 10:30:45,123 INFO [module.name] [trace_id] Message" +# The trace_id is left as part of the message group — callers that need it can +# parse it out; the log-health scanner does not. +LOG_LINE_PATTERN = re.compile(r"^(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d{3})\s+(\w+)\s+\[([^\]]+)\]\s+(.*)$") + + +class LogEntry(BaseModel): + """A single parsed log entry.""" + + timestamp: str + level: str + logger_name: str + message: str + + +def parse_log_line(line: str) -> LogEntry | None: + """Parse a single log line into a LogEntry, or None if it is not a line start.""" + match = LOG_LINE_PATTERN.match(line.strip()) + if match: + return LogEntry( + timestamp=match.group(1), + level=match.group(2), + logger_name=match.group(3), + message=match.group(4), + ) + return None + + +def read_log_entries( + limit: int = 200, + level_filter: str | None = None, + search: str | None = None, +) -> tuple[list[LogEntry], int]: + """Read and parse log entries from ``bambuddy.log``, newest first. + + Continuation lines (tracebacks etc.) are folded into the message of the + entry they belong to. Returns ``(entries, total_lines_in_file)``. + """ + log_file = settings.log_dir / "bambuddy.log" + if not log_file.exists(): + return [], 0 + + entries: list[LogEntry] = [] + total_lines = 0 + + try: + with open(log_file, encoding="utf-8", errors="replace") as f: + lines = f.readlines() + total_lines = len(lines) + + # Parse lines in reverse order (newest first) + current_entry: LogEntry | None = None + multi_line_buffer: list[str] = [] + + for line in reversed(lines): + parsed = parse_log_line(line) + if parsed: + # Found a new log entry start + if current_entry: + # Apply filters and add previous entry (without multi_line_buffer - it belongs to new entry) + should_include = True + + # Level filter + if level_filter and current_entry.level.upper() != level_filter.upper(): + should_include = False + + # Search filter (case-insensitive) + if search and should_include: + search_lower = search.lower() + if not ( + search_lower in current_entry.message.lower() + or search_lower in current_entry.logger_name.lower() + ): + should_include = False + + if should_include: + entries.append(current_entry) + + if len(entries) >= limit: + break + + # Set new entry and attach any accumulated multi-line content to it + # (in reverse order, continuation lines come before their parent entry) + current_entry = parsed + if multi_line_buffer: + current_entry.message += "\n" + "\n".join(reversed(multi_line_buffer)) + multi_line_buffer = [] + elif line.strip(): + # Continuation of multi-line log entry (will be attached to next parsed entry) + multi_line_buffer.append(line.rstrip()) + + # Don't forget the last (oldest) entry + # Note: any remaining multi_line_buffer would be orphaned lines before the first entry + if current_entry and len(entries) < limit: + should_include = True + if level_filter and current_entry.level.upper() != level_filter.upper(): + should_include = False + if search and should_include: + search_lower = search.lower() + if not ( + search_lower in current_entry.message.lower() + or search_lower in current_entry.logger_name.lower() + ): + should_include = False + if should_include: + entries.append(current_entry) + + except Exception as e: + logger.error("Error reading log file: %s", e) + return [], 0 + + # Entries are already in newest-first order + return entries, total_lines + + +def sanitize_log_content(content: str, sensitive_strings: dict[str, str] | None = None) -> str: + """Remove sensitive data from log content. + + ``sensitive_strings`` maps known exact values (printer names, serials, etc.) + to replacement labels; pass the result of :func:`collect_sensitive_strings`. + Regex passes additionally redact credentials in URLs, emails, serials, and + IP addresses that were not captured by exact matching. + """ + # First, replace known sensitive values (database-aware exact matching) + # This catches printer names, usernames, and other arbitrary user-chosen strings + # that regex patterns cannot detect + if sensitive_strings: + # Sort by length descending to avoid partial matches (e.g. "My Printer 1" before "My Printer") + for value, label in sorted(sensitive_strings.items(), key=lambda x: len(x[0]), reverse=True): + if len(value) < 3: + continue # Skip very short strings to prevent over-redaction + content = re.sub(re.escape(value), label, content) + + # Replace credentials in URLs (e.g. http://user:pass@host, rtsps://bblp:code@host) + content = re.sub(r"((?:https?|rtsps?)://)[^/:@\s]+:[^/@\s]+@", r"\1[CREDENTIALS]@", content) + + # Replace email addresses + content = re.sub(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]", content) + + # Replace Bambu Lab printer serial numbers (format: 00M/01D/01S/01P/03W + alphanumeric, 12-16 chars total) + content = re.sub(r"\b0[0-3][A-Z0-9][A-Z0-9]{9,13}\b", "[SERIAL]", content, flags=re.IGNORECASE) + + # Replace IPv4 addresses (skip firmware versions like 01.09.01.00 which have leading zeros) + content = re.sub( + r"\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\b", + "[IP]", + content, + ) + + # Replace paths with usernames + content = re.sub(r"/home/[^/\s]+/", "/home/[user]/", content) + content = re.sub(r"/Users/[^/\s]+/", "/Users/[user]/", content) + content = re.sub(r"/opt/[^/\s]+/", "/opt/[user]/", content) + + return content + + +async def collect_sensitive_strings(db: AsyncSession) -> dict[str, str]: + """Collect known sensitive values from the database for log redaction. + + Covers printer names, serial numbers, IP addresses, access codes, auth + usernames, and the Bambu Cloud email. Pass the result to + :func:`sanitize_log_content`. + """ + sensitive_strings: dict[str, str] = {} + + # Printer names, serial numbers, IP addresses, and access codes + result = await db.execute(select(Printer.name, Printer.serial_number, Printer.ip_address, Printer.access_code)) + for name, serial, ip_address, access_code in result.all(): + if name: + sensitive_strings[name] = "[PRINTER]" + if serial: + sensitive_strings[serial] = "[SERIAL]" + if ip_address: + sensitive_strings[ip_address] = "[IP]" + if access_code: + sensitive_strings[access_code] = "[ACCESS_CODE]" + + # Auth usernames + result = await db.execute(select(User.username)) + for (username,) in result.all(): + if username: + sensitive_strings[username] = "[USER]" + + # Bambu Cloud email + result = await db.execute(select(Settings.value).where(Settings.key == "bambu_cloud_email")) + cloud_email = result.scalar_one_or_none() + if cloud_email: + sensitive_strings[cloud_email] = "[EMAIL]" + + return sensitive_strings diff --git a/backend/tests/integration/test_support_api.py b/backend/tests/integration/test_support_api.py index bfeef52bc..cc7e8841a 100644 --- a/backend/tests/integration/test_support_api.py +++ b/backend/tests/integration/test_support_api.py @@ -22,7 +22,7 @@ class TestSupportLogsAPI: @pytest.mark.integration async def test_get_logs_empty_file(self, async_client: AsyncClient): """Verify get logs returns empty list when log file doesn't exist.""" - with patch("backend.app.api.routes.support.settings") as mock_settings: + with patch("backend.app.services.log_reader.settings") as mock_settings: mock_settings.log_dir = Path("/nonexistent/path") response = await async_client.get("/api/v1/support/logs") @@ -46,7 +46,7 @@ class TestSupportLogsAPI: log_file = Path(tmpdir) / "bambuddy.log" log_file.write_text(log_content) - with patch("backend.app.api.routes.support.settings") as mock_settings: + with patch("backend.app.services.log_reader.settings") as mock_settings: mock_settings.log_dir = Path(tmpdir) response = await async_client.get("/api/v1/support/logs") @@ -76,7 +76,7 @@ class TestSupportLogsAPI: log_file = Path(tmpdir) / "bambuddy.log" log_file.write_text(log_content) - with patch("backend.app.api.routes.support.settings") as mock_settings: + with patch("backend.app.services.log_reader.settings") as mock_settings: mock_settings.log_dir = Path(tmpdir) response = await async_client.get("/api/v1/support/logs?level=ERROR") @@ -100,7 +100,7 @@ class TestSupportLogsAPI: log_file = Path(tmpdir) / "bambuddy.log" log_file.write_text(log_content) - with patch("backend.app.api.routes.support.settings") as mock_settings: + with patch("backend.app.services.log_reader.settings") as mock_settings: mock_settings.log_dir = Path(tmpdir) response = await async_client.get("/api/v1/support/logs?search=printer") @@ -124,7 +124,7 @@ class TestSupportLogsAPI: log_file = Path(tmpdir) / "bambuddy.log" log_file.write_text(log_content) - with patch("backend.app.api.routes.support.settings") as mock_settings: + with patch("backend.app.services.log_reader.settings") as mock_settings: mock_settings.log_dir = Path(tmpdir) response = await async_client.get("/api/v1/support/logs?limit=2") @@ -153,7 +153,7 @@ ValueError: test error log_file = Path(tmpdir) / "bambuddy.log" log_file.write_text(log_content) - with patch("backend.app.api.routes.support.settings") as mock_settings: + with patch("backend.app.services.log_reader.settings") as mock_settings: mock_settings.log_dir = Path(tmpdir) response = await async_client.get("/api/v1/support/logs") @@ -214,7 +214,7 @@ class TestLogParsingHelpers: def test_parse_log_line_valid(self): """Verify _parse_log_line handles valid log lines.""" - from backend.app.api.routes.support import _parse_log_line + from backend.app.services.log_reader import parse_log_line as _parse_log_line line = "2024-01-15 10:30:45,123 INFO [backend.app.main] Server started" entry = _parse_log_line(line) @@ -227,7 +227,7 @@ class TestLogParsingHelpers: def test_parse_log_line_invalid(self): """Verify _parse_log_line returns None for invalid lines.""" - from backend.app.api.routes.support import _parse_log_line + from backend.app.services.log_reader import parse_log_line as _parse_log_line line = "This is not a valid log line" entry = _parse_log_line(line) @@ -236,7 +236,7 @@ class TestLogParsingHelpers: def test_parse_log_line_with_brackets_in_message(self): """Verify _parse_log_line handles messages with brackets.""" - from backend.app.api.routes.support import _parse_log_line + from backend.app.services.log_reader import parse_log_line as _parse_log_line line = "2024-01-15 10:30:45,123 INFO [backend.app.main] Processing [item 1] and [item 2]" entry = _parse_log_line(line) @@ -246,7 +246,7 @@ class TestLogParsingHelpers: def test_parse_log_line_all_levels(self): """Verify _parse_log_line handles all log levels.""" - from backend.app.api.routes.support import _parse_log_line + from backend.app.services.log_reader import parse_log_line as _parse_log_line levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] for level in levels: diff --git a/backend/tests/integration/test_system_api.py b/backend/tests/integration/test_system_api.py index 4a7b4e563..e445b15f5 100644 --- a/backend/tests/integration/test_system_api.py +++ b/backend/tests/integration/test_system_api.py @@ -308,3 +308,48 @@ class TestSystemHelperFunctions: result = format_uptime(30) # 30 seconds assert result == "< 1m" + + +class TestSystemHealthAPI: + """Integration tests for GET /api/v1/system/health (log-health scan).""" + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_health_clean_log(self, async_client: AsyncClient, tmp_path, monkeypatch): + """A log with no known issues returns an empty, healthy result.""" + from backend.app.core.config import settings + + (tmp_path / "bambuddy.log").write_text( + "2026-05-22 10:00:00,000 INFO [backend.app.main] Application startup complete\n", + encoding="utf-8", + ) + monkeypatch.setattr(settings, "log_dir", tmp_path) + + response = await async_client.get("/api/v1/system/health") + + assert response.status_code == 200 + result = response.json() + assert result["log_available"] is True + assert result["findings"] == [] + assert result["summary"]["total"] == 0 + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_health_detects_known_issue(self, async_client: AsyncClient, tmp_path, monkeypatch): + """A known signature in the log surfaces as a finding.""" + from backend.app.core.config import settings + + (tmp_path / "bambuddy.log").write_text( + "2026-05-22 10:00:00,000 WARNING [backend.app.services.bambu_ftp] " + "FTP connection permission error to 10.0.0.9: 530\n", + encoding="utf-8", + ) + monkeypatch.setattr(settings, "log_dir", tmp_path) + + response = await async_client.get("/api/v1/system/health") + + assert response.status_code == 200 + result = response.json() + ids = [f["signature_id"] for f in result["findings"]] + assert "ftp-auth-rejected" in ids + assert result["summary"]["layer8"] >= 1 diff --git a/backend/tests/unit/services/test_log_health.py b/backend/tests/unit/services/test_log_health.py new file mode 100644 index 000000000..e7a7be847 --- /dev/null +++ b/backend/tests/unit/services/test_log_health.py @@ -0,0 +1,169 @@ +"""Tests for the log-health scanner (backend/app/services/log_health.py).""" + +import pytest + +from backend.app.core.config import settings +from backend.app.services.log_health import SIGNATURES, scan_logs + + +def _line(level, logger, msg, ts="2026-05-22 10:00:00,000"): + """Build one log line in the app's log format: TS LEVEL [logger] message.""" + return f"{ts} {level} [{logger}] {msg}" + + +def _write_log(tmp_path, monkeypatch, lines): + log_file = tmp_path / "bambuddy.log" + log_file.write_text("\n".join(lines) + "\n", encoding="utf-8") + monkeypatch.setattr(settings, "log_dir", tmp_path) + return log_file + + +FTP_LOGGER = "backend.app.services.bambu_ftp" +MQTT_LOGGER = "backend.app.services.bambu_mqtt" +CAM_LOGGER = "backend.app.services.camera" + + +def test_clean_log_has_no_findings(tmp_path, monkeypatch): + _write_log( + tmp_path, + monkeypatch, + [ + _line("INFO", "backend.app.main", "Application startup complete"), + _line("INFO", FTP_LOGGER, "FTP connected, logging in as bblp"), + ], + ) + result = scan_logs() + assert result.findings == [] + assert result.log_available is True + assert result.scanned_entries == 2 + assert result.summary == {"total": 0, "layer8": 0, "environment": 0, "bug": 0} + + +def test_log_unavailable_when_file_missing(tmp_path, monkeypatch): + # No log file written. + monkeypatch.setattr(settings, "log_dir", tmp_path) + result = scan_logs() + assert result.log_available is False + assert result.findings == [] + + +def test_ftp_auth_rejected_is_detected(tmp_path, monkeypatch): + _write_log( + tmp_path, + monkeypatch, + [_line("WARNING", FTP_LOGGER, "FTP connection permission error to 10.0.0.9: 530 Login incorrect")], + ) + result = scan_logs() + assert len(result.findings) == 1 + f = result.findings[0] + assert f.signature_id == "ftp-auth-rejected" + assert f.severity == "error" + assert f.category == "layer8" + assert f.count == 1 + + +def test_min_count_gates_low_frequency_signals(tmp_path, monkeypatch): + # ftp-connection-timeout requires min_count=3 — two hits must not surface. + _write_log( + tmp_path, + monkeypatch, + [_line("WARNING", FTP_LOGGER, "FTP connection timed out to 10.0.0.9: timed out")] * 2, + ) + assert scan_logs().findings == [] + + _write_log( + tmp_path, + monkeypatch, + [_line("WARNING", FTP_LOGGER, "FTP connection timed out to 10.0.0.9: timed out")] * 3, + ) + findings = scan_logs().findings + assert len(findings) == 1 + assert findings[0].signature_id == "ftp-connection-timeout" + assert findings[0].count == 3 + + +def test_aggregation_tracks_count_and_seen_range(tmp_path, monkeypatch): + _write_log( + tmp_path, + monkeypatch, + [ + _line("WARNING", FTP_LOGGER, "FTP connection permission error to 10.0.0.9", ts="2026-05-22 09:00:00,000"), + _line("WARNING", FTP_LOGGER, "FTP connection permission error to 10.0.0.9", ts="2026-05-22 10:30:00,000"), + ], + ) + f = scan_logs().findings[0] + assert f.count == 2 + assert f.first_seen == "2026-05-22 09:00:00,000" + assert f.last_seen == "2026-05-22 10:30:00,000" + + +def test_logger_prefix_filters_unrelated_loggers(tmp_path, monkeypatch): + # Same text, but logged by an unrelated logger — must not match the + # bambu_ftp-scoped signature. + _write_log( + tmp_path, + monkeypatch, + [_line("WARNING", "backend.app.services.something_else", "FTP connection permission error to 10.0.0.9")], + ) + assert scan_logs().findings == [] + + +def test_min_level_filters_below_threshold(tmp_path, monkeypatch): + # ftp-auth-rejected requires at least WARNING — an INFO line must not match. + _write_log( + tmp_path, + monkeypatch, + [_line("INFO", FTP_LOGGER, "FTP connection permission error to 10.0.0.9")], + ) + assert scan_logs().findings == [] + + +def test_sample_is_sanitized(tmp_path, monkeypatch): + _write_log( + tmp_path, + monkeypatch, + [_line("WARNING", FTP_LOGGER, "FTP connection permission error to 192.168.1.50: 530")], + ) + f = scan_logs().findings[0] + assert "192.168.1.50" not in f.sample + assert "[IP]" in f.sample + + +def test_database_locked_matches_inside_traceback(tmp_path, monkeypatch): + # The signature text appears on a continuation line of a multi-line entry; + # read_log_entries folds it into the parent message. + _write_log( + tmp_path, + monkeypatch, + [ + _line("ERROR", "backend.app.core.database", "Unhandled DB error"), + "Traceback (most recent call last):", + "sqlite3.OperationalError: database is locked", + ], + ) + findings = scan_logs().findings + assert len(findings) == 1 + assert findings[0].signature_id == "database-locked" + assert findings[0].category == "environment" + + +def test_findings_sorted_layer8_then_environment(tmp_path, monkeypatch): + _write_log( + tmp_path, + monkeypatch, + [ + _line("ERROR", "backend.app.core.database", "x: database is locked"), + _line("WARNING", FTP_LOGGER, "FTP connection timed out to 10.0.0.9"), + _line("WARNING", FTP_LOGGER, "FTP connection timed out to 10.0.0.9"), + _line("WARNING", FTP_LOGGER, "FTP connection timed out to 10.0.0.9"), + _line("WARNING", FTP_LOGGER, "FTP connection permission error to 10.0.0.9"), + ], + ) + ids = [f.signature_id for f in scan_logs().findings] + # layer8 error, then layer8 warning, then environment. + assert ids == ["ftp-auth-rejected", "ftp-connection-timeout", "database-locked"] + + +def test_every_signature_id_is_unique(): + ids = [s.id for s in SIGNATURES] + assert len(ids) == len(set(ids)) diff --git a/backend/tests/unit/test_support_helpers.py b/backend/tests/unit/test_support_helpers.py index 40a0be978..40498cf1e 100644 --- a/backend/tests/unit/test_support_helpers.py +++ b/backend/tests/unit/test_support_helpers.py @@ -235,7 +235,7 @@ class TestSanitizeLogContent: def test_ipv4_addresses_redacted(self): """IPv4 addresses in log lines are replaced with [IP].""" - from backend.app.api.routes.support import _sanitize_log_content + from backend.app.services.log_reader import sanitize_log_content as _sanitize_log_content content = "2024-01-15 Connected to printer at 192.168.1.100 on port 8883" result = _sanitize_log_content(content) @@ -245,7 +245,7 @@ class TestSanitizeLogContent: def test_multiple_ipv4_addresses_redacted(self): """Multiple different IPs in the same line are all redacted.""" - from backend.app.api.routes.support import _sanitize_log_content + from backend.app.services.log_reader import sanitize_log_content as _sanitize_log_content content = "Proxy 10.0.0.1 -> 192.168.1.50" result = _sanitize_log_content(content) @@ -253,7 +253,7 @@ class TestSanitizeLogContent: def test_firmware_versions_with_leading_zeros_preserved(self): """Firmware versions like 01.09.01.00 have leading zeros and should NOT be redacted.""" - from backend.app.api.routes.support import _sanitize_log_content + from backend.app.services.log_reader import sanitize_log_content as _sanitize_log_content content = "Firmware version: 01.09.01.00" result = _sanitize_log_content(content) @@ -261,7 +261,7 @@ class TestSanitizeLogContent: def test_firmware_version_mixed_with_ip(self): """Firmware versions preserved while real IPs are redacted in the same line.""" - from backend.app.api.routes.support import _sanitize_log_content + from backend.app.services.log_reader import sanitize_log_content as _sanitize_log_content content = "Printer at 192.168.1.5 running firmware 01.07.02.00" result = _sanitize_log_content(content) @@ -271,7 +271,7 @@ class TestSanitizeLogContent: def test_printer_ip_from_sensitive_strings(self): """Printer IPs in sensitive_strings are replaced before regex pass.""" - from backend.app.api.routes.support import _sanitize_log_content + from backend.app.services.log_reader import sanitize_log_content as _sanitize_log_content content = "Connecting to 192.168.1.100" result = _sanitize_log_content(content, sensitive_strings={"192.168.1.100": "[IP]"}) @@ -279,7 +279,7 @@ class TestSanitizeLogContent: def test_edge_case_zero_ip(self): """0.0.0.0 is a valid IP and should be redacted.""" - from backend.app.api.routes.support import _sanitize_log_content + from backend.app.services.log_reader import sanitize_log_content as _sanitize_log_content content = "Binding to 0.0.0.0" result = _sanitize_log_content(content) @@ -287,7 +287,7 @@ class TestSanitizeLogContent: def test_edge_case_broadcast_ip(self): """255.255.255.255 is a valid IP and should be redacted.""" - from backend.app.api.routes.support import _sanitize_log_content + from backend.app.services.log_reader import sanitize_log_content as _sanitize_log_content content = "Broadcast to 255.255.255.255" result = _sanitize_log_content(content) @@ -295,7 +295,7 @@ class TestSanitizeLogContent: def test_invalid_octet_not_redacted(self): """Octets >255 are not valid IPs and should not be redacted.""" - from backend.app.api.routes.support import _sanitize_log_content + from backend.app.services.log_reader import sanitize_log_content as _sanitize_log_content content = "Value 999.999.999.999" result = _sanitize_log_content(content) @@ -303,7 +303,7 @@ class TestSanitizeLogContent: def test_existing_serial_redaction_still_works(self): """Serial number redaction still functions alongside IP redaction.""" - from backend.app.api.routes.support import _sanitize_log_content + from backend.app.services.log_reader import sanitize_log_content as _sanitize_log_content content = "Printer 01SABCDEF1234 at 10.0.0.5" result = _sanitize_log_content(content) @@ -314,7 +314,7 @@ class TestSanitizeLogContent: def test_existing_email_redaction_still_works(self): """Email redaction still functions alongside IP redaction.""" - from backend.app.api.routes.support import _sanitize_log_content + from backend.app.services.log_reader import sanitize_log_content as _sanitize_log_content content = "User user@example.com from 172.16.0.1" result = _sanitize_log_content(content) @@ -323,7 +323,7 @@ class TestSanitizeLogContent: def test_existing_path_redaction_still_works(self): """Path redaction still functions alongside IP redaction.""" - from backend.app.api.routes.support import _sanitize_log_content + from backend.app.services.log_reader import sanitize_log_content as _sanitize_log_content content = "Config at /home/john/config.yaml from 192.168.0.1" result = _sanitize_log_content(content) diff --git a/frontend/src/__tests__/components/AddPrinterPreflight.test.tsx b/frontend/src/__tests__/components/AddPrinterPreflight.test.tsx new file mode 100644 index 000000000..497b1a647 --- /dev/null +++ b/frontend/src/__tests__/components/AddPrinterPreflight.test.tsx @@ -0,0 +1,93 @@ +/** + * Tests for the Add-Printer setup-time pre-flight. + * + * On save, the modal runs the connection diagnostic; if any check fails it + * warns (rather than blocks) before the printer is added. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '../utils'; +import { PrintersPage } from '../../pages/PrintersPage'; +import { http, HttpResponse } from 'msw'; +import { server } from '../mocks/server'; + +describe('AddPrinterModal pre-flight', () => { + beforeEach(() => { + server.use( + http.get('/api/v1/printers/', () => HttpResponse.json([])), + http.get('/api/v1/queue/', () => HttpResponse.json([])), + http.get('/api/v1/discovery/info', () => + HttpResponse.json({ is_docker: false, ssdp_running: false, scan_running: false, subnets: [] }), + ), + ); + }); + + it('warns instead of saving when a connection check fails', async () => { + const user = userEvent.setup(); + server.use( + http.post('/api/v1/printers/diagnostic', () => + HttpResponse.json({ + printer_id: null, + ip_address: '192.168.1.55', + overall: 'problems', + checks: [{ id: 'developer_mode', status: 'fail', params: {} }], + }), + ), + ); + + render(); + await user.click(await screen.findByText(/add printer/i)); + + await user.type(await screen.findByPlaceholderText('My Printer'), 'Test Printer'); + await user.type(screen.getByPlaceholderText('192.168.1.100 or printer.local'), '192.168.1.55'); + await user.type(screen.getByPlaceholderText('01P00A000000000'), '01P00A000000000'); + await user.type(screen.getByPlaceholderText('From printer settings'), '12345678'); + + const submit = screen + .getAllByRole('button', { name: /add printer/i }) + .find((b) => b.getAttribute('type') === 'submit')!; + await user.click(submit); + + // The failed check surfaces a warning with a "save anyway" escape hatch. + expect(await screen.findByText(/Some connection checks failed/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /save anyway/i })).toBeInTheDocument(); + expect(screen.getByText(/LAN Developer Mode/i)).toBeInTheDocument(); + }); + + it('saves directly when all connection checks pass', async () => { + const user = userEvent.setup(); + let created = false; + server.use( + http.post('/api/v1/printers/diagnostic', () => + HttpResponse.json({ + printer_id: null, + ip_address: '192.168.1.55', + overall: 'ok', + checks: [{ id: 'developer_mode', status: 'pass', params: {} }], + }), + ), + http.post('/api/v1/printers/', async () => { + created = true; + return HttpResponse.json({ id: 9, name: 'Test Printer' }); + }), + ); + + render(); + await user.click(await screen.findByText(/add printer/i)); + + await user.type(await screen.findByPlaceholderText('My Printer'), 'Test Printer'); + await user.type(screen.getByPlaceholderText('192.168.1.100 or printer.local'), '192.168.1.55'); + await user.type(screen.getByPlaceholderText('01P00A000000000'), '01P00A000000000'); + await user.type(screen.getByPlaceholderText('From printer settings'), '12345678'); + + const submit = screen + .getAllByRole('button', { name: /add printer/i }) + .find((b) => b.getAttribute('type') === 'submit')!; + await user.click(submit); + + await waitFor(() => expect(created).toBe(true)); + expect(screen.queryByText(/Some connection checks failed/i)).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/components/BugReportBubble.test.tsx b/frontend/src/__tests__/components/BugReportBubble.test.tsx index eeaf95461..47db715d4 100644 --- a/frontend/src/__tests__/components/BugReportBubble.test.tsx +++ b/frontend/src/__tests__/components/BugReportBubble.test.tsx @@ -282,4 +282,36 @@ describe('BugReportBubble', () => { // Single problem → the checklist is expanded without a click. expect(await screen.findByText(/Found problems that explain/)).toBeInTheDocument(); }); + + it('shows the log-health panel when the scan finds known issues', async () => { + const user = userEvent.setup(); + setupDiagnosticEndpoints([{ id: 1, name: 'Solo Printer' }], { 1: 'ok' }); + server.use( + http.get('*/system/health', () => + HttpResponse.json({ + findings: [ + { + signature_id: 'ftp-auth-rejected', + severity: 'error', + category: 'layer8', + wiki_anchor: 'wrong-access-code', + count: 3, + first_seen: '2026-05-22 09:00:00,000', + last_seen: '2026-05-22 10:00:00,000', + sample: 'FTP connection permission error to [IP]', + }, + ], + scanned_entries: 500, + log_available: true, + summary: { total: 1, layer8: 1, environment: 0, bug: 0 }, + }) + ) + ); + + render(); + await user.click(screen.getByRole('button')); + + expect(await screen.findByText('Known issues found in your logs')).toBeInTheDocument(); + expect(screen.getByText('Printer rejected the access code')).toBeInTheDocument(); + }); }); diff --git a/frontend/src/__tests__/components/EditPrinterPreflight.test.tsx b/frontend/src/__tests__/components/EditPrinterPreflight.test.tsx new file mode 100644 index 000000000..f66a32951 --- /dev/null +++ b/frontend/src/__tests__/components/EditPrinterPreflight.test.tsx @@ -0,0 +1,109 @@ +/** + * Tests for the Edit-Printer setup-time pre-flight. + * + * Editing a printer runs the same connection diagnostic on save as the + * Add-Printer dialog, and warns (rather than blocks) when a check fails. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '../utils'; +import { PrintersPage } from '../../pages/PrintersPage'; +import { http, HttpResponse } from 'msw'; +import { server } from '../mocks/server'; + +const mockPrinter = { + id: 1, + name: 'X1 Carbon', + ip_address: '192.168.1.100', + serial_number: '00M09A350100001', + access_code: '12345678', + model: 'X1C', + enabled: true, + nozzle_diameter: 0.4, + nozzle_type: 'hardened_steel', + location: null, + auto_archive: true, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', +}; + +const mockStatus = { + connected: true, + state: 'IDLE', + progress: 0, + layer_num: 0, + total_layers: 0, + temperatures: { nozzle: 25, bed: 25, chamber: 25 }, + remaining_time: 0, + filename: null, + wifi_signal: -50, + vt_tray: [], +}; + +async function openEditModal() { + render(); + await waitFor(() => expect(screen.getByText('X1 Carbon')).toBeInTheDocument()); + + // Open the per-printer actions menu (kebab button), then click Edit. + const menuBtn = [...document.querySelectorAll('button')].find((b) => + b.querySelector('.lucide-ellipsis-vertical'), + )!; + await userEvent.click(menuBtn); + await userEvent.click(await screen.findByRole('button', { name: /^edit$/i })); + await screen.findByText('Edit Printer'); +} + +describe('EditPrinterModal pre-flight', () => { + beforeEach(() => { + server.use( + http.get('/api/v1/printers/', () => HttpResponse.json([mockPrinter])), + http.get('/api/v1/printers/:id/status', () => HttpResponse.json(mockStatus)), + http.get('/api/v1/queue/', () => HttpResponse.json([])), + ); + }); + + it('warns instead of saving when a connection check fails', async () => { + server.use( + http.post('/api/v1/printers/diagnostic', () => + HttpResponse.json({ + printer_id: null, + ip_address: '192.168.1.100', + overall: 'problems', + checks: [{ id: 'developer_mode', status: 'fail', params: {} }], + }), + ), + ); + + await openEditModal(); + await userEvent.click(screen.getByRole('button', { name: /save changes/i })); + + expect(await screen.findByText(/Some connection checks failed/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /save anyway/i })).toBeInTheDocument(); + }); + + it('saves directly when all connection checks pass', async () => { + let updated = false; + server.use( + http.post('/api/v1/printers/diagnostic', () => + HttpResponse.json({ + printer_id: null, + ip_address: '192.168.1.100', + overall: 'ok', + checks: [{ id: 'developer_mode', status: 'pass', params: {} }], + }), + ), + http.patch('/api/v1/printers/:id', async () => { + updated = true; + return HttpResponse.json({ ...mockPrinter, name: 'X1 Carbon' }); + }), + ); + + await openEditModal(); + await userEvent.click(screen.getByRole('button', { name: /save changes/i })); + + await waitFor(() => expect(updated).toBe(true)); + expect(screen.queryByText(/Some connection checks failed/i)).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/components/SystemHealthPanel.test.tsx b/frontend/src/__tests__/components/SystemHealthPanel.test.tsx new file mode 100644 index 000000000..1ba131665 --- /dev/null +++ b/frontend/src/__tests__/components/SystemHealthPanel.test.tsx @@ -0,0 +1,74 @@ +/** + * Tests for SystemHealthPanel — the shared log-health result renderer used by + * the System page and the bug reporter. + * + * Covers the three states (clean / log unavailable / findings) and that a + * finding renders its localized name, cause, fix, category badge, and a wiki + * deep-link built from the signature's anchor. + */ + +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { I18nextProvider } from 'react-i18next'; +import i18n from '../../i18n'; +import { SystemHealthPanel } from '../../components/SystemHealthPanel'; +import type { SystemHealthResult } from '../../api/client'; + +function renderPanel(result: SystemHealthResult) { + render( + + + , + ); +} + +const BASE: SystemHealthResult = { + findings: [], + scanned_entries: 1200, + log_available: true, + summary: { total: 0, layer8: 0, environment: 0, bug: 0 }, +}; + +describe('SystemHealthPanel', () => { + it('shows a healthy message when there are no findings', () => { + renderPanel(BASE); + expect(screen.getByText(/No known issues found/i)).toBeInTheDocument(); + }); + + it('shows a notice when file logging is unavailable', () => { + renderPanel({ ...BASE, log_available: false }); + expect(screen.getByText(/File logging is disabled/i)).toBeInTheDocument(); + }); + + it('renders a finding with localized name, cause, fix, badge, and wiki link', () => { + renderPanel({ + ...BASE, + log_available: true, + findings: [ + { + signature_id: 'ftp-auth-rejected', + severity: 'error', + category: 'layer8', + wiki_anchor: 'wrong-access-code', + count: 4, + first_seen: '2026-05-22 09:00:00,000', + last_seen: '2026-05-22 10:00:00,000', + sample: 'FTP connection permission error to [IP]', + }, + ], + summary: { total: 1, layer8: 1, environment: 0, bug: 0 }, + }); + + expect(screen.getByText('Printer rejected the access code')).toBeInTheDocument(); + expect(screen.getByText(/refused the file-transfer login/i)).toBeInTheDocument(); + expect(screen.getByText(/Re-copy the access code/i)).toBeInTheDocument(); + expect(screen.getByText('You can fix this')).toBeInTheDocument(); + expect(screen.getByText('FTP connection permission error to [IP]')).toBeInTheDocument(); + + const link = screen.getByRole('link', { name: /How to fix/i }); + expect(link).toHaveAttribute( + 'href', + 'https://wiki.bambuddy.cool/reference/troubleshooting/#wrong-access-code', + ); + }); +}); diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 82d3d170e..7ff367ebe 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -213,6 +213,35 @@ export interface PrinterDiagnosticResult { checks: DiagnosticCheck[]; } +// --- Log-health scan: self-service triage on the System page + bug reporter. +// The backend matches recent logs against a curated known-issue catalog; +// human-readable cause/fix text is rendered from i18n keys keyed by signature_id. +export type LogFindingSeverity = 'error' | 'warning'; +export type LogFindingCategory = 'layer8' | 'environment' | 'bug'; + +export interface LogFinding { + signature_id: string; + severity: LogFindingSeverity; + category: LogFindingCategory; + wiki_anchor: string; + count: number; + first_seen: string; + last_seen: string; + sample: string; +} + +export interface SystemHealthResult { + findings: LogFinding[]; + scanned_entries: number; + log_available: boolean; + summary: { + total: number; + layer8: number; + environment: number; + bug: number; + }; +} + // Long-lived camera-stream tokens (#1108). The `token` field is populated // only on the create response — listing endpoints set it to null because // the plaintext value is shown to the user exactly once. @@ -5351,6 +5380,7 @@ export const api = { // System Info getSystemInfo: () => request('/system/info'), + getSystemHealth: () => request('/system/health'), getStorageUsage: (options?: { refresh?: boolean }) => { const params = new URLSearchParams(); if (options?.refresh) { diff --git a/frontend/src/components/BugReportBubble.tsx b/frontend/src/components/BugReportBubble.tsx index 7df1133b1..4b80bfeff 100644 --- a/frontend/src/components/BugReportBubble.tsx +++ b/frontend/src/components/BugReportBubble.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { api, bugReportApi, type PrinterDiagnosticResult } from '../api/client'; import { DiagnosticChecklist } from './ConnectionDiagnostic'; +import { SystemHealthPanel } from './SystemHealthPanel'; import { Collapsible } from './Collapsible'; type ViewState = 'form' | 'logging' | 'stopping' | 'submitting' | 'success' | 'error'; @@ -85,6 +86,18 @@ export function BugReportBubble() { const diagnosticEntries = diagnosticScan.data ?? []; const diagnosticProblems = diagnosticEntries.filter((e) => e.result.overall === 'problems'); + // Scan recent logs against the known-issue catalog. Like the diagnostic + // above, this surfaces user-fixable ("layer 8") problems before a report is + // filed. Only shown when something matched — a clean scan stays silent so + // the form is uncluttered. + const logHealthScan = useQuery({ + queryKey: ['bugReportLogHealth'], + enabled: isOpen && viewState === 'form', + staleTime: 30_000, + queryFn: api.getSystemHealth, + }); + const logFindings = logHealthScan.data?.findings ?? []; + // Elapsed timer for logging phase — auto-stop at 5 minutes useEffect(() => { if (viewState !== 'logging') return; @@ -299,6 +312,25 @@ export function BugReportBubble() { )} + {/* Log-health scan — known issues found in recent logs. + Shown only when something matched. */} + {!logHealthScan.isLoading && logFindings.length > 0 && logHealthScan.data && ( +
+
+ +
+

+ {t('bugReport.logHealthSummary')} +

+

+ {t('bugReport.logHealthIntro')} +

+
+
+ +
+ )} + {/* Description */}
-
{ - e.preventDefault(); - onAdd(form); - }} - className="space-y-4" - > +
{t('diagnostic.runButton')} -
- - -
+ {saveWarning ? ( +
+
+ +

{t('printers.addPreflight.warning')}

+
+ +
+ + +
+
+ ) : ( +
+ + +
+ )} @@ -6271,6 +6316,11 @@ function EditPrinterModal({ auto_archive: printer.auto_archive, }); + // Setup-time pre-flight — same warn-on-save as the Add-Printer dialog, so an + // edit that breaks connectivity (e.g. a mistyped IP) is caught before save. + const [checkingSave, setCheckingSave] = useState(false); + const [saveWarning, setSaveWarning] = useState(null); + const updateMutation = useMutation({ mutationFn: (data: Partial) => api.updatePrinter(printer.id, data), onSuccess: () => { @@ -6290,8 +6340,7 @@ function EditPrinterModal({ return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); + const doSave = () => { const data: Partial = { name: form.name, ip_address: form.ip_address, @@ -6306,6 +6355,27 @@ function EditPrinterModal({ updateMutation.mutate(data); }; + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setCheckingSave(true); + try { + const result = await api.diagnoseConnection({ + ip_address: form.ip_address.trim(), + serial_number: printer.serial_number, + access_code: form.access_code || undefined, + }); + if (result.checks.some((c) => c.status === 'fail')) { + setSaveWarning(result); + return; + } + } catch { + // Diagnostic infrastructure failed — never block the save on it. + } finally { + setCheckingSave(false); + } + doSave(); + }; + return (
-
- - -
+ {saveWarning ? ( +
+
+ +

{t('printers.addPreflight.warning')}

+
+ +
+ + +
+
+ ) : ( +
+ + +
+ )} diff --git a/frontend/src/pages/SystemInfoPage.tsx b/frontend/src/pages/SystemInfoPage.tsx index 2398199ff..147588f22 100644 --- a/frontend/src/pages/SystemInfoPage.tsx +++ b/frontend/src/pages/SystemInfoPage.tsx @@ -22,11 +22,13 @@ import { Headphones, FolderOpen, Stethoscope, + HeartPulse, } from 'lucide-react'; import { api, supportApi, type Printer as PrinterModel } from '../api/client'; import { Card } from '../components/Card'; import { LogViewer } from '../components/LogViewer'; import { ConnectionDiagnosticModal } from '../components/ConnectionDiagnostic'; +import { SystemHealthPanel } from '../components/SystemHealthPanel'; import { formatDateTime, type TimeFormat } from '../utils/date'; function formatBytes(bytes: number): string { @@ -130,6 +132,16 @@ export function SystemInfoPage() { queryFn: api.getPrinters, }); + const { + data: systemHealth, + refetch: refetchHealth, + isFetching: healthFetching, + } = useQuery({ + queryKey: ['systemHealth'], + queryFn: api.getSystemHealth, + staleTime: 60 * 1000, + }); + const timeFormat: TimeFormat = settings?.time_format || 'system'; const handleToggleDebugLogging = async () => { @@ -397,6 +409,26 @@ export function SystemInfoPage() { )} + {/* System Health */} +
+
+

{t('systemHealth.sectionDescription')}

+ +
+ {systemHealth ? ( + + ) : ( +

{t('common.loading')}

+ )} +
+ {/* Database Stats */}
diff --git a/static/assets/index-BpyZgjkS.js b/static/assets/index-BL4kzp1A.js similarity index 64% rename from static/assets/index-BpyZgjkS.js rename to static/assets/index-BL4kzp1A.js index f32ff4905..a35c836f3 100644 --- a/static/assets/index-BpyZgjkS.js +++ b/static/assets/index-BL4kzp1A.js @@ -1,45 +1,45 @@ -function Jue(t,e){for(var a=0;an[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"]'))n(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function a(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function n(i){if(i.ep)return;i.ep=!0;const s=a(i);fetch(i.href,s)}})();var P_=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Ec(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var tE={exports:{}},t0={};var _H;function eme(){if(_H)return t0;_H=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function a(n,i,s){var o=null;if(s!==void 0&&(o=""+s),i.key!==void 0&&(o=""+i.key),"key"in i){s={};for(var l in i)l!=="key"&&(s[l]=i[l])}else s=i;return i=s.ref,{$$typeof:t,type:n,key:o,ref:i!==void 0?i:null,props:s}}return t0.Fragment=e,t0.jsx=a,t0.jsxs=a,t0}var NH;function tme(){return NH||(NH=1,tE.exports=eme()),tE.exports}var r=tme(),aE={exports:{}},Ka={};var CH;function ame(){if(CH)return Ka;CH=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),a=Symbol.for("react.fragment"),n=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),s=Symbol.for("react.consumer"),o=Symbol.for("react.context"),l=Symbol.for("react.forward_ref"),c=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),u=Symbol.for("react.lazy"),m=Symbol.for("react.activity"),p=Symbol.iterator;function f(X){return X===null||typeof X!="object"?null:(X=p&&X[p]||X["@@iterator"],typeof X=="function"?X:null)}var g={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},w=Object.assign,b={};function x(X,le,me){this.props=X,this.context=le,this.refs=b,this.updater=me||g}x.prototype.isReactComponent={},x.prototype.setState=function(X,le){if(typeof X!="object"&&typeof X!="function"&&X!=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,X,le,"setState")},x.prototype.forceUpdate=function(X){this.updater.enqueueForceUpdate(this,X,"forceUpdate")};function S(){}S.prototype=x.prototype;function N(X,le,me){this.props=X,this.context=le,this.refs=b,this.updater=me||g}var C=N.prototype=new S;C.constructor=N,w(C,x.prototype),C.isPureReactComponent=!0;var P=Array.isArray;function T(){}var j={H:null,A:null,T:null,S:null},F=Object.prototype.hasOwnProperty;function k(X,le,me){var de=me.ref;return{$$typeof:t,type:X,key:le,ref:de!==void 0?de:null,props:me}}function E(X,le){return k(X.type,le,X.props)}function U(X){return typeof X=="object"&&X!==null&&X.$$typeof===t}function I(X){var le={"=":"=0",":":"=2"};return"$"+X.replace(/[=:]/g,function(me){return le[me]})}var O=/\/+/g;function L(X,le){return typeof X=="object"&&X!==null&&X.key!=null?I(""+X.key):le.toString(36)}function Y(X){switch(X.status){case"fulfilled":return X.value;case"rejected":throw X.reason;default:switch(typeof X.status=="string"?X.then(T,T):(X.status="pending",X.then(function(le){X.status==="pending"&&(X.status="fulfilled",X.value=le)},function(le){X.status==="pending"&&(X.status="rejected",X.reason=le)})),X.status){case"fulfilled":return X.value;case"rejected":throw X.reason}}throw X}function ae(X,le,me,de,we){var H=typeof X;(H==="undefined"||H==="boolean")&&(X=null);var J=!1;if(X===null)J=!0;else switch(H){case"bigint":case"string":case"number":J=!0;break;case"object":switch(X.$$typeof){case t:case e:J=!0;break;case u:return J=X._init,ae(J(X._payload),le,me,de,we)}}if(J)return we=we(X),J=de===""?"."+L(X,0):de,P(we)?(me="",J!=null&&(me=J.replace(O,"$&/")+"/"),ae(we,le,me,"",function(q){return q})):we!=null&&(U(we)&&(we=E(we,me+(we.key==null||X&&X.key===we.key?"":(""+we.key).replace(O,"$&/")+"/")+J)),le.push(we)),1;J=0;var A=de===""?".":de+":";if(P(X))for(var M=0;M>>1,pe=ae[ue];if(0>>1;uei(me,ce))dei(we,me)?(ae[ue]=we,ae[de]=ce,ue=de):(ae[ue]=me,ae[le]=ce,ue=le);else if(dei(we,ce))ae[ue]=we,ae[de]=ce,ue=de;else break e}}return Z}function i(ae,Z){var ce=ae.sortIndex-Z.sortIndex;return ce!==0?ce:ae.id-Z.id}if(t.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var s=performance;t.unstable_now=function(){return s.now()}}else{var o=Date,l=o.now();t.unstable_now=function(){return o.now()-l}}var c=[],d=[],u=1,m=null,p=3,f=!1,g=!1,w=!1,b=!1,x=typeof setTimeout=="function"?setTimeout:null,S=typeof clearTimeout=="function"?clearTimeout:null,N=typeof setImmediate<"u"?setImmediate:null;function C(ae){for(var Z=a(d);Z!==null;){if(Z.callback===null)n(d);else if(Z.startTime<=ae)n(d),Z.sortIndex=Z.expirationTime,e(c,Z);else break;Z=a(d)}}function P(ae){if(w=!1,C(ae),!g)if(a(c)!==null)g=!0,T||(T=!0,I());else{var Z=a(d);Z!==null&&Y(P,Z.startTime-ae)}}var T=!1,j=-1,F=5,k=-1;function E(){return b?!0:!(t.unstable_now()-kae&&E());){var ue=m.callback;if(typeof ue=="function"){m.callback=null,p=m.priorityLevel;var pe=ue(m.expirationTime<=ae);if(ae=t.unstable_now(),typeof pe=="function"){m.callback=pe,C(ae),Z=!0;break t}m===a(c)&&n(c),C(ae)}else n(c);m=a(c)}if(m!==null)Z=!0;else{var X=a(d);X!==null&&Y(P,X.startTime-ae),Z=!1}}break e}finally{m=null,p=ce,f=!1}Z=void 0}}finally{Z?I():T=!1}}}var I;if(typeof N=="function")I=function(){N(U)};else if(typeof MessageChannel<"u"){var O=new MessageChannel,L=O.port2;O.port1.onmessage=U,I=function(){L.postMessage(null)}}else I=function(){x(U,0)};function Y(ae,Z){j=x(function(){ae(t.unstable_now())},Z)}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(ae){ae.callback=null},t.unstable_forceFrameRate=function(ae){0>ae||125ue?(ae.sortIndex=ce,e(d,ae),a(c)===null&&ae===a(d)&&(w?(S(j),j=-1):w=!0,Y(P,ce-ue))):(ae.sortIndex=pe,e(c,ae),g||f||(g=!0,T||(T=!0,I()))),ae},t.unstable_shouldYield=E,t.unstable_wrapCallback=function(ae){var Z=p;return function(){var ce=p;p=Z;try{return ae.apply(this,arguments)}finally{p=ce}}}})(iE)),iE}var AH;function ime(){return AH||(AH=1,nE.exports=nme()),nE.exports}var sE={exports:{}},Gs={};var jH;function sme(){if(jH)return Gs;jH=1;var t=sb();function e(c){var d="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(),sE.exports=sme(),sE.exports}var EH;function ome(){if(EH)return a0;EH=1;var t=ime(),e=sb(),a=KX();function n(h){var v="https://react.dev/errors/"+h;if(1pe||(h.current=ue[pe],ue[pe]=null,pe--)}function me(h,v){pe++,ue[pe]=h.current,h.current=v}var de=X(null),we=X(null),H=X(null),J=X(null);function A(h,v){switch(me(H,v),me(we,h),me(de,null),v.nodeType){case 9:case 11:h=(h=v.documentElement)&&(h=h.namespaceURI)?Wq(h):0;break;default:if(h=v.tagName,v=v.namespaceURI)v=Wq(v),h=Kq(v,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}le(de),me(de,h)}function M(){le(de),le(we),le(H)}function q(h){h.memoizedState!==null&&me(J,h);var v=de.current,_=Kq(v,h.type);v!==_&&(me(we,h),me(de,_))}function W(h){we.current===h&&(le(de),le(we)),J.current===h&&(le(J),Xv._currentValue=ce)}var z,ie;function B(h){if(z===void 0)try{throw Error()}catch(_){var v=_.stack.trim().match(/\n( *(at )?)/);z=v&&v[1]||"",ie=-1<_.stack.indexOf(` +function tme(t,e){for(var a=0;an[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"]'))n(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function a(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function n(i){if(i.ep)return;i.ep=!0;const s=a(i);fetch(i.href,s)}})();var P_=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Ec(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var aE={exports:{}},t0={};var NH;function ame(){if(NH)return t0;NH=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function a(n,i,s){var o=null;if(s!==void 0&&(o=""+s),i.key!==void 0&&(o=""+i.key),"key"in i){s={};for(var l in i)l!=="key"&&(s[l]=i[l])}else s=i;return i=s.ref,{$$typeof:t,type:n,key:o,ref:i!==void 0?i:null,props:s}}return t0.Fragment=e,t0.jsx=a,t0.jsxs=a,t0}var CH;function rme(){return CH||(CH=1,aE.exports=ame()),aE.exports}var r=rme(),rE={exports:{}},Ka={};var PH;function nme(){if(PH)return Ka;PH=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),a=Symbol.for("react.fragment"),n=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),s=Symbol.for("react.consumer"),o=Symbol.for("react.context"),l=Symbol.for("react.forward_ref"),c=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),u=Symbol.for("react.lazy"),m=Symbol.for("react.activity"),p=Symbol.iterator;function f(Z){return Z===null||typeof Z!="object"?null:(Z=p&&Z[p]||Z["@@iterator"],typeof Z=="function"?Z:null)}var g={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},w=Object.assign,b={};function x(Z,ce,he){this.props=Z,this.context=ce,this.refs=b,this.updater=he||g}x.prototype.isReactComponent={},x.prototype.setState=function(Z,ce){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,ce,"setState")},x.prototype.forceUpdate=function(Z){this.updater.enqueueForceUpdate(this,Z,"forceUpdate")};function S(){}S.prototype=x.prototype;function N(Z,ce,he){this.props=Z,this.context=ce,this.refs=b,this.updater=he||g}var C=N.prototype=new S;C.constructor=N,w(C,x.prototype),C.isPureReactComponent=!0;var P=Array.isArray;function T(){}var j={H:null,A:null,T:null,S:null},F=Object.prototype.hasOwnProperty;function k(Z,ce,he){var ue=he.ref;return{$$typeof:t,type:Z,key:ce,ref:ue!==void 0?ue:null,props:he}}function M(Z,ce){return k(Z.type,ce,Z.props)}function q(Z){return typeof Z=="object"&&Z!==null&&Z.$$typeof===t}function I(Z){var ce={"=":"=0",":":"=2"};return"$"+Z.replace(/[=:]/g,function(he){return ce[he]})}var O=/\/+/g;function L(Z,ce){return typeof Z=="object"&&Z!==null&&Z.key!=null?I(""+Z.key):ce.toString(36)}function X(Z){switch(Z.status){case"fulfilled":return Z.value;case"rejected":throw Z.reason;default:switch(typeof Z.status=="string"?Z.then(T,T):(Z.status="pending",Z.then(function(ce){Z.status==="pending"&&(Z.status="fulfilled",Z.value=ce)},function(ce){Z.status==="pending"&&(Z.status="rejected",Z.reason=ce)})),Z.status){case"fulfilled":return Z.value;case"rejected":throw Z.reason}}throw Z}function ee(Z,ce,he,ue,xe){var H=typeof Z;(H==="undefined"||H==="boolean")&&(Z=null);var J=!1;if(Z===null)J=!0;else switch(H){case"bigint":case"string":case"number":J=!0;break;case"object":switch(Z.$$typeof){case t:case e:J=!0;break;case u:return J=Z._init,ee(J(Z._payload),ce,he,ue,xe)}}if(J)return xe=xe(Z),J=ue===""?"."+L(Z,0):ue,P(xe)?(he="",J!=null&&(he=J.replace(O,"$&/")+"/"),ee(xe,ce,he,"",function(B){return B})):xe!=null&&(q(xe)&&(xe=M(xe,he+(xe.key==null||Z&&Z.key===xe.key?"":(""+xe.key).replace(O,"$&/")+"/")+J)),ce.push(xe)),1;J=0;var A=ue===""?".":ue+":";if(P(Z))for(var E=0;E>>1,me=ee[de];if(0>>1;dei(he,le))uei(xe,he)?(ee[de]=xe,ee[ue]=le,de=ue):(ee[de]=he,ee[ce]=le,de=ce);else if(uei(xe,le))ee[de]=xe,ee[ue]=le,de=ue;else break e}}return Q}function i(ee,Q){var le=ee.sortIndex-Q.sortIndex;return le!==0?le:ee.id-Q.id}if(t.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var s=performance;t.unstable_now=function(){return s.now()}}else{var o=Date,l=o.now();t.unstable_now=function(){return o.now()-l}}var c=[],d=[],u=1,m=null,p=3,f=!1,g=!1,w=!1,b=!1,x=typeof setTimeout=="function"?setTimeout:null,S=typeof clearTimeout=="function"?clearTimeout:null,N=typeof setImmediate<"u"?setImmediate:null;function C(ee){for(var Q=a(d);Q!==null;){if(Q.callback===null)n(d);else if(Q.startTime<=ee)n(d),Q.sortIndex=Q.expirationTime,e(c,Q);else break;Q=a(d)}}function P(ee){if(w=!1,C(ee),!g)if(a(c)!==null)g=!0,T||(T=!0,I());else{var Q=a(d);Q!==null&&X(P,Q.startTime-ee)}}var T=!1,j=-1,F=5,k=-1;function M(){return b?!0:!(t.unstable_now()-kee&&M());){var de=m.callback;if(typeof de=="function"){m.callback=null,p=m.priorityLevel;var me=de(m.expirationTime<=ee);if(ee=t.unstable_now(),typeof me=="function"){m.callback=me,C(ee),Q=!0;break t}m===a(c)&&n(c),C(ee)}else n(c);m=a(c)}if(m!==null)Q=!0;else{var Z=a(d);Z!==null&&X(P,Z.startTime-ee),Q=!1}}break e}finally{m=null,p=le,f=!1}Q=void 0}}finally{Q?I():T=!1}}}var I;if(typeof N=="function")I=function(){N(q)};else if(typeof MessageChannel<"u"){var O=new MessageChannel,L=O.port2;O.port1.onmessage=q,I=function(){L.postMessage(null)}}else I=function(){x(q,0)};function X(ee,Q){j=x(function(){ee(t.unstable_now())},Q)}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(ee){ee.callback=null},t.unstable_forceFrameRate=function(ee){0>ee||125de?(ee.sortIndex=le,e(d,ee),a(c)===null&&ee===a(d)&&(w?(S(j),j=-1):w=!0,X(P,le-de))):(ee.sortIndex=me,e(c,ee),g||f||(g=!0,T||(T=!0,I()))),ee},t.unstable_shouldYield=M,t.unstable_wrapCallback=function(ee){var Q=p;return function(){var le=p;p=Q;try{return ee.apply(this,arguments)}finally{p=le}}}})(sE)),sE}var jH;function ome(){return jH||(jH=1,iE.exports=sme()),iE.exports}var oE={exports:{}},Gs={};var MH;function lme(){if(MH)return Gs;MH=1;var t=ob();function e(c){var d="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(),oE.exports=lme(),oE.exports}var DH;function cme(){if(DH)return a0;DH=1;var t=ome(),e=ob(),a=YX();function n(h){var v="https://react.dev/errors/"+h;if(1me||(h.current=de[me],de[me]=null,me--)}function he(h,v){me++,de[me]=h.current,h.current=v}var ue=Z(null),xe=Z(null),H=Z(null),J=Z(null);function A(h,v){switch(he(H,v),he(xe,h),he(ue,null),v.nodeType){case 9:case 11:h=(h=v.documentElement)&&(h=h.namespaceURI)?Kq(h):0;break;default:if(h=v.tagName,v=v.namespaceURI)v=Kq(v),h=Qq(v,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}ce(ue),he(ue,h)}function E(){ce(ue),ce(xe),ce(H)}function B(h){h.memoizedState!==null&&he(J,h);var v=ue.current,_=Qq(v,h.type);v!==_&&(he(xe,h),he(ue,_))}function W(h){xe.current===h&&(ce(ue),ce(xe)),J.current===h&&(ce(J),Xv._currentValue=le)}var z,ie;function U(h){if(z===void 0)try{throw Error()}catch(_){var v=_.stack.trim().match(/\n( *(at )?)/);z=v&&v[1]||"",ie=-1<_.stack.indexOf(` at`)?" ()":-1<_.stack.indexOf("@")?"@unknown:0:0":""}return` -`+z+h+ie}var G=!1;function K(h,v){if(!h||G)return"";G=!0;var _=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var D={DetermineComponentFrameRoot:function(){try{if(v){var Wt=function(){throw Error()};if(Object.defineProperty(Wt.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(Wt,[])}catch(zt){var Lt=zt}Reflect.construct(h,[],Wt)}else{try{Wt.call()}catch(zt){Lt=zt}h.call(Wt.prototype)}}else{try{throw Error()}catch(zt){Lt=zt}(Wt=h())&&typeof Wt.catch=="function"&&Wt.catch(function(){})}}catch(zt){if(zt&&Lt&&typeof zt.stack=="string")return[zt.stack,Lt.stack]}return[null,null]}};D.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var V=Object.getOwnPropertyDescriptor(D.DetermineComponentFrameRoot,"name");V&&V.configurable&&Object.defineProperty(D.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var ee=D.DetermineComponentFrameRoot(),Se=ee[0],He=ee[1];if(Se&&He){var mt=Se.split(` +`+z+h+ie}var G=!1;function K(h,v){if(!h||G)return"";G=!0;var _=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var D={DetermineComponentFrameRoot:function(){try{if(v){var Wt=function(){throw Error()};if(Object.defineProperty(Wt.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(Wt,[])}catch(zt){var Lt=zt}Reflect.construct(h,[],Wt)}else{try{Wt.call()}catch(zt){Lt=zt}h.call(Wt.prototype)}}else{try{throw Error()}catch(zt){Lt=zt}(Wt=h())&&typeof Wt.catch=="function"&&Wt.catch(function(){})}}catch(zt){if(zt&&Lt&&typeof zt.stack=="string")return[zt.stack,Lt.stack]}return[null,null]}};D.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var V=Object.getOwnPropertyDescriptor(D.DetermineComponentFrameRoot,"name");V&&V.configurable&&Object.defineProperty(D.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var te=D.DetermineComponentFrameRoot(),Se=te[0],He=te[1];if(Se&&He){var mt=Se.split(` `),Et=He.split(` `);for(V=D=0;DV||mt[D]!==Et[V]){var qt=` -`+mt[D].replace(" at new "," at ");return h.displayName&&qt.includes("")&&(qt=qt.replace("",h.displayName)),qt}while(1<=D&&0<=V);break}}}finally{G=!1,Error.prepareStackTrace=_}return(_=h?h.displayName||h.name:"")?B(_):""}function Q(h,v){switch(h.tag){case 26:case 27:case 5:return B(h.type);case 16:return B("Lazy");case 13:return h.child!==v&&v!==null?B("Suspense Fallback"):B("Suspense");case 19:return B("SuspenseList");case 0:case 15:return K(h.type,!1);case 11:return K(h.type.render,!1);case 1:return K(h.type,!0);case 31:return B("Activity");default:return""}}function se(h){try{var v="",_=null;do v+=Q(h,_),_=h,h=h.return;while(h);return v}catch(D){return` +`+mt[D].replace(" at new "," at ");return h.displayName&&qt.includes("")&&(qt=qt.replace("",h.displayName)),qt}while(1<=D&&0<=V);break}}}finally{G=!1,Error.prepareStackTrace=_}return(_=h?h.displayName||h.name:"")?U(_):""}function Y(h,v){switch(h.tag){case 26:case 27:case 5:return U(h.type);case 16:return U("Lazy");case 13:return h.child!==v&&v!==null?U("Suspense Fallback"):U("Suspense");case 19:return U("SuspenseList");case 0:case 15:return K(h.type,!1);case 11:return K(h.type.render,!1);case 1:return K(h.type,!0);case 31:return U("Activity");default:return""}}function se(h){try{var v="",_=null;do v+=Y(h,_),_=h,h=h.return;while(h);return v}catch(D){return` Error generating stack: `+D.message+` -`+D.stack}}var oe=Object.prototype.hasOwnProperty,fe=t.unstable_scheduleCallback,te=t.unstable_cancelCallback,he=t.unstable_shouldYield,$=t.unstable_requestPaint,R=t.unstable_now,re=t.unstable_getCurrentPriorityLevel,be=t.unstable_ImmediatePriority,ye=t.unstable_UserBlockingPriority,ke=t.unstable_NormalPriority,xe=t.unstable_LowPriority,ge=t.unstable_IdlePriority,Me=t.log,Be=t.unstable_setDisableYieldValue,Le=null,Oe=null;function Re(h){if(typeof Me=="function"&&Be(h),Oe&&typeof Oe.setStrictMode=="function")try{Oe.setStrictMode(Le,h)}catch{}}var Ke=Math.clz32?Math.clz32:Ee,Ue=Math.log,lt=Math.LN2;function Ee(h){return h>>>=0,h===0?32:31-(Ue(h)/lt|0)|0}var st=256,Ne=262144,Ze=4194304;function Ye(h){var v=h&42;if(v!==0)return v;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,v,_){var D=h.pendingLanes;if(D===0)return 0;var V=0,ee=h.suspendedLanes,Se=h.pingedLanes;h=h.warmLanes;var He=D&134217727;return He!==0?(D=He&~ee,D!==0?V=Ye(D):(Se&=He,Se!==0?V=Ye(Se):_||(_=He&~h,_!==0&&(V=Ye(_))))):(He=D&~ee,He!==0?V=Ye(He):Se!==0?V=Ye(Se):_||(_=D&~h,_!==0&&(V=Ye(_)))),V===0?0:v!==0&&v!==V&&(v&ee)===0&&(ee=V&-V,_=v&-v,ee>=_||ee===32&&(_&4194048)!==0)?v:V}function Fe(h,v){return(h.pendingLanes&~(h.suspendedLanes&~h.pingedLanes)&v)===0}function et(h,v){switch(h){case 1:case 2:case 4:case 8:case 64:return v+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 v+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 We(){var h=Ze;return Ze<<=1,(Ze&62914560)===0&&(Ze=4194304),h}function Pe(h){for(var v=[],_=0;31>_;_++)v.push(h);return v}function at(h,v){h.pendingLanes|=v,v!==268435456&&(h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0)}function nt(h,v,_,D,V,ee){var Se=h.pendingLanes;h.pendingLanes=_,h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0,h.expiredLanes&=_,h.entangledLanes&=_,h.errorRecoveryDisabledLanes&=_,h.shellSuspendCounter=0;var He=h.entanglements,mt=h.expirationTimes,Et=h.hiddenUpdates;for(_=Se&~_;0<_;){var qt=31-Ke(_),Wt=1<"u")return null;try{return h.activeElement||h.body}catch{return h.body}}var Rr=/[\n"\\]/g;function $a(h){return h.replace(Rr,function(v){return"\\"+v.charCodeAt(0).toString(16)+" "})}function Un(h,v,_,D,V,ee,Se,He){h.name="",Se!=null&&typeof Se!="function"&&typeof Se!="symbol"&&typeof Se!="boolean"?h.type=Se:h.removeAttribute("type"),v!=null?Se==="number"?(v===0&&h.value===""||h.value!=v)&&(h.value=""+ca(v)):h.value!==""+ca(v)&&(h.value=""+ca(v)):Se!=="submit"&&Se!=="reset"||h.removeAttribute("value"),v!=null?$r(h,Se,ca(v)):_!=null?$r(h,Se,ca(_)):D!=null&&h.removeAttribute("value"),V==null&&ee!=null&&(h.defaultChecked=!!ee),V!=null&&(h.checked=V&&typeof V!="function"&&typeof V!="symbol"),He!=null&&typeof He!="function"&&typeof He!="symbol"&&typeof He!="boolean"?h.name=""+ca(He):h.removeAttribute("name")}function Ra(h,v,_,D,V,ee,Se,He){if(ee!=null&&typeof ee!="function"&&typeof ee!="symbol"&&typeof ee!="boolean"&&(h.type=ee),v!=null||_!=null){if(!(ee!=="submit"&&ee!=="reset"||v!=null)){pa(h);return}_=_!=null?""+ca(_):"",v=v!=null?""+ca(v):_,He||v===h.value||(h.value=v),h.defaultValue=v}D=D??V,D=typeof D!="function"&&typeof D!="symbol"&&!!D,h.checked=He?h.checked:!!D,h.defaultChecked=!!D,Se!=null&&typeof Se!="function"&&typeof Se!="symbol"&&typeof Se!="boolean"&&(h.name=Se),pa(h)}function $r(h,v,_){v==="number"&&tr(h.ownerDocument)===h||h.defaultValue===""+_||(h.defaultValue=""+_)}function bn(h,v,_,D){if(h=h.options,v){v={};for(var V=0;V<_.length;V++)v["$"+_[V]]=!0;for(_=0;_"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),$s=!1;if(vi)try{var Au={};Object.defineProperty(Au,"passive",{get:function(){$s=!0}}),window.addEventListener("test",Au,Au),window.removeEventListener("test",Au,Au)}catch{$s=!1}var Kn=null,Sd=null,Rc=null;function wi(){if(Rc)return Rc;var h,v=Sd,_=v.length,D,V="value"in Kn?Kn.value:Kn.textContent,ee=V.length;for(h=0;h<_&&v[h]===V[h];h++);var Se=_-h;for(D=1;D<=Se&&v[_-D]===V[ee-D];D++);return Rc=V.slice(h,1=Qn),ve=" ",Ut=!1;function sa(h,v){switch(h){case"keyup":return wf.indexOf(v.keyCode)!==-1;case"keydown":return v.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Ta(h){return h=h.detail,typeof h=="object"&&"data"in h?h.data:null}var Ia=!1;function kn(h,v){switch(h){case"compositionend":return Ta(v);case"keypress":return v.which!==32?null:(Ut=!0,ve);case"textInput":return h=v.data,h===ve&&Ut?null:h;default:return null}}function Si(h,v){if(Ia)return h==="compositionend"||!cl&&sa(h,v)?(h=wi(),Rc=Sd=Kn=null,Ia=!1,h):null;switch(h){case"paste":return null;case"keypress":if(!(v.ctrlKey||v.altKey||v.metaKey)||v.ctrlKey&&v.altKey){if(v.char&&1=v)return{node:_,offset:v-h};h=D}e:{for(;_;){if(_.nextSibling){_=_.nextSibling;break e}_=_.parentNode}_=void 0}_=Ro(_)}}function Io(h,v){return h&&v?h===v?!0:h&&h.nodeType===3?!1:v&&v.nodeType===3?Io(h,v.parentNode):"contains"in h?h.contains(v):h.compareDocumentPosition?!!(h.compareDocumentPosition(v)&16):!1:!1}function ho(h){h=h!=null&&h.ownerDocument!=null&&h.ownerDocument.defaultView!=null?h.ownerDocument.defaultView:window;for(var v=tr(h.document);v instanceof h.HTMLIFrameElement;){try{var _=typeof v.contentWindow.location.href=="string"}catch{_=!1}if(_)h=v.contentWindow;else break;v=tr(h.document)}return v}function ki(h){var v=h&&h.nodeName&&h.nodeName.toLowerCase();return v&&(v==="input"&&(h.type==="text"||h.type==="search"||h.type==="tel"||h.type==="url"||h.type==="password")||v==="textarea"||h.contentEditable==="true")}var Cd=vi&&"documentMode"in document&&11>=document.documentMode,pi=null,ur=null,Ar=null,Xn=!1;function bp(h,v,_){var D=_.window===_?_.document:_.nodeType===9?_:_.ownerDocument;Xn||pi==null||pi!==tr(D)||(D=pi,"selectionStart"in D&&ki(D)?D={start:D.selectionStart,end:D.selectionEnd}:(D=(D.ownerDocument&&D.ownerDocument.defaultView||window).getSelection(),D={anchorNode:D.anchorNode,anchorOffset:D.anchorOffset,focusNode:D.focusNode,focusOffset:D.focusOffset}),Ar&&_s(Ar,D)||(Ar=D,D=m_(ur,"onSelect"),0>=Se,V-=Se,Td=1<<32-Ke(v)+V|_<Za?(Sr=Ca,Ca=null):Sr=Ca.sibling;var Mr=Lt(St,Ca,jt[Za],Vt);if(Mr===null){Ca===null&&(Ca=Sr);break}h&&Ca&&Mr.alternate===null&&v(St,Ca),xt=ee(Mr,xt,Za),jr===null?Ea=Mr:jr.sibling=Mr,jr=Mr,Ca=Sr}if(Za===jt.length)return _(St,Ca),_r&&Ou(St,Za),Ea;if(Ca===null){for(;ZaZa?(Sr=Ca,Ca=null):Sr=Ca.sibling;var zp=Lt(St,Ca,Mr.value,Vt);if(zp===null){Ca===null&&(Ca=Sr);break}h&&Ca&&zp.alternate===null&&v(St,Ca),xt=ee(zp,xt,Za),jr===null?Ea=zp:jr.sibling=zp,jr=zp,Ca=Sr}if(Mr.done)return _(St,Ca),_r&&Ou(St,Za),Ea;if(Ca===null){for(;!Mr.done;Za++,Mr=jt.next())Mr=Wt(St,Mr.value,Vt),Mr!==null&&(xt=ee(Mr,xt,Za),jr===null?Ea=Mr:jr.sibling=Mr,jr=Mr);return _r&&Ou(St,Za),Ea}for(Ca=D(Ca);!Mr.done;Za++,Mr=jt.next())Mr=zt(Ca,St,Za,Mr.value,Vt),Mr!==null&&(h&&Mr.alternate!==null&&Ca.delete(Mr.key===null?Za:Mr.key),xt=ee(Mr,xt,Za),jr===null?Ea=Mr:jr.sibling=Mr,jr=Mr);return h&&Ca.forEach(function(Zue){return v(St,Zue)}),_r&&Ou(St,Za),Ea}function tn(St,xt,jt,Vt){if(typeof jt=="object"&&jt!==null&&jt.type===w&&jt.key===null&&(jt=jt.props.children),typeof jt=="object"&&jt!==null){switch(jt.$$typeof){case f:e:{for(var Ea=jt.key;xt!==null;){if(xt.key===Ea){if(Ea=jt.type,Ea===w){if(xt.tag===7){_(St,xt.sibling),Vt=V(xt,jt.props.children),Vt.return=St,St=Vt;break e}}else if(xt.elementType===Ea||typeof Ea=="object"&&Ea!==null&&Ea.$$typeof===F&&Af(Ea)===xt.type){_(St,xt.sibling),Vt=V(xt,jt.props),Tv(Vt,jt),Vt.return=St,St=Vt;break e}_(St,xt);break}else v(St,xt);xt=xt.sibling}jt.type===w?(Vt=_f(jt.props.children,St.mode,Vt,jt.key),Vt.return=St,St=Vt):(Vt=Ak(jt.type,jt.key,jt.props,null,St.mode,Vt),Tv(Vt,jt),Vt.return=St,St=Vt)}return Se(St);case g:e:{for(Ea=jt.key;xt!==null;){if(xt.key===Ea)if(xt.tag===4&&xt.stateNode.containerInfo===jt.containerInfo&&xt.stateNode.implementation===jt.implementation){_(St,xt.sibling),Vt=V(xt,jt.children||[]),Vt.return=St,St=Vt;break e}else{_(St,xt);break}else v(St,xt);xt=xt.sibling}Vt=yj(jt,St.mode,Vt),Vt.return=St,St=Vt}return Se(St);case F:return jt=Af(jt),tn(St,xt,jt,Vt)}if(Y(jt))return va(St,xt,jt,Vt);if(I(jt)){if(Ea=I(jt),typeof Ea!="function")throw Error(n(150));return jt=Ea.call(jt),za(St,xt,jt,Vt)}if(typeof jt.then=="function")return tn(St,xt,Rk(jt),Vt);if(jt.$$typeof===N)return tn(St,xt,Ek(St,jt),Vt);Ik(St,jt)}return typeof jt=="string"&&jt!==""||typeof jt=="number"||typeof jt=="bigint"?(jt=""+jt,xt!==null&&xt.tag===6?(_(St,xt.sibling),Vt=V(xt,jt),Vt.return=St,St=Vt):(_(St,xt),Vt=xj(jt,St.mode,Vt),Vt.return=St,St=Vt),Se(St)):_(St,xt)}return function(St,xt,jt,Vt){try{Pv=0;var Ea=tn(St,xt,jt,Vt);return Eb=null,Ea}catch(Ca){if(Ca===Mb||Ca===Fk)throw Ca;var jr=dl(29,Ca,null,St.mode);return jr.lanes=Vt,jr.return=St,jr}}}var Mf=OU(!0),zU=OU(!1),Sp=!1;function Mj(h){h.updateQueue={baseState:h.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Ej(h,v){h=h.updateQueue,v.updateQueue===h&&(v.updateQueue={baseState:h.baseState,firstBaseUpdate:h.firstBaseUpdate,lastBaseUpdate:h.lastBaseUpdate,shared:h.shared,callbacks:null})}function kp(h){return{lane:h,tag:0,payload:null,callback:null,next:null}}function _p(h,v,_){var D=h.updateQueue;if(D===null)return null;if(D=D.shared,(zr&2)!==0){var V=D.pending;return V===null?v.next=v:(v.next=V.next,V.next=v),D.pending=v,v=Tk(h),SU(h,null,_),v}return Pk(h,D,v,_),Tk(h)}function Av(h,v,_){if(v=v.updateQueue,v!==null&&(v=v.shared,(_&4194048)!==0)){var D=v.lanes;D&=h.pendingLanes,_|=D,v.lanes=_,Bt(h,_)}}function Dj(h,v){var _=h.updateQueue,D=h.alternate;if(D!==null&&(D=D.updateQueue,_===D)){var V=null,ee=null;if(_=_.firstBaseUpdate,_!==null){do{var Se={lane:_.lane,tag:_.tag,payload:_.payload,callback:null,next:null};ee===null?V=ee=Se:ee=ee.next=Se,_=_.next}while(_!==null);ee===null?V=ee=v:ee=ee.next=v}else V=ee=v;_={baseState:D.baseState,firstBaseUpdate:V,lastBaseUpdate:ee,shared:D.shared,callbacks:D.callbacks},h.updateQueue=_;return}h=_.lastBaseUpdate,h===null?_.firstBaseUpdate=v:h.next=v,_.lastBaseUpdate=v}var Fj=!1;function jv(){if(Fj){var h=jb;if(h!==null)throw h}}function Mv(h,v,_,D){Fj=!1;var V=h.updateQueue;Sp=!1;var ee=V.firstBaseUpdate,Se=V.lastBaseUpdate,He=V.shared.pending;if(He!==null){V.shared.pending=null;var mt=He,Et=mt.next;mt.next=null,Se===null?ee=Et:Se.next=Et,Se=mt;var qt=h.alternate;qt!==null&&(qt=qt.updateQueue,He=qt.lastBaseUpdate,He!==Se&&(He===null?qt.firstBaseUpdate=Et:He.next=Et,qt.lastBaseUpdate=mt))}if(ee!==null){var Wt=V.baseState;Se=0,qt=Et=mt=null,He=ee;do{var Lt=He.lane&-536870913,zt=Lt!==He.lane;if(zt?(wr&Lt)===Lt:(D&Lt)===Lt){Lt!==0&&Lt===Ab&&(Fj=!0),qt!==null&&(qt=qt.next={lane:0,tag:He.tag,payload:He.payload,callback:null,next:null});e:{var va=h,za=He;Lt=v;var tn=_;switch(za.tag){case 1:if(va=za.payload,typeof va=="function"){Wt=va.call(tn,Wt,Lt);break e}Wt=va;break e;case 3:va.flags=va.flags&-65537|128;case 0:if(va=za.payload,Lt=typeof va=="function"?va.call(tn,Wt,Lt):va,Lt==null)break e;Wt=m({},Wt,Lt);break e;case 2:Sp=!0}}Lt=He.callback,Lt!==null&&(h.flags|=64,zt&&(h.flags|=8192),zt=V.callbacks,zt===null?V.callbacks=[Lt]:zt.push(Lt))}else zt={lane:Lt,tag:He.tag,payload:He.payload,callback:He.callback,next:null},qt===null?(Et=qt=zt,mt=Wt):qt=qt.next=zt,Se|=Lt;if(He=He.next,He===null){if(He=V.shared.pending,He===null)break;zt=He,He=zt.next,zt.next=null,V.lastBaseUpdate=zt,V.shared.pending=null}}while(!0);qt===null&&(mt=Wt),V.baseState=mt,V.firstBaseUpdate=Et,V.lastBaseUpdate=qt,ee===null&&(V.shared.lanes=0),Ap|=Se,h.lanes=Se,h.memoizedState=Wt}}function UU(h,v){if(typeof h!="function")throw Error(n(191,h));h.call(v)}function BU(h,v){var _=h.callbacks;if(_!==null)for(h.callbacks=null,h=0;h<_.length;h++)UU(_[h],v)}var Db=X(null),Ok=X(0);function qU(h,v){h=Qu,me(Ok,h),me(Db,v),Qu=h|v.baseLanes}function Lj(){me(Ok,Qu),me(Db,Db.current)}function Rj(){Qu=Ok.current,le(Db),le(Ok)}var ul=X(null),Gl=null;function Np(h){var v=h.alternate;me(hi,hi.current&1),me(ul,h),Gl===null&&(v===null||Db.current!==null||v.memoizedState!==null)&&(Gl=h)}function Ij(h){me(hi,hi.current),me(ul,h),Gl===null&&(Gl=h)}function HU(h){h.tag===22?(me(hi,hi.current),me(ul,h),Gl===null&&(Gl=h)):Cp()}function Cp(){me(hi,hi.current),me(ul,ul.current)}function ml(h){le(ul),Gl===h&&(Gl=null),le(hi)}var hi=X(0);function zk(h){for(var v=h;v!==null;){if(v.tag===13){var _=v.memoizedState;if(_!==null&&(_=_.dehydrated,_===null||HM(_)||$M(_)))return v}else if(v.tag===19&&(v.memoizedProps.revealOrder==="forwards"||v.memoizedProps.revealOrder==="backwards"||v.memoizedProps.revealOrder==="unstable_legacy-backwards"||v.memoizedProps.revealOrder==="together")){if((v.flags&128)!==0)return v}else if(v.child!==null){v.child.return=v,v=v.child;continue}if(v===h)break;for(;v.sibling===null;){if(v.return===null||v.return===h)return null;v=v.return}v.sibling.return=v.return,v=v.sibling}return null}var Bu=0,Ya=null,Jr=null,Ni=null,Uk=!1,Fb=!1,Ef=!1,Bk=0,Ev=0,Lb=null,qde=0;function Zn(){throw Error(n(321))}function Oj(h,v){if(v===null)return!1;for(var _=0;_ee?ee:8;var Se=ae.T,He={};ae.T=He,Jj(h,!1,v,_);try{var mt=V(),Et=ae.S;if(Et!==null&&Et(He,mt),mt!==null&&typeof mt=="object"&&typeof mt.then=="function"){var qt=Bde(mt,D);Fv(h,v,qt,fl(h))}else Fv(h,v,D,fl(h))}catch(Wt){Fv(h,v,{then:function(){},status:"rejected",reason:Wt},fl())}finally{Z.p=ee,Se!==null&&He.types!==null&&(Se.types=He.types),ae.T=Se}}function Wde(){}function Xj(h,v,_,D){if(h.tag!==5)throw Error(n(476));var V=yB(h).queue;xB(h,V,v,ce,_===null?Wde:function(){return vB(h),_(D)})}function yB(h){var v=h.memoizedState;if(v!==null)return v;v={memoizedState:ce,baseState:ce,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:qu,lastRenderedState:ce},next:null};var _={};return v.next={memoizedState:_,baseState:_,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:qu,lastRenderedState:_},next:null},h.memoizedState=v,h=h.alternate,h!==null&&(h.memoizedState=v),v}function vB(h){var v=yB(h);v.next===null&&(v=h.alternate.memoizedState),Fv(h,v.next.queue,{},fl())}function Zj(){return Cs(Xv)}function wB(){return fi().memoizedState}function SB(){return fi().memoizedState}function Kde(h){for(var v=h.return;v!==null;){switch(v.tag){case 24:case 3:var _=fl();h=kp(_);var D=_p(v,h,_);D!==null&&(qo(D,v,_),Av(D,v,_)),v={cache:Pj()},h.payload=v;return}v=v.return}}function Qde(h,v,_){var D=fl();_={lane:D,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null},Wk(h)?_B(v,_):(_=gj(h,v,_,D),_!==null&&(qo(_,h,D),NB(_,v,D)))}function kB(h,v,_){var D=fl();Fv(h,v,_,D)}function Fv(h,v,_,D){var V={lane:D,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null};if(Wk(h))_B(v,V);else{var ee=h.alternate;if(h.lanes===0&&(ee===null||ee.lanes===0)&&(ee=v.lastRenderedReducer,ee!==null))try{var Se=v.lastRenderedState,He=ee(Se,_);if(V.hasEagerState=!0,V.eagerState=He,Mn(He,Se))return Pk(h,v,V,0),mn===null&&Ck(),!1}catch{}if(_=gj(h,v,V,D),_!==null)return qo(_,h,D),NB(_,v,D),!0}return!1}function Jj(h,v,_,D){if(D={lane:2,revertLane:MM(),gesture:null,action:D,hasEagerState:!1,eagerState:null,next:null},Wk(h)){if(v)throw Error(n(479))}else v=gj(h,_,D,2),v!==null&&qo(v,h,2)}function Wk(h){var v=h.alternate;return h===Ya||v!==null&&v===Ya}function _B(h,v){Fb=Uk=!0;var _=h.pending;_===null?v.next=v:(v.next=_.next,_.next=v),h.pending=v}function NB(h,v,_){if((_&4194048)!==0){var D=v.lanes;D&=h.pendingLanes,_|=D,v.lanes=_,Bt(h,_)}}var Lv={readContext:Cs,use:Hk,useCallback:Zn,useContext:Zn,useEffect:Zn,useImperativeHandle:Zn,useLayoutEffect:Zn,useInsertionEffect:Zn,useMemo:Zn,useReducer:Zn,useRef:Zn,useState:Zn,useDebugValue:Zn,useDeferredValue:Zn,useTransition:Zn,useSyncExternalStore:Zn,useId:Zn,useHostTransitionStatus:Zn,useFormState:Zn,useActionState:Zn,useOptimistic:Zn,useMemoCache:Zn,useCacheRefresh:Zn};Lv.useEffectEvent=Zn;var CB={readContext:Cs,use:Hk,useCallback:function(h,v){return fo().memoizedState=[h,v===void 0?null:v],h},useContext:Cs,useEffect:cB,useImperativeHandle:function(h,v,_){_=_!=null?_.concat([h]):null,Vk(4194308,4,pB.bind(null,v,h),_)},useLayoutEffect:function(h,v){return Vk(4194308,4,h,v)},useInsertionEffect:function(h,v){Vk(4,2,h,v)},useMemo:function(h,v){var _=fo();v=v===void 0?null:v;var D=h();if(Ef){Re(!0);try{h()}finally{Re(!1)}}return _.memoizedState=[D,v],D},useReducer:function(h,v,_){var D=fo();if(_!==void 0){var V=_(v);if(Ef){Re(!0);try{_(v)}finally{Re(!1)}}}else V=v;return D.memoizedState=D.baseState=V,h={pending:null,lanes:0,dispatch:null,lastRenderedReducer:h,lastRenderedState:V},D.queue=h,h=h.dispatch=Qde.bind(null,Ya,h),[D.memoizedState,h]},useRef:function(h){var v=fo();return h={current:h},v.memoizedState=h},useState:function(h){h=Gj(h);var v=h.queue,_=kB.bind(null,Ya,v);return v.dispatch=_,[h.memoizedState,_]},useDebugValue:Qj,useDeferredValue:function(h,v){var _=fo();return Yj(_,h,v)},useTransition:function(){var h=Gj(!1);return h=xB.bind(null,Ya,h.queue,!0,!1),fo().memoizedState=h,[!1,h]},useSyncExternalStore:function(h,v,_){var D=Ya,V=fo();if(_r){if(_===void 0)throw Error(n(407));_=_()}else{if(_=v(),mn===null)throw Error(n(349));(wr&127)!==0||WU(D,v,_)}V.memoizedState=_;var ee={value:_,getSnapshot:v};return V.queue=ee,cB(QU.bind(null,D,ee,h),[h]),D.flags|=2048,Rb(9,{destroy:void 0},KU.bind(null,D,ee,_,v),null),_},useId:function(){var h=fo(),v=mn.identifierPrefix;if(_r){var _=Ad,D=Td;_=(D&~(1<<32-Ke(D)-1)).toString(32)+_,v="_"+v+"R_"+_,_=Bk++,0<_&&(v+="H"+_.toString(32)),v+="_"}else _=qde++,v="_"+v+"r_"+_.toString(32)+"_";return h.memoizedState=v},useHostTransitionStatus:Zj,useFormState:nB,useActionState:nB,useOptimistic:function(h){var v=fo();v.memoizedState=v.baseState=h;var _={pending:null,lanes:0,dispatch:null,lastRenderedReducer:null,lastRenderedState:null};return v.queue=_,v=Jj.bind(null,Ya,!0,_),_.dispatch=v,[h,v]},useMemoCache:Hj,useCacheRefresh:function(){return fo().memoizedState=Kde.bind(null,Ya)},useEffectEvent:function(h){var v=fo(),_={impl:h};return v.memoizedState=_,function(){if((zr&2)!==0)throw Error(n(440));return _.impl.apply(void 0,arguments)}}},eM={readContext:Cs,use:Hk,useCallback:fB,useContext:Cs,useEffect:Kj,useImperativeHandle:hB,useInsertionEffect:uB,useLayoutEffect:mB,useMemo:gB,useReducer:$k,useRef:lB,useState:function(){return $k(qu)},useDebugValue:Qj,useDeferredValue:function(h,v){var _=fi();return bB(_,Jr.memoizedState,h,v)},useTransition:function(){var h=$k(qu)[0],v=fi().memoizedState;return[typeof h=="boolean"?h:Dv(h),v]},useSyncExternalStore:GU,useId:wB,useHostTransitionStatus:Zj,useFormState:iB,useActionState:iB,useOptimistic:function(h,v){var _=fi();return ZU(_,Jr,h,v)},useMemoCache:Hj,useCacheRefresh:SB};eM.useEffectEvent=dB;var PB={readContext:Cs,use:Hk,useCallback:fB,useContext:Cs,useEffect:Kj,useImperativeHandle:hB,useInsertionEffect:uB,useLayoutEffect:mB,useMemo:gB,useReducer:Vj,useRef:lB,useState:function(){return Vj(qu)},useDebugValue:Qj,useDeferredValue:function(h,v){var _=fi();return Jr===null?Yj(_,h,v):bB(_,Jr.memoizedState,h,v)},useTransition:function(){var h=Vj(qu)[0],v=fi().memoizedState;return[typeof h=="boolean"?h:Dv(h),v]},useSyncExternalStore:GU,useId:wB,useHostTransitionStatus:Zj,useFormState:oB,useActionState:oB,useOptimistic:function(h,v){var _=fi();return Jr!==null?ZU(_,Jr,h,v):(_.baseState=h,[h,_.queue.dispatch])},useMemoCache:Hj,useCacheRefresh:SB};PB.useEffectEvent=dB;function tM(h,v,_,D){v=h.memoizedState,_=_(D,v),_=_==null?v:m({},v,_),h.memoizedState=_,h.lanes===0&&(h.updateQueue.baseState=_)}var aM={enqueueSetState:function(h,v,_){h=h._reactInternals;var D=fl(),V=kp(D);V.payload=v,_!=null&&(V.callback=_),v=_p(h,V,D),v!==null&&(qo(v,h,D),Av(v,h,D))},enqueueReplaceState:function(h,v,_){h=h._reactInternals;var D=fl(),V=kp(D);V.tag=1,V.payload=v,_!=null&&(V.callback=_),v=_p(h,V,D),v!==null&&(qo(v,h,D),Av(v,h,D))},enqueueForceUpdate:function(h,v){h=h._reactInternals;var _=fl(),D=kp(_);D.tag=2,v!=null&&(D.callback=v),v=_p(h,D,_),v!==null&&(qo(v,h,_),Av(v,h,_))}};function TB(h,v,_,D,V,ee,Se){return h=h.stateNode,typeof h.shouldComponentUpdate=="function"?h.shouldComponentUpdate(D,ee,Se):v.prototype&&v.prototype.isPureReactComponent?!_s(_,D)||!_s(V,ee):!0}function AB(h,v,_,D){h=v.state,typeof v.componentWillReceiveProps=="function"&&v.componentWillReceiveProps(_,D),typeof v.UNSAFE_componentWillReceiveProps=="function"&&v.UNSAFE_componentWillReceiveProps(_,D),v.state!==h&&aM.enqueueReplaceState(v,v.state,null)}function Df(h,v){var _=v;if("ref"in v){_={};for(var D in v)D!=="ref"&&(_[D]=v[D])}if(h=h.defaultProps){_===v&&(_=m({},_));for(var V in h)_[V]===void 0&&(_[V]=h[V])}return _}function jB(h){Nk(h)}function MB(h){console.error(h)}function EB(h){Nk(h)}function Kk(h,v){try{var _=h.onUncaughtError;_(v.value,{componentStack:v.stack})}catch(D){setTimeout(function(){throw D})}}function DB(h,v,_){try{var D=h.onCaughtError;D(_.value,{componentStack:_.stack,errorBoundary:v.tag===1?v.stateNode:null})}catch(V){setTimeout(function(){throw V})}}function rM(h,v,_){return _=kp(_),_.tag=3,_.payload={element:null},_.callback=function(){Kk(h,v)},_}function FB(h){return h=kp(h),h.tag=3,h}function LB(h,v,_,D){var V=_.type.getDerivedStateFromError;if(typeof V=="function"){var ee=D.value;h.payload=function(){return V(ee)},h.callback=function(){DB(v,_,D)}}var Se=_.stateNode;Se!==null&&typeof Se.componentDidCatch=="function"&&(h.callback=function(){DB(v,_,D),typeof V!="function"&&(jp===null?jp=new Set([this]):jp.add(this));var He=D.stack;this.componentDidCatch(D.value,{componentStack:He!==null?He:""})})}function Yde(h,v,_,D,V){if(_.flags|=32768,D!==null&&typeof D=="object"&&typeof D.then=="function"){if(v=_.alternate,v!==null&&Tb(v,_,V,!0),_=ul.current,_!==null){switch(_.tag){case 31:case 13:return Gl===null?s_():_.alternate===null&&Jn===0&&(Jn=3),_.flags&=-257,_.flags|=65536,_.lanes=V,D===Lk?_.flags|=16384:(v=_.updateQueue,v===null?_.updateQueue=new Set([D]):v.add(D),TM(h,D,V)),!1;case 22:return _.flags|=65536,D===Lk?_.flags|=16384:(v=_.updateQueue,v===null?(v={transitions:null,markerInstances:null,retryQueue:new Set([D])},_.updateQueue=v):(_=v.retryQueue,_===null?v.retryQueue=new Set([D]):_.add(D)),TM(h,D,V)),!1}throw Error(n(435,_.tag))}return TM(h,D,V),s_(),!1}if(_r)return v=ul.current,v!==null?((v.flags&65536)===0&&(v.flags|=256),v.flags|=65536,v.lanes=V,D!==Sj&&(h=Error(n(422),{cause:D}),_v(ql(h,_)))):(D!==Sj&&(v=Error(n(423),{cause:D}),_v(ql(v,_))),h=h.current.alternate,h.flags|=65536,V&=-V,h.lanes|=V,D=ql(D,_),V=rM(h.stateNode,D,V),Dj(h,V),Jn!==4&&(Jn=2)),!1;var ee=Error(n(520),{cause:D});if(ee=ql(ee,_),Hv===null?Hv=[ee]:Hv.push(ee),Jn!==4&&(Jn=2),v===null)return!0;D=ql(D,_),_=v;do{switch(_.tag){case 3:return _.flags|=65536,h=V&-V,_.lanes|=h,h=rM(_.stateNode,D,h),Dj(_,h),!1;case 1:if(v=_.type,ee=_.stateNode,(_.flags&128)===0&&(typeof v.getDerivedStateFromError=="function"||ee!==null&&typeof ee.componentDidCatch=="function"&&(jp===null||!jp.has(ee))))return _.flags|=65536,V&=-V,_.lanes|=V,V=FB(V),LB(V,h,_,D),Dj(_,V),!1}_=_.return}while(_!==null);return!1}var nM=Error(n(461)),Ci=!1;function Ps(h,v,_,D){v.child=h===null?zU(v,null,_,D):Mf(v,h.child,_,D)}function RB(h,v,_,D,V){_=_.render;var ee=v.ref;if("ref"in D){var Se={};for(var He in D)He!=="ref"&&(Se[He]=D[He])}else Se=D;return Pf(v),D=zj(h,v,_,Se,ee,V),He=Uj(),h!==null&&!Ci?(Bj(h,v,V),Hu(h,v,V)):(_r&&He&&vj(v),v.flags|=1,Ps(h,v,D,V),v.child)}function IB(h,v,_,D,V){if(h===null){var ee=_.type;return typeof ee=="function"&&!bj(ee)&&ee.defaultProps===void 0&&_.compare===null?(v.tag=15,v.type=ee,OB(h,v,ee,D,V)):(h=Ak(_.type,null,D,v,v.mode,V),h.ref=v.ref,h.return=v,v.child=h)}if(ee=h.child,!mM(h,V)){var Se=ee.memoizedProps;if(_=_.compare,_=_!==null?_:_s,_(Se,D)&&h.ref===v.ref)return Hu(h,v,V)}return v.flags|=1,h=Iu(ee,D),h.ref=v.ref,h.return=v,v.child=h}function OB(h,v,_,D,V){if(h!==null){var ee=h.memoizedProps;if(_s(ee,D)&&h.ref===v.ref)if(Ci=!1,v.pendingProps=D=ee,mM(h,V))(h.flags&131072)!==0&&(Ci=!0);else return v.lanes=h.lanes,Hu(h,v,V)}return iM(h,v,_,D,V)}function zB(h,v,_,D){var V=D.children,ee=h!==null?h.memoizedState:null;if(h===null&&v.stateNode===null&&(v.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),D.mode==="hidden"){if((v.flags&128)!==0){if(ee=ee!==null?ee.baseLanes|_:_,h!==null){for(D=v.child=h.child,V=0;D!==null;)V=V|D.lanes|D.childLanes,D=D.sibling;D=V&~ee}else D=0,v.child=null;return UB(h,v,ee,_,D)}if((_&536870912)!==0)v.memoizedState={baseLanes:0,cachePool:null},h!==null&&Dk(v,ee!==null?ee.cachePool:null),ee!==null?qU(v,ee):Lj(),HU(v);else return D=v.lanes=536870912,UB(h,v,ee!==null?ee.baseLanes|_:_,_,D)}else ee!==null?(Dk(v,ee.cachePool),qU(v,ee),Cp(),v.memoizedState=null):(h!==null&&Dk(v,null),Lj(),Cp());return Ps(h,v,V,_),v.child}function Rv(h,v){return h!==null&&h.tag===22||v.stateNode!==null||(v.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),v.sibling}function UB(h,v,_,D,V){var ee=Aj();return ee=ee===null?null:{parent:_i._currentValue,pool:ee},v.memoizedState={baseLanes:_,cachePool:ee},h!==null&&Dk(v,null),Lj(),HU(v),h!==null&&Tb(h,v,D,!0),v.childLanes=V,null}function Qk(h,v){return v=Xk({mode:v.mode,children:v.children},h.mode),v.ref=h.ref,h.child=v,v.return=h,v}function BB(h,v,_){return Mf(v,h.child,null,_),h=Qk(v,v.pendingProps),h.flags|=2,ml(v),v.memoizedState=null,h}function Xde(h,v,_){var D=v.pendingProps,V=(v.flags&128)!==0;if(v.flags&=-129,h===null){if(_r){if(D.mode==="hidden")return h=Qk(v,D),v.lanes=536870912,Rv(null,h);if(Ij(v),(h=_n)?(h=Jq(h,Vl),h=h!==null&&h.data==="&"?h:null,h!==null&&(v.memoizedState={dehydrated:h,treeContext:xp!==null?{id:Td,overflow:Ad}:null,retryLane:536870912,hydrationErrors:null},_=_U(h),_.return=v,v.child=_,Ns=v,_n=null)):h=null,h===null)throw vp(v);return v.lanes=536870912,null}return Qk(v,D)}var ee=h.memoizedState;if(ee!==null){var Se=ee.dehydrated;if(Ij(v),V)if(v.flags&256)v.flags&=-257,v=BB(h,v,_);else if(v.memoizedState!==null)v.child=h.child,v.flags|=128,v=null;else throw Error(n(558));else if(Ci||Tb(h,v,_,!1),V=(_&h.childLanes)!==0,Ci||V){if(D=mn,D!==null&&(Se=gt(D,_),Se!==0&&Se!==ee.retryLane))throw ee.retryLane=Se,kf(h,Se),qo(D,h,Se),nM;s_(),v=BB(h,v,_)}else h=ee.treeContext,_n=Wl(Se.nextSibling),Ns=v,_r=!0,yp=null,Vl=!1,h!==null&&PU(v,h),v=Qk(v,D),v.flags|=4096;return v}return h=Iu(h.child,{mode:D.mode,children:D.children}),h.ref=v.ref,v.child=h,h.return=v,h}function Yk(h,v){var _=v.ref;if(_===null)h!==null&&h.ref!==null&&(v.flags|=4194816);else{if(typeof _!="function"&&typeof _!="object")throw Error(n(284));(h===null||h.ref!==_)&&(v.flags|=4194816)}}function iM(h,v,_,D,V){return Pf(v),_=zj(h,v,_,D,void 0,V),D=Uj(),h!==null&&!Ci?(Bj(h,v,V),Hu(h,v,V)):(_r&&D&&vj(v),v.flags|=1,Ps(h,v,_,V),v.child)}function qB(h,v,_,D,V,ee){return Pf(v),v.updateQueue=null,_=VU(v,D,_,V),$U(h),D=Uj(),h!==null&&!Ci?(Bj(h,v,ee),Hu(h,v,ee)):(_r&&D&&vj(v),v.flags|=1,Ps(h,v,_,ee),v.child)}function HB(h,v,_,D,V){if(Pf(v),v.stateNode===null){var ee=_b,Se=_.contextType;typeof Se=="object"&&Se!==null&&(ee=Cs(Se)),ee=new _(D,ee),v.memoizedState=ee.state!==null&&ee.state!==void 0?ee.state:null,ee.updater=aM,v.stateNode=ee,ee._reactInternals=v,ee=v.stateNode,ee.props=D,ee.state=v.memoizedState,ee.refs={},Mj(v),Se=_.contextType,ee.context=typeof Se=="object"&&Se!==null?Cs(Se):_b,ee.state=v.memoizedState,Se=_.getDerivedStateFromProps,typeof Se=="function"&&(tM(v,_,Se,D),ee.state=v.memoizedState),typeof _.getDerivedStateFromProps=="function"||typeof ee.getSnapshotBeforeUpdate=="function"||typeof ee.UNSAFE_componentWillMount!="function"&&typeof ee.componentWillMount!="function"||(Se=ee.state,typeof ee.componentWillMount=="function"&&ee.componentWillMount(),typeof ee.UNSAFE_componentWillMount=="function"&&ee.UNSAFE_componentWillMount(),Se!==ee.state&&aM.enqueueReplaceState(ee,ee.state,null),Mv(v,D,ee,V),jv(),ee.state=v.memoizedState),typeof ee.componentDidMount=="function"&&(v.flags|=4194308),D=!0}else if(h===null){ee=v.stateNode;var He=v.memoizedProps,mt=Df(_,He);ee.props=mt;var Et=ee.context,qt=_.contextType;Se=_b,typeof qt=="object"&&qt!==null&&(Se=Cs(qt));var Wt=_.getDerivedStateFromProps;qt=typeof Wt=="function"||typeof ee.getSnapshotBeforeUpdate=="function",He=v.pendingProps!==He,qt||typeof ee.UNSAFE_componentWillReceiveProps!="function"&&typeof ee.componentWillReceiveProps!="function"||(He||Et!==Se)&&AB(v,ee,D,Se),Sp=!1;var Lt=v.memoizedState;ee.state=Lt,Mv(v,D,ee,V),jv(),Et=v.memoizedState,He||Lt!==Et||Sp?(typeof Wt=="function"&&(tM(v,_,Wt,D),Et=v.memoizedState),(mt=Sp||TB(v,_,mt,D,Lt,Et,Se))?(qt||typeof ee.UNSAFE_componentWillMount!="function"&&typeof ee.componentWillMount!="function"||(typeof ee.componentWillMount=="function"&&ee.componentWillMount(),typeof ee.UNSAFE_componentWillMount=="function"&&ee.UNSAFE_componentWillMount()),typeof ee.componentDidMount=="function"&&(v.flags|=4194308)):(typeof ee.componentDidMount=="function"&&(v.flags|=4194308),v.memoizedProps=D,v.memoizedState=Et),ee.props=D,ee.state=Et,ee.context=Se,D=mt):(typeof ee.componentDidMount=="function"&&(v.flags|=4194308),D=!1)}else{ee=v.stateNode,Ej(h,v),Se=v.memoizedProps,qt=Df(_,Se),ee.props=qt,Wt=v.pendingProps,Lt=ee.context,Et=_.contextType,mt=_b,typeof Et=="object"&&Et!==null&&(mt=Cs(Et)),He=_.getDerivedStateFromProps,(Et=typeof He=="function"||typeof ee.getSnapshotBeforeUpdate=="function")||typeof ee.UNSAFE_componentWillReceiveProps!="function"&&typeof ee.componentWillReceiveProps!="function"||(Se!==Wt||Lt!==mt)&&AB(v,ee,D,mt),Sp=!1,Lt=v.memoizedState,ee.state=Lt,Mv(v,D,ee,V),jv();var zt=v.memoizedState;Se!==Wt||Lt!==zt||Sp||h!==null&&h.dependencies!==null&&Mk(h.dependencies)?(typeof He=="function"&&(tM(v,_,He,D),zt=v.memoizedState),(qt=Sp||TB(v,_,qt,D,Lt,zt,mt)||h!==null&&h.dependencies!==null&&Mk(h.dependencies))?(Et||typeof ee.UNSAFE_componentWillUpdate!="function"&&typeof ee.componentWillUpdate!="function"||(typeof ee.componentWillUpdate=="function"&&ee.componentWillUpdate(D,zt,mt),typeof ee.UNSAFE_componentWillUpdate=="function"&&ee.UNSAFE_componentWillUpdate(D,zt,mt)),typeof ee.componentDidUpdate=="function"&&(v.flags|=4),typeof ee.getSnapshotBeforeUpdate=="function"&&(v.flags|=1024)):(typeof ee.componentDidUpdate!="function"||Se===h.memoizedProps&&Lt===h.memoizedState||(v.flags|=4),typeof ee.getSnapshotBeforeUpdate!="function"||Se===h.memoizedProps&&Lt===h.memoizedState||(v.flags|=1024),v.memoizedProps=D,v.memoizedState=zt),ee.props=D,ee.state=zt,ee.context=mt,D=qt):(typeof ee.componentDidUpdate!="function"||Se===h.memoizedProps&&Lt===h.memoizedState||(v.flags|=4),typeof ee.getSnapshotBeforeUpdate!="function"||Se===h.memoizedProps&&Lt===h.memoizedState||(v.flags|=1024),D=!1)}return ee=D,Yk(h,v),D=(v.flags&128)!==0,ee||D?(ee=v.stateNode,_=D&&typeof _.getDerivedStateFromError!="function"?null:ee.render(),v.flags|=1,h!==null&&D?(v.child=Mf(v,h.child,null,V),v.child=Mf(v,null,_,V)):Ps(h,v,_,V),v.memoizedState=ee.state,h=v.child):h=Hu(h,v,V),h}function $B(h,v,_,D){return Nf(),v.flags|=256,Ps(h,v,_,D),v.child}var sM={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function oM(h){return{baseLanes:h,cachePool:DU()}}function lM(h,v,_){return h=h!==null?h.childLanes&~_:0,v&&(h|=hl),h}function VB(h,v,_){var D=v.pendingProps,V=!1,ee=(v.flags&128)!==0,Se;if((Se=ee)||(Se=h!==null&&h.memoizedState===null?!1:(hi.current&2)!==0),Se&&(V=!0,v.flags&=-129),Se=(v.flags&32)!==0,v.flags&=-33,h===null){if(_r){if(V?Np(v):Cp(),(h=_n)?(h=Jq(h,Vl),h=h!==null&&h.data!=="&"?h:null,h!==null&&(v.memoizedState={dehydrated:h,treeContext:xp!==null?{id:Td,overflow:Ad}:null,retryLane:536870912,hydrationErrors:null},_=_U(h),_.return=v,v.child=_,Ns=v,_n=null)):h=null,h===null)throw vp(v);return $M(h)?v.lanes=32:v.lanes=536870912,null}var He=D.children;return D=D.fallback,V?(Cp(),V=v.mode,He=Xk({mode:"hidden",children:He},V),D=_f(D,V,_,null),He.return=v,D.return=v,He.sibling=D,v.child=He,D=v.child,D.memoizedState=oM(_),D.childLanes=lM(h,Se,_),v.memoizedState=sM,Rv(null,D)):(Np(v),cM(v,He))}var mt=h.memoizedState;if(mt!==null&&(He=mt.dehydrated,He!==null)){if(ee)v.flags&256?(Np(v),v.flags&=-257,v=dM(h,v,_)):v.memoizedState!==null?(Cp(),v.child=h.child,v.flags|=128,v=null):(Cp(),He=D.fallback,V=v.mode,D=Xk({mode:"visible",children:D.children},V),He=_f(He,V,_,null),He.flags|=2,D.return=v,He.return=v,D.sibling=He,v.child=D,Mf(v,h.child,null,_),D=v.child,D.memoizedState=oM(_),D.childLanes=lM(h,Se,_),v.memoizedState=sM,v=Rv(null,D));else if(Np(v),$M(He)){if(Se=He.nextSibling&&He.nextSibling.dataset,Se)var Et=Se.dgst;Se=Et,D=Error(n(419)),D.stack="",D.digest=Se,_v({value:D,source:null,stack:null}),v=dM(h,v,_)}else if(Ci||Tb(h,v,_,!1),Se=(_&h.childLanes)!==0,Ci||Se){if(Se=mn,Se!==null&&(D=gt(Se,_),D!==0&&D!==mt.retryLane))throw mt.retryLane=D,kf(h,D),qo(Se,h,D),nM;HM(He)||s_(),v=dM(h,v,_)}else HM(He)?(v.flags|=192,v.child=h.child,v=null):(h=mt.treeContext,_n=Wl(He.nextSibling),Ns=v,_r=!0,yp=null,Vl=!1,h!==null&&PU(v,h),v=cM(v,D.children),v.flags|=4096);return v}return V?(Cp(),He=D.fallback,V=v.mode,mt=h.child,Et=mt.sibling,D=Iu(mt,{mode:"hidden",children:D.children}),D.subtreeFlags=mt.subtreeFlags&65011712,Et!==null?He=Iu(Et,He):(He=_f(He,V,_,null),He.flags|=2),He.return=v,D.return=v,D.sibling=He,v.child=D,Rv(null,D),D=v.child,He=h.child.memoizedState,He===null?He=oM(_):(V=He.cachePool,V!==null?(mt=_i._currentValue,V=V.parent!==mt?{parent:mt,pool:mt}:V):V=DU(),He={baseLanes:He.baseLanes|_,cachePool:V}),D.memoizedState=He,D.childLanes=lM(h,Se,_),v.memoizedState=sM,Rv(h.child,D)):(Np(v),_=h.child,h=_.sibling,_=Iu(_,{mode:"visible",children:D.children}),_.return=v,_.sibling=null,h!==null&&(Se=v.deletions,Se===null?(v.deletions=[h],v.flags|=16):Se.push(h)),v.child=_,v.memoizedState=null,_)}function cM(h,v){return v=Xk({mode:"visible",children:v},h.mode),v.return=h,h.child=v}function Xk(h,v){return h=dl(22,h,null,v),h.lanes=0,h}function dM(h,v,_){return Mf(v,h.child,null,_),h=cM(v,v.pendingProps.children),h.flags|=2,v.memoizedState=null,h}function GB(h,v,_){h.lanes|=v;var D=h.alternate;D!==null&&(D.lanes|=v),Nj(h.return,v,_)}function uM(h,v,_,D,V,ee){var Se=h.memoizedState;Se===null?h.memoizedState={isBackwards:v,rendering:null,renderingStartTime:0,last:D,tail:_,tailMode:V,treeForkCount:ee}:(Se.isBackwards=v,Se.rendering=null,Se.renderingStartTime=0,Se.last=D,Se.tail=_,Se.tailMode=V,Se.treeForkCount=ee)}function WB(h,v,_){var D=v.pendingProps,V=D.revealOrder,ee=D.tail;D=D.children;var Se=hi.current,He=(Se&2)!==0;if(He?(Se=Se&1|2,v.flags|=128):Se&=1,me(hi,Se),Ps(h,v,D,_),D=_r?kv:0,!He&&h!==null&&(h.flags&128)!==0)e:for(h=v.child;h!==null;){if(h.tag===13)h.memoizedState!==null&&GB(h,_,v);else if(h.tag===19)GB(h,_,v);else if(h.child!==null){h.child.return=h,h=h.child;continue}if(h===v)break e;for(;h.sibling===null;){if(h.return===null||h.return===v)break e;h=h.return}h.sibling.return=h.return,h=h.sibling}switch(V){case"forwards":for(_=v.child,V=null;_!==null;)h=_.alternate,h!==null&&zk(h)===null&&(V=_),_=_.sibling;_=V,_===null?(V=v.child,v.child=null):(V=_.sibling,_.sibling=null),uM(v,!1,V,_,ee,D);break;case"backwards":case"unstable_legacy-backwards":for(_=null,V=v.child,v.child=null;V!==null;){if(h=V.alternate,h!==null&&zk(h)===null){v.child=V;break}h=V.sibling,V.sibling=_,_=V,V=h}uM(v,!0,_,null,ee,D);break;case"together":uM(v,!1,null,null,void 0,D);break;default:v.memoizedState=null}return v.child}function Hu(h,v,_){if(h!==null&&(v.dependencies=h.dependencies),Ap|=v.lanes,(_&v.childLanes)===0)if(h!==null){if(Tb(h,v,_,!1),(_&v.childLanes)===0)return null}else return null;if(h!==null&&v.child!==h.child)throw Error(n(153));if(v.child!==null){for(h=v.child,_=Iu(h,h.pendingProps),v.child=_,_.return=v;h.sibling!==null;)h=h.sibling,_=_.sibling=Iu(h,h.pendingProps),_.return=v;_.sibling=null}return v.child}function mM(h,v){return(h.lanes&v)!==0?!0:(h=h.dependencies,!!(h!==null&&Mk(h)))}function Zde(h,v,_){switch(v.tag){case 3:A(v,v.stateNode.containerInfo),wp(v,_i,h.memoizedState.cache),Nf();break;case 27:case 5:q(v);break;case 4:A(v,v.stateNode.containerInfo);break;case 10:wp(v,v.type,v.memoizedProps.value);break;case 31:if(v.memoizedState!==null)return v.flags|=128,Ij(v),null;break;case 13:var D=v.memoizedState;if(D!==null)return D.dehydrated!==null?(Np(v),v.flags|=128,null):(_&v.child.childLanes)!==0?VB(h,v,_):(Np(v),h=Hu(h,v,_),h!==null?h.sibling:null);Np(v);break;case 19:var V=(h.flags&128)!==0;if(D=(_&v.childLanes)!==0,D||(Tb(h,v,_,!1),D=(_&v.childLanes)!==0),V){if(D)return WB(h,v,_);v.flags|=128}if(V=v.memoizedState,V!==null&&(V.rendering=null,V.tail=null,V.lastEffect=null),me(hi,hi.current),D)break;return null;case 22:return v.lanes=0,zB(h,v,_,v.pendingProps);case 24:wp(v,_i,h.memoizedState.cache)}return Hu(h,v,_)}function KB(h,v,_){if(h!==null)if(h.memoizedProps!==v.pendingProps)Ci=!0;else{if(!mM(h,_)&&(v.flags&128)===0)return Ci=!1,Zde(h,v,_);Ci=(h.flags&131072)!==0}else Ci=!1,_r&&(v.flags&1048576)!==0&&CU(v,kv,v.index);switch(v.lanes=0,v.tag){case 16:e:{var D=v.pendingProps;if(h=Af(v.elementType),v.type=h,typeof h=="function")bj(h)?(D=Df(h,D),v.tag=1,v=HB(null,v,h,D,_)):(v.tag=0,v=iM(null,v,h,D,_));else{if(h!=null){var V=h.$$typeof;if(V===C){v.tag=11,v=RB(null,v,h,D,_);break e}else if(V===j){v.tag=14,v=IB(null,v,h,D,_);break e}}throw v=L(h)||h,Error(n(306,v,""))}}return v;case 0:return iM(h,v,v.type,v.pendingProps,_);case 1:return D=v.type,V=Df(D,v.pendingProps),HB(h,v,D,V,_);case 3:e:{if(A(v,v.stateNode.containerInfo),h===null)throw Error(n(387));D=v.pendingProps;var ee=v.memoizedState;V=ee.element,Ej(h,v),Mv(v,D,null,_);var Se=v.memoizedState;if(D=Se.cache,wp(v,_i,D),D!==ee.cache&&Cj(v,[_i],_,!0),jv(),D=Se.element,ee.isDehydrated)if(ee={element:D,isDehydrated:!1,cache:Se.cache},v.updateQueue.baseState=ee,v.memoizedState=ee,v.flags&256){v=$B(h,v,D,_);break e}else if(D!==V){V=ql(Error(n(424)),v),_v(V),v=$B(h,v,D,_);break e}else for(h=v.stateNode.containerInfo,h.nodeType===9?h=h.body:h=h.nodeName==="HTML"?h.ownerDocument.body:h,_n=Wl(h.firstChild),Ns=v,_r=!0,yp=null,Vl=!0,_=zU(v,null,D,_),v.child=_;_;)_.flags=_.flags&-3|4096,_=_.sibling;else{if(Nf(),D===V){v=Hu(h,v,_);break e}Ps(h,v,D,_)}v=v.child}return v;case 26:return Yk(h,v),h===null?(_=iH(v.type,null,v.pendingProps,null))?v.memoizedState=_:_r||(_=v.type,h=v.pendingProps,D=p_(H.current).createElement(_),D[_t]=v,D[Ct]=h,Ts(D,_,h),it(D),v.stateNode=D):v.memoizedState=iH(v.type,h.memoizedProps,v.pendingProps,h.memoizedState),null;case 27:return q(v),h===null&&_r&&(D=v.stateNode=aH(v.type,v.pendingProps,H.current),Ns=v,Vl=!0,V=_n,Fp(v.type)?(VM=V,_n=Wl(D.firstChild)):_n=V),Ps(h,v,v.pendingProps.children,_),Yk(h,v),h===null&&(v.flags|=4194304),v.child;case 5:return h===null&&_r&&((V=D=_n)&&(D=Tue(D,v.type,v.pendingProps,Vl),D!==null?(v.stateNode=D,Ns=v,_n=Wl(D.firstChild),Vl=!1,V=!0):V=!1),V||vp(v)),q(v),V=v.type,ee=v.pendingProps,Se=h!==null?h.memoizedProps:null,D=ee.children,UM(V,ee)?D=null:Se!==null&&UM(V,Se)&&(v.flags|=32),v.memoizedState!==null&&(V=zj(h,v,Hde,null,null,_),Xv._currentValue=V),Yk(h,v),Ps(h,v,D,_),v.child;case 6:return h===null&&_r&&((h=_=_n)&&(_=Aue(_,v.pendingProps,Vl),_!==null?(v.stateNode=_,Ns=v,_n=null,h=!0):h=!1),h||vp(v)),null;case 13:return VB(h,v,_);case 4:return A(v,v.stateNode.containerInfo),D=v.pendingProps,h===null?v.child=Mf(v,null,D,_):Ps(h,v,D,_),v.child;case 11:return RB(h,v,v.type,v.pendingProps,_);case 7:return Ps(h,v,v.pendingProps,_),v.child;case 8:return Ps(h,v,v.pendingProps.children,_),v.child;case 12:return Ps(h,v,v.pendingProps.children,_),v.child;case 10:return D=v.pendingProps,wp(v,v.type,D.value),Ps(h,v,D.children,_),v.child;case 9:return V=v.type._context,D=v.pendingProps.children,Pf(v),V=Cs(V),D=D(V),v.flags|=1,Ps(h,v,D,_),v.child;case 14:return IB(h,v,v.type,v.pendingProps,_);case 15:return OB(h,v,v.type,v.pendingProps,_);case 19:return WB(h,v,_);case 31:return Xde(h,v,_);case 22:return zB(h,v,_,v.pendingProps);case 24:return Pf(v),D=Cs(_i),h===null?(V=Aj(),V===null&&(V=mn,ee=Pj(),V.pooledCache=ee,ee.refCount++,ee!==null&&(V.pooledCacheLanes|=_),V=ee),v.memoizedState={parent:D,cache:V},Mj(v),wp(v,_i,V)):((h.lanes&_)!==0&&(Ej(h,v),Mv(v,null,null,_),jv()),V=h.memoizedState,ee=v.memoizedState,V.parent!==D?(V={parent:D,cache:D},v.memoizedState=V,v.lanes===0&&(v.memoizedState=v.updateQueue.baseState=V),wp(v,_i,D)):(D=ee.cache,wp(v,_i,D),D!==V.cache&&Cj(v,[_i],_,!0))),Ps(h,v,v.pendingProps.children,_),v.child;case 29:throw v.pendingProps}throw Error(n(156,v.tag))}function $u(h){h.flags|=4}function pM(h,v,_,D,V){if((v=(h.mode&32)!==0)&&(v=!1),v){if(h.flags|=16777216,(V&335544128)===V)if(h.stateNode.complete)h.flags|=8192;else if(wq())h.flags|=8192;else throw jf=Lk,jj}else h.flags&=-16777217}function QB(h,v){if(v.type!=="stylesheet"||(v.state.loading&4)!==0)h.flags&=-16777217;else if(h.flags|=16777216,!dH(v))if(wq())h.flags|=8192;else throw jf=Lk,jj}function Zk(h,v){v!==null&&(h.flags|=4),h.flags&16384&&(v=h.tag!==22?We():536870912,h.lanes|=v,Ub|=v)}function Iv(h,v){if(!_r)switch(h.tailMode){case"hidden":v=h.tail;for(var _=null;v!==null;)v.alternate!==null&&(_=v),v=v.sibling;_===null?h.tail=null:_.sibling=null;break;case"collapsed":_=h.tail;for(var D=null;_!==null;)_.alternate!==null&&(D=_),_=_.sibling;D===null?v||h.tail===null?h.tail=null:h.tail.sibling=null:D.sibling=null}}function Nn(h){var v=h.alternate!==null&&h.alternate.child===h.child,_=0,D=0;if(v)for(var V=h.child;V!==null;)_|=V.lanes|V.childLanes,D|=V.subtreeFlags&65011712,D|=V.flags&65011712,V.return=h,V=V.sibling;else for(V=h.child;V!==null;)_|=V.lanes|V.childLanes,D|=V.subtreeFlags,D|=V.flags,V.return=h,V=V.sibling;return h.subtreeFlags|=D,h.childLanes=_,v}function Jde(h,v,_){var D=v.pendingProps;switch(wj(v),v.tag){case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Nn(v),null;case 1:return Nn(v),null;case 3:return _=v.stateNode,D=null,h!==null&&(D=h.memoizedState.cache),v.memoizedState.cache!==D&&(v.flags|=2048),Uu(_i),M(),_.pendingContext&&(_.context=_.pendingContext,_.pendingContext=null),(h===null||h.child===null)&&(Pb(v)?$u(v):h===null||h.memoizedState.isDehydrated&&(v.flags&256)===0||(v.flags|=1024,kj())),Nn(v),null;case 26:var V=v.type,ee=v.memoizedState;return h===null?($u(v),ee!==null?(Nn(v),QB(v,ee)):(Nn(v),pM(v,V,null,D,_))):ee?ee!==h.memoizedState?($u(v),Nn(v),QB(v,ee)):(Nn(v),v.flags&=-16777217):(h=h.memoizedProps,h!==D&&$u(v),Nn(v),pM(v,V,h,D,_)),null;case 27:if(W(v),_=H.current,V=v.type,h!==null&&v.stateNode!=null)h.memoizedProps!==D&&$u(v);else{if(!D){if(v.stateNode===null)throw Error(n(166));return Nn(v),null}h=de.current,Pb(v)?TU(v):(h=aH(V,D,_),v.stateNode=h,$u(v))}return Nn(v),null;case 5:if(W(v),V=v.type,h!==null&&v.stateNode!=null)h.memoizedProps!==D&&$u(v);else{if(!D){if(v.stateNode===null)throw Error(n(166));return Nn(v),null}if(ee=de.current,Pb(v))TU(v);else{var Se=p_(H.current);switch(ee){case 1:ee=Se.createElementNS("http://www.w3.org/2000/svg",V);break;case 2:ee=Se.createElementNS("http://www.w3.org/1998/Math/MathML",V);break;default:switch(V){case"svg":ee=Se.createElementNS("http://www.w3.org/2000/svg",V);break;case"math":ee=Se.createElementNS("http://www.w3.org/1998/Math/MathML",V);break;case"script":ee=Se.createElement("div"),ee.innerHTML=" - + +