From 6bc6a1d68387e269efd78c446658e7721e02e4c2 Mon Sep 17 00:00:00 2001 From: maziggy Date: Fri, 22 May 2026 13:24:35 +0200 Subject: [PATCH] feat(virtual-printer): setup diagnostic + one-click slicer-certificate export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two recurring virtual-printer support pains, both on the Virtual Printers settings page. Setup check: a stethoscope action on each VP card runs a pass/fail/warn/skip checklist — VP enabled, services running, bind interface still exists, access code set, target printer (proxy mode), and a live TCP probe of the FTP / MQTT / discovery ports on the bind IP. start_server swallows per-service bind errors, so a service object can exist while nothing is listening; probing the bind IP from outside is the only reliable signal and it catches the common "VP not visible in the slicer" bind-IP-conflict and stale-interface cases. Slicer certificate: virtual printers present a TLS cert signed by a shared CA the slicer must trust. Until now users had to docker exec in and cat bbl_ca.crt. A "Slicer certificate" row on the settings card now offers Copy and Download (bambuddy-virtual-printer-ca.crt) plus the SHA-256 fingerprint. GET /virtual-printers/ca-certificate returns only the public certificate; the CA private key never leaves the backend. The CA is generated on demand so the button works before the first VP is enabled. Backend: - services/virtual_printer/diagnostic.py — run_vp_diagnostic + port probes - schemas/virtual_printer.py — VPDiagnosticResult - CertificateService.get_ca_certificate_info() + manager helper - routes: GET /virtual-printers/ca-certificate, /{vp_id}/diagnostic Frontend: - VirtualPrinterDiagnosticModal.tsx; stethoscope button on VirtualPrinterCard - caCert row on VirtualPrinterList; utils/clipboard.ts (shared copy w/ non-secure-context fallback + downloadTextFile), de-duplicating the existing FQDN-copy logic - vpDiagnostic.* + virtualPrinter.caCert.* across all 9 locales 9 backend unit tests + 4 route integration tests + 6 frontend tests. Backend ruff clean, frontend build clean, i18n parity green. --- CHANGELOG.md | 1 + backend/app/api/routes/virtual_printers.py | 44 + backend/app/schemas/virtual_printer.py | 23 + .../services/virtual_printer/certificate.py | 22 + .../services/virtual_printer/diagnostic.py | 170 ++++ .../app/services/virtual_printer/manager.py | 12 + .../integration/test_virtual_printer_api.py | 55 ++ .../tests/unit/services/test_vp_diagnostic.py | 167 ++++ frontend/scripts/check-i18n-parity.mjs | 2 +- .../VirtualPrinterDiagnosticModal.test.tsx | 75 ++ .../src/__tests__/utils/clipboard.test.ts | 66 ++ frontend/src/api/client.ts | 39 + .../src/components/VirtualPrinterCard.tsx | 47 +- .../VirtualPrinterDiagnosticModal.tsx | 159 +++ .../src/components/VirtualPrinterList.tsx | 136 ++- frontend/src/i18n/locales/de.ts | 61 ++ frontend/src/i18n/locales/en.ts | 61 ++ frontend/src/i18n/locales/es.ts | 61 ++ frontend/src/i18n/locales/fr.ts | 61 ++ frontend/src/i18n/locales/it.ts | 61 ++ frontend/src/i18n/locales/ja.ts | 61 ++ frontend/src/i18n/locales/pt-BR.ts | 61 ++ frontend/src/i18n/locales/zh-CN.ts | 61 ++ frontend/src/i18n/locales/zh-TW.ts | 61 ++ frontend/src/utils/clipboard.ts | 43 + .../{index-CVVS0VHp.js => index-BpyZgjkS.js} | 916 +++++++++--------- static/index.html | 2 +- 27 files changed, 2012 insertions(+), 516 deletions(-) create mode 100644 backend/app/schemas/virtual_printer.py create mode 100644 backend/app/services/virtual_printer/diagnostic.py create mode 100644 backend/tests/unit/services/test_vp_diagnostic.py create mode 100644 frontend/src/__tests__/components/VirtualPrinterDiagnosticModal.test.tsx create mode 100644 frontend/src/__tests__/utils/clipboard.test.ts create mode 100644 frontend/src/components/VirtualPrinterDiagnosticModal.tsx create mode 100644 frontend/src/utils/clipboard.ts rename static/assets/{index-CVVS0VHp.js => index-BpyZgjkS.js} (60%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d720f1e7..e2b7f0370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to Bambuddy will be documented in this file. - **Connection Diagnostic — self-service triage for "printer won't connect / won't print"** — A triage review of recently-closed issues found roughly a third were user-side setup errors (printer not in LAN developer mode, blocked ports, Docker bridge networking, wrong access code, printer on a different subnet), each costing a multi-round-trip "enable debug logging → build a support bundle → upload it" exchange. A new diagnostic (`backend/app/services/printer_diagnostic.py`) runs those checks automatically: TCP reachability of MQTT 8883 / FTPS 990 / RTSPS 322, LAN developer mode, Docker network mode, printer/host subnet match, and MQTT credential class — each returning a pass / fail / warn / skip status with a localized plain-language fix. Exposed via `GET /printers/{id}/diagnostic` (saved printer) and `POST /printers/diagnostic` (pre-save Add-Printer flow), and surfaced as a one-click "Run diagnostic" from the printer card actions menu (plus a quick button on the card when a printer is offline), the Add-Printer dialog, and a new Connection Diagnostic section on the System page. The in-app bug reporter scans configured printers when the report form opens and always shows the result — a healthy confirmation when nothing's wrong, or the detected problem and its fix inline — so setup mistakes get self-resolved instead of becoming GitHub issues. The GitHub `config.yml` troubleshooting link was repointed from the wiki source repo to the rendered troubleshooting page. Backend service unit tests (15) and frontend modal tests (3) added; all diagnostic strings translated across the 8 locales. Backend ruff clean, frontend build clean, i18n parity green. ### Changed +- **Virtual printer: setup diagnostic + one-click slicer-certificate export** — Two recurring virtual-printer support pains, addressed on the Virtual Printers settings page. **(1) Setup check** — a new stethoscope action on each VP card runs `GET /virtual-printers/{id}/diagnostic` and shows a pass/fail/warn/skip checklist: VP enabled, services running, bind interface still exists, access code set, target printer (proxy mode), and — decisively — a live TCP probe of the FTP/MQTT/discovery ports on the bind IP. The manager swallows per-service start errors (`run_with_logging`), so a service object can exist while nothing is actually listening; probing the bind IP from outside is the only reliable signal, and it catches the common "VP doesn't show up in the slicer" bind-IP-conflict and stale-interface cases. New `backend/app/services/virtual_printer/diagnostic.py` + `VPDiagnosticResult` schema + `VirtualPrinterDiagnosticModal.tsx`. **(2) Slicer certificate** — virtual printers present a TLS cert signed by a shared CA the slicer must trust; until now users had to `docker exec` in and `cat bbl_ca.crt` to get it. A new "Slicer certificate" row on the Virtual Printers settings card (alongside the Archive name source toggle) offers Copy and Download (`bambuddy-virtual-printer-ca.crt`) plus the CA's SHA-256 fingerprint, served by `GET /virtual-printers/ca-certificate` — only the public certificate, never the CA private key. The CA is generated on demand so the button works before the first VP is enabled. Copy uses a non-secure-context fallback (Bambuddy is usually on plain-HTTP LAN), extracted into a shared `utils/clipboard.ts`. 9 backend diagnostic/CA unit tests + 4 route integration tests + 6 frontend tests (diagnostic modal, clipboard helpers); all `vpDiagnostic.*` / `virtualPrinter.caCert.*` strings translated across the 9 locales. Backend ruff clean, frontend build clean, i18n parity green. - **Bug-report panel: connection diagnostic no longer overflows on multi-printer setups** — The "Report a Bug" panel scans every configured printer on open and surfaces connection problems inline so users can self-fix before filing. The first cut rendered a full ~6-row checklist for *each* problem printer stacked vertically; a user with many printers all reporting issues pushed the description box, screenshot uploader and Submit button far below the fold in the `max-w-md` / `max-h-[80vh]` panel. The diagnostic section is now a compact summary — one line ("N of M printers have connection issues") followed by the affected printers as collapsed rows (healthy printers count toward M but render no detail). Each row expands on demand to that printer's full checklist via the shared `Collapsible` widget; when exactly one printer has problems the row is auto-expanded since that's the case where inline detail is wanted with no extra click. The panel now stays a fixed ~3 lines plus one row per affected printer regardless of fleet size, keeping the report form reachable. Healthy-fleet confirmation line is unchanged. New `bugReport.diagnosticSummary` key (with `{{problems}}`/`{{total}}`) replaces the static `diagnosticHeading`; `diagnosticIntro` reworded to be printer-count-neutral and point at the expand affordance — both translated across all 9 locales. 2 new tests in `BugReportBubble.test.tsx` (multiple problems stay collapsed and expand on click; a single problem auto-expands); 11 tests green; frontend build clean; i18n parity holds at 4903 keys × 9 locales. - **Color Catalog sync now identifies itself as Bambuddy to filamentcolors.xyz** — The FilamentColors.xyz sync client in `inventory.py` created its `httpx.AsyncClient` with no `User-Agent`, so it leaked httpx's default `python-httpx/x.y` string — the only outbound client that did (`bambu_cloud`, `makerworld`, `firmware_check` all send the honest `Bambuddy/1.0 (+https://github.com/maziggy/bambuddy)`). It now sends the same honest UA, consistent with the rest of the codebase. Surfaced while investigating #1456 (a Cloudflare `403` on the sync that turned out to be the reporter's network/IP reputation, not Bambuddy — the UA leak was a separate inconsistency found in passing, and this change does not by itself resolve a Cloudflare IP block). - **Filament inventory: grouped rows now show group totals (#1368, requested by a user)** — With "Group similar" enabled, the collapsed group row showed the values of a single member (the first spool) — so a group of five 1 kg spools displayed "1000 g" instead of the 5 kg it actually held. The group header now aggregates across all members: the table view's Label, Net, Gross, Used and Remaining columns and the grid card's weight figure show group totals, while identity columns (Material, Brand, Colour) and the Cost/kg rate stay per-spool-correct. Per-spool-only fields with no meaningful total (dates, location, note, tag ID) keep showing the representative member's value; the expanded individual rows are unchanged. New `aggregateGroupSpool` helper in `frontend/src/utils/inventoryGrouping.ts` with 4 unit tests. Frontend-only — all data was already in the spool list. — Previous behaviour disabled the Slice button whenever the source 3MF's bound printer model didn't match the user's picked printer profile, on the theory that the slicer CLI "cannot re-slice a 3MF for a different printer" and would silently fall back to embedded settings to produce a wrong-printer file. Step 0 empirical test on 2026-05-20 disproved that: an 18-color H2D-bound `Trent900.3mf` sliced via the X1C bundle (`POST /slice` with `bundle=cb…X1C, printerName=# Bambu Lab X1 Carbon 0.4 nozzle`) produced 2.3 MB of genuinely X1C-compatible G-code in 1.8 s — `printer_model` overridden to `Bambu Lab X1 Carbon`, `printable_area` to 256×256 (X1C bed, not H2D's 350×320), `printable_height` 250 (vs 325), `bed_exclude_area` populated with X1C's 18×28 corner zone, `nozzle_diameter` single 0.4 (vs H2D's dual `0.4,0.4`), and the full X1C `machine_start_gcode` sequence baked in. The sidecar takes printer / process / first-N filament names from the picked bundle and only inherits embedded values for unused trailing slots — bed size, kinematics, start sequence all come from the target. **Behavioural change**: dropped `!printerMismatch` from the SliceModal `isReady` predicate so the Slice button stays enabled when models differ. The amber banner was first softened to an info message, then removed entirely — re-slicing across printers is now just a normal slice, the picker UI already shows which printer was picked, no second confirmation needed. **Dead-code removal (same drop)**: with no banner, the `source_printer_model` field on the `/library/files/{id}/plates` and `/archives/{id}/plates` responses had zero consumers; the `extract_source_printer_model_from_3mf` helper in `threemf_tools.py` (which opened the 3MF zip and read `Metadata/project_settings.config` on every plate request) had zero callers. Removed both response keys, both backend extractions, both `threemf_tools` imports, the helper itself, its 6 unit tests, the `source_printer_model` field from `frontend/src/types/plates.ts` (PlateMetadata + LibraryFilePlatesResponse), and 2 obsolete SliceModal tests that exercised the now-impossible matched-printer / legacy-archive paths. **i18n discipline cleanup (same drop, per [[feedback_no_followups]] + [[feedback_translate_dont_fallback]])**: every t() callsite in SliceModal.tsx had an inline English `defaultValue:` or positional-second-arg English fallback — 22 sites in total. With 8 locales shipped, those fallbacks are dead weight at best, and an actual i18n-violation when the key is missing because non-English users would silently see English. Audit found 3 keys (`slice.bundle`, `slice.bundleNone`, `slice.bundleAllRequired`) that had **no** corresponding entry in any locale file — they were being served from the inline English fallback exclusively, meaning every non-English user was already seeing those three labels in English. Added all 3 to all 8 locales with real translations, then stripped the English fallback from every t() call in SliceModal.tsx. The `slice.printerMismatch` key was removed from all 8 locales (banner is gone). **Why this matters**: a recurring pain point for users importing MakerWorld project files where the original creator's printer often differs from the user's; previously they had to round-trip through BambuStudio's "convert project" flow to re-export. Now Bambuddy re-slices in-place with no UI friction. **Tests**: the existing SliceModal "shows mismatch warning AND disables Slice" test was rewritten to assert "does not surface any cross-printer banner AND keeps Slice enabled when models differ" (regression guard against the gate being re-added); 2 obsolete tests deleted. 32 SliceModal tests green (was 34, -2 dead tests); 49 threemf_tools tests green (was 55, -6 helper tests); 24 plates-route tests green; frontend build clean; backend ruff clean; i18n parity check passes 4858 keys × 8 locales (net +2 vs pre-fix: +3 bundle keys, -1 printerMismatch). diff --git a/backend/app/api/routes/virtual_printers.py b/backend/app/api/routes/virtual_printers.py index b69f5de93..874bc0d24 100644 --- a/backend/app/api/routes/virtual_printers.py +++ b/backend/app/api/routes/virtual_printers.py @@ -10,6 +10,7 @@ from backend.app.core.auth import RequirePermissionIfAuthEnabled from backend.app.core.database import get_db from backend.app.core.permissions import Permission from backend.app.models.user import User +from backend.app.schemas.virtual_printer import VPDiagnosticResult # Imported at module scope so tests can patch # backend.app.api.routes.virtual_printers.tailscale_service. @@ -254,6 +255,49 @@ async def get_tailscale_status( ) +@router.get("/ca-certificate") +async def get_ca_certificate( + _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_READ), +): + """Return the shared virtual-printer CA certificate (PEM) for slicer trust import. + + One CA is shared by every virtual printer — the user imports it into their + slicer's trust store once. Only the public certificate is returned; the CA + private key never leaves the backend. + """ + from backend.app.services.virtual_printer import virtual_printer_manager + + try: + return virtual_printer_manager.get_ca_certificate_info() + except Exception as e: + logger.error("Failed to obtain virtual printer CA certificate: %s", e) + return JSONResponse(status_code=500, content={"detail": "Could not generate the CA certificate"}) + + +@router.get("/{vp_id}/diagnostic", response_model=VPDiagnosticResult) +async def diagnose_virtual_printer( + vp_id: int, + db: AsyncSession = Depends(get_db), + _: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_READ), +): + """Run setup diagnostics for a virtual printer. + + Probes the VP's own bind IP and services so the user can self-diagnose the + common "my virtual printer doesn't show up in the slicer" failures. + """ + from backend.app.models.virtual_printer import VirtualPrinter + from backend.app.services.virtual_printer import virtual_printer_manager + from backend.app.services.virtual_printer.diagnostic import run_vp_diagnostic + + result = await db.execute(select(VirtualPrinter).where(VirtualPrinter.id == vp_id)) + vp = result.scalar_one_or_none() + if not vp: + return JSONResponse(status_code=404, content={"detail": "Virtual printer not found"}) + + instance = virtual_printer_manager.get_instance(vp.id) + return await run_vp_diagnostic(vp, instance) + + @router.get("/{vp_id}") async def get_virtual_printer( vp_id: int, diff --git a/backend/app/schemas/virtual_printer.py b/backend/app/schemas/virtual_printer.py new file mode 100644 index 000000000..f424849fe --- /dev/null +++ b/backend/app/schemas/virtual_printer.py @@ -0,0 +1,23 @@ +"""Schemas for virtual printer diagnostics.""" + +from pydantic import BaseModel + +from backend.app.schemas.printer import DiagnosticCheck + + +class VPDiagnosticResult(BaseModel): + """Result of a virtual-printer setup diagnostic run. + + Mirrors ``PrinterDiagnosticResult`` but keyed to a virtual printer: the + checks probe the VP's own bind IP and local services rather than a remote + printer. ``checks[].id`` values are VP-specific (enabled, running, + bind_interface, access_code, target_printer, port_ftps, port_mqtt, + port_bind, certificate); the frontend renders the localized title and + fix text from id + status. + """ + + vp_id: int + vp_name: str + mode: str + overall: str # "ok" | "warnings" | "problems" + checks: list[DiagnosticCheck] diff --git a/backend/app/services/virtual_printer/certificate.py b/backend/app/services/virtual_printer/certificate.py index 164c6486b..620f52646 100644 --- a/backend/app/services/virtual_printer/certificate.py +++ b/backend/app/services/virtual_printer/certificate.py @@ -330,6 +330,28 @@ class CertificateService: logger.info(" Printer: CN=%s", self.serial) return self.cert_path, self.key_path + def get_ca_certificate_info(self) -> dict: + """Return the shared CA certificate as PEM text plus identifying metadata. + + Generates the CA if it does not exist yet. Safe to expose over the + API: this is the *public* CA certificate users import into their + slicer's trust store. The CA private key (``bbl_ca.key``) is never + included and never leaves the backend. + + Returns: + Dict with ``pem`` (PEM-encoded certificate), ``fingerprint_sha256`` + (colon-separated uppercase hex) and ``not_valid_after`` (ISO 8601). + """ + _ca_key, ca_cert = self._get_or_create_ca() + pem = ca_cert.public_bytes(serialization.Encoding.PEM).decode("ascii") + digest = ca_cert.fingerprint(hashes.SHA256()).hex().upper() + fingerprint = ":".join(digest[i : i + 2] for i in range(0, len(digest), 2)) + return { + "pem": pem, + "fingerprint_sha256": fingerprint, + "not_valid_after": ca_cert.not_valid_after_utc.isoformat(), + } + def delete_printer_certificate(self) -> None: """Delete only the printer certificate (preserves CA).""" for path in [self.cert_path, self.key_path]: diff --git a/backend/app/services/virtual_printer/diagnostic.py b/backend/app/services/virtual_printer/diagnostic.py new file mode 100644 index 000000000..144e3f64c --- /dev/null +++ b/backend/app/services/virtual_printer/diagnostic.py @@ -0,0 +1,170 @@ +"""Setup diagnostic for a virtual printer. + +A virtual printer fails for the user in ways a real printer never does: the +bind IP no longer exists after a host/network change, a service silently +failed to bind its port, the access code was never set, the slicer was never +told to trust the CA. The manager swallows per-service start errors +(``run_with_logging`` in ``start_server``), so a service object can exist +while nothing is actually listening — the only reliable signal is probing the +bind IP's ports from the outside, which is what this does. + +Each check carries a stable ``id`` and a ``status`` of pass / fail / warn / +skip; the frontend renders the localized title and fix text keyed on that +id + status. +""" + +import asyncio +import logging + +from backend.app.models.virtual_printer import VirtualPrinter +from backend.app.schemas.printer import DiagnosticCheck +from backend.app.schemas.virtual_printer import VPDiagnosticResult + +logger = logging.getLogger(__name__) + +# Server-mode listening ports — see virtual_printer/manager.py start_server(). +PORT_FTPS = 990 # implicit FTPS — slicer file upload +PORT_MQTT = 8883 # MQTT over TLS — control + status +PORT_BIND = 3002 # bind/detect (TLS) — slicer discovery handshake + +_PORT_PROBE_TIMEOUT = 2.0 + + +async def _check_port(ip: str, port: int, timeout: float = _PORT_PROBE_TIMEOUT) -> bool: + """Test TCP connectivity to ip:port. Returns True if something is listening.""" + try: + _reader, writer = await asyncio.wait_for(asyncio.open_connection(ip, port), timeout=timeout) + writer.close() + try: + await writer.wait_closed() + except Exception: + pass + return True + except Exception: + return False + + +async def run_vp_diagnostic(vp: VirtualPrinter, instance) -> VPDiagnosticResult: + """Run setup checks for a virtual printer. + + Args: + vp: The virtual printer DB row. + instance: The running ``VirtualPrinterInstance`` from the manager, or + ``None`` if the VP is not currently instantiated. + """ + checks: list[DiagnosticCheck] = [] + is_proxy = vp.mode == "proxy" + running = bool(instance and instance.is_running) + + # --- VP enabled --- + checks.append(DiagnosticCheck(id="enabled", status="pass" if vp.enabled else "fail")) + + # --- Instance running --- + if not vp.enabled: + checks.append(DiagnosticCheck(id="running", status="skip")) + else: + checks.append(DiagnosticCheck(id="running", status="pass" if running else "fail")) + + # --- Bind interface still exists --- + # A bind IP picked weeks ago can vanish after a Docker restart or a router + # handing out a different lease — the VP then binds nothing and is invisible. + if not vp.bind_ip: + checks.append(DiagnosticCheck(id="bind_interface", status="fail")) + else: + from backend.app.services.network_utils import find_interface_for_ip + + iface = find_interface_for_ip(vp.bind_ip) + checks.append( + DiagnosticCheck( + id="bind_interface", + status="pass" if iface else "fail", + params={"bind_ip": vp.bind_ip}, + ) + ) + + # --- Access code (non-proxy modes only) --- + if is_proxy: + checks.append(DiagnosticCheck(id="access_code", status="skip")) + else: + checks.append(DiagnosticCheck(id="access_code", status="pass" if vp.access_code else "fail")) + + # --- Target printer (proxy mode only) --- + if not is_proxy: + checks.append(DiagnosticCheck(id="target_printer", status="skip")) + elif not vp.target_printer_id: + checks.append(DiagnosticCheck(id="target_printer", status="fail")) + else: + from backend.app.services.printer_manager import printer_manager + + state = printer_manager.get_status(vp.target_printer_id) + online = bool(state and state.connected) + # A configured-but-offline target degrades proxying but isn't a setup + # error on the VP's side — warn rather than fail. + checks.append(DiagnosticCheck(id="target_printer", status="pass" if online else "warn")) + + # --- Service ports actually listening on the bind IP --- + # The decisive check: a service object can exist while its socket never + # bound (port already in use, permission denied) because start errors are + # logged and swallowed. Probe the bind IP directly. + bind_ip = vp.bind_ip + if not running or not bind_ip: + for cid, port in (("port_ftps", PORT_FTPS), ("port_mqtt", PORT_MQTT), ("port_bind", PORT_BIND)): + checks.append(DiagnosticCheck(id=cid, status="skip", params={"port": port})) + elif is_proxy: + # Proxy mode listens on dynamic ports reported by the proxy manager, + # and runs no bind/detect server. + proxy_status = instance.get_status().get("proxy", {}) + ftp_port = proxy_status.get("ftp_port") + mqtt_port = proxy_status.get("mqtt_port") + ftp_ok = await _check_port(bind_ip, ftp_port) if ftp_port else False + mqtt_ok = await _check_port(bind_ip, mqtt_port) if mqtt_port else False + checks.append( + DiagnosticCheck( + id="port_ftps", + status="pass" if ftp_ok else "fail", + params={"port": ftp_port or PORT_FTPS}, + ) + ) + checks.append( + DiagnosticCheck( + id="port_mqtt", + status="pass" if mqtt_ok else "fail", + params={"port": mqtt_port or PORT_MQTT}, + ) + ) + checks.append(DiagnosticCheck(id="port_bind", status="skip", params={"port": PORT_BIND})) + else: + ftp_ok, mqtt_ok, bind_ok = await asyncio.gather( + _check_port(bind_ip, PORT_FTPS), + _check_port(bind_ip, PORT_MQTT), + _check_port(bind_ip, PORT_BIND), + ) + checks.append(DiagnosticCheck(id="port_ftps", status="pass" if ftp_ok else "fail", params={"port": PORT_FTPS})) + checks.append(DiagnosticCheck(id="port_mqtt", status="pass" if mqtt_ok else "fail", params={"port": PORT_MQTT})) + checks.append(DiagnosticCheck(id="port_bind", status="pass" if bind_ok else "fail", params={"port": PORT_BIND})) + + # --- TLS certificate --- + # When running, the cert chain must exist on disk for the slicer's TLS + # handshake to succeed. This is a pass/fail on the file; the localized + # detail text reminds the user to import the CA into the slicer. + if not running: + checks.append(DiagnosticCheck(id="certificate", status="skip")) + else: + cert_ok = bool(instance and instance.cert_path.exists()) + checks.append(DiagnosticCheck(id="certificate", status="pass" if cert_ok else "fail")) + + statuses = {c.status for c in checks} + if "fail" in statuses: + overall = "problems" + elif "warn" in statuses: + overall = "warnings" + else: + overall = "ok" + + return VPDiagnosticResult( + vp_id=vp.id, + vp_name=vp.name, + mode=vp.mode, + overall=overall, + checks=checks, + ) diff --git a/backend/app/services/virtual_printer/manager.py b/backend/app/services/virtual_printer/manager.py index 991dc2c65..14f7c6124 100644 --- a/backend/app/services/virtual_printer/manager.py +++ b/backend/app/services/virtual_printer/manager.py @@ -877,6 +877,18 @@ class VirtualPrinterManager: """Inject the global printer_manager so non-proxy VPs can mirror their target's MQTT stream.""" self._printer_manager = printer_manager + def get_ca_certificate_info(self) -> dict: + """Return the shared virtual-printer CA certificate for slicer-trust import. + + The CA is shared by every VP (one import covers all of them). It is + generated on demand here if no VP has triggered cert generation yet, + so the "copy/download certificate" UI works even before the first VP + is enabled. + """ + certs_dir = self._base_dir / "certs" + cert_service = CertificateService(cert_dir=certs_dir, shared_ca_dir=certs_dir) + return cert_service.get_ca_certificate_info() + @property def is_enabled(self) -> bool: """Check if any virtual printer is running.""" diff --git a/backend/tests/integration/test_virtual_printer_api.py b/backend/tests/integration/test_virtual_printer_api.py index 692770952..3307709f3 100644 --- a/backend/tests/integration/test_virtual_printer_api.py +++ b/backend/tests/integration/test_virtual_printer_api.py @@ -384,3 +384,58 @@ class TestVirtualPrinterTailscaleToggleAPI: assert enable_resp.json()["tailscale_disabled"] is False assert disable_resp.status_code == 200 assert disable_resp.json()["tailscale_disabled"] is True + + +class TestVirtualPrinterCaCertificateAPI: + """Integration tests for GET /api/v1/virtual-printers/ca-certificate.""" + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_get_ca_certificate_returns_pem(self, async_client: AsyncClient): + """The shared CA certificate is returned as PEM with identifying metadata.""" + response = await async_client.get("/api/v1/virtual-printers/ca-certificate") + + assert response.status_code == 200 + result = response.json() + assert result["pem"].startswith("-----BEGIN CERTIFICATE-----") + assert "PRIVATE KEY" not in result["pem"] # never expose the CA key + assert len(result["fingerprint_sha256"].split(":")) == 32 + assert result["not_valid_after"] + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_ca_certificate_route_precedes_vp_id_route(self, async_client: AsyncClient): + """'ca-certificate' must not be swallowed by the /{vp_id} int route.""" + response = await async_client.get("/api/v1/virtual-printers/ca-certificate") + # A 200 (not 422 from int-parsing "ca-certificate") proves route ordering. + assert response.status_code == 200 + + +class TestVirtualPrinterDiagnosticAPI: + """Integration tests for GET /api/v1/virtual-printers/{vp_id}/diagnostic.""" + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_diagnose_unknown_vp_returns_404(self, async_client: AsyncClient): + response = await async_client.get("/api/v1/virtual-printers/999999/diagnostic") + assert response.status_code == 404 + + @pytest.mark.asyncio + @pytest.mark.integration + async def test_diagnose_disabled_vp_reports_problems(self, async_client: AsyncClient): + """A freshly created (disabled) VP fails the 'enabled' check.""" + create_resp = await async_client.post( + "/api/v1/virtual-printers", + json={"name": "TestDiagVP", "mode": "immediate", "access_code": "12345678"}, + ) + assert create_resp.status_code == 200 + vp_id = create_resp.json()["id"] + + response = await async_client.get(f"/api/v1/virtual-printers/{vp_id}/diagnostic") + assert response.status_code == 200 + result = response.json() + assert result["vp_id"] == vp_id + assert result["overall"] == "problems" + by_id = {c["id"]: c["status"] for c in result["checks"]} + assert by_id["enabled"] == "fail" + assert by_id["running"] == "skip" diff --git a/backend/tests/unit/services/test_vp_diagnostic.py b/backend/tests/unit/services/test_vp_diagnostic.py new file mode 100644 index 000000000..a410fde9b --- /dev/null +++ b/backend/tests/unit/services/test_vp_diagnostic.py @@ -0,0 +1,167 @@ +"""Unit tests for the virtual printer setup diagnostic.""" + +import tempfile +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import AsyncMock, patch + +import pytest + +from backend.app.services.virtual_printer.certificate import CertificateService +from backend.app.services.virtual_printer.diagnostic import run_vp_diagnostic + +_DIAG = "backend.app.services.virtual_printer.diagnostic._check_port" +_FIND_IFACE = "backend.app.services.network_utils.find_interface_for_ip" + + +def _vp(**overrides): + """A virtual-printer DB row stand-in with sensible healthy defaults.""" + base = { + "id": 1, + "name": "Test VP", + "mode": "immediate", + "enabled": True, + "bind_ip": "192.168.1.50", + "access_code": "12345678", + "target_printer_id": None, + } + base.update(overrides) + return SimpleNamespace(**base) + + +class _FakeInstance: + """Minimal VirtualPrinterInstance stand-in for the diagnostic.""" + + def __init__(self, running=True, cert_exists=True, proxy_status=None): + self.is_running = running + self._cert_exists = cert_exists + self._proxy_status = proxy_status + + @property + def cert_path(self): + return SimpleNamespace(exists=lambda: self._cert_exists) + + def get_status(self): + return {"proxy": self._proxy_status} if self._proxy_status is not None else {} + + +def _checks(result): + return {c.id: c.status for c in result.checks} + + +class TestRunVpDiagnostic: + @pytest.mark.asyncio + async def test_disabled_vp_reports_problems(self): + """A disabled VP fails the 'enabled' check; running/port checks skip.""" + result = await run_vp_diagnostic(_vp(enabled=False, bind_ip=None, access_code=None), None) + c = _checks(result) + assert result.overall == "problems" + assert c["enabled"] == "fail" + assert c["running"] == "skip" + assert c["port_ftps"] == c["port_mqtt"] == c["port_bind"] == "skip" + assert c["certificate"] == "skip" + + @pytest.mark.asyncio + async def test_running_server_vp_all_pass(self): + """Enabled + running + every port listening + cert present → overall ok.""" + with ( + patch(_DIAG, AsyncMock(return_value=True)), + patch(_FIND_IFACE, return_value={"name": "eth0", "ip": "192.168.1.50"}), + ): + result = await run_vp_diagnostic(_vp(), _FakeInstance()) + c = _checks(result) + assert result.overall == "ok" + assert c["enabled"] == "pass" + assert c["running"] == "pass" + assert c["bind_interface"] == "pass" + assert c["access_code"] == "pass" + assert c["target_printer"] == "skip" # not proxy mode + assert c["port_ftps"] == c["port_mqtt"] == c["port_bind"] == "pass" + assert c["certificate"] == "pass" + + @pytest.mark.asyncio + async def test_port_not_listening_is_a_problem(self): + """A service object can exist while its socket never bound — the probe + is what catches it, so a dead port must surface as a failure.""" + with ( + patch(_DIAG, AsyncMock(return_value=False)), + patch(_FIND_IFACE, return_value={"name": "eth0", "ip": "192.168.1.50"}), + ): + result = await run_vp_diagnostic(_vp(), _FakeInstance()) + c = _checks(result) + assert result.overall == "problems" + assert c["port_ftps"] == c["port_mqtt"] == c["port_bind"] == "fail" + + @pytest.mark.asyncio + async def test_stale_bind_ip_fails_interface_check(self): + """A bind IP that no longer matches any interface fails the check.""" + with ( + patch(_DIAG, AsyncMock(return_value=True)), + patch(_FIND_IFACE, return_value=None), + ): + result = await run_vp_diagnostic(_vp(), _FakeInstance()) + c = _checks(result) + assert c["bind_interface"] == "fail" + assert result.overall == "problems" + + @pytest.mark.asyncio + async def test_missing_access_code_fails_non_proxy(self): + with ( + patch(_DIAG, AsyncMock(return_value=True)), + patch(_FIND_IFACE, return_value={"name": "eth0", "ip": "192.168.1.50"}), + ): + result = await run_vp_diagnostic(_vp(access_code=None), _FakeInstance()) + assert _checks(result)["access_code"] == "fail" + + @pytest.mark.asyncio + async def test_proxy_mode_skips_access_code_and_bind_port(self): + """Proxy mode has no access code and runs no bind/detect server.""" + instance = _FakeInstance(proxy_status={"ftp_port": 3001, "mqtt_port": 3003}) + with ( + patch(_DIAG, AsyncMock(return_value=True)), + patch(_FIND_IFACE, return_value={"name": "eth0", "ip": "192.168.1.50"}), + ): + result = await run_vp_diagnostic(_vp(mode="proxy", target_printer_id=7), instance) + c = _checks(result) + assert c["access_code"] == "skip" + assert c["port_bind"] == "skip" + assert c["port_ftps"] == "pass" + assert c["port_mqtt"] == "pass" + + @pytest.mark.asyncio + async def test_proxy_without_target_fails(self): + """Proxy mode with no target printer fails the target check.""" + with ( + patch(_DIAG, AsyncMock(return_value=True)), + patch(_FIND_IFACE, return_value={"name": "eth0", "ip": "192.168.1.50"}), + ): + result = await run_vp_diagnostic( + _vp(mode="proxy", target_printer_id=None, access_code=None), _FakeInstance() + ) + c = _checks(result) + assert c["target_printer"] == "fail" + assert result.overall == "problems" + + +class TestCaCertificateInfo: + def test_get_ca_certificate_info_generates_and_returns_pem(self): + """The CA is generated on demand; the returned PEM is the public cert.""" + with tempfile.TemporaryDirectory() as d: + service = CertificateService(cert_dir=Path(d), shared_ca_dir=Path(d)) + info = service.get_ca_certificate_info() + assert info["pem"].startswith("-----BEGIN CERTIFICATE-----") + assert "-----END CERTIFICATE-----" in info["pem"] + # SHA-256 fingerprint: 32 colon-separated uppercase hex bytes. + parts = info["fingerprint_sha256"].split(":") + assert len(parts) == 32 + assert all(len(p) == 2 and p == p.upper() for p in parts) + assert info["not_valid_after"] + + def test_ca_certificate_info_is_stable_across_calls(self): + """A second call reuses the persisted CA — same fingerprint, no key leak.""" + with tempfile.TemporaryDirectory() as d: + service = CertificateService(cert_dir=Path(d), shared_ca_dir=Path(d)) + first = service.get_ca_certificate_info() + second = service.get_ca_certificate_info() + assert first["fingerprint_sha256"] == second["fingerprint_sha256"] + assert "PRIVATE KEY" not in first["pem"] diff --git a/frontend/scripts/check-i18n-parity.mjs b/frontend/scripts/check-i18n-parity.mjs index 4ad45417c..02191c02e 100644 --- a/frontend/scripts/check-i18n-parity.mjs +++ b/frontend/scripts/check-i18n-parity.mjs @@ -124,7 +124,7 @@ function isAlwaysAllowedIdentical(value) { if (/^https?:\/\//.test(value)) return true; // URL if (/^ON,\s+true,\s+1$/.test(value)) return true; // literal example "ON, true, 1" // Brand / technical names that ship verbatim everywhere. - if (/^(Bambuddy|BamBuddy|SpoolBuddy|Bambu Lab|Bambu Studio|Bambu Studio 2\.6\+|Bambu Studio sidecar URL|OrcaSlicer|OrcaSlicer sidecar URL|MakerWorld|Spoolman|\(Spoolman\)|Spoolman URL|Tailscale|GitHub|GitLab|Gitea|Forgejo|Discord|MQTT|FTP|HTTPS?|JSON|YAML|RTSP|TLS|SSL|CSRF|OIDC|SSO|SSO \/ OIDC|LDAP|TOTP|2FA|MFA|API|AMS|CRC|SHA256|kWh|MB|GB|KB|RGBA?|HSL|RGB|UTC|ISO|UI|HTTP|HTTP Method|H2D|H2D Pro|X1C|X1E|P1S|P1P|A1|A1 Mini|H2C|N3F|N3S|PETG|PLA|ABS|PA|TPU|PEI|PA-CF|PVA|HIPS|ASA|PC|PETG-HF|G\.code|G-code|gcode|cm³|°C|°F|GCODE|SOURCE|ntfy|Pushover|Telegram|Webhook|Webhook URL|Home Assistant|Home Assistant URL|CallMeBot\/WhatsApp|Bambuddy URL|Cool Plate|Cool Plate SuperTack|Engineering Plate|High Temp Plate|Smooth PEI Plate|Textured PEI Plate|Ext-L|Ext-R|ISO \(YYYY-MM-DD\))$/.test(value)) return true; + if (/^(Bambuddy|BamBuddy|SpoolBuddy|Bambu Lab|Bambu Studio|Bambu Studio 2\.6\+|Bambu Studio sidecar URL|OrcaSlicer|OrcaSlicer sidecar URL|MakerWorld|Spoolman|\(Spoolman\)|Spoolman URL|Tailscale|GitHub|GitLab|Gitea|Forgejo|Discord|MQTT|FTP|HTTPS?|JSON|YAML|RTSP|TLS|SSL|CSRF|OIDC|SSO|SSO \/ OIDC|LDAP|TOTP|2FA|MFA|API|AMS|CRC|SHA256|SHA-256|kWh|MB|GB|KB|RGBA?|HSL|RGB|UTC|ISO|UI|HTTP|HTTP Method|H2D|H2D Pro|X1C|X1E|P1S|P1P|A1|A1 Mini|H2C|N3F|N3S|PETG|PLA|ABS|PA|TPU|PEI|PA-CF|PVA|HIPS|ASA|PC|PETG-HF|G\.code|G-code|gcode|cm³|°C|°F|GCODE|SOURCE|ntfy|Pushover|Telegram|Webhook|Webhook URL|Home Assistant|Home Assistant URL|CallMeBot\/WhatsApp|Bambuddy URL|Cool Plate|Cool Plate SuperTack|Engineering Plate|High Temp Plate|Smooth PEI Plate|Textured PEI Plate|Ext-L|Ext-R|ISO \(YYYY-MM-DD\))$/.test(value)) return true; return false; } diff --git a/frontend/src/__tests__/components/VirtualPrinterDiagnosticModal.test.tsx b/frontend/src/__tests__/components/VirtualPrinterDiagnosticModal.test.tsx new file mode 100644 index 000000000..65a0e7bb7 --- /dev/null +++ b/frontend/src/__tests__/components/VirtualPrinterDiagnosticModal.test.tsx @@ -0,0 +1,75 @@ +/** + * Tests for the VirtualPrinterDiagnosticModal component. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { http, HttpResponse } from 'msw'; +import { server } from '../mocks/server'; +import { render } from '../utils'; +import { VirtualPrinterDiagnosticModal } from '../../components/VirtualPrinterDiagnosticModal'; +import type { VPDiagnosticResult } from '../../api/client'; + +const problemResult: VPDiagnosticResult = { + vp_id: 3, + vp_name: 'Garage VP', + mode: 'immediate', + overall: 'problems', + checks: [ + { id: 'enabled', status: 'pass', params: {} }, + { id: 'running', status: 'fail', params: {} }, + { id: 'port_mqtt', status: 'fail', params: { port: 8883 } }, + ], +}; + +/** Stub the diagnostic endpoint and count how often it is hit. */ +function setupDiagnostic(result: VPDiagnosticResult): { calls: () => number } { + let count = 0; + server.use( + http.get('*/virtual-printers/:id/diagnostic', () => { + count += 1; + return HttpResponse.json(result); + }), + ); + return { calls: () => count }; +} + +describe('VirtualPrinterDiagnosticModal', () => { + it('runs the diagnostic on mount and renders the checks', async () => { + const probe = setupDiagnostic(problemResult); + + render( {}} />); + + expect(await screen.findByText(/Found problems that explain/)).toBeInTheDocument(); + expect(probe.calls()).toBe(1); + // Per-check titles render; the port param is interpolated into the title. + expect(screen.getByText('Services running')).toBeInTheDocument(); + expect(screen.getByText('Control service (port 8883)')).toBeInTheDocument(); + }); + + it('re-runs the diagnostic when "Run again" is clicked', async () => { + const probe = setupDiagnostic(problemResult); + const user = userEvent.setup(); + + render( {}} />); + + await screen.findByText(/Found problems that explain/); + expect(probe.calls()).toBe(1); + + await user.click(screen.getByText('Run again')); + await waitFor(() => expect(probe.calls()).toBe(2)); + }); + + it('calls onClose when the Close button is clicked', async () => { + setupDiagnostic({ ...problemResult, overall: 'ok' }); + const onClose = vi.fn(); + const user = userEvent.setup(); + + render(); + + await screen.findByText(/set up correctly/); + await user.click(screen.getByText('Close')); + expect(onClose).toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/__tests__/utils/clipboard.test.ts b/frontend/src/__tests__/utils/clipboard.test.ts new file mode 100644 index 000000000..7c7fc059f --- /dev/null +++ b/frontend/src/__tests__/utils/clipboard.test.ts @@ -0,0 +1,66 @@ +/** + * Tests for the clipboard / file-download helpers. + */ + +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { copyTextToClipboard, downloadTextFile } from '../../utils/clipboard'; + +describe('copyTextToClipboard', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('uses navigator.clipboard in a secure context', async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + vi.stubGlobal('navigator', { clipboard: { writeText } }); + vi.stubGlobal('isSecureContext', true); + + const ok = await copyTextToClipboard('hello'); + + expect(ok).toBe(true); + expect(writeText).toHaveBeenCalledWith('hello'); + vi.unstubAllGlobals(); + }); + + it('falls back to execCommand when clipboard write rejects', async () => { + const writeText = vi.fn().mockRejectedValue(new Error('blocked')); + vi.stubGlobal('navigator', { clipboard: { writeText } }); + vi.stubGlobal('isSecureContext', true); + const execCommand = vi.fn().mockReturnValue(true); + // jsdom does not implement execCommand — supply it for the fallback path. + (document as unknown as { execCommand: unknown }).execCommand = execCommand; + + const ok = await copyTextToClipboard('lan-fallback'); + + expect(ok).toBe(true); + expect(execCommand).toHaveBeenCalledWith('copy'); + vi.unstubAllGlobals(); + }); +}); + +describe('downloadTextFile', () => { + it('triggers a download with the given filename', () => { + const createObjectURL = vi.fn().mockReturnValue('blob:fake'); + const revokeObjectURL = vi.fn(); + (URL as unknown as { createObjectURL: unknown }).createObjectURL = createObjectURL; + (URL as unknown as { revokeObjectURL: unknown }).revokeObjectURL = revokeObjectURL; + + let clickedHref = ''; + let clickedDownload = ''; + const clickSpy = vi + .spyOn(HTMLAnchorElement.prototype, 'click') + .mockImplementation(function (this: HTMLAnchorElement) { + clickedHref = this.href; + clickedDownload = this.download; + }); + + downloadTextFile('cert-body', 'bambuddy-virtual-printer-ca.crt', 'application/x-pem-file'); + + expect(clickSpy).toHaveBeenCalled(); + expect(clickedHref).toContain('blob:fake'); + expect(clickedDownload).toBe('bambuddy-virtual-printer-ca.crt'); + expect(revokeObjectURL).toHaveBeenCalledWith('blob:fake'); + + clickSpy.mockRestore(); + }); +}); diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index a57c6c242..82d3d170e 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -6383,8 +6383,47 @@ export const multiVirtualPrinterApi = { getTailscaleStatus: () => request('/virtual-printers/tailscale-status'), + + getCaCertificate: () => + request('/virtual-printers/ca-certificate'), + + diagnose: (id: number) => + request(`/virtual-printers/${id}/diagnostic`), }; +/** The shared CA certificate every virtual printer presents — imported once + * into the slicer's trust store. Only the public certificate is returned. */ +export interface VPCaCertificate { + pem: string; + fingerprint_sha256: string; + not_valid_after: string; +} + +export type VPDiagnosticStatus = 'pass' | 'fail' | 'warn' | 'skip'; + +export interface VPDiagnosticCheck { + id: + | 'enabled' + | 'running' + | 'bind_interface' + | 'access_code' + | 'target_printer' + | 'port_ftps' + | 'port_mqtt' + | 'port_bind' + | 'certificate'; + status: VPDiagnosticStatus; + params: Record; +} + +export interface VPDiagnosticResult { + vp_id: number; + vp_name: string; + mode: string; + overall: 'ok' | 'warnings' | 'problems'; + checks: VPDiagnosticCheck[]; +} + export interface TailscaleStatusResponse { available: boolean; fqdn: string; diff --git a/frontend/src/components/VirtualPrinterCard.tsx b/frontend/src/components/VirtualPrinterCard.tsx index a444a0106..96c05da76 100644 --- a/frontend/src/components/VirtualPrinterCard.tsx +++ b/frontend/src/components/VirtualPrinterCard.tsx @@ -3,14 +3,16 @@ import { useTranslation } from 'react-i18next'; import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query'; import { Loader2, Check, AlertTriangle, Eye, EyeOff, Info, - ChevronDown, ChevronRight, ArrowRightLeft, Trash2, ShieldCheck, Copy, + ChevronDown, ChevronRight, ArrowRightLeft, Trash2, ShieldCheck, Copy, Stethoscope, } from 'lucide-react'; import { api, multiVirtualPrinterApi } from '../api/client'; import type { VirtualPrinterConfig } from '../api/client'; import { Card, CardContent } from './Card'; import { Button } from './Button'; import { ConfirmModal } from './ConfirmModal'; +import { VirtualPrinterDiagnosticModal } from './VirtualPrinterDiagnosticModal'; import { useToast } from '../contexts/ToastContext'; +import { copyTextToClipboard } from '../utils/clipboard'; type LocalMode = 'immediate' | 'review' | 'print_queue' | 'proxy'; @@ -48,6 +50,7 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps) const [showAccessCode, setShowAccessCode] = useState(false); const [pendingAction, setPendingAction] = useState(null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [showDiagnostic, setShowDiagnostic] = useState(false); const [fqdnCopied, setFqdnCopied] = useState(false); // Host-level Tailscale identity (same for every VP) — shown inline on the card when @@ -66,32 +69,7 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps) e.stopPropagation(); const fqdn = tailscaleFqdn; if (!fqdn) return; - let ok = false; - // Modern API — only works in secure contexts (HTTPS / localhost). - if (navigator.clipboard && window.isSecureContext) { - try { - await navigator.clipboard.writeText(fqdn); - ok = true; - } catch { - // fall through to legacy - } - } - // Legacy fallback for HTTP (common when Bambuddy is reached over LAN / tailnet IP). - if (!ok) { - const ta = document.createElement('textarea'); - ta.value = fqdn; - ta.style.position = 'fixed'; - ta.style.opacity = '0'; - document.body.appendChild(ta); - try { - ta.select(); - ok = document.execCommand('copy'); - } catch { - ok = false; - } finally { - if (ta.parentNode) ta.parentNode.removeChild(ta); - } - } + const ok = await copyTextToClipboard(fqdn); if (ok) { setFqdnCopied(true); showToast(t('printers.copied')); @@ -298,6 +276,13 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps) onKeyDown={(e) => e.key === 'Enter' && handleNameChange()} className="flex-1 text-sm text-white bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-md px-3 py-1.5 focus:border-bambu-green focus:outline-none" /> + + + +
+ {diagnose.isPending && ( +
+ + {t('vpDiagnostic.running')} +
+ )} + + {diagnose.isError && ( +
+ {t('vpDiagnostic.runFailed', { error: (diagnose.error as Error).message })} +
+ )} + + {result && ( +
+
    {result.checks.map(renderCheck)}
+
+ {t(`vpDiagnostic.overall.${result.overall}`)} +
+
+ )} +
+ +
+ + +
+ + + ); +} diff --git a/frontend/src/components/VirtualPrinterList.tsx b/frontend/src/components/VirtualPrinterList.tsx index 70ed8f1ca..1c98f1d16 100644 --- a/frontend/src/components/VirtualPrinterList.tsx +++ b/frontend/src/components/VirtualPrinterList.tsx @@ -1,12 +1,13 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { Loader2, Plus, Printer, ExternalLink, AlertTriangle, Info, FileText } from 'lucide-react'; +import { Loader2, Plus, Printer, ExternalLink, AlertTriangle, Info, FileText, ShieldCheck, Copy, Check, Download } from 'lucide-react'; import { multiVirtualPrinterApi, virtualPrinterApi } from '../api/client'; import { Card, CardContent } from './Card'; import { Button } from './Button'; import { Toggle } from './Toggle'; import { useToast } from '../contexts/ToastContext'; +import { copyTextToClipboard, downloadTextFile } from '../utils/clipboard'; import { VirtualPrinterCard } from './VirtualPrinterCard'; import { VirtualPrinterAddDialog } from './VirtualPrinterAddDialog'; @@ -41,6 +42,32 @@ export function VirtualPrinterList() { const useFilename = globalSettings?.archive_name_source === 'filename'; + // Shared CA certificate — the slicer imports it once to trust every VP's + // TLS connection. Generated on demand by the backend, never changes. + const { data: caCert } = useQuery({ + queryKey: ['vp-ca-certificate'], + queryFn: multiVirtualPrinterApi.getCaCertificate, + staleTime: Infinity, + }); + const [caCopied, setCaCopied] = useState(false); + + const handleCopyCert = async () => { + if (!caCert) return; + const ok = await copyTextToClipboard(caCert.pem); + if (ok) { + setCaCopied(true); + showToast(t('virtualPrinter.caCert.copied')); + setTimeout(() => setCaCopied(false), 2000); + } else { + showToast(t('virtualPrinter.toast.copyFailed'), 'error'); + } + }; + + const handleDownloadCert = () => { + if (!caCert) return; + downloadTextFile(caCert.pem, 'bambuddy-virtual-printer-ca.crt', 'application/x-pem-file'); + }; + if (isLoading) { return ( @@ -96,37 +123,88 @@ export function VirtualPrinterList() { - {/* Global VP behavior settings */} - - -
- -
-
-

- {t('virtualPrinter.archiveNameSource.title')} -

-
- - {t('virtualPrinter.archiveNameSource.metadata')} - - archiveNameSourceMutation.mutate(checked ? 'filename' : 'metadata')} - disabled={archiveNameSourceMutation.isPending} - /> - - {t('virtualPrinter.archiveNameSource.filename')} - + {/* Global VP behavior settings — two side-by-side cards, not full width */} +
+ {/* Slicer CA certificate — shared by every VP, imported into the + slicer's trust store once instead of fetching it from the CLI. */} + + +
+ +
+
+

+ {t('virtualPrinter.caCert.title')} +

+
+ + +
+

+ {t('virtualPrinter.caCert.description')} +

+ {caCert && ( +

+ {t('virtualPrinter.caCert.fingerprint')}: {caCert.fingerprint_sha256} +

+ )}
-

- {t('virtualPrinter.archiveNameSource.description')} -

-
- - + + + + {/* Archive name source */} + + +
+ +
+
+

+ {t('virtualPrinter.archiveNameSource.title')} +

+
+ + {t('virtualPrinter.archiveNameSource.metadata')} + + archiveNameSourceMutation.mutate(checked ? 'filename' : 'metadata')} + disabled={archiveNameSourceMutation.isPending} + /> + + {t('virtualPrinter.archiveNameSource.filename')} + +
+
+

+ {t('virtualPrinter.archiveNameSource.description')} +

+
+
+
+
+
{/* Header with add button */}
diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 04a01336e..98b4cd543 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -4364,6 +4364,14 @@ export default { metadata: 'Metadaten', filename: 'Dateiname', }, + caCert: { + title: 'Slicer-Zertifikat', + description: 'Virtuelle Drucker verwenden ein TLS-Zertifikat, das von der Bambuddy-CA signiert ist. Importieren Sie dieses CA-Zertifikat einmalig in den Vertrauensspeicher Ihres Slicers, damit er die Verbindung akzeptiert — kein Abrufen über die Kommandozeile mehr nötig.', + copy: 'Kopieren', + copied: 'Kopiert', + download: 'Herunterladen', + fingerprint: 'SHA-256', + }, howItWorks: { title: 'So funktioniert es', step1: 'Im selben LAN erscheinen virtuelle Drucker automatisch in deinem Slicer (Bambu Studio / OrcaSlicer). Aus anderen Netzwerken füge sie manuell per IP-Adresse und Zugangscode hinzu.', @@ -5526,6 +5534,59 @@ export default { }, }, + vpDiagnostic: { + title: 'Einrichtungsprüfung — {{name}}', + runButton: 'Einrichtungsprüfung ausführen', + running: 'Einrichtungsprüfung läuft...', + runFailed: 'Einrichtungsprüfung konnte nicht ausgeführt werden: {{error}}', + retry: 'Erneut prüfen', + overall: { + ok: 'Alle Prüfungen bestanden — dieser virtuelle Drucker ist korrekt eingerichtet.', + warnings: 'Der virtuelle Drucker sollte funktionieren, aber einige Punkte brauchen Aufmerksamkeit.', + problems: 'Es wurden Probleme gefunden, die erklären, warum der Slicer diesen virtuellen Drucker nicht sehen oder nutzen kann.', + }, + check: { + enabled: { + title: 'Virtueller Drucker aktiviert', + fail: 'Dieser virtuelle Drucker ist ausgeschaltet. Schalten Sie ihn ein, damit er erkennbar wird.', + }, + running: { + title: 'Dienste laufen', + fail: 'Der virtuelle Drucker ist aktiviert, aber seine Dienste laufen nicht. Prüfen Sie das Bambuddy-Protokoll — meist stoppt sie ein Bind-IP-Konflikt oder ein Berechtigungsfehler.', + }, + bind_interface: { + title: 'Bind-Netzwerkschnittstelle', + fail: 'Die Bind-Schnittstelle ist nicht gesetzt oder existiert auf diesem Host nicht mehr. Wählen Sie im Dropdown "Bind-Schnittstelle" eine aktuelle Schnittstelle.', + }, + access_code: { + title: 'Zugangscode gesetzt', + fail: 'Es ist kein Zugangscode gesetzt. Dem Slicer muss derselbe 8-stellige Zugangscode angegeben werden, den Sie hier festlegen.', + }, + target_printer: { + title: 'Zieldrucker', + fail: 'Es ist kein Zieldrucker ausgewählt. Der Proxy-Modus benötigt einen echten Drucker zum Weiterleiten.', + warn: 'Der Zieldrucker ist gerade offline — die Weiterleitung wird fortgesetzt, sobald er sich wieder verbindet.', + }, + port_ftps: { + title: 'Datei-Upload-Dienst (Port {{port}})', + fail: 'Auf Port {{port}} der Bind-IP lauscht nichts, daher kann der Slicer keine Dateien hochladen. Ursache ist meist ein Port-Konflikt auf dieser Schnittstelle.', + }, + port_mqtt: { + title: 'Steuerungsdienst (Port {{port}})', + fail: 'Auf Port {{port}} der Bind-IP lauscht nichts, daher kann der Slicer sich nicht verbinden oder den Status anzeigen.', + }, + port_bind: { + title: 'Erkennungsdienst (Port {{port}})', + fail: 'Auf Port {{port}} der Bind-IP lauscht nichts, daher schlägt der Erkennungs-Handshake des Slicers fehl.', + }, + certificate: { + title: 'TLS-Zertifikat', + pass: 'Zertifikat bereit. Stellen Sie sicher, dass das Bambuddy-CA-Zertifikat (oben) in den Vertrauensspeicher Ihres Slicers importiert ist.', + fail: 'Das TLS-Zertifikat für diesen virtuellen Drucker fehlt. Prüfen Sie, ob das Bambuddy-Datenverzeichnis beschreibbar ist.', + }, + }, + }, + bugReport: { title: 'Fehler melden', description: 'Beschreibung', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 3b3cb0634..a578bd2ff 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -4377,6 +4377,14 @@ export default { metadata: 'Metadata', filename: 'Filename', }, + caCert: { + title: 'Slicer certificate', + description: 'Virtual printers use a TLS certificate signed by the Bambuddy CA. Import this CA certificate into your slicer\'s trust store once so it accepts the connection — no need to copy it from the command line.', + copy: 'Copy', + copied: 'Copied', + download: 'Download', + fingerprint: 'SHA-256', + }, howItWorks: { title: 'How it works', step1: 'On the same LAN, virtual printers appear in your slicer (Bambu Studio / OrcaSlicer) automatically via discovery. From other networks, add them manually by IP address and access code.', @@ -5539,6 +5547,59 @@ export default { }, }, + vpDiagnostic: { + title: 'Setup check — {{name}}', + runButton: 'Run setup check', + running: 'Running setup check...', + runFailed: 'Could not run the setup check: {{error}}', + retry: 'Run again', + overall: { + ok: 'All checks passed — this virtual printer is set up correctly.', + warnings: 'The virtual printer should work, but some things need attention.', + problems: 'Found problems that explain why the slicer can\'t see or use this virtual printer.', + }, + check: { + enabled: { + title: 'Virtual printer enabled', + fail: 'This virtual printer is switched off. Toggle it on to make it discoverable.', + }, + running: { + title: 'Services running', + fail: 'The virtual printer is enabled but its services are not running. Check the Bambuddy log — a bind IP conflict or a permission error usually stops them.', + }, + bind_interface: { + title: 'Bind network interface', + fail: 'The bind interface is not set, or no longer exists on this host. Pick a current interface in the Bind Interface dropdown.', + }, + access_code: { + title: 'Access code set', + fail: 'No access code is set. The slicer must be given the same 8-character access code you set here.', + }, + target_printer: { + title: 'Target printer', + fail: 'No target printer is selected. Proxy mode needs a real printer to forward to.', + warn: 'The target printer is offline right now — proxying will resume once it reconnects.', + }, + port_ftps: { + title: 'File-upload service (port {{port}})', + fail: 'Nothing is listening on port {{port}} of the bind IP, so the slicer cannot upload files. A port conflict on this interface is the usual cause.', + }, + port_mqtt: { + title: 'Control service (port {{port}})', + fail: 'Nothing is listening on port {{port}} of the bind IP, so the slicer cannot connect or show status.', + }, + port_bind: { + title: 'Discovery service (port {{port}})', + fail: 'Nothing is listening on port {{port}} of the bind IP, so the slicer\'s discovery handshake fails.', + }, + certificate: { + title: 'TLS certificate', + pass: 'Certificate ready. Make sure the Bambuddy CA certificate (above) is imported into your slicer\'s trust store.', + fail: 'The TLS certificate for this virtual printer is missing. Check that the Bambuddy data directory is writable.', + }, + }, + }, + bugReport: { title: 'Report a Bug', description: 'Description', diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts index 12367fd3e..370f0769b 100644 --- a/frontend/src/i18n/locales/es.ts +++ b/frontend/src/i18n/locales/es.ts @@ -4373,6 +4373,14 @@ export default { metadata: 'Metadatos', filename: 'Nombre de archivo', }, + caCert: { + title: 'Certificado del laminador', + description: 'Las impresoras virtuales usan un certificado TLS firmado por la CA de Bambuddy. Importe este certificado de CA en el almacén de confianza de su laminador una sola vez para que acepte la conexión — sin necesidad de copiarlo desde la línea de comandos.', + copy: 'Copiar', + copied: 'Copiado', + download: 'Descargar', + fingerprint: 'SHA-256', + }, howItWorks: { title: 'Cómo funciona', step1: 'En la misma LAN, las impresoras virtuales aparecen automáticamente en su laminador (Bambu Studio / OrcaSlicer) mediante detección. Desde otras redes, añádalas manualmente por dirección IP y código de acceso.', @@ -5535,6 +5543,59 @@ export default { }, }, + vpDiagnostic: { + title: 'Comprobación de configuración — {{name}}', + runButton: 'Ejecutar comprobación de configuración', + running: 'Ejecutando comprobación de configuración...', + runFailed: 'No se pudo ejecutar la comprobación de configuración: {{error}}', + retry: 'Volver a comprobar', + overall: { + ok: 'Todas las comprobaciones se superaron — esta impresora virtual está configurada correctamente.', + warnings: 'La impresora virtual debería funcionar, pero algunas cosas requieren atención.', + problems: 'Se encontraron problemas que explican por qué el laminador no puede ver ni usar esta impresora virtual.', + }, + check: { + enabled: { + title: 'Impresora virtual habilitada', + fail: 'Esta impresora virtual está apagada. Actívela para que sea detectable.', + }, + running: { + title: 'Servicios en ejecución', + fail: 'La impresora virtual está habilitada pero sus servicios no se están ejecutando. Revise el registro de Bambuddy — normalmente los detiene un conflicto de IP de enlace o un error de permisos.', + }, + bind_interface: { + title: 'Interfaz de red de enlace', + fail: 'La interfaz de enlace no está configurada o ya no existe en este host. Elija una interfaz actual en el menú "Interfaz de enlace".', + }, + access_code: { + title: 'Código de acceso configurado', + fail: 'No hay ningún código de acceso configurado. Al laminador se le debe dar el mismo código de acceso de 8 caracteres que configura aquí.', + }, + target_printer: { + title: 'Impresora de destino', + fail: 'No hay ninguna impresora de destino seleccionada. El modo proxy necesita una impresora real a la que reenviar.', + warn: 'La impresora de destino está desconectada ahora mismo — el reenvío se reanudará cuando vuelva a conectarse.', + }, + port_ftps: { + title: 'Servicio de subida de archivos (puerto {{port}})', + fail: 'No hay nada escuchando en el puerto {{port}} de la IP de enlace, por lo que el laminador no puede subir archivos. La causa habitual es un conflicto de puerto en esta interfaz.', + }, + port_mqtt: { + title: 'Servicio de control (puerto {{port}})', + fail: 'No hay nada escuchando en el puerto {{port}} de la IP de enlace, por lo que el laminador no puede conectarse ni mostrar el estado.', + }, + port_bind: { + title: 'Servicio de detección (puerto {{port}})', + fail: 'No hay nada escuchando en el puerto {{port}} de la IP de enlace, por lo que falla el protocolo de detección del laminador.', + }, + certificate: { + title: 'Certificado TLS', + pass: 'Certificado listo. Asegúrese de que el certificado de CA de Bambuddy (arriba) esté importado en el almacén de confianza de su laminador.', + fail: 'Falta el certificado TLS de esta impresora virtual. Compruebe que el directorio de datos de Bambuddy tenga permisos de escritura.', + }, + }, + }, + bugReport: { title: 'Informar de un error', description: 'Descripción', diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index 69ede0021..69de426ec 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -4411,6 +4411,14 @@ export default { metadata: 'Métadonnées', filename: 'Nom de fichier', }, + caCert: { + title: 'Certificat du slicer', + description: 'Les imprimantes virtuelles utilisent un certificat TLS signé par l\'autorité de certification Bambuddy. Importez ce certificat CA dans le magasin de confiance de votre slicer une seule fois pour qu\'il accepte la connexion — plus besoin de le copier depuis la ligne de commande.', + copy: 'Copier', + copied: 'Copié', + download: 'Télécharger', + fingerprint: 'SHA-256', + }, }, // Model Viewer @@ -5516,6 +5524,59 @@ export default { }, }, + vpDiagnostic: { + title: 'Vérification de configuration — {{name}}', + runButton: 'Lancer la vérification', + running: 'Vérification de configuration en cours...', + runFailed: 'Impossible de lancer la vérification de configuration : {{error}}', + retry: 'Relancer', + overall: { + ok: 'Toutes les vérifications ont réussi — cette imprimante virtuelle est correctement configurée.', + warnings: 'L\'imprimante virtuelle devrait fonctionner, mais certains points nécessitent votre attention.', + problems: 'Des problèmes ont été trouvés qui expliquent pourquoi le slicer ne voit pas ou ne peut pas utiliser cette imprimante virtuelle.', + }, + check: { + enabled: { + title: 'Imprimante virtuelle activée', + fail: 'Cette imprimante virtuelle est désactivée. Activez-la pour qu\'elle soit détectable.', + }, + running: { + title: 'Services en cours d\'exécution', + fail: 'L\'imprimante virtuelle est activée mais ses services ne sont pas en cours d\'exécution. Consultez le journal Bambuddy — un conflit d\'IP de liaison ou une erreur de permission les arrête généralement.', + }, + bind_interface: { + title: 'Interface réseau de liaison', + fail: 'L\'interface de liaison n\'est pas définie ou n\'existe plus sur cet hôte. Choisissez une interface actuelle dans la liste "Interface de liaison".', + }, + access_code: { + title: 'Code d\'accès défini', + fail: 'Aucun code d\'accès n\'est défini. Le slicer doit recevoir le même code d\'accès à 8 caractères que celui que vous définissez ici.', + }, + target_printer: { + title: 'Imprimante cible', + fail: 'Aucune imprimante cible n\'est sélectionnée. Le mode proxy a besoin d\'une vraie imprimante vers laquelle transférer.', + warn: 'L\'imprimante cible est hors ligne pour le moment — le transfert reprendra dès qu\'elle se reconnectera.', + }, + port_ftps: { + title: 'Service de téléversement de fichiers (port {{port}})', + fail: 'Rien n\'écoute sur le port {{port}} de l\'IP de liaison, le slicer ne peut donc pas téléverser de fichiers. La cause habituelle est un conflit de port sur cette interface.', + }, + port_mqtt: { + title: 'Service de contrôle (port {{port}})', + fail: 'Rien n\'écoute sur le port {{port}} de l\'IP de liaison, le slicer ne peut donc pas se connecter ni afficher l\'état.', + }, + port_bind: { + title: 'Service de détection (port {{port}})', + fail: 'Rien n\'écoute sur le port {{port}} de l\'IP de liaison, la poignée de main de détection du slicer échoue donc.', + }, + certificate: { + title: 'Certificat TLS', + pass: 'Certificat prêt. Assurez-vous que le certificat CA Bambuddy (ci-dessus) est importé dans le magasin de confiance de votre slicer.', + fail: 'Le certificat TLS de cette imprimante virtuelle est manquant. Vérifiez que le répertoire de données Bambuddy est accessible en écriture.', + }, + }, + }, + bugReport: { title: 'Signaler un bug', description: 'Description', diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index 12c8bcffa..c9d3c87f7 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -4410,6 +4410,14 @@ export default { metadata: 'Metadati', filename: 'Nome file', }, + caCert: { + title: 'Certificato dello slicer', + description: 'Le stampanti virtuali usano un certificato TLS firmato dalla CA di Bambuddy. Importa questo certificato CA nell\'archivio attendibile del tuo slicer una sola volta affinché accetti la connessione — senza doverlo copiare dalla riga di comando.', + copy: 'Copia', + copied: 'Copiato', + download: 'Scarica', + fingerprint: 'SHA-256', + }, }, // Model Viewer @@ -5515,6 +5523,59 @@ export default { }, }, + vpDiagnostic: { + title: 'Controllo configurazione — {{name}}', + runButton: 'Esegui controllo configurazione', + running: 'Controllo configurazione in corso...', + runFailed: 'Impossibile eseguire il controllo della configurazione: {{error}}', + retry: 'Esegui di nuovo', + overall: { + ok: 'Tutti i controlli superati — questa stampante virtuale è configurata correttamente.', + warnings: 'La stampante virtuale dovrebbe funzionare, ma alcuni aspetti richiedono attenzione.', + problems: 'Sono stati trovati problemi che spiegano perché lo slicer non riesce a vedere o usare questa stampante virtuale.', + }, + check: { + enabled: { + title: 'Stampante virtuale abilitata', + fail: 'Questa stampante virtuale è spenta. Attivala per renderla rilevabile.', + }, + running: { + title: 'Servizi in esecuzione', + fail: 'La stampante virtuale è abilitata ma i suoi servizi non sono in esecuzione. Controlla il log di Bambuddy — di solito li ferma un conflitto di IP di binding o un errore di permessi.', + }, + bind_interface: { + title: 'Interfaccia di rete di binding', + fail: 'L\'interfaccia di binding non è impostata o non esiste più su questo host. Scegli un\'interfaccia attuale nel menu "Interfaccia di binding".', + }, + access_code: { + title: 'Codice di accesso impostato', + fail: 'Nessun codice di accesso impostato. Allo slicer deve essere fornito lo stesso codice di accesso di 8 caratteri impostato qui.', + }, + target_printer: { + title: 'Stampante di destinazione', + fail: 'Nessuna stampante di destinazione selezionata. La modalità proxy richiede una stampante reale a cui inoltrare.', + warn: 'La stampante di destinazione è offline in questo momento — l\'inoltro riprenderà quando si riconnetterà.', + }, + port_ftps: { + title: 'Servizio di caricamento file (porta {{port}})', + fail: 'Nulla è in ascolto sulla porta {{port}} dell\'IP di binding, quindi lo slicer non può caricare i file. La causa abituale è un conflitto di porta su questa interfaccia.', + }, + port_mqtt: { + title: 'Servizio di controllo (porta {{port}})', + fail: 'Nulla è in ascolto sulla porta {{port}} dell\'IP di binding, quindi lo slicer non può connettersi o mostrare lo stato.', + }, + port_bind: { + title: 'Servizio di rilevamento (porta {{port}})', + fail: 'Nulla è in ascolto sulla porta {{port}} dell\'IP di binding, quindi l\'handshake di rilevamento dello slicer fallisce.', + }, + certificate: { + title: 'Certificato TLS', + pass: 'Certificato pronto. Assicurati che il certificato CA di Bambuddy (sopra) sia importato nell\'archivio attendibile del tuo slicer.', + fail: 'Il certificato TLS per questa stampante virtuale è mancante. Verifica che la directory dei dati di Bambuddy sia scrivibile.', + }, + }, + }, + bugReport: { title: 'Segnala un bug', description: 'Descrizione', diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index f176e5981..c811f7bab 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -4422,6 +4422,14 @@ export default { metadata: 'メタデータ', filename: 'ファイル名', }, + caCert: { + title: 'スライサー証明書', + description: '仮想プリンターは Bambuddy CA が署名した TLS 証明書を使用します。この CA 証明書をスライサーの信頼ストアに一度インポートすると接続が受け入れられます — コマンドラインからコピーする必要はありません。', + copy: 'コピー', + copied: 'コピーしました', + download: 'ダウンロード', + fingerprint: 'SHA-256', + }, }, // Model Viewer @@ -5527,6 +5535,59 @@ export default { }, }, + vpDiagnostic: { + title: 'セットアップチェック — {{name}}', + runButton: 'セットアップチェックを実行', + running: 'セットアップチェックを実行中...', + runFailed: 'セットアップチェックを実行できませんでした: {{error}}', + retry: '再実行', + overall: { + ok: 'すべてのチェックに合格しました — この仮想プリンターは正しく設定されています。', + warnings: '仮想プリンターは動作するはずですが、いくつかの点に注意が必要です。', + problems: 'スライサーがこの仮想プリンターを認識または使用できない理由となる問題が見つかりました。', + }, + check: { + enabled: { + title: '仮想プリンターが有効', + fail: 'この仮想プリンターはオフです。検出可能にするにはオンに切り替えてください。', + }, + running: { + title: 'サービスが稼働中', + fail: '仮想プリンターは有効ですが、サービスが稼働していません。Bambuddy のログを確認してください — 通常はバインド IP の競合または権限エラーが原因で停止します。', + }, + bind_interface: { + title: 'バインドネットワークインターフェース', + fail: 'バインドインターフェースが設定されていないか、このホストに存在しなくなっています。「バインドインターフェース」のドロップダウンから現在のインターフェースを選択してください。', + }, + access_code: { + title: 'アクセスコードが設定済み', + fail: 'アクセスコードが設定されていません。ここで設定したものと同じ 8 文字のアクセスコードをスライサーに入力する必要があります。', + }, + target_printer: { + title: 'ターゲットプリンター', + fail: 'ターゲットプリンターが選択されていません。プロキシモードには転送先となる実際のプリンターが必要です。', + warn: 'ターゲットプリンターは現在オフラインです — 再接続されると転送が再開されます。', + }, + port_ftps: { + title: 'ファイルアップロードサービス(ポート {{port}})', + fail: 'バインド IP のポート {{port}} で待ち受けているものがないため、スライサーはファイルをアップロードできません。通常はこのインターフェースでのポート競合が原因です。', + }, + port_mqtt: { + title: '制御サービス(ポート {{port}})', + fail: 'バインド IP のポート {{port}} で待ち受けているものがないため、スライサーは接続もステータス表示もできません。', + }, + port_bind: { + title: '検出サービス(ポート {{port}})', + fail: 'バインド IP のポート {{port}} で待ち受けているものがないため、スライサーの検出ハンドシェイクが失敗します。', + }, + certificate: { + title: 'TLS 証明書', + pass: '証明書の準備ができています。Bambuddy CA 証明書(上記)がスライサーの信頼ストアにインポートされていることを確認してください。', + fail: 'この仮想プリンターの TLS 証明書がありません。Bambuddy のデータディレクトリが書き込み可能か確認してください。', + }, + }, + }, + bugReport: { title: 'バグを報告', description: '説明', diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index 7bdd5825b..6abafc5db 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/frontend/src/i18n/locales/pt-BR.ts @@ -4410,6 +4410,14 @@ export default { metadata: 'Metadados', filename: 'Nome do arquivo', }, + caCert: { + title: 'Certificado do slicer', + description: 'As impressoras virtuais usam um certificado TLS assinado pela CA do Bambuddy. Importe este certificado CA para o armazenamento de confiança do seu slicer uma única vez para que ele aceite a conexão — sem precisar copiá-lo pela linha de comando.', + copy: 'Copiar', + copied: 'Copiado', + download: 'Baixar', + fingerprint: 'SHA-256', + }, }, // Model Viewer @@ -5515,6 +5523,59 @@ export default { }, }, + vpDiagnostic: { + title: 'Verificação de configuração — {{name}}', + runButton: 'Executar verificação de configuração', + running: 'Executando verificação de configuração...', + runFailed: 'Não foi possível executar a verificação de configuração: {{error}}', + retry: 'Verificar novamente', + overall: { + ok: 'Todas as verificações passaram — esta impressora virtual está configurada corretamente.', + warnings: 'A impressora virtual deve funcionar, mas alguns pontos precisam de atenção.', + problems: 'Foram encontrados problemas que explicam por que o slicer não consegue ver ou usar esta impressora virtual.', + }, + check: { + enabled: { + title: 'Impressora virtual habilitada', + fail: 'Esta impressora virtual está desligada. Ative-a para torná-la detectável.', + }, + running: { + title: 'Serviços em execução', + fail: 'A impressora virtual está habilitada, mas seus serviços não estão em execução. Verifique o log do Bambuddy — geralmente um conflito de IP de vínculo ou um erro de permissão os interrompe.', + }, + bind_interface: { + title: 'Interface de rede de vínculo', + fail: 'A interface de vínculo não está definida ou não existe mais neste host. Escolha uma interface atual no menu "Interface de vínculo".', + }, + access_code: { + title: 'Código de acesso definido', + fail: 'Nenhum código de acesso definido. O slicer precisa receber o mesmo código de acesso de 8 caracteres que você define aqui.', + }, + target_printer: { + title: 'Impressora de destino', + fail: 'Nenhuma impressora de destino selecionada. O modo proxy precisa de uma impressora real para a qual encaminhar.', + warn: 'A impressora de destino está offline no momento — o encaminhamento será retomado quando ela se reconectar.', + }, + port_ftps: { + title: 'Serviço de upload de arquivos (porta {{port}})', + fail: 'Nada está escutando na porta {{port}} do IP de vínculo, então o slicer não consegue enviar arquivos. A causa habitual é um conflito de porta nesta interface.', + }, + port_mqtt: { + title: 'Serviço de controle (porta {{port}})', + fail: 'Nada está escutando na porta {{port}} do IP de vínculo, então o slicer não consegue conectar nem mostrar o status.', + }, + port_bind: { + title: 'Serviço de descoberta (porta {{port}})', + fail: 'Nada está escutando na porta {{port}} do IP de vínculo, então a negociação de descoberta do slicer falha.', + }, + certificate: { + title: 'Certificado TLS', + pass: 'Certificado pronto. Verifique se o certificado CA do Bambuddy (acima) está importado no armazenamento de confiança do seu slicer.', + fail: 'O certificado TLS desta impressora virtual está ausente. Verifique se o diretório de dados do Bambuddy tem permissão de escrita.', + }, + }, + }, + bugReport: { title: 'Reportar um bug', description: 'Descrição', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index f50d94b5c..b632bb8dc 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -4353,6 +4353,14 @@ export default { metadata: '元数据', filename: '文件名', }, + caCert: { + title: '切片软件证书', + description: '虚拟打印机使用由 Bambuddy CA 签发的 TLS 证书。将此 CA 证书导入切片软件的信任库一次,切片软件即可接受连接 — 无需再通过命令行获取。', + copy: '复制', + copied: '已复制', + download: '下载', + fingerprint: 'SHA-256', + }, howItWorks: { title: '工作原理', step1: '在同一局域网中,虚拟打印机会通过发现机制自动出现在您的切片软件(Bambu Studio / OrcaSlicer)中。从其他网络,通过 IP 地址和访问码手动添加。', @@ -5514,6 +5522,59 @@ export default { }, }, + vpDiagnostic: { + title: '设置检查 — {{name}}', + runButton: '运行设置检查', + running: '正在运行设置检查...', + runFailed: '无法运行设置检查:{{error}}', + retry: '重新运行', + overall: { + ok: '所有检查均已通过 — 此虚拟打印机已正确设置。', + warnings: '虚拟打印机应该可以工作,但有些方面需要注意。', + problems: '发现的问题可以解释切片软件为何无法看到或使用此虚拟打印机。', + }, + check: { + enabled: { + title: '虚拟打印机已启用', + fail: '此虚拟打印机已关闭。请将其打开以使其可被发现。', + }, + running: { + title: '服务正在运行', + fail: '虚拟打印机已启用,但其服务未运行。请检查 Bambuddy 日志 — 通常是绑定 IP 冲突或权限错误使其停止。', + }, + bind_interface: { + title: '绑定网络接口', + fail: '绑定接口未设置,或在此主机上已不存在。请在"绑定接口"下拉菜单中选择一个当前的接口。', + }, + access_code: { + title: '已设置访问码', + fail: '未设置访问码。必须向切片软件提供与您在此处设置的相同的 8 位访问码。', + }, + target_printer: { + title: '目标打印机', + fail: '未选择目标打印机。代理模式需要一台实际的打印机进行转发。', + warn: '目标打印机当前处于离线状态 — 重新连接后将恢复转发。', + }, + port_ftps: { + title: '文件上传服务(端口 {{port}})', + fail: '绑定 IP 的端口 {{port}} 上没有任何监听,因此切片软件无法上传文件。通常是此接口上的端口冲突所致。', + }, + port_mqtt: { + title: '控制服务(端口 {{port}})', + fail: '绑定 IP 的端口 {{port}} 上没有任何监听,因此切片软件无法连接或显示状态。', + }, + port_bind: { + title: '发现服务(端口 {{port}})', + fail: '绑定 IP 的端口 {{port}} 上没有任何监听,因此切片软件的发现握手会失败。', + }, + certificate: { + title: 'TLS 证书', + pass: '证书已就绪。请确保已将 Bambuddy CA 证书(上方)导入切片软件的信任库。', + fail: '此虚拟打印机的 TLS 证书缺失。请检查 Bambuddy 数据目录是否可写。', + }, + }, + }, + bugReport: { title: '报告错误', description: '描述', diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts index c843f397f..a0ce1c525 100644 --- a/frontend/src/i18n/locales/zh-TW.ts +++ b/frontend/src/i18n/locales/zh-TW.ts @@ -4353,6 +4353,14 @@ export default { metadata: '元資料', filename: '檔名', }, + caCert: { + title: '切片軟體憑證', + description: '虛擬印表機使用由 Bambuddy CA 簽發的 TLS 憑證。將此 CA 憑證匯入切片軟體的信任庫一次,切片軟體即可接受連線 — 無需再透過命令列取得。', + copy: '複製', + copied: '已複製', + download: '下載', + fingerprint: 'SHA-256', + }, howItWorks: { title: '工作原理', step1: '在同一區域網路中,虛擬印表機會透過發現機制自動出現在您的切片軟體(Bambu Studio / OrcaSlicer)中。從其他網路,透過 IP 位址和存取碼手動新增。', @@ -5514,6 +5522,59 @@ export default { }, }, + vpDiagnostic: { + title: '設定檢查 — {{name}}', + runButton: '執行設定檢查', + running: '正在執行設定檢查...', + runFailed: '無法執行設定檢查:{{error}}', + retry: '重新執行', + overall: { + ok: '所有檢查均已通過 — 此虛擬印表機已正確設定。', + warnings: '虛擬印表機應該可以運作,但有些方面需要注意。', + problems: '發現的問題可以解釋切片軟體為何無法看到或使用此虛擬印表機。', + }, + check: { + enabled: { + title: '虛擬印表機已啟用', + fail: '此虛擬印表機已關閉。請將其開啟以使其可被探索。', + }, + running: { + title: '服務正在執行', + fail: '虛擬印表機已啟用,但其服務未執行。請檢查 Bambuddy 記錄 — 通常是繫結 IP 衝突或權限錯誤使其停止。', + }, + bind_interface: { + title: '繫結網路介面', + fail: '繫結介面未設定,或在此主機上已不存在。請在「繫結介面」下拉選單中選擇一個目前的介面。', + }, + access_code: { + title: '已設定存取碼', + fail: '未設定存取碼。必須向切片軟體提供與您在此處設定的相同的 8 位存取碼。', + }, + target_printer: { + title: '目標印表機', + fail: '未選擇目標印表機。代理模式需要一台實際的印表機進行轉送。', + warn: '目標印表機目前處於離線狀態 — 重新連線後將恢復轉送。', + }, + port_ftps: { + title: '檔案上傳服務(連接埠 {{port}})', + fail: '繫結 IP 的連接埠 {{port}} 上沒有任何監聽,因此切片軟體無法上傳檔案。通常是此介面上的連接埠衝突所致。', + }, + port_mqtt: { + title: '控制服務(連接埠 {{port}})', + fail: '繫結 IP 的連接埠 {{port}} 上沒有任何監聽,因此切片軟體無法連線或顯示狀態。', + }, + port_bind: { + title: '探索服務(連接埠 {{port}})', + fail: '繫結 IP 的連接埠 {{port}} 上沒有任何監聽,因此切片軟體的探索交握會失敗。', + }, + certificate: { + title: 'TLS 憑證', + pass: '憑證已就緒。請確保已將 Bambuddy CA 憑證(上方)匯入切片軟體的信任庫。', + fail: '此虛擬印表機的 TLS 憑證遺失。請檢查 Bambuddy 資料目錄是否可寫入。', + }, + }, + }, + bugReport: { title: '報告錯誤', description: '描述', diff --git a/frontend/src/utils/clipboard.ts b/frontend/src/utils/clipboard.ts new file mode 100644 index 000000000..77ee003e7 --- /dev/null +++ b/frontend/src/utils/clipboard.ts @@ -0,0 +1,43 @@ +/** + * Copy text to the clipboard, with a fallback for non-secure contexts. + * + * Bambuddy is usually reached over plain HTTP on a LAN / tailnet IP, where + * `navigator.clipboard` is unavailable — so the hidden-textarea + execCommand + * fallback is required, not optional. Returns true if the copy succeeded. + */ +export async function copyTextToClipboard(text: string): Promise { + if (navigator.clipboard && window.isSecureContext) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + // Fall through to the legacy path. + } + } + const ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + document.body.appendChild(ta); + try { + ta.select(); + return document.execCommand('copy'); + } catch { + return false; + } finally { + if (ta.parentNode) ta.parentNode.removeChild(ta); + } +} + +/** Trigger a browser download of `text` as a file named `filename`. */ +export function downloadTextFile(text: string, filename: string, mimeType = 'text/plain'): void { + const blob = new Blob([text], { type: mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); +} diff --git a/static/assets/index-CVVS0VHp.js b/static/assets/index-BpyZgjkS.js similarity index 60% rename from static/assets/index-CVVS0VHp.js rename to static/assets/index-BpyZgjkS.js index 8ef6c0e73..f32ff4905 100644 --- a/static/assets/index-CVVS0VHp.js +++ b/static/assets/index-BpyZgjkS.js @@ -1,45 +1,45 @@ -function Zue(t,e){for(var a=0;ar[i]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(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 r(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 Mc(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var tE={exports:{}},Jv={};var kq;function Jue(){if(kq)return Jv;kq=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function a(r,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:r,key:o,ref:i!==void 0?i:null,props:s}}return Jv.Fragment=e,Jv.jsx=a,Jv.jsxs=a,Jv}var _q;function eme(){return _q||(_q=1,tE.exports=Jue()),tE.exports}var n=eme(),aE={exports:{}},Ka={};var Nq;function tme(){if(Nq)return Ka;Nq=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),a=Symbol.for("react.fragment"),r=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(Y){return Y===null||typeof Y!="object"?null:(Y=p&&Y[p]||Y["@@iterator"],typeof Y=="function"?Y:null)}var g={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},w=Object.assign,b={};function x(Y,oe,me){this.props=Y,this.context=oe,this.refs=b,this.updater=me||g}x.prototype.isReactComponent={},x.prototype.setState=function(Y,oe){if(typeof Y!="object"&&typeof Y!="function"&&Y!=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,Y,oe,"setState")},x.prototype.forceUpdate=function(Y){this.updater.enqueueForceUpdate(this,Y,"forceUpdate")};function S(){}S.prototype=x.prototype;function N(Y,oe,me){this.props=Y,this.context=oe,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(Y,oe,me){var de=me.ref;return{$$typeof:t,type:Y,key:oe,ref:de!==void 0?de:null,props:me}}function M(Y,oe){return k(Y.type,oe,Y.props)}function z(Y){return typeof Y=="object"&&Y!==null&&Y.$$typeof===t}function I(Y){var oe={"=":"=0",":":"=2"};return"$"+Y.replace(/[=:]/g,function(me){return oe[me]})}var O=/\/+/g;function L(Y,oe){return typeof Y=="object"&&Y!==null&&Y.key!=null?I(""+Y.key):oe.toString(36)}function X(Y){switch(Y.status){case"fulfilled":return Y.value;case"rejected":throw Y.reason;default:switch(typeof Y.status=="string"?Y.then(T,T):(Y.status="pending",Y.then(function(oe){Y.status==="pending"&&(Y.status="fulfilled",Y.value=oe)},function(oe){Y.status==="pending"&&(Y.status="rejected",Y.reason=oe)})),Y.status){case"fulfilled":return Y.value;case"rejected":throw Y.reason}}throw Y}function ae(Y,oe,me,de,we){var q=typeof Y;(q==="undefined"||q==="boolean")&&(Y=null);var J=!1;if(Y===null)J=!0;else switch(q){case"bigint":case"string":case"number":J=!0;break;case"object":switch(Y.$$typeof){case t:case e:J=!0;break;case u:return J=Y._init,ae(J(Y._payload),oe,me,de,we)}}if(J)return we=we(Y),J=de===""?"."+L(Y,0):de,P(we)?(me="",J!=null&&(me=J.replace(O,"$&/")+"/"),ae(we,oe,me,"",function(H){return H})):we!=null&&(z(we)&&(we=M(we,me+(we.key==null||Y&&Y.key===we.key?"":(""+we.key).replace(O,"$&/")+"/")+J)),oe.push(we)),1;J=0;var A=de===""?".":de+":";if(P(Y))for(var E=0;E>>1,pe=ae[ue];if(0>>1;uei(me,le))dei(we,me)?(ae[ue]=we,ae[de]=le,ue=de):(ae[ue]=me,ae[oe]=le,ue=oe);else if(dei(we,le))ae[ue]=we,ae[de]=le,ue=de;else break e}}return Z}function i(ae,Z){var le=ae.sortIndex-Z.sortIndex;return le!==0?le: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)r(d);else if(Z.startTime<=ae)r(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&&X(P,Z.startTime-ae)}}var T=!1,j=-1,F=5,k=-1;function M(){return b?!0:!(t.unstable_now()-kae&&M());){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)&&r(c),C(ae)}else r(c);m=a(c)}if(m!==null)Z=!0;else{var Y=a(d);Y!==null&&X(P,Y.startTime-ae),Z=!1}}break e}finally{m=null,p=le,f=!1}Z=void 0}}finally{Z?I():T=!1}}}var I;if(typeof N=="function")I=function(){N(z)};else if(typeof MessageChannel<"u"){var O=new MessageChannel,L=O.port2;O.port1.onmessage=z,I=function(){L.postMessage(null)}}else I=function(){x(z,0)};function X(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=le,e(d,ae),a(c)===null&&ae===a(d)&&(w?(S(j),j=-1):w=!0,X(P,le-ue))):(ae.sortIndex=pe,e(c,ae),g||f||(g=!0,T||(T=!0,I()))),ae},t.unstable_shouldYield=M,t.unstable_wrapCallback=function(ae){var Z=p;return function(){var le=p;p=Z;try{return ae.apply(this,arguments)}finally{p=le}}}})(iE)),iE}var Tq;function rme(){return Tq||(Tq=1,rE.exports=nme()),rE.exports}var sE={exports:{}},Gs={};var Aq;function ime(){if(Aq)return Gs;Aq=1;var t=ib();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=ime(),sE.exports}var Mq;function sme(){if(Mq)return e0;Mq=1;var t=rme(),e=ib(),a=WX();function r(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=Y(null),we=Y(null),q=Y(null),J=Y(null);function A(h,v){switch(me(q,v),me(we,h),me(de,null),v.nodeType){case 9:case 11:h=(h=v.documentElement)&&(h=h.namespaceURI)?GH(h):0;break;default:if(h=v.tagName,v=v.namespaceURI)v=GH(v),h=WH(v,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}oe(de),me(de,h)}function E(){oe(de),oe(we),oe(q)}function H(h){h.memoizedState!==null&&me(J,h);var v=de.current,_=WH(v,h.type);v!==_&&(me(we,h),me(de,_))}function W(h){we.current===h&&(oe(de),oe(we)),J.current===h&&(oe(J),Qv._currentValue=le)}var U,ie;function B(h){if(U===void 0)try{throw Error()}catch(_){var v=_.stack.trim().match(/\n( *(at )?)/);U=v&&v[1]||"",ie=-1<_.stack.indexOf(` +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(` at`)?" ()":-1<_.stack.indexOf("@")?"@unknown:0:0":""}return` -`+U+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],qe=ee[1];if(Se&&qe){var mt=Se.split(` -`),Mt=qe.split(` -`);for(V=D=0;DV||mt[D]!==Mt[V]){var Ht=` -`+mt[D].replace(" at new "," at ");return h.displayName&&Ht.includes("")&&(Ht=Ht.replace("",h.displayName)),Ht}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` +`+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(` +`),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` Error generating stack: `+D.message+` -`+D.stack}}var te=Object.prototype.hasOwnProperty,fe=t.unstable_scheduleCallback,ce=t.unstable_cancelCallback,he=t.unstable_shouldYield,$=t.unstable_requestPaint,R=t.unstable_now,ne=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,ot=Math.LN2;function Ee(h){return h>>>=0,h===0?32:31-(Ue(h)/ot|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 qe=D&134217727;return qe!==0?(D=qe&~ee,D!==0?V=Ye(D):(Se&=qe,Se!==0?V=Ye(Se):_||(_=qe&~h,_!==0&&(V=Ye(_))))):(qe=D&~ee,qe!==0?V=Ye(qe):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 rt(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 qe=h.entanglements,mt=h.expirationTimes,Mt=h.hiddenUpdates;for(_=Se&~_;0<_;){var Ht=31-Ke(_),Wt=1<"u")return null;try{return h.activeElement||h.body}catch{return h.body}}var Rn=/[\n"\\]/g;function $a(h){return h.replace(Rn,function(v){return"\\"+v.charCodeAt(0).toString(16)+" "})}function Ur(h,v,_,D,V,ee,Se,qe){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?Vn(h,Se,ca(v)):_!=null?Vn(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"),qe!=null&&typeof qe!="function"&&typeof qe!="symbol"&&typeof qe!="boolean"?h.name=""+ca(qe):h.removeAttribute("name")}function Ra(h,v,_,D,V,ee,Se,qe){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):_,qe||v===h.value||(h.value=v),h.defaultValue=v}D=D??V,D=typeof D!="function"&&typeof D!="symbol"&&!!D,h.checked=qe?h.checked:!!D,h.defaultChecked=!!D,Se!=null&&typeof Se!="function"&&typeof Se!="symbol"&&typeof Se!="boolean"&&(h.name=Se),pa(h)}function Vn(h,v,_){v==="number"&&tn(h.ownerDocument)===h||h.defaultValue===""+_||(h.defaultValue=""+_)}function br(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(yi)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 Kr=null,wd=null,Lc=null;function vi(){if(Lc)return Lc;var h,v=wd,_=v.length,D,V="value"in Kr?Kr.value:Kr.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 Lc=V.slice(h,1=Qr),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 kr(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 wi(h,v){if(Ia)return h==="compositionend"||!cl&&sa(h,v)?(h=vi(),Lc=wd=Kr=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}_=Lo(_)}}function Ro(h,v){return h&&v?h===v?!0:h&&h.nodeType===3?!1:v&&v.nodeType===3?Ro(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=tn(h.document);v instanceof h.HTMLIFrameElement;){try{var _=typeof v.contentWindow.location.href=="string"}catch{_=!1}if(_)h=v.contentWindow;else break;v=tn(h.document)}return v}function Si(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 Nd=yi&&"documentMode"in document&&11>=document.documentMode,pi=null,mn=null,jn=null,Xr=!1;function bp(h,v,_){var D=_.window===_?_.document:_.nodeType===9?_:_.ownerDocument;Xr||pi==null||pi!==tn(D)||(D=pi,"selectionStart"in D&&Si(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}),jn&&_s(jn,D)||(jn=D,D=m_(mn,"onSelect"),0>=Se,V-=Se,Pd=1<<32-Ke(v)+V|_<Za?(kn=Ca,Ca=null):kn=Ca.sibling;var En=Lt(St,Ca,jt[Za],Vt);if(En===null){Ca===null&&(Ca=kn);break}h&&Ca&&En.alternate===null&&v(St,Ca),xt=ee(En,xt,Za),Mn===null?Ea=En:Mn.sibling=En,Mn=En,Ca=kn}if(Za===jt.length)return _(St,Ca),_n&&Ou(St,Za),Ea;if(Ca===null){for(;ZaZa?(kn=Ca,Ca=null):kn=Ca.sibling;var zp=Lt(St,Ca,En.value,Vt);if(zp===null){Ca===null&&(Ca=kn);break}h&&Ca&&zp.alternate===null&&v(St,Ca),xt=ee(zp,xt,Za),Mn===null?Ea=zp:Mn.sibling=zp,Mn=zp,Ca=kn}if(En.done)return _(St,Ca),_n&&Ou(St,Za),Ea;if(Ca===null){for(;!En.done;Za++,En=jt.next())En=Wt(St,En.value,Vt),En!==null&&(xt=ee(En,xt,Za),Mn===null?Ea=En:Mn.sibling=En,Mn=En);return _n&&Ou(St,Za),Ea}for(Ca=D(Ca);!En.done;Za++,En=jt.next())En=zt(Ca,St,Za,En.value,Vt),En!==null&&(h&&En.alternate!==null&&Ca.delete(En.key===null?Za:En.key),xt=ee(En,xt,Za),Mn===null?Ea=En:Mn.sibling=En,Mn=En);return h&&Ca.forEach(function(Xue){return v(St,Xue)}),_n&&Ou(St,Za),Ea}function ar(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),Cv(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),Cv(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),ar(St,xt,jt,Vt)}if(X(jt))return va(St,xt,jt,Vt);if(I(jt)){if(Ea=I(jt),typeof Ea!="function")throw Error(r(150));return jt=Ea.call(jt),za(St,xt,jt,Vt)}if(typeof jt.then=="function")return ar(St,xt,Rk(jt),Vt);if(jt.$$typeof===N)return ar(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{Nv=0;var Ea=ar(St,xt,jt,Vt);return Mb=null,Ea}catch(Ca){if(Ca===jb||Ca===Fk)throw Ca;var Mn=dl(29,Ca,null,St.mode);return Mn.lanes=Vt,Mn.return=St,Mn}}}var Mf=IU(!0),OU=IU(!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,(zn&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),wU(h,null,_),v}return Pk(h,D,v,_),Tk(h)}function Pv(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 Tv(){if(Fj){var h=Ab;if(h!==null)throw h}}function Av(h,v,_,D){Fj=!1;var V=h.updateQueue;Sp=!1;var ee=V.firstBaseUpdate,Se=V.lastBaseUpdate,qe=V.shared.pending;if(qe!==null){V.shared.pending=null;var mt=qe,Mt=mt.next;mt.next=null,Se===null?ee=Mt:Se.next=Mt,Se=mt;var Ht=h.alternate;Ht!==null&&(Ht=Ht.updateQueue,qe=Ht.lastBaseUpdate,qe!==Se&&(qe===null?Ht.firstBaseUpdate=Mt:qe.next=Mt,Ht.lastBaseUpdate=mt))}if(ee!==null){var Wt=V.baseState;Se=0,Ht=Mt=mt=null,qe=ee;do{var Lt=qe.lane&-536870913,zt=Lt!==qe.lane;if(zt?(Sn&Lt)===Lt:(D&Lt)===Lt){Lt!==0&&Lt===Tb&&(Fj=!0),Ht!==null&&(Ht=Ht.next={lane:0,tag:qe.tag,payload:qe.payload,callback:null,next:null});e:{var va=h,za=qe;Lt=v;var ar=_;switch(za.tag){case 1:if(va=za.payload,typeof va=="function"){Wt=va.call(ar,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(ar,Wt,Lt):va,Lt==null)break e;Wt=m({},Wt,Lt);break e;case 2:Sp=!0}}Lt=qe.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:qe.tag,payload:qe.payload,callback:qe.callback,next:null},Ht===null?(Mt=Ht=zt,mt=Wt):Ht=Ht.next=zt,Se|=Lt;if(qe=qe.next,qe===null){if(qe=V.shared.pending,qe===null)break;zt=qe,qe=zt.next,zt.next=null,V.lastBaseUpdate=zt,V.shared.pending=null}}while(!0);Ht===null&&(mt=Wt),V.baseState=mt,V.firstBaseUpdate=Mt,V.lastBaseUpdate=Ht,ee===null&&(V.shared.lanes=0),Ap|=Se,h.lanes=Se,h.memoizedState=Wt}}function zU(h,v){if(typeof h!="function")throw Error(r(191,h));h.call(v)}function UU(h,v){var _=h.callbacks;if(_!==null)for(h.callbacks=null,h=0;h<_.length;h++)zU(_[h],v)}var Eb=Y(null),Ok=Y(0);function BU(h,v){h=Qu,me(Ok,h),me(Eb,v),Qu=h|v.baseLanes}function Lj(){me(Ok,Qu),me(Eb,Eb.current)}function Rj(){Qu=Ok.current,oe(Eb),oe(Ok)}var ul=Y(null),Gl=null;function Np(h){var v=h.alternate;me(hi,hi.current&1),me(ul,h),Gl===null&&(v===null||Eb.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){oe(ul),Gl===h&&(Gl=null),oe(hi)}var hi=Y(0);function zk(h){for(var v=h;v!==null;){if(v.tag===13){var _=v.memoizedState;if(_!==null&&(_=_.dehydrated,_===null||qM(_)||$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,er=null,_i=null,Uk=!1,Db=!1,Ef=!1,Bk=0,jv=0,Fb=null,Bde=0;function Zr(){throw Error(r(321))}function Oj(h,v){if(v===null)return!1;for(var _=0;_ee?ee:8;var Se=ae.T,qe={};ae.T=qe,Jj(h,!1,v,_);try{var mt=V(),Mt=ae.S;if(Mt!==null&&Mt(qe,mt),mt!==null&&typeof mt=="object"&&typeof mt.then=="function"){var Ht=Ude(mt,D);Ev(h,v,Ht,fl(h))}else Ev(h,v,D,fl(h))}catch(Wt){Ev(h,v,{then:function(){},status:"rejected",reason:Wt},fl())}finally{Z.p=ee,Se!==null&&qe.types!==null&&(Se.types=qe.types),ae.T=Se}}function Gde(){}function Xj(h,v,_,D){if(h.tag!==5)throw Error(r(476));var V=xB(h).queue;bB(h,V,v,le,_===null?Gde:function(){return yB(h),_(D)})}function xB(h){var v=h.memoizedState;if(v!==null)return v;v={memoizedState:le,baseState:le,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Hu,lastRenderedState:le},next:null};var _={};return v.next={memoizedState:_,baseState:_,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Hu,lastRenderedState:_},next:null},h.memoizedState=v,h=h.alternate,h!==null&&(h.memoizedState=v),v}function yB(h){var v=xB(h);v.next===null&&(v=h.alternate.memoizedState),Ev(h,v.next.queue,{},fl())}function Zj(){return Cs(Qv)}function vB(){return fi().memoizedState}function wB(){return fi().memoizedState}function Wde(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&&(Bo(D,v,_),Pv(D,v,_)),v={cache:Pj()},h.payload=v;return}v=v.return}}function Kde(h,v,_){var D=fl();_={lane:D,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null},Wk(h)?kB(v,_):(_=gj(h,v,_,D),_!==null&&(Bo(_,h,D),_B(_,v,D)))}function SB(h,v,_){var D=fl();Ev(h,v,_,D)}function Ev(h,v,_,D){var V={lane:D,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null};if(Wk(h))kB(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,qe=ee(Se,_);if(V.hasEagerState=!0,V.eagerState=qe,Mr(qe,Se))return Pk(h,v,V,0),mr===null&&Ck(),!1}catch{}if(_=gj(h,v,V,D),_!==null)return Bo(_,h,D),_B(_,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(r(479))}else v=gj(h,_,D,2),v!==null&&Bo(v,h,2)}function Wk(h){var v=h.alternate;return h===Ya||v!==null&&v===Ya}function kB(h,v){Db=Uk=!0;var _=h.pending;_===null?v.next=v:(v.next=_.next,_.next=v),h.pending=v}function _B(h,v,_){if((_&4194048)!==0){var D=v.lanes;D&=h.pendingLanes,_|=D,v.lanes=_,Bt(h,_)}}var Dv={readContext:Cs,use:qk,useCallback:Zr,useContext:Zr,useEffect:Zr,useImperativeHandle:Zr,useLayoutEffect:Zr,useInsertionEffect:Zr,useMemo:Zr,useReducer:Zr,useRef:Zr,useState:Zr,useDebugValue:Zr,useDeferredValue:Zr,useTransition:Zr,useSyncExternalStore:Zr,useId:Zr,useHostTransitionStatus:Zr,useFormState:Zr,useActionState:Zr,useOptimistic:Zr,useMemoCache:Zr,useCacheRefresh:Zr};Dv.useEffectEvent=Zr;var NB={readContext:Cs,use:qk,useCallback:function(h,v){return fo().memoizedState=[h,v===void 0?null:v],h},useContext:Cs,useEffect:lB,useImperativeHandle:function(h,v,_){_=_!=null?_.concat([h]):null,Vk(4194308,4,mB.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=Kde.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,_=SB.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=bB.bind(null,Ya,h.queue,!0,!1),fo().memoizedState=h,[!1,h]},useSyncExternalStore:function(h,v,_){var D=Ya,V=fo();if(_n){if(_===void 0)throw Error(r(407));_=_()}else{if(_=v(),mr===null)throw Error(r(349));(Sn&127)!==0||GU(D,v,_)}V.memoizedState=_;var ee={value:_,getSnapshot:v};return V.queue=ee,lB(KU.bind(null,D,ee,h),[h]),D.flags|=2048,Lb(9,{destroy:void 0},WU.bind(null,D,ee,_,v),null),_},useId:function(){var h=fo(),v=mr.identifierPrefix;if(_n){var _=Td,D=Pd;_=(D&~(1<<32-Ke(D)-1)).toString(32)+_,v="_"+v+"R_"+_,_=Bk++,0<_&&(v+="H"+_.toString(32)),v+="_"}else _=Bde++,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:qj,useCacheRefresh:function(){return fo().memoizedState=Wde.bind(null,Ya)},useEffectEvent:function(h){var v=fo(),_={impl:h};return v.memoizedState=_,function(){if((zn&2)!==0)throw Error(r(440));return _.impl.apply(void 0,arguments)}}},eM={readContext:Cs,use:qk,useCallback:hB,useContext:Cs,useEffect:Kj,useImperativeHandle:pB,useInsertionEffect:dB,useLayoutEffect:uB,useMemo:fB,useReducer:$k,useRef:oB,useState:function(){return $k(Hu)},useDebugValue:Qj,useDeferredValue:function(h,v){var _=fi();return gB(_,er.memoizedState,h,v)},useTransition:function(){var h=$k(Hu)[0],v=fi().memoizedState;return[typeof h=="boolean"?h:Mv(h),v]},useSyncExternalStore:VU,useId:vB,useHostTransitionStatus:Zj,useFormState:rB,useActionState:rB,useOptimistic:function(h,v){var _=fi();return XU(_,er,h,v)},useMemoCache:qj,useCacheRefresh:wB};eM.useEffectEvent=cB;var CB={readContext:Cs,use:qk,useCallback:hB,useContext:Cs,useEffect:Kj,useImperativeHandle:pB,useInsertionEffect:dB,useLayoutEffect:uB,useMemo:fB,useReducer:Vj,useRef:oB,useState:function(){return Vj(Hu)},useDebugValue:Qj,useDeferredValue:function(h,v){var _=fi();return er===null?Yj(_,h,v):gB(_,er.memoizedState,h,v)},useTransition:function(){var h=Vj(Hu)[0],v=fi().memoizedState;return[typeof h=="boolean"?h:Mv(h),v]},useSyncExternalStore:VU,useId:vB,useHostTransitionStatus:Zj,useFormState:sB,useActionState:sB,useOptimistic:function(h,v){var _=fi();return er!==null?XU(_,er,h,v):(_.baseState=h,[h,_.queue.dispatch])},useMemoCache:qj,useCacheRefresh:wB};CB.useEffectEvent=cB;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&&(Bo(v,h,D),Pv(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&&(Bo(v,h,D),Pv(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&&(Bo(v,h,_),Pv(v,h,_))}};function PB(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 TB(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 AB(h){Nk(h)}function jB(h){console.error(h)}function MB(h){Nk(h)}function Kk(h,v){try{var _=h.onUncaughtError;_(v.value,{componentStack:v.stack})}catch(D){setTimeout(function(){throw D})}}function EB(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 nM(h,v,_){return _=kp(_),_.tag=3,_.payload={element:null},_.callback=function(){Kk(h,v)},_}function DB(h){return h=kp(h),h.tag=3,h}function FB(h,v,_,D){var V=_.type.getDerivedStateFromError;if(typeof V=="function"){var ee=D.value;h.payload=function(){return V(ee)},h.callback=function(){EB(v,_,D)}}var Se=_.stateNode;Se!==null&&typeof Se.componentDidCatch=="function"&&(h.callback=function(){EB(v,_,D),typeof V!="function"&&(jp===null?jp=new Set([this]):jp.add(this));var qe=D.stack;this.componentDidCatch(D.value,{componentStack:qe!==null?qe:""})})}function Qde(h,v,_,D,V){if(_.flags|=32768,D!==null&&typeof D=="object"&&typeof D.then=="function"){if(v=_.alternate,v!==null&&Pb(v,_,V,!0),_=ul.current,_!==null){switch(_.tag){case 31:case 13:return Gl===null?s_():_.alternate===null&&Jr===0&&(Jr=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(r(435,_.tag))}return TM(h,D,V),s_(),!1}if(_n)return v=ul.current,v!==null?((v.flags&65536)===0&&(v.flags|=256),v.flags|=65536,v.lanes=V,D!==Sj&&(h=Error(r(422),{cause:D}),Sv(Hl(h,_)))):(D!==Sj&&(v=Error(r(423),{cause:D}),Sv(Hl(v,_))),h=h.current.alternate,h.flags|=65536,V&=-V,h.lanes|=V,D=Hl(D,_),V=nM(h.stateNode,D,V),Dj(h,V),Jr!==4&&(Jr=2)),!1;var ee=Error(r(520),{cause:D});if(ee=Hl(ee,_),Bv===null?Bv=[ee]:Bv.push(ee),Jr!==4&&(Jr=2),v===null)return!0;D=Hl(D,_),_=v;do{switch(_.tag){case 3:return _.flags|=65536,h=V&-V,_.lanes|=h,h=nM(_.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=DB(V),FB(V,h,_,D),Dj(_,V),!1}_=_.return}while(_!==null);return!1}var rM=Error(r(461)),Ni=!1;function Ps(h,v,_,D){v.child=h===null?OU(v,null,_,D):Mf(v,h.child,_,D)}function LB(h,v,_,D,V){_=_.render;var ee=v.ref;if("ref"in D){var Se={};for(var qe in D)qe!=="ref"&&(Se[qe]=D[qe])}else Se=D;return Pf(v),D=zj(h,v,_,Se,ee,V),qe=Uj(),h!==null&&!Ni?(Bj(h,v,V),qu(h,v,V)):(_n&&qe&&vj(v),v.flags|=1,Ps(h,v,D,V),v.child)}function RB(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,IB(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 qu(h,v,V)}return v.flags|=1,h=Iu(ee,D),h.ref=v.ref,h.return=v,v.child=h}function IB(h,v,_,D,V){if(h!==null){var ee=h.memoizedProps;if(_s(ee,D)&&h.ref===v.ref)if(Ni=!1,v.pendingProps=D=ee,mM(h,V))(h.flags&131072)!==0&&(Ni=!0);else return v.lanes=h.lanes,qu(h,v,V)}return iM(h,v,_,D,V)}function OB(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 zB(h,v,ee,_,D)}if((_&536870912)!==0)v.memoizedState={baseLanes:0,cachePool:null},h!==null&&Dk(v,ee!==null?ee.cachePool:null),ee!==null?BU(v,ee):Lj(),HU(v);else return D=v.lanes=536870912,zB(h,v,ee!==null?ee.baseLanes|_:_,_,D)}else ee!==null?(Dk(v,ee.cachePool),BU(v,ee),Cp(),v.memoizedState=null):(h!==null&&Dk(v,null),Lj(),Cp());return Ps(h,v,V,_),v.child}function Fv(h,v){return h!==null&&h.tag===22||v.stateNode!==null||(v.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),v.sibling}function zB(h,v,_,D,V){var ee=Aj();return ee=ee===null?null:{parent:ki._currentValue,pool:ee},v.memoizedState={baseLanes:_,cachePool:ee},h!==null&&Dk(v,null),Lj(),HU(v),h!==null&&Pb(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 UB(h,v,_){return Mf(v,h.child,null,_),h=Qk(v,v.pendingProps),h.flags|=2,ml(v),v.memoizedState=null,h}function Yde(h,v,_){var D=v.pendingProps,V=(v.flags&128)!==0;if(v.flags&=-129,h===null){if(_n){if(D.mode==="hidden")return h=Qk(v,D),v.lanes=536870912,Fv(null,h);if(Ij(v),(h=_r)?(h=ZH(h,Vl),h=h!==null&&h.data==="&"?h:null,h!==null&&(v.memoizedState={dehydrated:h,treeContext:xp!==null?{id:Pd,overflow:Td}:null,retryLane:536870912,hydrationErrors:null},_=kU(h),_.return=v,v.child=_,Ns=v,_r=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=UB(h,v,_);else if(v.memoizedState!==null)v.child=h.child,v.flags|=128,v=null;else throw Error(r(558));else if(Ni||Pb(h,v,_,!1),V=(_&h.childLanes)!==0,Ni||V){if(D=mr,D!==null&&(Se=gt(D,_),Se!==0&&Se!==ee.retryLane))throw ee.retryLane=Se,kf(h,Se),Bo(D,h,Se),rM;s_(),v=UB(h,v,_)}else h=ee.treeContext,_r=Wl(Se.nextSibling),Ns=v,_n=!0,yp=null,Vl=!1,h!==null&&CU(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(r(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&&!Ni?(Bj(h,v,V),qu(h,v,V)):(_n&&D&&vj(v),v.flags|=1,Ps(h,v,_,V),v.child)}function BB(h,v,_,D,V,ee){return Pf(v),v.updateQueue=null,_=$U(v,D,_,V),qU(h),D=Uj(),h!==null&&!Ni?(Bj(h,v,ee),qu(h,v,ee)):(_n&&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=kb,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):kb,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),Av(v,D,ee,V),Tv(),ee.state=v.memoizedState),typeof ee.componentDidMount=="function"&&(v.flags|=4194308),D=!0}else if(h===null){ee=v.stateNode;var qe=v.memoizedProps,mt=Df(_,qe);ee.props=mt;var Mt=ee.context,Ht=_.contextType;Se=kb,typeof Ht=="object"&&Ht!==null&&(Se=Cs(Ht));var Wt=_.getDerivedStateFromProps;Ht=typeof Wt=="function"||typeof ee.getSnapshotBeforeUpdate=="function",qe=v.pendingProps!==qe,Ht||typeof ee.UNSAFE_componentWillReceiveProps!="function"&&typeof ee.componentWillReceiveProps!="function"||(qe||Mt!==Se)&&TB(v,ee,D,Se),Sp=!1;var Lt=v.memoizedState;ee.state=Lt,Av(v,D,ee,V),Tv(),Mt=v.memoizedState,qe||Lt!==Mt||Sp?(typeof Wt=="function"&&(tM(v,_,Wt,D),Mt=v.memoizedState),(mt=Sp||PB(v,_,mt,D,Lt,Mt,Se))?(Ht||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=Mt),ee.props=D,ee.state=Mt,ee.context=Se,D=mt):(typeof ee.componentDidMount=="function"&&(v.flags|=4194308),D=!1)}else{ee=v.stateNode,Ej(h,v),Se=v.memoizedProps,Ht=Df(_,Se),ee.props=Ht,Wt=v.pendingProps,Lt=ee.context,Mt=_.contextType,mt=kb,typeof Mt=="object"&&Mt!==null&&(mt=Cs(Mt)),qe=_.getDerivedStateFromProps,(Mt=typeof qe=="function"||typeof ee.getSnapshotBeforeUpdate=="function")||typeof ee.UNSAFE_componentWillReceiveProps!="function"&&typeof ee.componentWillReceiveProps!="function"||(Se!==Wt||Lt!==mt)&&TB(v,ee,D,mt),Sp=!1,Lt=v.memoizedState,ee.state=Lt,Av(v,D,ee,V),Tv();var zt=v.memoizedState;Se!==Wt||Lt!==zt||Sp||h!==null&&h.dependencies!==null&&Mk(h.dependencies)?(typeof qe=="function"&&(tM(v,_,qe,D),zt=v.memoizedState),(Ht=Sp||PB(v,_,Ht,D,Lt,zt,mt)||h!==null&&h.dependencies!==null&&Mk(h.dependencies))?(Mt||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=Ht):(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=qu(h,v,V),h}function qB(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:EU()}}function lM(h,v,_){return h=h!==null?h.childLanes&~_:0,v&&(h|=hl),h}function $B(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(_n){if(V?Np(v):Cp(),(h=_r)?(h=ZH(h,Vl),h=h!==null&&h.data!=="&"?h:null,h!==null&&(v.memoizedState={dehydrated:h,treeContext:xp!==null?{id:Pd,overflow:Td}:null,retryLane:536870912,hydrationErrors:null},_=kU(h),_.return=v,v.child=_,Ns=v,_r=null)):h=null,h===null)throw vp(v);return $M(h)?v.lanes=32:v.lanes=536870912,null}var qe=D.children;return D=D.fallback,V?(Cp(),V=v.mode,qe=Xk({mode:"hidden",children:qe},V),D=_f(D,V,_,null),qe.return=v,D.return=v,qe.sibling=D,v.child=qe,D=v.child,D.memoizedState=oM(_),D.childLanes=lM(h,Se,_),v.memoizedState=sM,Fv(null,D)):(Np(v),cM(v,qe))}var mt=h.memoizedState;if(mt!==null&&(qe=mt.dehydrated,qe!==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(),qe=D.fallback,V=v.mode,D=Xk({mode:"visible",children:D.children},V),qe=_f(qe,V,_,null),qe.flags|=2,D.return=v,qe.return=v,D.sibling=qe,v.child=D,Mf(v,h.child,null,_),D=v.child,D.memoizedState=oM(_),D.childLanes=lM(h,Se,_),v.memoizedState=sM,v=Fv(null,D));else if(Np(v),$M(qe)){if(Se=qe.nextSibling&&qe.nextSibling.dataset,Se)var Mt=Se.dgst;Se=Mt,D=Error(r(419)),D.stack="",D.digest=Se,Sv({value:D,source:null,stack:null}),v=dM(h,v,_)}else if(Ni||Pb(h,v,_,!1),Se=(_&h.childLanes)!==0,Ni||Se){if(Se=mr,Se!==null&&(D=gt(Se,_),D!==0&&D!==mt.retryLane))throw mt.retryLane=D,kf(h,D),Bo(Se,h,D),rM;qM(qe)||s_(),v=dM(h,v,_)}else qM(qe)?(v.flags|=192,v.child=h.child,v=null):(h=mt.treeContext,_r=Wl(qe.nextSibling),Ns=v,_n=!0,yp=null,Vl=!1,h!==null&&CU(v,h),v=cM(v,D.children),v.flags|=4096);return v}return V?(Cp(),qe=D.fallback,V=v.mode,mt=h.child,Mt=mt.sibling,D=Iu(mt,{mode:"hidden",children:D.children}),D.subtreeFlags=mt.subtreeFlags&65011712,Mt!==null?qe=Iu(Mt,qe):(qe=_f(qe,V,_,null),qe.flags|=2),qe.return=v,D.return=v,D.sibling=qe,v.child=D,Fv(null,D),D=v.child,qe=h.child.memoizedState,qe===null?qe=oM(_):(V=qe.cachePool,V!==null?(mt=ki._currentValue,V=V.parent!==mt?{parent:mt,pool:mt}:V):V=EU(),qe={baseLanes:qe.baseLanes|_,cachePool:V}),D.memoizedState=qe,D.childLanes=lM(h,Se,_),v.memoizedState=sM,Fv(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 VB(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 GB(h,v,_){var D=v.pendingProps,V=D.revealOrder,ee=D.tail;D=D.children;var Se=hi.current,qe=(Se&2)!==0;if(qe?(Se=Se&1|2,v.flags|=128):Se&=1,me(hi,Se),Ps(h,v,D,_),D=_n?wv:0,!qe&&h!==null&&(h.flags&128)!==0)e:for(h=v.child;h!==null;){if(h.tag===13)h.memoizedState!==null&&VB(h,_,v);else if(h.tag===19)VB(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 qu(h,v,_){if(h!==null&&(v.dependencies=h.dependencies),Ap|=v.lanes,(_&v.childLanes)===0)if(h!==null){if(Pb(h,v,_,!1),(_&v.childLanes)===0)return null}else return null;if(h!==null&&v.child!==h.child)throw Error(r(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 Xde(h,v,_){switch(v.tag){case 3:A(v,v.stateNode.containerInfo),wp(v,ki,h.memoizedState.cache),Nf();break;case 27:case 5:H(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?$B(h,v,_):(Np(v),h=qu(h,v,_),h!==null?h.sibling:null);Np(v);break;case 19:var V=(h.flags&128)!==0;if(D=(_&v.childLanes)!==0,D||(Pb(h,v,_,!1),D=(_&v.childLanes)!==0),V){if(D)return GB(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,OB(h,v,_,v.pendingProps);case 24:wp(v,ki,h.memoizedState.cache)}return qu(h,v,_)}function WB(h,v,_){if(h!==null)if(h.memoizedProps!==v.pendingProps)Ni=!0;else{if(!mM(h,_)&&(v.flags&128)===0)return Ni=!1,Xde(h,v,_);Ni=(h.flags&131072)!==0}else Ni=!1,_n&&(v.flags&1048576)!==0&&NU(v,wv,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=LB(null,v,h,D,_);break e}else if(V===j){v.tag=14,v=RB(null,v,h,D,_);break e}}throw v=L(h)||h,Error(r(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(r(387));D=v.pendingProps;var ee=v.memoizedState;V=ee.element,Ej(h,v),Av(v,D,null,_);var Se=v.memoizedState;if(D=Se.cache,wp(v,ki,D),D!==ee.cache&&Cj(v,[ki],_,!0),Tv(),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=qB(h,v,D,_);break e}else if(D!==V){V=Hl(Error(r(424)),v),Sv(V),v=qB(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,_r=Wl(h.firstChild),Ns=v,_n=!0,yp=null,Vl=!0,_=OU(v,null,D,_),v.child=_;_;)_.flags=_.flags&-3|4096,_=_.sibling;else{if(Nf(),D===V){v=qu(h,v,_);break e}Ps(h,v,D,_)}v=v.child}return v;case 26:return Yk(h,v),h===null?(_=rq(v.type,null,v.pendingProps,null))?v.memoizedState=_:_n||(_=v.type,h=v.pendingProps,D=p_(q.current).createElement(_),D[_t]=v,D[Ct]=h,Ts(D,_,h),it(D),v.stateNode=D):v.memoizedState=rq(v.type,h.memoizedProps,v.pendingProps,h.memoizedState),null;case 27:return H(v),h===null&&_n&&(D=v.stateNode=tq(v.type,v.pendingProps,q.current),Ns=v,Vl=!0,V=_r,Fp(v.type)?(VM=V,_r=Wl(D.firstChild)):_r=V),Ps(h,v,v.pendingProps.children,_),Yk(h,v),h===null&&(v.flags|=4194304),v.child;case 5:return h===null&&_n&&((V=D=_r)&&(D=Pue(D,v.type,v.pendingProps,Vl),D!==null?(v.stateNode=D,Ns=v,_r=Wl(D.firstChild),Vl=!1,V=!0):V=!1),V||vp(v)),H(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,_),Qv._currentValue=V),Yk(h,v),Ps(h,v,D,_),v.child;case 6:return h===null&&_n&&((h=_=_r)&&(_=Tue(_,v.pendingProps,Vl),_!==null?(v.stateNode=_,Ns=v,_r=null,h=!0):h=!1),h||vp(v)),null;case 13:return $B(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 LB(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 RB(h,v,v.type,v.pendingProps,_);case 15:return IB(h,v,v.type,v.pendingProps,_);case 19:return GB(h,v,_);case 31:return Yde(h,v,_);case 22:return OB(h,v,_,v.pendingProps);case 24:return Pf(v),D=Cs(ki),h===null?(V=Aj(),V===null&&(V=mr,ee=Pj(),V.pooledCache=ee,ee.refCount++,ee!==null&&(V.pooledCacheLanes|=_),V=ee),v.memoizedState={parent:D,cache:V},Mj(v),wp(v,ki,V)):((h.lanes&_)!==0&&(Ej(h,v),Av(v,null,null,_),Tv()),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,ki,D)):(D=ee.cache,wp(v,ki,D),D!==V.cache&&Cj(v,[ki],_,!0))),Ps(h,v,v.pendingProps.children,_),v.child;case 29:throw v.pendingProps}throw Error(r(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(vH())h.flags|=8192;else throw jf=Lk,jj}else h.flags&=-16777217}function KB(h,v){if(v.type!=="stylesheet"||(v.state.loading&4)!==0)h.flags&=-16777217;else if(h.flags|=16777216,!cq(v))if(vH())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,zb|=v)}function Lv(h,v){if(!_n)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 Nr(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 Zde(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 Nr(v),null;case 1:return Nr(v),null;case 3:return _=v.stateNode,D=null,h!==null&&(D=h.memoizedState.cache),v.memoizedState.cache!==D&&(v.flags|=2048),Uu(ki),E(),_.pendingContext&&(_.context=_.pendingContext,_.pendingContext=null),(h===null||h.child===null)&&(Cb(v)?$u(v):h===null||h.memoizedState.isDehydrated&&(v.flags&256)===0||(v.flags|=1024,kj())),Nr(v),null;case 26:var V=v.type,ee=v.memoizedState;return h===null?($u(v),ee!==null?(Nr(v),KB(v,ee)):(Nr(v),pM(v,V,null,D,_))):ee?ee!==h.memoizedState?($u(v),Nr(v),KB(v,ee)):(Nr(v),v.flags&=-16777217):(h=h.memoizedProps,h!==D&&$u(v),Nr(v),pM(v,V,h,D,_)),null;case 27:if(W(v),_=q.current,V=v.type,h!==null&&v.stateNode!=null)h.memoizedProps!==D&&$u(v);else{if(!D){if(v.stateNode===null)throw Error(r(166));return Nr(v),null}h=de.current,Cb(v)?PU(v):(h=tq(V,D,_),v.stateNode=h,$u(v))}return Nr(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(r(166));return Nr(v),null}if(ee=de.current,Cb(v))PU(v);else{var Se=p_(q.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=" +