diff --git a/CHANGELOG.md b/CHANGELOG.md index 37c766ab9..d0ee7caa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ All notable changes to Bambuddy will be documented in this file. - **Filament Track Switch (FTS) support — print modal filament dropdown is no longer empty when an X2D / H2D has the FTS accessory installed** ([#1162](https://github.com/maziggy/bambuddy/issues/1162), reported by @mkavalecz) — When the FTS accessory is installed the printer's MQTT changes one nibble of the per-AMS `info` bitmask: bits 8-11 flip from a fixed extruder ID (0x0 / 0x1) to `0xE` ("uninitialized"), because the AMS is no longer wired to a single nozzle — the FTS dynamically routes any slot to either extruder. Bambuddy's MQTT parser already skipped 0xE entries when building `ams_extruder_map` (matching BambuStudio's reading for boot-time transient state), so with the FTS installed the map ended up empty and the print modal's filament dropdown — which filters by `extruderId === nozzle_id` to prevent cross-nozzle assignment ("position of left hotend is abnormal" failures) — filtered out *every* loaded slot. Net effect: empty Filament Mapping dropdown on every dual-nozzle print with the FTS, even when the AMS was fully loaded with the right material. Detection comes from a new MQTT field — `print.device.fila_switch` — which is non-null only when the accessory is installed; it carries the routing topology as two arrays: `in[track] = currently fed slot (-1 = empty)` and `out[track] = extruder this track terminates at`. The fix surfaces this through a new `FilaSwitchState` dataclass on `PrinterState` (`installed`, `in_slots`, `out_extruders`, `stat`, `info`) and the equivalent `FilaSwitchResponse` Pydantic schema on the `GET /printers/{id}/status` route. Frontend (`useFilamentMapping.ts` + `FilamentMapping.tsx`) skips the per-extruder filter when `printerStatus.fila_switch?.installed === true` so any compatible AMS slot can satisfy any nozzle's filament requirement, since the FTS handles the routing. Slots currently fed into a track also get a routing badge in the dropdown — `[L]` or `[R]` — so the user can tell at a glance which slot the FTS is currently routing where (idle slots get no badge: they can be routed to either extruder on demand). The hard "no cross-nozzle assignment" filter on real dual-nozzle printers without the FTS stays untouched (still trips the same way it always has — `fila_switch == null` keeps the existing behaviour). 4 backend tests in `test_bambu_mqtt.py::TestFilamentTrackSwitchDetection` (default-not-installed, detect-from-MQTT-using-the-reporter's-bundle, no-fila_switch-field-stays-not-installed, missing-in-out-arrays-don't-crash) and 2 frontend tests in `useFilamentMapping.test.ts` (FTS-active drops the nozzle filter; explicit `fila_switch: null` keeps the filter applied). Upstream fila_switch payloads with anything other than the documented shape are tolerated — `installed` flips on the *presence* of the field, the routing arrays default to empty lists if missing, and the dropdown skips the badge for slots not currently in `in_slots`. ### Fixed +- **Virtual Printer queue mode auto-dispatched onto the wrong colour when multiple compatible printers were available** ([#1188](https://github.com/maziggy/bambuddy/issues/1188), reported by @EdwardChamberlain) — Sending a sliced 3MF to a queue-mode VP via Orca / Studio with auto-dispatch on caused Bambuddy to schedule the job onto a printer of the right model but the wrong loaded filament: a print sliced for matte white PLA would land on a printer with no white loaded, and the printer would start the job using whatever was the closest available match. Edward's diagnosis was exact (`virtual_printer/manager.py:325-326`): the manual /api/v1/print-queue/ POST flow extracts the 3MF's per-slot filament requirements at queue-add time and writes `required_filament_types`, `filament_overrides`, and `ams_mapping` on the resulting `PrintQueueItem`, so the scheduler's color-match enforcement (`print_scheduler.py:512` — keys on `filament_overrides[].force_color_match === true`) actually runs. The VP queue-write path (`_add_to_print_queue`) skipped all of that and built a bare `PrintQueueItem` with only `printer_id`, `target_model`, `archive_id`, `plate_id`, `position`, `status`, `manual_start`. Net effect: the scheduler reached the model-only-matching fallback and accepted the first available printer of the target model regardless of loaded colour, exactly as he described. **Fix:** the scheduler's existing `_get_filament_requirements` 3MF parser is extracted into a shared helper (`backend/app/services/filament_requirements.py:extract_filament_requirements`) so the VP path can reuse it at upload time. The VP's `_add_to_print_queue` now calls that helper after archiving and populates `required_filament_types` unconditionally (cheap; helps the scheduler reject obvious type mismatches even without `force_color_match`); and writes `filament_overrides` with `force_color_match: true` per consumed slot when a new per-VP setting `queue_force_color_match` is on. Default is **off** to preserve current behaviour for upgraders — a fresh-install user who wants the bug-free behaviour flips the toggle once on the VP card; an existing user gets exactly the model-only-matching they had before until they opt in. Auto-dispatch onto the wrong material happens loudly enough that anyone affected can find the toggle. **Why default-off** rather than default-on: existing automation that relies on "send to queue VP, get printed somewhere" without caring about colour shouldn't silently start blocking on colour matching after an upgrade. The toggle has clear UI copy (`virtualPrinter.queueForceColorMatch`) explaining the trade-off. **Defence in depth:** a malformed or unparseable 3MF (e.g. fake bytes from a misconfigured upload tool) leaves both fields None and the scheduler falls back to model-only matching, matching pre-fix behaviour for the unhappy path. The scheduler itself is unchanged — it already handled `force_color_match` correctly when the field was populated; the bug was purely the VP path not populating it. **Schema:** one nullable column `virtual_printers.queue_force_color_match BOOLEAN DEFAULT 0/FALSE` (Postgres-safe) added via the existing `_safe_execute` migration pattern. **API:** `VirtualPrinterCreate` and `VirtualPrinterUpdate` Pydantic schemas + `_vp_to_dict` response shape carry `queue_force_color_match`, the create + update routes wire it through to the model, and `VirtualPrinterInstance` constructor + `multiVirtualPrinterApi` TypeScript client mirror the field. **UI:** new toggle on `VirtualPrinterCard` rendered only when `mode === 'print_queue'` (parallels the existing `auto_dispatch` toggle's mode-gating), with `pendingAction` state for the in-flight indicator. **i18n:** new `virtualPrinter.queueForceColorMatch.{title,description}` keys in all 8 locales — English fully translated, German fully translated, the other 6 locales seeded with English copy pending native translation (matches the project's existing flow for newly-added user-facing features). 11 new tests: 8 in `test_filament_requirements.py` covering the extracted parser end-to-end (per-slot dicts, zero-use slots filtered, plate filtering, no-plate flat-walk fallback, unparseable / missing / config-less files, sorted output); 3 in `test_virtual_printer.py::TestVirtualPrinterInstance` covering the VP write path (setting-off → only `required_filament_types` populated; setting-on → `filament_overrides` populated with `force_color_match: true` per slot; unparseable 3MF → both fields None, no crash). Existing scheduler tests still pass against the refactored helper (verified end-to-end across the scheduler / virtual_printer / print_queue / filament test suites — 479 tests). Edward's "out of scope nice-to-have" suggestion of a "Requires Color Match" pill on queue cards is deferred to a follow-up so this PR stays scoped to his repro. + - **Slicing a library file via API key fails with "no Bambu Cloud session is stored" even when the key has cloud access** ([#1182](https://github.com/maziggy/bambuddy/issues/1182) follow-up, reported by @turulix) — Tim shipped the headless slicing pipeline #1182 was filed for, then hit a second wall: `GET /api/v1/cloud/settings` returned the cloud preset IDs correctly (the `/cloud/*` router-level gate from #1182 was doing its job), but `POST /api/v1/library/files/{id}/slice` with those IDs in the request body failed the slice job with `error_status: 400, error_detail: "Cloud preset selected for printer, but no Bambu Cloud session is stored. Sign in to Bambu Cloud and retry."` Cause: the `/cloud/*` fix routes the API key's owner User through `cloud_caller` (a router-level gate stashes the owner on `request.state.api_key_owner`, route-level deps pull it back out), but the slice route lives on `/library/*` — different router, no gate, so when the auth dep returned `None` for the API-keyed request the slice route passed `current_user_id=None` straight through to `_run_slicer_with_fallback` → `_resolve_cloud(db, user=None)` → `get_stored_token(db, None)`, which falls back to the auth-disabled global `Settings` table. That table is empty in auth-enabled deployments, so cloud preset resolution failed even though the key's owner User had a perfectly valid `cloud_token` on their User row. Fix is a new route-level dep `resolve_api_key_cloud_owner` in `cloud.py` that's permissive (returns the owner User if the key has `can_access_cloud=true`, otherwise None — never raises) so it can be safely added to non-`/cloud/*` routes without breaking the local-presets path: a request with an API key that lacks the cloud scope still slices fine against local presets, and only fails with the existing "no Bambu Cloud session" error if it actually selects a cloud preset. Wired into `POST /library/files/{id}/slice` (Tim's blocker) and `GET /slicer/presets` (the SliceModal preset dropdown source — same root cause, would have hit anyone using the UI through an API-keyed reverse proxy). Both routes now resolve the cloud-token owner via `current_user or api_key_cloud_owner` instead of `current_user.id if current_user else None`. The auth gate's None-return for API keys is unchanged — keeping that fix scoped to the routes that actually need cloud-token resolution prevents accidental scope creep into other routes that fence on `current_user is None`. 4 new integration tests in `test_api_key_cloud_access.py::TestSliceRouteCloudOwnerResolution` pin the dep contract: returns the owner for a key with `can_access_cloud=True` and a valid owner; returns None for an owned key without the cloud scope (so cloud presets still 400 cleanly, local presets still slice); returns None for legacy ownerless keys; no-op for JWT and anonymous callers. - **Project cover photo thumbnail too small to recognise the print** ([#1155](https://github.com/maziggy/bambuddy/issues/1155) follow-up, reported by @smandon) — The 40×40 thumbnail @smandon's MakerWorld download workflow relied on for "is this the model I'm looking for?" wasn't readable at that size; he asked for either a larger thumbnail or a click-to-enlarge full preview. Enlarging the thumbnail itself would shift the card layout and cost the dense grid he chose to use for browsing many projects, so the fix keeps the 40×40 thumbnail and shows a portal-mounted 384×384 popover on hover. The popover renders the full image in `object-contain` so tall portrait MakerWorld photos aren't cropped to a square, has `pointer-events-none` so it can't intercept hover and create a flicker loop, and `z-[100]` so it stacks above every sibling card in the grid. **Why a portal:** ProjectCard carries `overflow-hidden` (for its rounded-corner clipping and the color accent bar), so an in-tree popover gets clipped by the card the moment it extends past the card's bounds — exactly the cut-off behaviour @smandon reported on the second iteration. Rendering via `createPortal(..., document.body)` escapes every ancestor clipping context, and `position: fixed` with measurements from `getBoundingClientRect()` keeps the popover pinned next to the thumbnail regardless of where the card sits in the grid. **Edge handling:** if the thumbnail is near the viewport's right edge the popover flips to the LEFT side of the thumbnail; vertical position is clamped so the popover never overflows the window top or bottom. The thumbnail's own `onClick` is `stopPropagation`'d so hovering the popover area never accidentally triggers the parent card's "open project" navigation. 2 new tests in `ProjectsPage.test.tsx` pin the contract: hovering mounts the popover at document.body level (not nested in the card — a future refactor that drops the portal would re-introduce the clipping bug, and the test catches that); leaving unmounts it; the popover img points at the same cover-image URL as the small thumbnail with `object-contain`; cards without a cover_image_filename never mount the portal-rendering component (so a hover doesn't flash an empty preview). diff --git a/README.md b/README.md index 53ccc1579..bbe89299e 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,7 @@ Perfect for remote print farms, traveling makers, or accessing your home printer - Send prints directly from Bambu Studio/Orca Slicer - Configurable printer model (X1C, P1S, A1, H2D, etc.) - Archive mode, Review mode, Queue mode, or Proxy mode +- Queue mode: optional **force-color-match** so the scheduler refuses to dispatch onto a printer with the wrong filament loaded - SSDP discovery (same LAN) or manual IP entry (VPN/remote) - Network interface override for multi-NIC/Docker/VPN setups - Secure TLS/MQTT/FTP communication diff --git a/backend/app/api/routes/virtual_printers.py b/backend/app/api/routes/virtual_printers.py index 33fc60ccf..cb7d8ae5c 100644 --- a/backend/app/api/routes/virtual_printers.py +++ b/backend/app/api/routes/virtual_printers.py @@ -37,6 +37,7 @@ class VirtualPrinterCreate(BaseModel): access_code: str | None = None target_printer_id: int | None = None auto_dispatch: bool = True + queue_force_color_match: bool = False bind_ip: str | None = None remote_interface_ip: str | None = None @@ -49,6 +50,7 @@ class VirtualPrinterUpdate(BaseModel): access_code: str | None = None target_printer_id: int | None = None auto_dispatch: bool | None = None + queue_force_color_match: bool | None = None bind_ip: str | None = None remote_interface_ip: str | None = None tailscale_disabled: bool | None = None @@ -90,6 +92,7 @@ def _vp_to_dict(vp, status: dict | None = None) -> dict: "serial": serial, "target_printer_id": vp.target_printer_id, "auto_dispatch": vp.auto_dispatch, + "queue_force_color_match": vp.queue_force_color_match, "bind_ip": vp.bind_ip, "remote_interface_ip": vp.remote_interface_ip, "tailscale_disabled": vp.tailscale_disabled, @@ -209,6 +212,7 @@ async def create_virtual_printer( access_code=body.access_code, target_printer_id=body.target_printer_id, auto_dispatch=body.auto_dispatch, + queue_force_color_match=body.queue_force_color_match, bind_ip=body.bind_ip, remote_interface_ip=body.remote_interface_ip, serial_suffix=new_suffix, @@ -331,6 +335,8 @@ async def update_virtual_printer( vp.model = _resolve_printer_model(target_printer.model) or target_printer.model if body.auto_dispatch is not None: vp.auto_dispatch = body.auto_dispatch + if body.queue_force_color_match is not None: + vp.queue_force_color_match = body.queue_force_color_match if body.bind_ip is not None: vp.bind_ip = body.bind_ip if body.remote_interface_ip is not None: diff --git a/backend/app/core/database.py b/backend/app/core/database.py index e0102da51..f50d7cbc3 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -637,6 +637,18 @@ async def run_migrations(conn): # Migration: Add ams_mapping column to print_queue for storing filament slot assignments await _safe_execute(conn, "ALTER TABLE print_queue ADD COLUMN ams_mapping TEXT") + # Migration: Add queue_force_color_match column to virtual_printers (#1188). + # Opt-in flag: when true, VP queue-mode uploads pin the per-slot type+color + # from the 3MF onto the queue item's filament_overrides so the scheduler + # refuses to dispatch onto a printer with the wrong filament loaded. + # Default false to preserve current behaviour for upgraders. + if is_sqlite(): + await _safe_execute(conn, "ALTER TABLE virtual_printers ADD COLUMN queue_force_color_match BOOLEAN DEFAULT 0") + else: + await _safe_execute( + conn, "ALTER TABLE virtual_printers ADD COLUMN queue_force_color_match BOOLEAN DEFAULT FALSE" + ) + # Migration: Add target_parts_count column to projects for tracking total parts needed await _safe_execute(conn, "ALTER TABLE projects ADD COLUMN target_parts_count INTEGER") diff --git a/backend/app/models/virtual_printer.py b/backend/app/models/virtual_printer.py index 60e971738..f583cb2b0 100644 --- a/backend/app/models/virtual_printer.py +++ b/backend/app/models/virtual_printer.py @@ -18,6 +18,11 @@ class VirtualPrinter(Base): auto_dispatch: Mapped[bool] = mapped_column( Boolean, server_default="true" ) # print_queue mode: auto-start or manual + queue_force_color_match: Mapped[bool] = mapped_column( + Boolean, server_default="false" + ) # print_queue mode: pin per-slot type+color from the 3MF onto the queue + # item so the scheduler refuses to dispatch onto a printer with the wrong + # filament loaded (#1188). model: Mapped[str | None] = mapped_column(String(50), nullable=True) # SSDP model code (server mode) access_code: Mapped[str | None] = mapped_column(String(8), nullable=True) # 8 chars (server mode) target_printer_id: Mapped[int | None] = mapped_column( diff --git a/backend/app/services/filament_requirements.py b/backend/app/services/filament_requirements.py new file mode 100644 index 000000000..b03280fd2 --- /dev/null +++ b/backend/app/services/filament_requirements.py @@ -0,0 +1,113 @@ +"""Parse per-slot filament requirements out of a 3MF file. + +The scheduler used to own this logic (`PrintScheduler._get_filament_requirements`) +because it ran during dispatch decisions. Extracted here so the VP queue-mode +write path can use the same parser to populate `filament_overrides` / +`required_filament_types` at upload time (#1188 — Bambuddy was creating queue +items with no filament fields, which made the scheduler fall through to +model-only matching and dispatch onto whatever printer happened to be free +regardless of loaded colour). + +The shape returned here matches the `filament_overrides` JSON shape the +scheduler validates against, minus the `force_color_match` flag — callers +add that themselves based on their own setting. +""" + +from __future__ import annotations + +import logging +import xml.etree.ElementTree as ET +import zipfile +from pathlib import Path + +from backend.app.utils.threemf_tools import extract_nozzle_mapping_from_3mf + +logger = logging.getLogger(__name__) + + +def extract_filament_requirements(file_path: Path, plate_id: int | None = None) -> list[dict]: + """Parse `[{slot_id, type, color, tray_info_idx, used_grams, nozzle_id?}]` from a 3MF. + + Args: + file_path: Path to the 3MF. + plate_id: When set, only return filaments used on that plate. When + None, return every filament with `used_g > 0` across the file. + + Returns: + Sorted list (by `slot_id`) of filament dicts. Empty list when the + 3MF is unreadable, missing `Metadata/slice_info.config`, or has no + filaments matching the plate filter — callers treat that as "no + requirements" rather than an error so a malformed 3MF doesn't break + the upload path. + """ + if not file_path.exists(): + return [] + + filaments: list[dict] = [] + try: + with zipfile.ZipFile(file_path, "r") as zf: + if "Metadata/slice_info.config" not in zf.namelist(): + return [] + + content = zf.read("Metadata/slice_info.config").decode() + root = ET.fromstring(content) # noqa: S314 # nosec B314 + + if plate_id is not None: + for plate_elem in root.findall("./plate"): + plate_index = None + for meta in plate_elem.findall("metadata"): + if meta.get("key") == "index": + try: + plate_index = int(meta.get("value", "0")) + except ValueError: + pass + break + if plate_index == plate_id: + _collect_filaments(plate_elem, filaments) + break + else: + _collect_filaments(root, filaments) + + filaments.sort(key=lambda x: x["slot_id"]) + + # Dual-nozzle printers (H2D / X2D) — annotate which extruder each + # slot is fed into. Empty mapping for single-nozzle printers, in + # which case we just don't add the key. + nozzle_mapping = extract_nozzle_mapping_from_3mf(zf) + if nozzle_mapping: + for filament in filaments: + filament["nozzle_id"] = nozzle_mapping.get(filament["slot_id"]) + except Exception as e: + logger.warning("Failed to parse filament requirements from %s: %s", file_path, e) + return [] + + return filaments + + +def _collect_filaments(parent: ET.Element, into: list[dict]) -> None: + """Walk every `./filament` child under `parent` and append normalised + entries to `into`. Skips filaments with `used_g <= 0` (slot present in + the slicer config but not consumed by this plate).""" + for filament_elem in parent.findall("./filament"): + filament_id = filament_elem.get("id") + if not filament_id: + continue + try: + used_grams = float(filament_elem.get("used_g", "0")) + except (ValueError, TypeError): + continue + if used_grams <= 0: + continue + try: + slot_id = int(filament_id) + except (ValueError, TypeError): + continue + into.append( + { + "slot_id": slot_id, + "type": filament_elem.get("type", ""), + "color": filament_elem.get("color", ""), + "tray_info_idx": filament_elem.get("tray_info_idx", ""), + "used_grams": round(used_grams, 1), + } + ) diff --git a/backend/app/services/print_scheduler.py b/backend/app/services/print_scheduler.py index 3a99e336b..296e79e70 100644 --- a/backend/app/services/print_scheduler.py +++ b/backend/app/services/print_scheduler.py @@ -4,11 +4,9 @@ import asyncio import json import logging import time -import zipfile from datetime import datetime, timezone from pathlib import Path -import defusedxml.ElementTree as ET from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession @@ -31,7 +29,6 @@ from backend.app.services.notification_service import notification_service from backend.app.services.printer_manager import printer_manager, supports_drying from backend.app.services.smart_plug_manager import smart_plug_manager from backend.app.utils.printer_models import normalize_printer_model -from backend.app.utils.threemf_tools import extract_nozzle_mapping_from_3mf logger = logging.getLogger(__name__) @@ -825,17 +822,15 @@ class PrintScheduler: return self._match_filaments_to_slots(filament_reqs, loaded_filaments, prefer_lowest) async def _get_filament_requirements(self, db: AsyncSession, item: PrintQueueItem) -> list[dict] | None: - """Extract filament requirements from the source 3MF file. - - Args: - db: Database session - item: Queue item with archive_id or library_file_id - - Returns: - List of filament requirement dicts with slot_id, type, color, used_grams + """Resolve the queue item's source 3MF and parse the per-slot + filament requirements out of it. Thin DB-resolver wrapper around + ``filament_requirements.extract_filament_requirements`` so the VP + queue-mode write path (#1188) can reuse the same parser at upload + time. """ - file_path: Path | None = None + from backend.app.services.filament_requirements import extract_filament_requirements + file_path: Path | None = None if item.archive_id: result = await db.execute(select(PrintArchive).where(PrintArchive.id == item.archive_id)) archive = result.scalar_one_or_none() @@ -851,82 +846,7 @@ class PrintScheduler: if not file_path or not file_path.exists(): return None - filaments = [] - try: - with zipfile.ZipFile(file_path, "r") as zf: - if "Metadata/slice_info.config" not in zf.namelist(): - return None - - content = zf.read("Metadata/slice_info.config").decode() - root = ET.fromstring(content) - - # Check if plate_id is specified - use that plate's filaments - plate_id = item.plate_id - if plate_id: - for plate_elem in root.findall("./plate"): - plate_index = None - for meta in plate_elem.findall("metadata"): - if meta.get("key") == "index": - plate_index = int(meta.get("value", "0")) - break - if plate_index == plate_id: - for filament_elem in plate_elem.findall("./filament"): - filament_id = filament_elem.get("id") - filament_type = filament_elem.get("type", "") - filament_color = filament_elem.get("color", "") - # tray_info_idx identifies the specific spool selected when slicing - tray_info_idx = filament_elem.get("tray_info_idx", "") - used_g = filament_elem.get("used_g", "0") - try: - used_grams = float(used_g) - if used_grams > 0 and filament_id: - filaments.append( - { - "slot_id": int(filament_id), - "type": filament_type, - "color": filament_color, - "tray_info_idx": tray_info_idx, - "used_grams": round(used_grams, 1), - } - ) - except (ValueError, TypeError): - pass # Skip filament entry with unparseable usage data - break - else: - # No plate_id - extract all filaments with used_g > 0 - for filament_elem in root.findall("./filament"): - filament_id = filament_elem.get("id") - filament_type = filament_elem.get("type", "") - filament_color = filament_elem.get("color", "") - # tray_info_idx identifies the specific spool selected when slicing - tray_info_idx = filament_elem.get("tray_info_idx", "") - used_g = filament_elem.get("used_g", "0") - try: - used_grams = float(used_g) - if used_grams > 0 and filament_id: - filaments.append( - { - "slot_id": int(filament_id), - "type": filament_type, - "color": filament_color, - "tray_info_idx": tray_info_idx, - "used_grams": round(used_grams, 1), - } - ) - except (ValueError, TypeError): - pass # Skip filament entry with unparseable usage data - - filaments.sort(key=lambda x: x["slot_id"]) - - # Enrich with nozzle mapping for dual-nozzle printers - nozzle_mapping = extract_nozzle_mapping_from_3mf(zf) - if nozzle_mapping: - for filament in filaments: - filament["nozzle_id"] = nozzle_mapping.get(filament["slot_id"]) - except Exception as e: - logger.warning("Failed to parse filament requirements: %s", e) - return None - + filaments = extract_filament_requirements(file_path, plate_id=item.plate_id) return filaments if filaments else None def _build_loaded_filaments(self, status) -> list[dict]: diff --git a/backend/app/services/virtual_printer/manager.py b/backend/app/services/virtual_printer/manager.py index 75f5f7602..9b543dfe6 100644 --- a/backend/app/services/virtual_printer/manager.py +++ b/backend/app/services/virtual_printer/manager.py @@ -111,6 +111,7 @@ class VirtualPrinterInstance: target_printer_serial: str = "", target_printer_id: int | None = None, auto_dispatch: bool = True, + queue_force_color_match: bool = False, bind_ip: str = "", remote_interface_ip: str = "", tailscale_disabled: bool = True, @@ -127,6 +128,7 @@ class VirtualPrinterInstance: self.target_printer_serial = target_printer_serial self.target_printer_id = target_printer_id self.auto_dispatch = auto_dispatch + self.queue_force_color_match = queue_force_color_match self.bind_ip = bind_ip self.remote_interface_ip = remote_interface_ip self.tailscale_disabled = tailscale_disabled @@ -330,9 +332,12 @@ class VirtualPrinterInstance: return try: + import json + from backend.app.api.routes.settings import get_setting from backend.app.models.print_queue import PrintQueueItem from backend.app.services.archive import ArchiveService + from backend.app.services.filament_requirements import extract_filament_requirements async with self._session_factory() as db: name_source = await get_setting(db, "virtual_printer_archive_name_source") @@ -355,6 +360,38 @@ class VirtualPrinterInstance: if not self.target_printer_id and self.model: target_model = VIRTUAL_PRINTER_MODELS.get(self.model) plate_id = self._extract_plate_id(file_path) + + # Parse the 3MF for per-slot filament requirements (#1188). + # The manual /print-queue/ POST flow does this at queue-add + # time; the VP path used to skip it, so the scheduler fell + # through to model-only matching and dispatched onto whatever + # printer happened to be free regardless of loaded colour. + # required_filament_types is populated unconditionally — it's + # cheap, lets the scheduler reject obvious mis-matches even + # without force_color_match. filament_overrides only carries + # force_color_match=True when the per-VP setting is on, so + # upgraders keep the old behaviour by default. + required_filament_types_json: str | None = None + filament_overrides_json: str | None = None + requirements = extract_filament_requirements(file_path, plate_id) + if requirements: + types = sorted({r["type"] for r in requirements if r.get("type")}) + if types: + required_filament_types_json = json.dumps(types) + if self.queue_force_color_match: + overrides = [ + { + "slot_id": r["slot_id"], + "type": r.get("type", ""), + "color": r.get("color", ""), + "force_color_match": True, + } + for r in requirements + if r.get("type") and r.get("color") + ] + if overrides: + filament_overrides_json = json.dumps(overrides) + queue_item = PrintQueueItem( printer_id=self.target_printer_id, target_model=target_model, @@ -363,6 +400,8 @@ class VirtualPrinterInstance: position=1, status="pending", manual_start=not self.auto_dispatch, + required_filament_types=required_filament_types_json, + filament_overrides=filament_overrides_json, ) db.add(queue_item) await db.commit() @@ -894,6 +933,7 @@ class VirtualPrinterManager: serial_suffix=vp.serial_suffix, target_printer_id=vp.target_printer_id, auto_dispatch=vp.auto_dispatch, + queue_force_color_match=vp.queue_force_color_match, bind_ip=vp.bind_ip or "", remote_interface_ip=vp.remote_interface_ip or "", tailscale_disabled=vp.tailscale_disabled, diff --git a/backend/tests/unit/services/test_filament_requirements.py b/backend/tests/unit/services/test_filament_requirements.py new file mode 100644 index 000000000..7397eca27 --- /dev/null +++ b/backend/tests/unit/services/test_filament_requirements.py @@ -0,0 +1,144 @@ +"""Unit tests for `extract_filament_requirements` (#1188). + +The helper is the parser the scheduler used to own and the VP queue-mode +write path now also uses. Pin the contract end-to-end so a refactor of one +caller can't silently break the other. +""" + +from __future__ import annotations + +import zipfile +from pathlib import Path + +from backend.app.services.filament_requirements import extract_filament_requirements + + +def _make_3mf( + file_path: Path, + *, + plates: list[tuple[int, list[dict]]] | None = None, + flat_filaments: list[dict] | None = None, +) -> None: + """Build a minimal 3MF zip. Either ``plates`` (list of + ``(plate_index, filaments)``) or ``flat_filaments`` (no plate wrapper) + drives the slice_info.config shape.""" + + def _filament_xml(filaments: list[dict]) -> str: + return "".join( + f'' + for f in filaments + ) + + if plates is not None: + plate_xml = "".join( + f'{_filament_xml(fs)}' for idx, fs in plates + ) + body = plate_xml + elif flat_filaments is not None: + body = _filament_xml(flat_filaments) + else: + body = "" + config = f'{body}' + with zipfile.ZipFile(file_path, "w") as zf: + zf.writestr("Metadata/slice_info.config", config) + + +class TestExtractFilamentRequirements: + def test_returns_per_slot_dicts_for_plate(self, tmp_path: Path): + f = tmp_path / "model.3mf" + _make_3mf( + f, + plates=[ + ( + 1, + [ + {"id": "1", "type": "PLA", "color": "#FFFFFF", "used_g": "12.5"}, + {"id": "2", "type": "PETG", "color": "#000000", "used_g": "4.2"}, + ], + ) + ], + ) + out = extract_filament_requirements(f, plate_id=1) + assert out == [ + {"slot_id": 1, "type": "PLA", "color": "#FFFFFF", "tray_info_idx": "", "used_grams": 12.5}, + {"slot_id": 2, "type": "PETG", "color": "#000000", "tray_info_idx": "", "used_grams": 4.2}, + ] + + def test_skips_zero_use_filaments(self, tmp_path: Path): + """Slot present in slice_info.config but `used_g <= 0` means the + plate doesn't actually consume that filament — must not show up.""" + f = tmp_path / "model.3mf" + _make_3mf( + f, + plates=[ + ( + 1, + [ + {"id": "1", "type": "PLA", "color": "#FFFFFF", "used_g": "10.0"}, + {"id": "2", "type": "ABS", "color": "#FF0000", "used_g": "0"}, + {"id": "3", "type": "PETG", "color": "#00FF00", "used_g": "-1"}, + ], + ) + ], + ) + out = extract_filament_requirements(f, plate_id=1) + assert [r["slot_id"] for r in out] == [1] + + def test_filters_to_requested_plate(self, tmp_path: Path): + f = tmp_path / "multi.3mf" + _make_3mf( + f, + plates=[ + (1, [{"id": "1", "type": "PLA", "color": "#FFF", "used_g": "5"}]), + (2, [{"id": "1", "type": "PETG", "color": "#000", "used_g": "5"}]), + ], + ) + assert extract_filament_requirements(f, plate_id=1)[0]["type"] == "PLA" + assert extract_filament_requirements(f, plate_id=2)[0]["type"] == "PETG" + + def test_no_plate_id_walks_flat_filaments(self, tmp_path: Path): + """When the slice_info.config has no plate wrapper (some older + Studio versions), we still pick up flat ``./filament`` children.""" + f = tmp_path / "flat.3mf" + _make_3mf( + f, + flat_filaments=[{"id": "1", "type": "PLA", "color": "#FFF", "used_g": "5"}], + ) + out = extract_filament_requirements(f, plate_id=None) + assert len(out) == 1 + assert out[0]["type"] == "PLA" + + def test_returns_empty_list_for_unparseable_file(self, tmp_path: Path): + f = tmp_path / "bad.3mf" + f.write_bytes(b"not a zip") + assert extract_filament_requirements(f, plate_id=1) == [] + + def test_returns_empty_list_for_missing_file(self, tmp_path: Path): + assert extract_filament_requirements(tmp_path / "nope.3mf", plate_id=1) == [] + + def test_returns_empty_list_when_slice_info_missing(self, tmp_path: Path): + """3MF without `Metadata/slice_info.config` (e.g. a model-only + export) must degrade gracefully.""" + f = tmp_path / "no-config.3mf" + with zipfile.ZipFile(f, "w") as zf: + zf.writestr("3D/3dmodel.model", "") + assert extract_filament_requirements(f, plate_id=1) == [] + + def test_results_are_sorted_by_slot_id(self, tmp_path: Path): + f = tmp_path / "unordered.3mf" + _make_3mf( + f, + plates=[ + ( + 1, + [ + {"id": "3", "type": "PLA", "color": "#FFF", "used_g": "1"}, + {"id": "1", "type": "PLA", "color": "#000", "used_g": "1"}, + {"id": "2", "type": "PLA", "color": "#F00", "used_g": "1"}, + ], + ) + ], + ) + out = extract_filament_requirements(f, plate_id=1) + assert [r["slot_id"] for r in out] == [1, 2, 3] diff --git a/backend/tests/unit/services/test_virtual_printer.py b/backend/tests/unit/services/test_virtual_printer.py index c062d15e3..4fb4abbfc 100644 --- a/backend/tests/unit/services/test_virtual_printer.py +++ b/backend/tests/unit/services/test_virtual_printer.py @@ -4,12 +4,38 @@ Tests the virtual printer manager, FTP server, and SSDP server components. """ import asyncio +import json +import zipfile from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest +def _write_3mf_with_filaments(file_path: Path, filaments: list[dict], plate_index: int = 1) -> None: + """Build a minimal 3MF zip with `Metadata/slice_info.config` carrying the + given per-slot filament entries. Each `filaments` dict needs `id`, `type`, + `color`, `used_g`. Used by the #1188 VP queue-mode tests below.""" + filament_xml = "".join( + f'' + for f in filaments + ) + config = ( + '' + "" + f'' + f"{filament_xml}" + "" + "" + ) + with zipfile.ZipFile(file_path, "w") as zf: + zf.writestr("Metadata/slice_info.config", config) + # Plate gcode is referenced for plate-id detection in the VP path — + # presence is enough; contents don't matter. + zf.writestr(f"Metadata/plate_{plate_index}.gcode", "; gcode\n") + + class TestVirtualPrinterInstance: """Tests for VirtualPrinterInstance class.""" @@ -291,6 +317,207 @@ class TestVirtualPrinterInstance: queue_item = added_items[0] assert queue_item.manual_start is True + @pytest.mark.asyncio + async def test_add_to_print_queue_populates_required_filament_types(self, tmp_path): + """#1188: VP queue-mode used to create PrintQueueItems with no + filament fields, so the scheduler fell through to model-only matching + and dispatched onto whatever printer was free regardless of loaded + colour. ``required_filament_types`` is populated unconditionally + (cheap, helps the scheduler validate type even without + ``force_color_match``) — pin that contract here.""" + from backend.app.services.virtual_printer.manager import VirtualPrinterInstance + + added_items = [] + mock_db = AsyncMock() + mock_db.add = MagicMock(side_effect=added_items.append) + mock_db.commit = AsyncMock() + mock_session_factory = MagicMock() + mock_session_ctx = AsyncMock() + mock_session_ctx.__aenter__ = AsyncMock(return_value=mock_db) + mock_session_ctx.__aexit__ = AsyncMock(return_value=False) + mock_session_factory.return_value = mock_session_ctx + + inst = VirtualPrinterInstance( + vp_id=21, + name="Reqs", + mode="print_queue", + model="C12", + access_code="12345678", + serial_suffix="391800021", + auto_dispatch=True, + queue_force_color_match=False, # off → only required_filament_types + base_dir=tmp_path, + session_factory=mock_session_factory, + ) + + file_path = tmp_path / "multi.3mf" + _write_3mf_with_filaments( + file_path, + [ + {"id": "1", "type": "PLA", "color": "#FFFFFF", "used_g": "12.3"}, + {"id": "2", "type": "PETG", "color": "#000000", "used_g": "4.5"}, + # used_g=0 → not actually consumed by this plate, must be ignored + {"id": "3", "type": "ABS", "color": "#FF0000", "used_g": "0"}, + ], + plate_index=1, + ) + + mock_archive = MagicMock() + mock_archive.id = 1 + mock_archive.print_name = "multi" + + with ( + patch( + "backend.app.api.routes.settings.get_setting", + new_callable=AsyncMock, + return_value=None, + ), + patch( + "backend.app.services.archive.ArchiveService.archive_print", + new_callable=AsyncMock, + return_value=mock_archive, + ), + ): + await inst._add_to_print_queue(file_path, "192.168.1.100") + + assert len(added_items) == 1 + queue_item = added_items[0] + # Type-only fallback always populated. Sorted, deduped, no zero-use ABS. + assert queue_item.required_filament_types is not None + assert json.loads(queue_item.required_filament_types) == ["PETG", "PLA"] + # Setting off → no force_color_match overrides leaked. + assert queue_item.filament_overrides is None + + @pytest.mark.asyncio + async def test_add_to_print_queue_force_color_match_writes_overrides(self, tmp_path): + """#1188 core fix: when the per-VP ``queue_force_color_match`` toggle + is on, every consumed slot lands as a ``filament_overrides`` entry + with ``force_color_match: true``. This is the field the scheduler + keys on (``print_scheduler.py:512``) — without it, slot-by-slot + type+color matching never runs.""" + from backend.app.services.virtual_printer.manager import VirtualPrinterInstance + + added_items = [] + mock_db = AsyncMock() + mock_db.add = MagicMock(side_effect=added_items.append) + mock_db.commit = AsyncMock() + mock_session_factory = MagicMock() + mock_session_ctx = AsyncMock() + mock_session_ctx.__aenter__ = AsyncMock(return_value=mock_db) + mock_session_ctx.__aexit__ = AsyncMock(return_value=False) + mock_session_factory.return_value = mock_session_ctx + + inst = VirtualPrinterInstance( + vp_id=22, + name="ForceColor", + mode="print_queue", + model="C12", + access_code="12345678", + serial_suffix="391800022", + auto_dispatch=True, + queue_force_color_match=True, # on + base_dir=tmp_path, + session_factory=mock_session_factory, + ) + + file_path = tmp_path / "forced.3mf" + _write_3mf_with_filaments( + file_path, + [ + {"id": "1", "type": "PLA", "color": "#FFFFFF", "used_g": "10.0"}, + {"id": "2", "type": "PLA", "color": "#FF00FF", "used_g": "5.0"}, + ], + plate_index=1, + ) + + mock_archive = MagicMock() + mock_archive.id = 1 + mock_archive.print_name = "forced" + + with ( + patch( + "backend.app.api.routes.settings.get_setting", + new_callable=AsyncMock, + return_value=None, + ), + patch( + "backend.app.services.archive.ArchiveService.archive_print", + new_callable=AsyncMock, + return_value=mock_archive, + ), + ): + await inst._add_to_print_queue(file_path, "192.168.1.100") + + assert len(added_items) == 1 + queue_item = added_items[0] + assert queue_item.filament_overrides is not None + overrides = json.loads(queue_item.filament_overrides) + assert overrides == [ + {"slot_id": 1, "type": "PLA", "color": "#FFFFFF", "force_color_match": True}, + {"slot_id": 2, "type": "PLA", "color": "#FF00FF", "force_color_match": True}, + ] + # required_filament_types still populated alongside overrides. + assert json.loads(queue_item.required_filament_types) == ["PLA"] + + @pytest.mark.asyncio + async def test_add_to_print_queue_force_color_match_skips_when_3mf_unparseable(self, tmp_path): + """A malformed or fake-bytes 3MF must not crash the upload path — + we just write the queue item with no filament fields and let the + scheduler fall back to model-only matching (the pre-#1188 default). + Regression guard for the existing fake-bytes happy-path tests.""" + from backend.app.services.virtual_printer.manager import VirtualPrinterInstance + + added_items = [] + mock_db = AsyncMock() + mock_db.add = MagicMock(side_effect=added_items.append) + mock_db.commit = AsyncMock() + mock_session_factory = MagicMock() + mock_session_ctx = AsyncMock() + mock_session_ctx.__aenter__ = AsyncMock(return_value=mock_db) + mock_session_ctx.__aexit__ = AsyncMock(return_value=False) + mock_session_factory.return_value = mock_session_ctx + + inst = VirtualPrinterInstance( + vp_id=23, + name="Unparseable", + mode="print_queue", + model="C12", + access_code="12345678", + serial_suffix="391800023", + auto_dispatch=True, + queue_force_color_match=True, + base_dir=tmp_path, + session_factory=mock_session_factory, + ) + + file_path = tmp_path / "bad.3mf" + file_path.write_bytes(b"not a real 3mf zip") + + mock_archive = MagicMock() + mock_archive.id = 1 + mock_archive.print_name = "bad" + + with ( + patch( + "backend.app.api.routes.settings.get_setting", + new_callable=AsyncMock, + return_value=None, + ), + patch( + "backend.app.services.archive.ArchiveService.archive_print", + new_callable=AsyncMock, + return_value=mock_archive, + ), + ): + await inst._add_to_print_queue(file_path, "192.168.1.100") + + assert len(added_items) == 1 + queue_item = added_items[0] + # No filament data extractable → both fields stay None (graceful + # fallback to model-only scheduling). + assert queue_item.required_filament_types is None + assert queue_item.filament_overrides is None + # ======================================================================== # Tests for archive_name_source setting (#1152) # ======================================================================== diff --git a/frontend/src/__tests__/components/VirtualPrinterCard.test.tsx b/frontend/src/__tests__/components/VirtualPrinterCard.test.tsx index 140dce51e..65b75cad5 100644 --- a/frontend/src/__tests__/components/VirtualPrinterCard.test.tsx +++ b/frontend/src/__tests__/components/VirtualPrinterCard.test.tsx @@ -45,6 +45,7 @@ const createMockPrinter = (overrides: Partial = {}): Virtu serial: '00M00A391800001', target_printer_id: null, auto_dispatch: true, + queue_force_color_match: false, bind_ip: null, remote_interface_ip: null, position: 0, @@ -135,6 +136,101 @@ describe('VirtualPrinterCard - auto-dispatch toggle', () => { }); }); +// #1188 — VP queue mode now pins per-slot type+color so the scheduler refuses +// to dispatch onto a printer with the wrong filament loaded. The toggle is +// mode-gated to print_queue (mirroring the auto-dispatch toggle), defaults +// off (preserves pre-fix behaviour for upgraders), and the click both flips +// the local state and POSTs the new value to the backend. +describe('VirtualPrinterCard - force color match toggle (#1188)', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(multiVirtualPrinterApi.update).mockResolvedValue(createMockPrinter()); + }); + + it('renders force-color-match toggle when mode is print_queue', async () => { + const printer = createMockPrinter({ mode: 'print_queue' }); + render(); + + await waitFor(() => { + expect(screen.getByText('Force color match')).toBeInTheDocument(); + }); + }); + + it('does not render force-color-match toggle when mode is immediate', async () => { + const printer = createMockPrinter({ mode: 'immediate' }); + render(); + + await waitFor(() => { + expect(screen.getByText('Test VP')).toBeInTheDocument(); + }); + expect(screen.queryByText('Force color match')).not.toBeInTheDocument(); + }); + + it('does not render force-color-match toggle when mode is proxy', async () => { + const printer = createMockPrinter({ mode: 'proxy' }); + render(); + + await waitFor(() => { + expect(screen.getByText('Test VP')).toBeInTheDocument(); + }); + expect(screen.queryByText('Force color match')).not.toBeInTheDocument(); + }); + + it('force-color-match toggle defaults off (not green) — preserves pre-fix behaviour', async () => { + const printer = createMockPrinter({ mode: 'print_queue', queue_force_color_match: false }); + render(); + + await waitFor(() => { + expect(screen.getByText('Force color match')).toBeInTheDocument(); + }); + + const title = screen.getByText('Force color match'); + const section = title.closest('.flex.items-center.justify-between'); + expect(section).toBeTruthy(); + const toggleButton = section!.querySelector('button'); + expect(toggleButton).toBeTruthy(); + expect(toggleButton!.className).not.toContain('bg-bambu-green'); + }); + + it('force-color-match toggle renders enabled (green) when queue_force_color_match is true', async () => { + const printer = createMockPrinter({ mode: 'print_queue', queue_force_color_match: true }); + render(); + + await waitFor(() => { + expect(screen.getByText('Force color match')).toBeInTheDocument(); + }); + + const title = screen.getByText('Force color match'); + const section = title.closest('.flex.items-center.justify-between'); + const toggleButton = section!.querySelector('button'); + expect(toggleButton!.className).toContain('bg-bambu-green'); + }); + + it('clicking force-color-match toggle posts queue_force_color_match in update body', async () => { + const user = userEvent.setup(); + const printer = createMockPrinter({ mode: 'print_queue', queue_force_color_match: false }); + vi.mocked(multiVirtualPrinterApi.update).mockResolvedValue( + createMockPrinter({ mode: 'print_queue', queue_force_color_match: true }) + ); + + render(); + + await waitFor(() => { + expect(screen.getByText('Force color match')).toBeInTheDocument(); + }); + + const title = screen.getByText('Force color match'); + const section = title.closest('.flex.items-center.justify-between'); + const toggleButton = section!.querySelector('button'); + + await user.click(toggleButton!); + + await waitFor(() => { + expect(multiVirtualPrinterApi.update).toHaveBeenCalledWith(1, { queue_force_color_match: true }); + }); + }); +}); + describe('VirtualPrinterCard - tailscale toggle', () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 6fd3bd439..deb19661e 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -5710,6 +5710,7 @@ export interface VirtualPrinterConfig { serial: string; target_printer_id: number | null; auto_dispatch: boolean; + queue_force_color_match: boolean; tailscale_disabled: boolean; bind_ip: string | null; remote_interface_ip: string | null; @@ -5735,6 +5736,7 @@ export const multiVirtualPrinterApi = { access_code?: string; target_printer_id?: number; auto_dispatch?: boolean; + queue_force_color_match?: boolean; bind_ip?: string; remote_interface_ip?: string; }) => @@ -5751,6 +5753,7 @@ export const multiVirtualPrinterApi = { access_code?: string; target_printer_id?: number; auto_dispatch?: boolean; + queue_force_color_match?: boolean; tailscale_disabled?: boolean; bind_ip?: string; remote_interface_ip?: string; diff --git a/frontend/src/components/VirtualPrinterCard.tsx b/frontend/src/components/VirtualPrinterCard.tsx index 263628610..808cd3836 100644 --- a/frontend/src/components/VirtualPrinterCard.tsx +++ b/frontend/src/components/VirtualPrinterCard.tsx @@ -43,6 +43,7 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps) const [localRemoteInterfaceIp, setLocalRemoteInterfaceIp] = useState(printer.remote_interface_ip || ''); const [localModel, setLocalModel] = useState(printer.model || ''); const [localAutoDispatch, setLocalAutoDispatch] = useState(printer.auto_dispatch ?? true); + const [localQueueForceColorMatch, setLocalQueueForceColorMatch] = useState(printer.queue_force_color_match ?? false); const [localTailscaleDisabled, setLocalTailscaleDisabled] = useState(printer.tailscale_disabled ?? true); const [showAccessCode, setShowAccessCode] = useState(false); const [pendingAction, setPendingAction] = useState(null); @@ -99,6 +100,7 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps) setLocalRemoteInterfaceIp(printer.remote_interface_ip || ''); setLocalModel(printer.model || ''); setLocalAutoDispatch(printer.auto_dispatch ?? true); + setLocalQueueForceColorMatch(printer.queue_force_color_match ?? false); setLocalTailscaleDisabled(printer.tailscale_disabled ?? true); } }, [printer, pendingAction]); @@ -380,6 +382,36 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps) )} + {/* Force-color-match toggle - only for print_queue mode (#1188) */} + {localMode === 'print_queue' && ( +
+
+
+
{t('virtualPrinter.queueForceColorMatch.title')}
+
{t('virtualPrinter.queueForceColorMatch.description')}
+
+ +
+
+ )} + {/* Tailscale toggle */}
diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 22f30390e..e97500479 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -4054,6 +4054,10 @@ export default { title: 'Automatisch starten', description: 'Drucke automatisch starten, wenn sie zur Warteschlange hinzugefügt werden. Wenn deaktiviert, warten Drucke auf manuellen Start.', }, + queueForceColorMatch: { + title: 'Farbabgleich erzwingen', + description: 'Druckaufträge nur an Drucker senden, bei denen der genaue Filament-Typ und die genaue Farbe geladen sind. Standardmäßig deaktiviert — ohne diese Option verwendet die Warteschlange nur den Drucker-Modell-Abgleich und wählt möglicherweise einen Drucker mit der falschen Farbe.', + }, tailscaleDisabled: { title: 'Tailscale-Integration', description: 'Wenn aktiviert, werden Tailscale-zertifizierte TLS-Zertifikate verwendet. Deaktivieren für selbstsignierte Zertifikate.', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index c26942e42..dfd10909a 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -4063,6 +4063,10 @@ export default { title: 'Auto-dispatch', description: 'Automatically start prints when added to queue. When off, prints wait for manual dispatch.', }, + queueForceColorMatch: { + title: 'Force color match', + description: 'Refuse to dispatch onto a printer that does not have the exact filament type and color loaded. Off by default — without this, the queue uses model-only matching and may pick a printer with the wrong colour loaded.', + }, tailscaleDisabled: { title: 'Tailscale integration', description: 'When enabled, uses Tailscale for trusted TLS certs. Disable to use self-signed cert only.', diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index ead4dd15e..d4273690a 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -4042,6 +4042,10 @@ export default { title: 'Lancement automatique', description: 'Lancer automatiquement les impressions ajoutées à la file. Désactivé, les impressions attendent un lancement manuel.', }, + queueForceColorMatch: { + title: 'Force color match', + description: 'Refuse to dispatch onto a printer that does not have the exact filament type and color loaded. Off by default — without this, the queue uses model-only matching and may pick a printer with the wrong colour loaded.', + }, tailscaleDisabled: { title: 'Intégration Tailscale', description: 'Lorsqu\'activé, utilise Tailscale pour des certificats TLS de confiance. Désactiver pour n\'utiliser que des certificats auto-signés.', diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index 5f1c3c87b..f7767fe5f 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -4041,6 +4041,10 @@ export default { title: 'Avvio automatico', description: 'Avvia automaticamente le stampe aggiunte alla coda. Se disattivato, le stampe attendono l\'avvio manuale.', }, + queueForceColorMatch: { + title: 'Force color match', + description: 'Refuse to dispatch onto a printer that does not have the exact filament type and color loaded. Off by default — without this, the queue uses model-only matching and may pick a printer with the wrong colour loaded.', + }, tailscaleDisabled: { title: 'Integrazione Tailscale', description: 'Quando abilitato, utilizza Tailscale per certificati TLS affidabili. Disabilita per utilizzare solo certificati auto-firmati.', diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 82540ae78..8aebf0f5f 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -4054,6 +4054,10 @@ export default { title: '自動ディスパッチ', description: 'キューに追加されたときに自動的に印刷を開始します。オフの場合、手動ディスパッチを待ちます。', }, + queueForceColorMatch: { + title: 'Force color match', + description: 'Refuse to dispatch onto a printer that does not have the exact filament type and color loaded. Off by default — without this, the queue uses model-only matching and may pick a printer with the wrong colour loaded.', + }, tailscaleDisabled: { title: 'Tailscale統合', description: '有効にすると、Tailscaleを使用して信頼できるTLS証明書を使用します。自己署名証明書のみを使用する場合は無効にします。', diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index d5b0535a3..277284ccc 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/frontend/src/i18n/locales/pt-BR.ts @@ -4041,6 +4041,10 @@ export default { title: 'Envio automático', description: 'Iniciar impressões automaticamente quando adicionadas à fila. Quando desativado, as impressões aguardam envio manual.', }, + queueForceColorMatch: { + title: 'Force color match', + description: 'Refuse to dispatch onto a printer that does not have the exact filament type and color loaded. Off by default — without this, the queue uses model-only matching and may pick a printer with the wrong colour loaded.', + }, tailscaleDisabled: { title: 'Integração Tailscale', description: 'Quando ativado, usa Tailscale para certificados TLS confiáveis. Desative para usar apenas certificado autoassinado.', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 5aa44be54..3e89c1040 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -4042,6 +4042,10 @@ export default { title: '自动派发', description: '添加到队列时自动开始打印。关闭后,打印任务等待手动派发。', }, + queueForceColorMatch: { + title: 'Force color match', + description: 'Refuse to dispatch onto a printer that does not have the exact filament type and color loaded. Off by default — without this, the queue uses model-only matching and may pick a printer with the wrong colour loaded.', + }, tailscaleDisabled: { title: 'Tailscale 集成', description: '启用后,使用 Tailscale 获取受信任的 TLS 证书。禁用则仅使用自签名证书。', diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts index 2534d6f12..e26d24589 100644 --- a/frontend/src/i18n/locales/zh-TW.ts +++ b/frontend/src/i18n/locales/zh-TW.ts @@ -4042,6 +4042,10 @@ export default { title: '自動派發', description: '新增到佇列時自動開始列印。關閉後,列印任務等待手動派發。', }, + queueForceColorMatch: { + title: 'Force color match', + description: 'Refuse to dispatch onto a printer that does not have the exact filament type and color loaded. Off by default — without this, the queue uses model-only matching and may pick a printer with the wrong colour loaded.', + }, tailscaleDisabled: { title: 'Tailscale 整合', description: '啟用後,使用 Tailscale 取得受信任的 TLS 憑證。停用則僅使用自簽憑證。', diff --git a/static/assets/index-BijNyfT8.js b/static/assets/index-BeWa0UNk.js similarity index 90% rename from static/assets/index-BijNyfT8.js rename to static/assets/index-BeWa0UNk.js index af9a7d373..7f27ffe7b 100644 --- a/static/assets/index-BijNyfT8.js +++ b/static/assets/index-BeWa0UNk.js @@ -1,38 +1,38 @@ -function due(t,e){for(var n=0;nr[i]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const 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 n(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=n(i);fetch(i.href,s)}})();var Z_=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function gc(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var NM={exports:{}},Ev={};var X8;function uue(){if(X8)return Ev;X8=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function n(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 Ev.Fragment=e,Ev.jsx=n,Ev.jsxs=n,Ev}var Y8;function mue(){return Y8||(Y8=1,NM.exports=uue()),NM.exports}var a=mue(),CM={exports:{}},Wn={};var Q8;function hue(){if(Q8)return Wn;Q8=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),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(K){return K===null||typeof K!="object"?null:(K=p&&K[p]||K["@@iterator"],typeof K=="function"?K:null)}var x={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},v=Object.assign,b={};function g(K,ae,pe){this.props=K,this.context=ae,this.refs=b,this.updater=pe||x}g.prototype.isReactComponent={},g.prototype.setState=function(K,ae){if(typeof K!="object"&&typeof K!="function"&&K!=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,K,ae,"setState")},g.prototype.forceUpdate=function(K){this.updater.enqueueForceUpdate(this,K,"forceUpdate")};function S(){}S.prototype=g.prototype;function C(K,ae,pe){this.props=K,this.context=ae,this.refs=b,this.updater=pe||x}var P=C.prototype=new S;P.constructor=C,v(P,g.prototype),P.isPureReactComponent=!0;var N=Array.isArray;function A(){}var T={H:null,A:null,T:null,S:null},M=Object.prototype.hasOwnProperty;function k(K,ae,pe){var be=pe.ref;return{$$typeof:t,type:K,key:ae,ref:be!==void 0?be:null,props:pe}}function D(K,ae){return k(K.type,ae,K.props)}function $(K){return typeof K=="object"&&K!==null&&K.$$typeof===t}function z(K){var ae={"=":"=0",":":"=2"};return"$"+K.replace(/[=:]/g,function(pe){return ae[pe]})}var X=/\/+/g;function L(K,ae){return typeof K=="object"&&K!==null&&K.key!=null?z(""+K.key):ae.toString(36)}function ne(K){switch(K.status){case"fulfilled":return K.value;case"rejected":throw K.reason;default:switch(typeof K.status=="string"?K.then(A,A):(K.status="pending",K.then(function(ae){K.status==="pending"&&(K.status="fulfilled",K.value=ae)},function(ae){K.status==="pending"&&(K.status="rejected",K.reason=ae)})),K.status){case"fulfilled":return K.value;case"rejected":throw K.reason}}throw K}function re(K,ae,pe,be,fe){var B=typeof K;(B==="undefined"||B==="boolean")&&(K=null);var Y=!1;if(K===null)Y=!0;else switch(B){case"bigint":case"string":case"number":Y=!0;break;case"object":switch(K.$$typeof){case t:case e:Y=!0;break;case u:return Y=K._init,re(Y(K._payload),ae,pe,be,fe)}}if(Y)return fe=fe(K),Y=be===""?"."+L(K,0):be,N(fe)?(pe="",Y!=null&&(pe=Y.replace(X,"$&/")+"/"),re(fe,ae,pe,"",function(U){return U})):fe!=null&&($(fe)&&(fe=D(fe,pe+(fe.key==null||K&&K.key===fe.key?"":(""+fe.key).replace(X,"$&/")+"/")+Y)),ae.push(fe)),1;Y=0;var F=be===""?".":be+":";if(N(K))for(var E=0;E>>1,se=re[ie];if(0>>1;iei(pe,ue))bei(fe,pe)?(re[ie]=fe,re[be]=ue,ie=be):(re[ie]=pe,re[ae]=ue,ie=ae);else if(bei(fe,ue))re[ie]=fe,re[be]=ue,ie=be;else break e}}return J}function i(re,J){var ue=re.sortIndex-J.sortIndex;return ue!==0?ue:re.id-J.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,x=!1,v=!1,b=!1,g=typeof setTimeout=="function"?setTimeout:null,S=typeof clearTimeout=="function"?clearTimeout:null,C=typeof setImmediate<"u"?setImmediate:null;function P(re){for(var J=n(d);J!==null;){if(J.callback===null)r(d);else if(J.startTime<=re)r(d),J.sortIndex=J.expirationTime,e(c,J);else break;J=n(d)}}function N(re){if(v=!1,P(re),!x)if(n(c)!==null)x=!0,A||(A=!0,z());else{var J=n(d);J!==null&&ne(N,J.startTime-re)}}var A=!1,T=-1,M=5,k=-1;function D(){return b?!0:!(t.unstable_now()-kre&&D());){var ie=m.callback;if(typeof ie=="function"){m.callback=null,p=m.priorityLevel;var se=ie(m.expirationTime<=re);if(re=t.unstable_now(),typeof se=="function"){m.callback=se,P(re),J=!0;break t}m===n(c)&&r(c),P(re)}else r(c);m=n(c)}if(m!==null)J=!0;else{var K=n(d);K!==null&&ne(N,K.startTime-re),J=!1}}break e}finally{m=null,p=ue,f=!1}J=void 0}}finally{J?z():A=!1}}}var z;if(typeof C=="function")z=function(){C($)};else if(typeof MessageChannel<"u"){var X=new MessageChannel,L=X.port2;X.port1.onmessage=$,z=function(){L.postMessage(null)}}else z=function(){g($,0)};function ne(re,J){T=g(function(){re(t.unstable_now())},J)}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(re){re.callback=null},t.unstable_forceFrameRate=function(re){0>re||125ie?(re.sortIndex=ue,e(d,re),n(c)===null&&re===n(d)&&(v?(S(T),T=-1):v=!0,ne(N,ue-ie))):(re.sortIndex=se,e(c,re),x||f||(x=!0,A||(A=!0,z()))),re},t.unstable_shouldYield=D,t.unstable_wrapCallback=function(re){var J=p;return function(){var ue=p;p=J;try{return re.apply(this,arguments)}finally{p=ue}}}})(AM)),AM}var eH;function gue(){return eH||(eH=1,TM.exports=fue()),TM.exports}var jM={exports:{}},Ds={};var tH;function bue(){if(tH)return Ds;tH=1;var t=Bg();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(),jM.exports=bue(),jM.exports}var rH;function xue(){if(rH)return Dv;rH=1;var t=gue(),e=Bg(),n=dQ();function r(h){var y="https://react.dev/errors/"+h;if(1se||(h.current=ie[se],ie[se]=null,se--)}function pe(h,y){se++,ie[se]=h.current,h.current=y}var be=K(null),fe=K(null),B=K(null),Y=K(null);function F(h,y){switch(pe(B,y),pe(fe,h),pe(be,null),y.nodeType){case 9:case 11:h=(h=y.documentElement)&&(h=h.namespaceURI)?x8(h):0;break;default:if(h=y.tagName,y=y.namespaceURI)y=x8(y),h=y8(y,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}ae(be),pe(be,h)}function E(){ae(be),ae(fe),ae(B)}function U(h){h.memoizedState!==null&&pe(Y,h);var y=be.current,_=y8(y,h.type);y!==_&&(pe(fe,h),pe(be,_))}function Q(h){fe.current===h&&(ae(be),ae(fe)),Y.current===h&&(ae(Y),Tv._currentValue=ue)}var O,ce;function I(h){if(O===void 0)try{throw Error()}catch(_){var y=_.stack.trim().match(/\n( *(at )?)/);O=y&&y[1]||"",ce=-1<_.stack.indexOf(` +function due(t,e){for(var n=0;nr[i]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const 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 n(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=n(i);fetch(i.href,s)}})();var Z_=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function gc(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var NM={exports:{}},Ev={};var X8;function uue(){if(X8)return Ev;X8=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function n(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 Ev.Fragment=e,Ev.jsx=n,Ev.jsxs=n,Ev}var Y8;function mue(){return Y8||(Y8=1,NM.exports=uue()),NM.exports}var a=mue(),CM={exports:{}},Wn={};var Q8;function hue(){if(Q8)return Wn;Q8=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),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(K){return K===null||typeof K!="object"?null:(K=p&&K[p]||K["@@iterator"],typeof K=="function"?K:null)}var x={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},v=Object.assign,b={};function g(K,ie,pe){this.props=K,this.context=ie,this.refs=b,this.updater=pe||x}g.prototype.isReactComponent={},g.prototype.setState=function(K,ie){if(typeof K!="object"&&typeof K!="function"&&K!=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,K,ie,"setState")},g.prototype.forceUpdate=function(K){this.updater.enqueueForceUpdate(this,K,"forceUpdate")};function S(){}S.prototype=g.prototype;function C(K,ie,pe){this.props=K,this.context=ie,this.refs=b,this.updater=pe||x}var P=C.prototype=new S;P.constructor=C,v(P,g.prototype),P.isPureReactComponent=!0;var N=Array.isArray;function A(){}var T={H:null,A:null,T:null,S:null},M=Object.prototype.hasOwnProperty;function k(K,ie,pe){var be=pe.ref;return{$$typeof:t,type:K,key:ie,ref:be!==void 0?be:null,props:pe}}function D(K,ie){return k(K.type,ie,K.props)}function $(K){return typeof K=="object"&&K!==null&&K.$$typeof===t}function U(K){var ie={"=":"=0",":":"=2"};return"$"+K.replace(/[=:]/g,function(pe){return ie[pe]})}var X=/\/+/g;function L(K,ie){return typeof K=="object"&&K!==null&&K.key!=null?U(""+K.key):ie.toString(36)}function ee(K){switch(K.status){case"fulfilled":return K.value;case"rejected":throw K.reason;default:switch(typeof K.status=="string"?K.then(A,A):(K.status="pending",K.then(function(ie){K.status==="pending"&&(K.status="fulfilled",K.value=ie)},function(ie){K.status==="pending"&&(K.status="rejected",K.reason=ie)})),K.status){case"fulfilled":return K.value;case"rejected":throw K.reason}}throw K}function re(K,ie,pe,be,fe){var H=typeof K;(H==="undefined"||H==="boolean")&&(K=null);var Y=!1;if(K===null)Y=!0;else switch(H){case"bigint":case"string":case"number":Y=!0;break;case"object":switch(K.$$typeof){case t:case e:Y=!0;break;case u:return Y=K._init,re(Y(K._payload),ie,pe,be,fe)}}if(Y)return fe=fe(K),Y=be===""?"."+L(K,0):be,N(fe)?(pe="",Y!=null&&(pe=Y.replace(X,"$&/")+"/"),re(fe,ie,pe,"",function(B){return B})):fe!=null&&($(fe)&&(fe=D(fe,pe+(fe.key==null||K&&K.key===fe.key?"":(""+fe.key).replace(X,"$&/")+"/")+Y)),ie.push(fe)),1;Y=0;var F=be===""?".":be+":";if(N(K))for(var E=0;E>>1,se=re[ae];if(0>>1;aei(pe,ue))bei(fe,pe)?(re[ae]=fe,re[be]=ue,ae=be):(re[ae]=pe,re[ie]=ue,ae=ie);else if(bei(fe,ue))re[ae]=fe,re[be]=ue,ae=be;else break e}}return J}function i(re,J){var ue=re.sortIndex-J.sortIndex;return ue!==0?ue:re.id-J.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,x=!1,v=!1,b=!1,g=typeof setTimeout=="function"?setTimeout:null,S=typeof clearTimeout=="function"?clearTimeout:null,C=typeof setImmediate<"u"?setImmediate:null;function P(re){for(var J=n(d);J!==null;){if(J.callback===null)r(d);else if(J.startTime<=re)r(d),J.sortIndex=J.expirationTime,e(c,J);else break;J=n(d)}}function N(re){if(v=!1,P(re),!x)if(n(c)!==null)x=!0,A||(A=!0,U());else{var J=n(d);J!==null&&ee(N,J.startTime-re)}}var A=!1,T=-1,M=5,k=-1;function D(){return b?!0:!(t.unstable_now()-kre&&D());){var ae=m.callback;if(typeof ae=="function"){m.callback=null,p=m.priorityLevel;var se=ae(m.expirationTime<=re);if(re=t.unstable_now(),typeof se=="function"){m.callback=se,P(re),J=!0;break t}m===n(c)&&r(c),P(re)}else r(c);m=n(c)}if(m!==null)J=!0;else{var K=n(d);K!==null&&ee(N,K.startTime-re),J=!1}}break e}finally{m=null,p=ue,f=!1}J=void 0}}finally{J?U():A=!1}}}var U;if(typeof C=="function")U=function(){C($)};else if(typeof MessageChannel<"u"){var X=new MessageChannel,L=X.port2;X.port1.onmessage=$,U=function(){L.postMessage(null)}}else U=function(){g($,0)};function ee(re,J){T=g(function(){re(t.unstable_now())},J)}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(re){re.callback=null},t.unstable_forceFrameRate=function(re){0>re||125ae?(re.sortIndex=ue,e(d,re),n(c)===null&&re===n(d)&&(v?(S(T),T=-1):v=!0,ee(N,ue-ae))):(re.sortIndex=se,e(c,re),x||f||(x=!0,A||(A=!0,U()))),re},t.unstable_shouldYield=D,t.unstable_wrapCallback=function(re){var J=p;return function(){var ue=p;p=J;try{return re.apply(this,arguments)}finally{p=ue}}}})(AM)),AM}var eH;function gue(){return eH||(eH=1,TM.exports=fue()),TM.exports}var jM={exports:{}},Ds={};var tH;function bue(){if(tH)return Ds;tH=1;var t=Bg();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(),jM.exports=bue(),jM.exports}var rH;function xue(){if(rH)return Dv;rH=1;var t=gue(),e=Bg(),n=dQ();function r(h){var y="https://react.dev/errors/"+h;if(1se||(h.current=ae[se],ae[se]=null,se--)}function pe(h,y){se++,ae[se]=h.current,h.current=y}var be=K(null),fe=K(null),H=K(null),Y=K(null);function F(h,y){switch(pe(H,y),pe(fe,h),pe(be,null),y.nodeType){case 9:case 11:h=(h=y.documentElement)&&(h=h.namespaceURI)?x8(h):0;break;default:if(h=y.tagName,y=y.namespaceURI)y=x8(y),h=y8(y,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}ie(be),pe(be,h)}function E(){ie(be),ie(fe),ie(H)}function B(h){h.memoizedState!==null&&pe(Y,h);var y=be.current,_=y8(y,h.type);y!==_&&(pe(fe,h),pe(be,_))}function Q(h){fe.current===h&&(ie(be),ie(fe)),Y.current===h&&(ie(Y),Tv._currentValue=ue)}var I,ce;function z(h){if(I===void 0)try{throw Error()}catch(_){var y=_.stack.trim().match(/\n( *(at )?)/);I=y&&y[1]||"",ce=-1<_.stack.indexOf(` at`)?" ()":-1<_.stack.indexOf("@")?"@unknown:0:0":""}return` -`+O+h+ce}var G=!1;function q(h,y){if(!h||G)return"";G=!0;var _=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var j={DetermineComponentFrameRoot:function(){try{if(y){var Ut=function(){throw Error()};if(Object.defineProperty(Ut.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(Ut,[])}catch(At){var kt=At}Reflect.construct(h,[],Ut)}else{try{Ut.call()}catch(At){kt=At}h.call(Ut.prototype)}}else{try{throw Error()}catch(At){kt=At}(Ut=h())&&typeof Ut.catch=="function"&&Ut.catch(function(){})}}catch(At){if(At&&kt&&typeof At.stack=="string")return[At.stack,kt.stack]}return[null,null]}};j.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var V=Object.getOwnPropertyDescriptor(j.DetermineComponentFrameRoot,"name");V&&V.configurable&&Object.defineProperty(j.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var Z=j.DetermineComponentFrameRoot(),Ne=Z[0],qe=Z[1];if(Ne&&qe){var lt=Ne.split(` +`+I+h+ce}var W=!1;function O(h,y){if(!h||W)return"";W=!0;var _=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var j={DetermineComponentFrameRoot:function(){try{if(y){var Ut=function(){throw Error()};if(Object.defineProperty(Ut.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(Ut,[])}catch(At){var kt=At}Reflect.construct(h,[],Ut)}else{try{Ut.call()}catch(At){kt=At}h.call(Ut.prototype)}}else{try{throw Error()}catch(At){kt=At}(Ut=h())&&typeof Ut.catch=="function"&&Ut.catch(function(){})}}catch(At){if(At&&kt&&typeof At.stack=="string")return[At.stack,kt.stack]}return[null,null]}};j.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var V=Object.getOwnPropertyDescriptor(j.DetermineComponentFrameRoot,"name");V&&V.configurable&&Object.defineProperty(j.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var Z=j.DetermineComponentFrameRoot(),Ne=Z[0],qe=Z[1];if(Ne&&qe){var lt=Ne.split(` `),vt=qe.split(` `);for(V=j=0;jV||lt[j]!==vt[V]){var Rt=` -`+lt[j].replace(" at new "," at ");return h.displayName&&Rt.includes("")&&(Rt=Rt.replace("",h.displayName)),Rt}while(1<=j&&0<=V);break}}}finally{G=!1,Error.prepareStackTrace=_}return(_=h?h.displayName||h.name:"")?I(_):""}function W(h,y){switch(h.tag){case 26:case 27:case 5:return I(h.type);case 16:return I("Lazy");case 13:return h.child!==y&&y!==null?I("Suspense Fallback"):I("Suspense");case 19:return I("SuspenseList");case 0:case 15:return q(h.type,!1);case 11:return q(h.type.render,!1);case 1:return q(h.type,!0);case 31:return I("Activity");default:return""}}function te(h){try{var y="",_=null;do y+=W(h,_),_=h,h=h.return;while(h);return y}catch(j){return` +`+lt[j].replace(" at new "," at ");return h.displayName&&Rt.includes("")&&(Rt=Rt.replace("",h.displayName)),Rt}while(1<=j&&0<=V);break}}}finally{W=!1,Error.prepareStackTrace=_}return(_=h?h.displayName||h.name:"")?z(_):""}function G(h,y){switch(h.tag){case 26:case 27:case 5:return z(h.type);case 16:return z("Lazy");case 13:return h.child!==y&&y!==null?z("Suspense Fallback"):z("Suspense");case 19:return z("SuspenseList");case 0:case 15:return O(h.type,!1);case 11:return O(h.type.render,!1);case 1:return O(h.type,!0);case 31:return z("Activity");default:return""}}function ne(h){try{var y="",_=null;do y+=G(h,_),_=h,h=h.return;while(h);return y}catch(j){return` Error generating stack: `+j.message+` -`+j.stack}}var le=Object.prototype.hasOwnProperty,ye=t.unstable_scheduleCallback,he=t.unstable_cancelCallback,me=t.unstable_shouldYield,H=t.unstable_requestPaint,R=t.unstable_now,ee=t.unstable_getCurrentPriorityLevel,ge=t.unstable_ImmediatePriority,ke=t.unstable_UserBlockingPriority,je=t.unstable_NormalPriority,Te=t.unstable_LowPriority,xe=t.unstable_IdlePriority,Fe=t.log,Be=t.unstable_setDisableYieldValue,Ce=null,ze=null;function Me(h){if(typeof Fe=="function"&&Be(h),ze&&typeof ze.setStrictMode=="function")try{ze.setStrictMode(Ce,h)}catch{}}var Oe=Math.clz32?Math.clz32:ve,Ge=Math.log,et=Math.LN2;function ve(h){return h>>>=0,h===0?32:31-(Ge(h)/et|0)|0}var Xe=256,Pe=262144,Ve=4194304;function we(h){var y=h&42;if(y!==0)return y;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 de(h,y,_){var j=h.pendingLanes;if(j===0)return 0;var V=0,Z=h.suspendedLanes,Ne=h.pingedLanes;h=h.warmLanes;var qe=j&134217727;return qe!==0?(j=qe&~Z,j!==0?V=we(j):(Ne&=qe,Ne!==0?V=we(Ne):_||(_=qe&~h,_!==0&&(V=we(_))))):(qe=j&~Z,qe!==0?V=we(qe):Ne!==0?V=we(Ne):_||(_=j&~h,_!==0&&(V=we(_)))),V===0?0:y!==0&&y!==V&&(y&Z)===0&&(Z=V&-V,_=y&-y,Z>=_||Z===32&&(_&4194048)!==0)?y:V}function Se(h,y){return(h.pendingLanes&~(h.suspendedLanes&~h.pingedLanes)&y)===0}function $e(h,y){switch(h){case 1:case 2:case 4:case 8:case 64:return y+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 y+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 st(){var h=Ve;return Ve<<=1,(Ve&62914560)===0&&(Ve=4194304),h}function He(h){for(var y=[],_=0;31>_;_++)y.push(h);return y}function at(h,y){h.pendingLanes|=y,y!==268435456&&(h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0)}function ht(h,y,_,j,V,Z){var Ne=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,lt=h.expirationTimes,vt=h.hiddenUpdates;for(_=Ne&~_;0<_;){var Rt=31-Oe(_),Ut=1<"u")return null;try{return h.activeElement||h.body}catch{return h.body}}var Tr=/[\n"\\]/g;function On(h){return h.replace(Tr,function(y){return"\\"+y.charCodeAt(0).toString(16)+" "})}function Ra(h,y,_,j,V,Z,Ne,qe){h.name="",Ne!=null&&typeof Ne!="function"&&typeof Ne!="symbol"&&typeof Ne!="boolean"?h.type=Ne:h.removeAttribute("type"),y!=null?Ne==="number"?(y===0&&h.value===""||h.value!=y)&&(h.value=""+on(y)):h.value!==""+on(y)&&(h.value=""+on(y)):Ne!=="submit"&&Ne!=="reset"||h.removeAttribute("value"),y!=null?Br(h,Ne,on(y)):_!=null?Br(h,Ne,on(_)):j!=null&&h.removeAttribute("value"),V==null&&Z!=null&&(h.defaultChecked=!!Z),V!=null&&(h.checked=V&&typeof V!="function"&&typeof V!="symbol"),qe!=null&&typeof qe!="function"&&typeof qe!="symbol"&&typeof qe!="boolean"?h.name=""+on(qe):h.removeAttribute("name")}function $n(h,y,_,j,V,Z,Ne,qe){if(Z!=null&&typeof Z!="function"&&typeof Z!="symbol"&&typeof Z!="boolean"&&(h.type=Z),y!=null||_!=null){if(!(Z!=="submit"&&Z!=="reset"||y!=null)){_n(h);return}_=_!=null?""+on(_):"",y=y!=null?""+on(y):_,qe||y===h.value||(h.value=y),h.defaultValue=y}j=j??V,j=typeof j!="function"&&typeof j!="symbol"&&!!j,h.checked=qe?h.checked:!!j,h.defaultChecked=!!j,Ne!=null&&typeof Ne!="function"&&typeof Ne!="symbol"&&typeof Ne!="boolean"&&(h.name=Ne),_n(h)}function Br(h,y,_){y==="number"&&br(h.ownerDocument)===h||h.defaultValue===""+_||(h.defaultValue=""+_)}function Kr(h,y,_,j){if(h=h.options,y){y={};for(var V=0;V<_.length;V++)y["$"+_[V]]=!0;for(_=0;_"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),zi=!1;if($o)try{var wl={};Object.defineProperty(wl,"passive",{get:function(){zi=!0}}),window.addEventListener("test",wl,wl),window.removeEventListener("test",wl,wl)}catch{zi=!1}var wo=null,ha=null,vc=null;function Im(){if(vc)return vc;var h,y=ha,_=y.length,j,V="value"in wo?wo.value:wo.textContent,Z=V.length;for(h=0;h<_&&y[h]===V[h];h++);var Ne=_-h;for(j=1;j<=Ne&&y[_-j]===V[Z-j];j++);return vc=V.slice(h,1=it),ct=" ",Mn=!1;function xr(h,y){switch(h){case"keyup":return Fn.indexOf(y.keyCode)!==-1;case"keydown":return y.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function lr(h){return h=h.detail,typeof h=="object"&&"data"in h?h.data:null}var Or=!1;function La(h,y){switch(h){case"compositionend":return lr(y);case"keypress":return y.which!==32?null:(Mn=!0,ct);case"textInput":return h=y.data,h===ct&&Mn?null:h;default:return null}}function un(h,y){if(Or)return h==="compositionend"||!kn&&xr(h,y)?(h=Im(),vc=ha=wo=null,Or=!1,h):null;switch(h){case"paste":return null;case"keypress":if(!(y.ctrlKey||y.altKey||y.metaKey)||y.ctrlKey&&y.altKey){if(y.char&&1=y)return{node:_,offset:y-h};h=j}e:{for(;_;){if(_.nextSibling){_=_.nextSibling;break e}_=_.parentNode}_=void 0}_=Yp(_)}}function Jy(h,y){return h&&y?h===y?!0:h&&h.nodeType===3?!1:y&&y.nodeType===3?Jy(h,y.parentNode):"contains"in h?h.contains(y):h.compareDocumentPosition?!!(h.compareDocumentPosition(y)&16):!1:!1}function YS(h){h=h!=null&&h.ownerDocument!=null&&h.ownerDocument.defaultView!=null?h.ownerDocument.defaultView:window;for(var y=br(h.document);y instanceof h.HTMLIFrameElement;){try{var _=typeof y.contentWindow.location.href=="string"}catch{_=!1}if(_)h=y.contentWindow;else break;y=br(h.document)}return y}function FA(h){var y=h&&h.nodeName&&h.nodeName.toLowerCase();return y&&(y==="input"&&(h.type==="text"||h.type==="search"||h.type==="tel"||h.type==="url"||h.type==="password")||y==="textarea"||h.contentEditable==="true")}var qce=$o&&"documentMode"in document&&11>=document.documentMode,tb=null,RA=null,Zy=null,LA=!1;function U5(h,y,_){var j=_.window===_?_.document:_.nodeType===9?_:_.ownerDocument;LA||tb==null||tb!==br(j)||(j=tb,"selectionStart"in j&&FA(j)?j={start:j.selectionStart,end:j.selectionEnd}:(j=(j.ownerDocument&&j.ownerDocument.defaultView||window).getSelection(),j={anchorNode:j.anchorNode,anchorOffset:j.anchorOffset,focusNode:j.focusNode,focusOffset:j.focusOffset}),Zy&&Vm(Zy,j)||(Zy=j,j=O_(RA,"onSelect"),0>=Ne,V-=Ne,ld=1<<32-Oe(y)+V|_<Qn?(vr=vn,vn=null):vr=vn.sibling;var Dr=kt(gt,vn,yt[Qn],It);if(Dr===null){vn===null&&(vn=vr);break}h&&vn&&Dr.alternate===null&&y(gt,vn),pt=Z(Dr,pt,Qn),Er===null?An=Dr:Er.sibling=Dr,Er=Dr,vn=vr}if(Qn===yt.length)return _(gt,vn),Sr&&uu(gt,Qn),An;if(vn===null){for(;QnQn?(vr=vn,vn=null):vr=vn.sibling;var hh=kt(gt,vn,Dr.value,It);if(hh===null){vn===null&&(vn=vr);break}h&&vn&&hh.alternate===null&&y(gt,vn),pt=Z(hh,pt,Qn),Er===null?An=hh:Er.sibling=hh,Er=hh,vn=vr}if(Dr.done)return _(gt,vn),Sr&&uu(gt,Qn),An;if(vn===null){for(;!Dr.done;Qn++,Dr=yt.next())Dr=Ut(gt,Dr.value,It),Dr!==null&&(pt=Z(Dr,pt,Qn),Er===null?An=Dr:Er.sibling=Dr,Er=Dr);return Sr&&uu(gt,Qn),An}for(vn=j(vn);!Dr.done;Qn++,Dr=yt.next())Dr=At(vn,gt,Qn,Dr.value,It),Dr!==null&&(h&&Dr.alternate!==null&&vn.delete(Dr.key===null?Qn:Dr.key),pt=Z(Dr,pt,Qn),Er===null?An=Dr:Er.sibling=Dr,Er=Dr);return h&&vn.forEach(function(cue){return y(gt,cue)}),Sr&&uu(gt,Qn),An}function Qr(gt,pt,yt,It){if(typeof yt=="object"&&yt!==null&&yt.type===v&&yt.key===null&&(yt=yt.props.children),typeof yt=="object"&&yt!==null){switch(yt.$$typeof){case f:e:{for(var An=yt.key;pt!==null;){if(pt.key===An){if(An=yt.type,An===v){if(pt.tag===7){_(gt,pt.sibling),It=V(pt,yt.props.children),It.return=gt,gt=It;break e}}else if(pt.elementType===An||typeof An=="object"&&An!==null&&An.$$typeof===M&&sf(An)===pt.type){_(gt,pt.sibling),It=V(pt,yt.props),iv(It,yt),It.return=gt,gt=It;break e}_(gt,pt);break}else y(gt,pt);pt=pt.sibling}yt.type===v?(It=ef(yt.props.children,gt.mode,It,yt.key),It.return=gt,gt=It):(It=t_(yt.type,yt.key,yt.props,null,gt.mode,It),iv(It,yt),It.return=gt,gt=It)}return Ne(gt);case x:e:{for(An=yt.key;pt!==null;){if(pt.key===An)if(pt.tag===4&&pt.stateNode.containerInfo===yt.containerInfo&&pt.stateNode.implementation===yt.implementation){_(gt,pt.sibling),It=V(pt,yt.children||[]),It.return=gt,gt=It;break e}else{_(gt,pt);break}else y(gt,pt);pt=pt.sibling}It=qA(yt,gt.mode,It),It.return=gt,gt=It}return Ne(gt);case M:return yt=sf(yt),Qr(gt,pt,yt,It)}if(ne(yt))return bn(gt,pt,yt,It);if(z(yt)){if(An=z(yt),typeof An!="function")throw Error(r(150));return yt=An.call(yt),In(gt,pt,yt,It)}if(typeof yt.then=="function")return Qr(gt,pt,l_(yt),It);if(yt.$$typeof===C)return Qr(gt,pt,a_(gt,yt),It);c_(gt,yt)}return typeof yt=="string"&&yt!==""||typeof yt=="number"||typeof yt=="bigint"?(yt=""+yt,pt!==null&&pt.tag===6?(_(gt,pt.sibling),It=V(pt,yt),It.return=gt,gt=It):(_(gt,pt),It=HA(yt,gt.mode,It),It.return=gt,gt=It),Ne(gt)):_(gt,pt)}return function(gt,pt,yt,It){try{av=0;var An=Qr(gt,pt,yt,It);return mb=null,An}catch(vn){if(vn===ub||vn===s_)throw vn;var Er=Ko(29,vn,null,gt.mode);return Er.lanes=It,Er.return=gt,Er}}}var lf=cU(!0),dU=cU(!1),Ym=!1;function tj(h){h.updateQueue={baseState:h.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function nj(h,y){h=h.updateQueue,y.updateQueue===h&&(y.updateQueue={baseState:h.baseState,firstBaseUpdate:h.firstBaseUpdate,lastBaseUpdate:h.lastBaseUpdate,shared:h.shared,callbacks:null})}function Qm(h){return{lane:h,tag:0,payload:null,callback:null,next:null}}function Jm(h,y,_){var j=h.updateQueue;if(j===null)return null;if(j=j.shared,(Ir&2)!==0){var V=j.pending;return V===null?y.next=y:(y.next=V.next,V.next=y),j.pending=y,y=e_(h),W5(h,null,_),y}return ZS(h,j,y,_),e_(h)}function sv(h,y,_){if(y=y.updateQueue,y!==null&&(y=y.shared,(_&4194048)!==0)){var j=y.lanes;j&=h.pendingLanes,_|=j,y.lanes=_,qt(h,_)}}function rj(h,y){var _=h.updateQueue,j=h.alternate;if(j!==null&&(j=j.updateQueue,_===j)){var V=null,Z=null;if(_=_.firstBaseUpdate,_!==null){do{var Ne={lane:_.lane,tag:_.tag,payload:_.payload,callback:null,next:null};Z===null?V=Z=Ne:Z=Z.next=Ne,_=_.next}while(_!==null);Z===null?V=Z=y:Z=Z.next=y}else V=Z=y;_={baseState:j.baseState,firstBaseUpdate:V,lastBaseUpdate:Z,shared:j.shared,callbacks:j.callbacks},h.updateQueue=_;return}h=_.lastBaseUpdate,h===null?_.firstBaseUpdate=y:h.next=y,_.lastBaseUpdate=y}var aj=!1;function ov(){if(aj){var h=db;if(h!==null)throw h}}function lv(h,y,_,j){aj=!1;var V=h.updateQueue;Ym=!1;var Z=V.firstBaseUpdate,Ne=V.lastBaseUpdate,qe=V.shared.pending;if(qe!==null){V.shared.pending=null;var lt=qe,vt=lt.next;lt.next=null,Ne===null?Z=vt:Ne.next=vt,Ne=lt;var Rt=h.alternate;Rt!==null&&(Rt=Rt.updateQueue,qe=Rt.lastBaseUpdate,qe!==Ne&&(qe===null?Rt.firstBaseUpdate=vt:qe.next=vt,Rt.lastBaseUpdate=lt))}if(Z!==null){var Ut=V.baseState;Ne=0,Rt=vt=lt=null,qe=Z;do{var kt=qe.lane&-536870913,At=kt!==qe.lane;if(At?(yr&kt)===kt:(j&kt)===kt){kt!==0&&kt===cb&&(aj=!0),Rt!==null&&(Rt=Rt.next={lane:0,tag:qe.tag,payload:qe.payload,callback:null,next:null});e:{var bn=h,In=qe;kt=y;var Qr=_;switch(In.tag){case 1:if(bn=In.payload,typeof bn=="function"){Ut=bn.call(Qr,Ut,kt);break e}Ut=bn;break e;case 3:bn.flags=bn.flags&-65537|128;case 0:if(bn=In.payload,kt=typeof bn=="function"?bn.call(Qr,Ut,kt):bn,kt==null)break e;Ut=m({},Ut,kt);break e;case 2:Ym=!0}}kt=qe.callback,kt!==null&&(h.flags|=64,At&&(h.flags|=8192),At=V.callbacks,At===null?V.callbacks=[kt]:At.push(kt))}else At={lane:kt,tag:qe.tag,payload:qe.payload,callback:qe.callback,next:null},Rt===null?(vt=Rt=At,lt=Ut):Rt=Rt.next=At,Ne|=kt;if(qe=qe.next,qe===null){if(qe=V.shared.pending,qe===null)break;At=qe,qe=At.next,At.next=null,V.lastBaseUpdate=At,V.shared.pending=null}}while(!0);Rt===null&&(lt=Ut),V.baseState=lt,V.firstBaseUpdate=vt,V.lastBaseUpdate=Rt,Z===null&&(V.shared.lanes=0),rh|=Ne,h.lanes=Ne,h.memoizedState=Ut}}function uU(h,y){if(typeof h!="function")throw Error(r(191,h));h.call(y)}function mU(h,y){var _=h.callbacks;if(_!==null)for(h.callbacks=null,h=0;h<_.length;h++)uU(_[h],y)}var hb=K(null),d_=K(0);function hU(h,y){h=Su,pe(d_,h),pe(hb,y),Su=h|y.baseLanes}function ij(){pe(d_,Su),pe(hb,hb.current)}function sj(){Su=d_.current,ae(hb),ae(d_)}var Xo=K(null),Dl=null;function Zm(h){var y=h.alternate;pe(ai,ai.current&1),pe(Xo,h),Dl===null&&(y===null||hb.current!==null||y.memoizedState!==null)&&(Dl=h)}function oj(h){pe(ai,ai.current),pe(Xo,h),Dl===null&&(Dl=h)}function pU(h){h.tag===22?(pe(ai,ai.current),pe(Xo,h),Dl===null&&(Dl=h)):eh()}function eh(){pe(ai,ai.current),pe(Xo,Xo.current)}function Yo(h){ae(Xo),Dl===h&&(Dl=null),ae(ai)}var ai=K(0);function u_(h){for(var y=h;y!==null;){if(y.tag===13){var _=y.memoizedState;if(_!==null&&(_=_.dehydrated,_===null||hM(_)||pM(_)))return y}else if(y.tag===19&&(y.memoizedProps.revealOrder==="forwards"||y.memoizedProps.revealOrder==="backwards"||y.memoizedProps.revealOrder==="unstable_legacy-backwards"||y.memoizedProps.revealOrder==="together")){if((y.flags&128)!==0)return y}else if(y.child!==null){y.child.return=y,y=y.child;continue}if(y===h)break;for(;y.sibling===null;){if(y.return===null||y.return===h)return null;y=y.return}y.sibling.return=y.return,y=y.sibling}return null}var pu=0,Xn=null,Xr=null,mi=null,m_=!1,pb=!1,cf=!1,h_=0,cv=0,fb=null,Zce=0;function $a(){throw Error(r(321))}function lj(h,y){if(y===null)return!1;for(var _=0;_Z?Z:8;var Ne=re.T,qe={};re.T=qe,_j(h,!1,y,_);try{var lt=V(),vt=re.S;if(vt!==null&&vt(qe,lt),lt!==null&&typeof lt=="object"&&typeof lt.then=="function"){var Rt=Jce(lt,j);uv(h,y,Rt,Zo(h))}else uv(h,y,j,Zo(h))}catch(Ut){uv(h,y,{then:function(){},status:"rejected",reason:Ut},Zo())}finally{J.p=Z,Ne!==null&&qe.types!==null&&(Ne.types=qe.types),re.T=Ne}}function ade(){}function wj(h,y,_,j){if(h.tag!==5)throw Error(r(476));var V=$U(h).queue;qU(h,V,y,ue,_===null?ade:function(){return VU(h),_(j)})}function $U(h){var y=h.memoizedState;if(y!==null)return y;y={memoizedState:ue,baseState:ue,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:fu,lastRenderedState:ue},next:null};var _={};return y.next={memoizedState:_,baseState:_,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:fu,lastRenderedState:_},next:null},h.memoizedState=y,h=h.alternate,h!==null&&(h.memoizedState=y),y}function VU(h){var y=$U(h);y.next===null&&(y=h.alternate.memoizedState),uv(h,y.next.queue,{},Zo())}function Sj(){return ds(Tv)}function GU(){return ii().memoizedState}function WU(){return ii().memoizedState}function ide(h){for(var y=h.return;y!==null;){switch(y.tag){case 24:case 3:var _=Zo();h=Qm(_);var j=Jm(y,h,_);j!==null&&(To(j,y,_),sv(j,y,_)),y={cache:QA()},h.payload=y;return}y=y.return}}function sde(h,y,_){var j=Zo();_={lane:j,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null},y_(h)?XU(y,_):(_=UA(h,y,_,j),_!==null&&(To(_,h,j),YU(_,y,j)))}function KU(h,y,_){var j=Zo();uv(h,y,_,j)}function uv(h,y,_,j){var V={lane:j,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null};if(y_(h))XU(y,V);else{var Z=h.alternate;if(h.lanes===0&&(Z===null||Z.lanes===0)&&(Z=y.lastRenderedReducer,Z!==null))try{var Ne=y.lastRenderedState,qe=Z(Ne,_);if(V.hasEagerState=!0,V.eagerState=qe,_o(qe,Ne))return ZS(h,y,V,0),oa===null&&JS(),!1}catch{}if(_=UA(h,y,V,j),_!==null)return To(_,h,j),YU(_,y,j),!0}return!1}function _j(h,y,_,j){if(j={lane:2,revertLane:tM(),gesture:null,action:j,hasEagerState:!1,eagerState:null,next:null},y_(h)){if(y)throw Error(r(479))}else y=UA(h,_,j,2),y!==null&&To(y,h,2)}function y_(h){var y=h.alternate;return h===Xn||y!==null&&y===Xn}function XU(h,y){pb=m_=!0;var _=h.pending;_===null?y.next=y:(y.next=_.next,_.next=y),h.pending=y}function YU(h,y,_){if((_&4194048)!==0){var j=y.lanes;j&=h.pendingLanes,_|=j,y.lanes=_,qt(h,_)}}var mv={readContext:ds,use:f_,useCallback:$a,useContext:$a,useEffect:$a,useImperativeHandle:$a,useLayoutEffect:$a,useInsertionEffect:$a,useMemo:$a,useReducer:$a,useRef:$a,useState:$a,useDebugValue:$a,useDeferredValue:$a,useTransition:$a,useSyncExternalStore:$a,useId:$a,useHostTransitionStatus:$a,useFormState:$a,useActionState:$a,useOptimistic:$a,useMemoCache:$a,useCacheRefresh:$a};mv.useEffectEvent=$a;var QU={readContext:ds,use:f_,useCallback:function(h,y){return Zs().memoizedState=[h,y===void 0?null:y],h},useContext:ds,useEffect:FU,useImperativeHandle:function(h,y,_){_=_!=null?_.concat([h]):null,b_(4194308,4,OU.bind(null,y,h),_)},useLayoutEffect:function(h,y){return b_(4194308,4,h,y)},useInsertionEffect:function(h,y){b_(4,2,h,y)},useMemo:function(h,y){var _=Zs();y=y===void 0?null:y;var j=h();if(cf){Me(!0);try{h()}finally{Me(!1)}}return _.memoizedState=[j,y],j},useReducer:function(h,y,_){var j=Zs();if(_!==void 0){var V=_(y);if(cf){Me(!0);try{_(y)}finally{Me(!1)}}}else V=y;return j.memoizedState=j.baseState=V,h={pending:null,lanes:0,dispatch:null,lastRenderedReducer:h,lastRenderedState:V},j.queue=h,h=h.dispatch=sde.bind(null,Xn,h),[j.memoizedState,h]},useRef:function(h){var y=Zs();return h={current:h},y.memoizedState=h},useState:function(h){h=gj(h);var y=h.queue,_=KU.bind(null,Xn,y);return y.dispatch=_,[h.memoizedState,_]},useDebugValue:yj,useDeferredValue:function(h,y){var _=Zs();return vj(_,h,y)},useTransition:function(){var h=gj(!1);return h=qU.bind(null,Xn,h.queue,!0,!1),Zs().memoizedState=h,[!1,h]},useSyncExternalStore:function(h,y,_){var j=Xn,V=Zs();if(Sr){if(_===void 0)throw Error(r(407));_=_()}else{if(_=y(),oa===null)throw Error(r(349));(yr&127)!==0||xU(j,y,_)}V.memoizedState=_;var Z={value:_,getSnapshot:y};return V.queue=Z,FU(vU.bind(null,j,Z,h),[h]),j.flags|=2048,gb(9,{destroy:void 0},yU.bind(null,j,Z,_,y),null),_},useId:function(){var h=Zs(),y=oa.identifierPrefix;if(Sr){var _=cd,j=ld;_=(j&~(1<<32-Oe(j)-1)).toString(32)+_,y="_"+y+"R_"+_,_=h_++,0<_&&(y+="H"+_.toString(32)),y+="_"}else _=Zce++,y="_"+y+"r_"+_.toString(32)+"_";return h.memoizedState=y},useHostTransitionStatus:Sj,useFormState:AU,useActionState:AU,useOptimistic:function(h){var y=Zs();y.memoizedState=y.baseState=h;var _={pending:null,lanes:0,dispatch:null,lastRenderedReducer:null,lastRenderedState:null};return y.queue=_,y=_j.bind(null,Xn,!0,_),_.dispatch=y,[h,y]},useMemoCache:hj,useCacheRefresh:function(){return Zs().memoizedState=ide.bind(null,Xn)},useEffectEvent:function(h){var y=Zs(),_={impl:h};return y.memoizedState=_,function(){if((Ir&2)!==0)throw Error(r(440));return _.impl.apply(void 0,arguments)}}},kj={readContext:ds,use:f_,useCallback:UU,useContext:ds,useEffect:xj,useImperativeHandle:zU,useInsertionEffect:LU,useLayoutEffect:IU,useMemo:BU,useReducer:g_,useRef:DU,useState:function(){return g_(fu)},useDebugValue:yj,useDeferredValue:function(h,y){var _=ii();return HU(_,Xr.memoizedState,h,y)},useTransition:function(){var h=g_(fu)[0],y=ii().memoizedState;return[typeof h=="boolean"?h:dv(h),y]},useSyncExternalStore:bU,useId:GU,useHostTransitionStatus:Sj,useFormState:jU,useActionState:jU,useOptimistic:function(h,y){var _=ii();return _U(_,Xr,h,y)},useMemoCache:hj,useCacheRefresh:WU};kj.useEffectEvent=RU;var JU={readContext:ds,use:f_,useCallback:UU,useContext:ds,useEffect:xj,useImperativeHandle:zU,useInsertionEffect:LU,useLayoutEffect:IU,useMemo:BU,useReducer:fj,useRef:DU,useState:function(){return fj(fu)},useDebugValue:yj,useDeferredValue:function(h,y){var _=ii();return Xr===null?vj(_,h,y):HU(_,Xr.memoizedState,h,y)},useTransition:function(){var h=fj(fu)[0],y=ii().memoizedState;return[typeof h=="boolean"?h:dv(h),y]},useSyncExternalStore:bU,useId:GU,useHostTransitionStatus:Sj,useFormState:EU,useActionState:EU,useOptimistic:function(h,y){var _=ii();return Xr!==null?_U(_,Xr,h,y):(_.baseState=h,[h,_.queue.dispatch])},useMemoCache:hj,useCacheRefresh:WU};JU.useEffectEvent=RU;function Nj(h,y,_,j){y=h.memoizedState,_=_(j,y),_=_==null?y:m({},y,_),h.memoizedState=_,h.lanes===0&&(h.updateQueue.baseState=_)}var Cj={enqueueSetState:function(h,y,_){h=h._reactInternals;var j=Zo(),V=Qm(j);V.payload=y,_!=null&&(V.callback=_),y=Jm(h,V,j),y!==null&&(To(y,h,j),sv(y,h,j))},enqueueReplaceState:function(h,y,_){h=h._reactInternals;var j=Zo(),V=Qm(j);V.tag=1,V.payload=y,_!=null&&(V.callback=_),y=Jm(h,V,j),y!==null&&(To(y,h,j),sv(y,h,j))},enqueueForceUpdate:function(h,y){h=h._reactInternals;var _=Zo(),j=Qm(_);j.tag=2,y!=null&&(j.callback=y),y=Jm(h,j,_),y!==null&&(To(y,h,_),sv(y,h,_))}};function ZU(h,y,_,j,V,Z,Ne){return h=h.stateNode,typeof h.shouldComponentUpdate=="function"?h.shouldComponentUpdate(j,Z,Ne):y.prototype&&y.prototype.isPureReactComponent?!Vm(_,j)||!Vm(V,Z):!0}function eB(h,y,_,j){h=y.state,typeof y.componentWillReceiveProps=="function"&&y.componentWillReceiveProps(_,j),typeof y.UNSAFE_componentWillReceiveProps=="function"&&y.UNSAFE_componentWillReceiveProps(_,j),y.state!==h&&Cj.enqueueReplaceState(y,y.state,null)}function df(h,y){var _=y;if("ref"in y){_={};for(var j in y)j!=="ref"&&(_[j]=y[j])}if(h=h.defaultProps){_===y&&(_=m({},_));for(var V in h)_[V]===void 0&&(_[V]=h[V])}return _}function tB(h){QS(h)}function nB(h){console.error(h)}function rB(h){QS(h)}function v_(h,y){try{var _=h.onUncaughtError;_(y.value,{componentStack:y.stack})}catch(j){setTimeout(function(){throw j})}}function aB(h,y,_){try{var j=h.onCaughtError;j(_.value,{componentStack:_.stack,errorBoundary:y.tag===1?y.stateNode:null})}catch(V){setTimeout(function(){throw V})}}function Pj(h,y,_){return _=Qm(_),_.tag=3,_.payload={element:null},_.callback=function(){v_(h,y)},_}function iB(h){return h=Qm(h),h.tag=3,h}function sB(h,y,_,j){var V=_.type.getDerivedStateFromError;if(typeof V=="function"){var Z=j.value;h.payload=function(){return V(Z)},h.callback=function(){aB(y,_,j)}}var Ne=_.stateNode;Ne!==null&&typeof Ne.componentDidCatch=="function"&&(h.callback=function(){aB(y,_,j),typeof V!="function"&&(ah===null?ah=new Set([this]):ah.add(this));var qe=j.stack;this.componentDidCatch(j.value,{componentStack:qe!==null?qe:""})})}function ode(h,y,_,j,V){if(_.flags|=32768,j!==null&&typeof j=="object"&&typeof j.then=="function"){if(y=_.alternate,y!==null&&lb(y,_,V,!0),_=Xo.current,_!==null){switch(_.tag){case 31:case 13:return Dl===null?E_():_.alternate===null&&Va===0&&(Va=3),_.flags&=-257,_.flags|=65536,_.lanes=V,j===o_?_.flags|=16384:(y=_.updateQueue,y===null?_.updateQueue=new Set([j]):y.add(j),Jj(h,j,V)),!1;case 22:return _.flags|=65536,j===o_?_.flags|=16384:(y=_.updateQueue,y===null?(y={transitions:null,markerInstances:null,retryQueue:new Set([j])},_.updateQueue=y):(_=y.retryQueue,_===null?y.retryQueue=new Set([j]):_.add(j)),Jj(h,j,V)),!1}throw Error(r(435,_.tag))}return Jj(h,j,V),E_(),!1}if(Sr)return y=Xo.current,y!==null?((y.flags&65536)===0&&(y.flags|=256),y.flags|=65536,y.lanes=V,j!==GA&&(h=Error(r(422),{cause:j}),tv(Al(h,_)))):(j!==GA&&(y=Error(r(423),{cause:j}),tv(Al(y,_))),h=h.current.alternate,h.flags|=65536,V&=-V,h.lanes|=V,j=Al(j,_),V=Pj(h.stateNode,j,V),rj(h,V),Va!==4&&(Va=2)),!1;var Z=Error(r(520),{cause:j});if(Z=Al(Z,_),vv===null?vv=[Z]:vv.push(Z),Va!==4&&(Va=2),y===null)return!0;j=Al(j,_),_=y;do{switch(_.tag){case 3:return _.flags|=65536,h=V&-V,_.lanes|=h,h=Pj(_.stateNode,j,h),rj(_,h),!1;case 1:if(y=_.type,Z=_.stateNode,(_.flags&128)===0&&(typeof y.getDerivedStateFromError=="function"||Z!==null&&typeof Z.componentDidCatch=="function"&&(ah===null||!ah.has(Z))))return _.flags|=65536,V&=-V,_.lanes|=V,V=iB(V),sB(V,h,_,j),rj(_,V),!1}_=_.return}while(_!==null);return!1}var Tj=Error(r(461)),hi=!1;function us(h,y,_,j){y.child=h===null?dU(y,null,_,j):lf(y,h.child,_,j)}function oB(h,y,_,j,V){_=_.render;var Z=y.ref;if("ref"in j){var Ne={};for(var qe in j)qe!=="ref"&&(Ne[qe]=j[qe])}else Ne=j;return rf(y),j=cj(h,y,_,Ne,Z,V),qe=dj(),h!==null&&!hi?(uj(h,y,V),gu(h,y,V)):(Sr&&qe&&$A(y),y.flags|=1,us(h,y,j,V),y.child)}function lB(h,y,_,j,V){if(h===null){var Z=_.type;return typeof Z=="function"&&!BA(Z)&&Z.defaultProps===void 0&&_.compare===null?(y.tag=15,y.type=Z,cB(h,y,Z,j,V)):(h=t_(_.type,null,j,y,y.mode,V),h.ref=y.ref,h.return=y,y.child=h)}if(Z=h.child,!Lj(h,V)){var Ne=Z.memoizedProps;if(_=_.compare,_=_!==null?_:Vm,_(Ne,j)&&h.ref===y.ref)return gu(h,y,V)}return y.flags|=1,h=du(Z,j),h.ref=y.ref,h.return=y,y.child=h}function cB(h,y,_,j,V){if(h!==null){var Z=h.memoizedProps;if(Vm(Z,j)&&h.ref===y.ref)if(hi=!1,y.pendingProps=j=Z,Lj(h,V))(h.flags&131072)!==0&&(hi=!0);else return y.lanes=h.lanes,gu(h,y,V)}return Aj(h,y,_,j,V)}function dB(h,y,_,j){var V=j.children,Z=h!==null?h.memoizedState:null;if(h===null&&y.stateNode===null&&(y.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),j.mode==="hidden"){if((y.flags&128)!==0){if(Z=Z!==null?Z.baseLanes|_:_,h!==null){for(j=y.child=h.child,V=0;j!==null;)V=V|j.lanes|j.childLanes,j=j.sibling;j=V&~Z}else j=0,y.child=null;return uB(h,y,Z,_,j)}if((_&536870912)!==0)y.memoizedState={baseLanes:0,cachePool:null},h!==null&&i_(y,Z!==null?Z.cachePool:null),Z!==null?hU(y,Z):ij(),pU(y);else return j=y.lanes=536870912,uB(h,y,Z!==null?Z.baseLanes|_:_,_,j)}else Z!==null?(i_(y,Z.cachePool),hU(y,Z),eh(),y.memoizedState=null):(h!==null&&i_(y,null),ij(),eh());return us(h,y,V,_),y.child}function hv(h,y){return h!==null&&h.tag===22||y.stateNode!==null||(y.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),y.sibling}function uB(h,y,_,j,V){var Z=ZA();return Z=Z===null?null:{parent:ui._currentValue,pool:Z},y.memoizedState={baseLanes:_,cachePool:Z},h!==null&&i_(y,null),ij(),pU(y),h!==null&&lb(h,y,j,!0),y.childLanes=V,null}function w_(h,y){return y=__({mode:y.mode,children:y.children},h.mode),y.ref=h.ref,h.child=y,y.return=h,y}function mB(h,y,_){return lf(y,h.child,null,_),h=w_(y,y.pendingProps),h.flags|=2,Yo(y),y.memoizedState=null,h}function lde(h,y,_){var j=y.pendingProps,V=(y.flags&128)!==0;if(y.flags&=-129,h===null){if(Sr){if(j.mode==="hidden")return h=w_(y,j),y.lanes=536870912,hv(null,h);if(oj(y),(h=xa)?(h=k8(h,El),h=h!==null&&h.data==="&"?h:null,h!==null&&(y.memoizedState={dehydrated:h,treeContext:Gm!==null?{id:ld,overflow:cd}:null,retryLane:536870912,hydrationErrors:null},_=X5(h),_.return=y,y.child=_,cs=y,xa=null)):h=null,h===null)throw Km(y);return y.lanes=536870912,null}return w_(y,j)}var Z=h.memoizedState;if(Z!==null){var Ne=Z.dehydrated;if(oj(y),V)if(y.flags&256)y.flags&=-257,y=mB(h,y,_);else if(y.memoizedState!==null)y.child=h.child,y.flags|=128,y=null;else throw Error(r(558));else if(hi||lb(h,y,_,!1),V=(_&h.childLanes)!==0,hi||V){if(j=oa,j!==null&&(Ne=Vt(j,_),Ne!==0&&Ne!==Z.retryLane))throw Z.retryLane=Ne,Zp(h,Ne),To(j,h,Ne),Tj;E_(),y=mB(h,y,_)}else h=Z.treeContext,xa=Fl(Ne.nextSibling),cs=y,Sr=!0,Wm=null,El=!1,h!==null&&J5(y,h),y=w_(y,j),y.flags|=4096;return y}return h=du(h.child,{mode:j.mode,children:j.children}),h.ref=y.ref,y.child=h,h.return=y,h}function S_(h,y){var _=y.ref;if(_===null)h!==null&&h.ref!==null&&(y.flags|=4194816);else{if(typeof _!="function"&&typeof _!="object")throw Error(r(284));(h===null||h.ref!==_)&&(y.flags|=4194816)}}function Aj(h,y,_,j,V){return rf(y),_=cj(h,y,_,j,void 0,V),j=dj(),h!==null&&!hi?(uj(h,y,V),gu(h,y,V)):(Sr&&j&&$A(y),y.flags|=1,us(h,y,_,V),y.child)}function hB(h,y,_,j,V,Z){return rf(y),y.updateQueue=null,_=gU(y,j,_,V),fU(h),j=dj(),h!==null&&!hi?(uj(h,y,Z),gu(h,y,Z)):(Sr&&j&&$A(y),y.flags|=1,us(h,y,_,Z),y.child)}function pB(h,y,_,j,V){if(rf(y),y.stateNode===null){var Z=ab,Ne=_.contextType;typeof Ne=="object"&&Ne!==null&&(Z=ds(Ne)),Z=new _(j,Z),y.memoizedState=Z.state!==null&&Z.state!==void 0?Z.state:null,Z.updater=Cj,y.stateNode=Z,Z._reactInternals=y,Z=y.stateNode,Z.props=j,Z.state=y.memoizedState,Z.refs={},tj(y),Ne=_.contextType,Z.context=typeof Ne=="object"&&Ne!==null?ds(Ne):ab,Z.state=y.memoizedState,Ne=_.getDerivedStateFromProps,typeof Ne=="function"&&(Nj(y,_,Ne,j),Z.state=y.memoizedState),typeof _.getDerivedStateFromProps=="function"||typeof Z.getSnapshotBeforeUpdate=="function"||typeof Z.UNSAFE_componentWillMount!="function"&&typeof Z.componentWillMount!="function"||(Ne=Z.state,typeof Z.componentWillMount=="function"&&Z.componentWillMount(),typeof Z.UNSAFE_componentWillMount=="function"&&Z.UNSAFE_componentWillMount(),Ne!==Z.state&&Cj.enqueueReplaceState(Z,Z.state,null),lv(y,j,Z,V),ov(),Z.state=y.memoizedState),typeof Z.componentDidMount=="function"&&(y.flags|=4194308),j=!0}else if(h===null){Z=y.stateNode;var qe=y.memoizedProps,lt=df(_,qe);Z.props=lt;var vt=Z.context,Rt=_.contextType;Ne=ab,typeof Rt=="object"&&Rt!==null&&(Ne=ds(Rt));var Ut=_.getDerivedStateFromProps;Rt=typeof Ut=="function"||typeof Z.getSnapshotBeforeUpdate=="function",qe=y.pendingProps!==qe,Rt||typeof Z.UNSAFE_componentWillReceiveProps!="function"&&typeof Z.componentWillReceiveProps!="function"||(qe||vt!==Ne)&&eB(y,Z,j,Ne),Ym=!1;var kt=y.memoizedState;Z.state=kt,lv(y,j,Z,V),ov(),vt=y.memoizedState,qe||kt!==vt||Ym?(typeof Ut=="function"&&(Nj(y,_,Ut,j),vt=y.memoizedState),(lt=Ym||ZU(y,_,lt,j,kt,vt,Ne))?(Rt||typeof Z.UNSAFE_componentWillMount!="function"&&typeof Z.componentWillMount!="function"||(typeof Z.componentWillMount=="function"&&Z.componentWillMount(),typeof Z.UNSAFE_componentWillMount=="function"&&Z.UNSAFE_componentWillMount()),typeof Z.componentDidMount=="function"&&(y.flags|=4194308)):(typeof Z.componentDidMount=="function"&&(y.flags|=4194308),y.memoizedProps=j,y.memoizedState=vt),Z.props=j,Z.state=vt,Z.context=Ne,j=lt):(typeof Z.componentDidMount=="function"&&(y.flags|=4194308),j=!1)}else{Z=y.stateNode,nj(h,y),Ne=y.memoizedProps,Rt=df(_,Ne),Z.props=Rt,Ut=y.pendingProps,kt=Z.context,vt=_.contextType,lt=ab,typeof vt=="object"&&vt!==null&&(lt=ds(vt)),qe=_.getDerivedStateFromProps,(vt=typeof qe=="function"||typeof Z.getSnapshotBeforeUpdate=="function")||typeof Z.UNSAFE_componentWillReceiveProps!="function"&&typeof Z.componentWillReceiveProps!="function"||(Ne!==Ut||kt!==lt)&&eB(y,Z,j,lt),Ym=!1,kt=y.memoizedState,Z.state=kt,lv(y,j,Z,V),ov();var At=y.memoizedState;Ne!==Ut||kt!==At||Ym||h!==null&&h.dependencies!==null&&r_(h.dependencies)?(typeof qe=="function"&&(Nj(y,_,qe,j),At=y.memoizedState),(Rt=Ym||ZU(y,_,Rt,j,kt,At,lt)||h!==null&&h.dependencies!==null&&r_(h.dependencies))?(vt||typeof Z.UNSAFE_componentWillUpdate!="function"&&typeof Z.componentWillUpdate!="function"||(typeof Z.componentWillUpdate=="function"&&Z.componentWillUpdate(j,At,lt),typeof Z.UNSAFE_componentWillUpdate=="function"&&Z.UNSAFE_componentWillUpdate(j,At,lt)),typeof Z.componentDidUpdate=="function"&&(y.flags|=4),typeof Z.getSnapshotBeforeUpdate=="function"&&(y.flags|=1024)):(typeof Z.componentDidUpdate!="function"||Ne===h.memoizedProps&&kt===h.memoizedState||(y.flags|=4),typeof Z.getSnapshotBeforeUpdate!="function"||Ne===h.memoizedProps&&kt===h.memoizedState||(y.flags|=1024),y.memoizedProps=j,y.memoizedState=At),Z.props=j,Z.state=At,Z.context=lt,j=Rt):(typeof Z.componentDidUpdate!="function"||Ne===h.memoizedProps&&kt===h.memoizedState||(y.flags|=4),typeof Z.getSnapshotBeforeUpdate!="function"||Ne===h.memoizedProps&&kt===h.memoizedState||(y.flags|=1024),j=!1)}return Z=j,S_(h,y),j=(y.flags&128)!==0,Z||j?(Z=y.stateNode,_=j&&typeof _.getDerivedStateFromError!="function"?null:Z.render(),y.flags|=1,h!==null&&j?(y.child=lf(y,h.child,null,V),y.child=lf(y,null,_,V)):us(h,y,_,V),y.memoizedState=Z.state,h=y.child):h=gu(h,y,V),h}function fB(h,y,_,j){return tf(),y.flags|=256,us(h,y,_,j),y.child}var jj={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function Mj(h){return{baseLanes:h,cachePool:aU()}}function Ej(h,y,_){return h=h!==null?h.childLanes&~_:0,y&&(h|=Jo),h}function gB(h,y,_){var j=y.pendingProps,V=!1,Z=(y.flags&128)!==0,Ne;if((Ne=Z)||(Ne=h!==null&&h.memoizedState===null?!1:(ai.current&2)!==0),Ne&&(V=!0,y.flags&=-129),Ne=(y.flags&32)!==0,y.flags&=-33,h===null){if(Sr){if(V?Zm(y):eh(),(h=xa)?(h=k8(h,El),h=h!==null&&h.data!=="&"?h:null,h!==null&&(y.memoizedState={dehydrated:h,treeContext:Gm!==null?{id:ld,overflow:cd}:null,retryLane:536870912,hydrationErrors:null},_=X5(h),_.return=y,y.child=_,cs=y,xa=null)):h=null,h===null)throw Km(y);return pM(h)?y.lanes=32:y.lanes=536870912,null}var qe=j.children;return j=j.fallback,V?(eh(),V=y.mode,qe=__({mode:"hidden",children:qe},V),j=ef(j,V,_,null),qe.return=y,j.return=y,qe.sibling=j,y.child=qe,j=y.child,j.memoizedState=Mj(_),j.childLanes=Ej(h,Ne,_),y.memoizedState=jj,hv(null,j)):(Zm(y),Dj(y,qe))}var lt=h.memoizedState;if(lt!==null&&(qe=lt.dehydrated,qe!==null)){if(Z)y.flags&256?(Zm(y),y.flags&=-257,y=Fj(h,y,_)):y.memoizedState!==null?(eh(),y.child=h.child,y.flags|=128,y=null):(eh(),qe=j.fallback,V=y.mode,j=__({mode:"visible",children:j.children},V),qe=ef(qe,V,_,null),qe.flags|=2,j.return=y,qe.return=y,j.sibling=qe,y.child=j,lf(y,h.child,null,_),j=y.child,j.memoizedState=Mj(_),j.childLanes=Ej(h,Ne,_),y.memoizedState=jj,y=hv(null,j));else if(Zm(y),pM(qe)){if(Ne=qe.nextSibling&&qe.nextSibling.dataset,Ne)var vt=Ne.dgst;Ne=vt,j=Error(r(419)),j.stack="",j.digest=Ne,tv({value:j,source:null,stack:null}),y=Fj(h,y,_)}else if(hi||lb(h,y,_,!1),Ne=(_&h.childLanes)!==0,hi||Ne){if(Ne=oa,Ne!==null&&(j=Vt(Ne,_),j!==0&&j!==lt.retryLane))throw lt.retryLane=j,Zp(h,j),To(Ne,h,j),Tj;hM(qe)||E_(),y=Fj(h,y,_)}else hM(qe)?(y.flags|=192,y.child=h.child,y=null):(h=lt.treeContext,xa=Fl(qe.nextSibling),cs=y,Sr=!0,Wm=null,El=!1,h!==null&&J5(y,h),y=Dj(y,j.children),y.flags|=4096);return y}return V?(eh(),qe=j.fallback,V=y.mode,lt=h.child,vt=lt.sibling,j=du(lt,{mode:"hidden",children:j.children}),j.subtreeFlags=lt.subtreeFlags&65011712,vt!==null?qe=du(vt,qe):(qe=ef(qe,V,_,null),qe.flags|=2),qe.return=y,j.return=y,j.sibling=qe,y.child=j,hv(null,j),j=y.child,qe=h.child.memoizedState,qe===null?qe=Mj(_):(V=qe.cachePool,V!==null?(lt=ui._currentValue,V=V.parent!==lt?{parent:lt,pool:lt}:V):V=aU(),qe={baseLanes:qe.baseLanes|_,cachePool:V}),j.memoizedState=qe,j.childLanes=Ej(h,Ne,_),y.memoizedState=jj,hv(h.child,j)):(Zm(y),_=h.child,h=_.sibling,_=du(_,{mode:"visible",children:j.children}),_.return=y,_.sibling=null,h!==null&&(Ne=y.deletions,Ne===null?(y.deletions=[h],y.flags|=16):Ne.push(h)),y.child=_,y.memoizedState=null,_)}function Dj(h,y){return y=__({mode:"visible",children:y},h.mode),y.return=h,h.child=y}function __(h,y){return h=Ko(22,h,null,y),h.lanes=0,h}function Fj(h,y,_){return lf(y,h.child,null,_),h=Dj(y,y.pendingProps.children),h.flags|=2,y.memoizedState=null,h}function bB(h,y,_){h.lanes|=y;var j=h.alternate;j!==null&&(j.lanes|=y),XA(h.return,y,_)}function Rj(h,y,_,j,V,Z){var Ne=h.memoizedState;Ne===null?h.memoizedState={isBackwards:y,rendering:null,renderingStartTime:0,last:j,tail:_,tailMode:V,treeForkCount:Z}:(Ne.isBackwards=y,Ne.rendering=null,Ne.renderingStartTime=0,Ne.last=j,Ne.tail=_,Ne.tailMode=V,Ne.treeForkCount=Z)}function xB(h,y,_){var j=y.pendingProps,V=j.revealOrder,Z=j.tail;j=j.children;var Ne=ai.current,qe=(Ne&2)!==0;if(qe?(Ne=Ne&1|2,y.flags|=128):Ne&=1,pe(ai,Ne),us(h,y,j,_),j=Sr?ev:0,!qe&&h!==null&&(h.flags&128)!==0)e:for(h=y.child;h!==null;){if(h.tag===13)h.memoizedState!==null&&bB(h,_,y);else if(h.tag===19)bB(h,_,y);else if(h.child!==null){h.child.return=h,h=h.child;continue}if(h===y)break e;for(;h.sibling===null;){if(h.return===null||h.return===y)break e;h=h.return}h.sibling.return=h.return,h=h.sibling}switch(V){case"forwards":for(_=y.child,V=null;_!==null;)h=_.alternate,h!==null&&u_(h)===null&&(V=_),_=_.sibling;_=V,_===null?(V=y.child,y.child=null):(V=_.sibling,_.sibling=null),Rj(y,!1,V,_,Z,j);break;case"backwards":case"unstable_legacy-backwards":for(_=null,V=y.child,y.child=null;V!==null;){if(h=V.alternate,h!==null&&u_(h)===null){y.child=V;break}h=V.sibling,V.sibling=_,_=V,V=h}Rj(y,!0,_,null,Z,j);break;case"together":Rj(y,!1,null,null,void 0,j);break;default:y.memoizedState=null}return y.child}function gu(h,y,_){if(h!==null&&(y.dependencies=h.dependencies),rh|=y.lanes,(_&y.childLanes)===0)if(h!==null){if(lb(h,y,_,!1),(_&y.childLanes)===0)return null}else return null;if(h!==null&&y.child!==h.child)throw Error(r(153));if(y.child!==null){for(h=y.child,_=du(h,h.pendingProps),y.child=_,_.return=y;h.sibling!==null;)h=h.sibling,_=_.sibling=du(h,h.pendingProps),_.return=y;_.sibling=null}return y.child}function Lj(h,y){return(h.lanes&y)!==0?!0:(h=h.dependencies,!!(h!==null&&r_(h)))}function cde(h,y,_){switch(y.tag){case 3:F(y,y.stateNode.containerInfo),Xm(y,ui,h.memoizedState.cache),tf();break;case 27:case 5:U(y);break;case 4:F(y,y.stateNode.containerInfo);break;case 10:Xm(y,y.type,y.memoizedProps.value);break;case 31:if(y.memoizedState!==null)return y.flags|=128,oj(y),null;break;case 13:var j=y.memoizedState;if(j!==null)return j.dehydrated!==null?(Zm(y),y.flags|=128,null):(_&y.child.childLanes)!==0?gB(h,y,_):(Zm(y),h=gu(h,y,_),h!==null?h.sibling:null);Zm(y);break;case 19:var V=(h.flags&128)!==0;if(j=(_&y.childLanes)!==0,j||(lb(h,y,_,!1),j=(_&y.childLanes)!==0),V){if(j)return xB(h,y,_);y.flags|=128}if(V=y.memoizedState,V!==null&&(V.rendering=null,V.tail=null,V.lastEffect=null),pe(ai,ai.current),j)break;return null;case 22:return y.lanes=0,dB(h,y,_,y.pendingProps);case 24:Xm(y,ui,h.memoizedState.cache)}return gu(h,y,_)}function yB(h,y,_){if(h!==null)if(h.memoizedProps!==y.pendingProps)hi=!0;else{if(!Lj(h,_)&&(y.flags&128)===0)return hi=!1,cde(h,y,_);hi=(h.flags&131072)!==0}else hi=!1,Sr&&(y.flags&1048576)!==0&&Q5(y,ev,y.index);switch(y.lanes=0,y.tag){case 16:e:{var j=y.pendingProps;if(h=sf(y.elementType),y.type=h,typeof h=="function")BA(h)?(j=df(h,j),y.tag=1,y=pB(null,y,h,j,_)):(y.tag=0,y=Aj(null,y,h,j,_));else{if(h!=null){var V=h.$$typeof;if(V===P){y.tag=11,y=oB(null,y,h,j,_);break e}else if(V===T){y.tag=14,y=lB(null,y,h,j,_);break e}}throw y=L(h)||h,Error(r(306,y,""))}}return y;case 0:return Aj(h,y,y.type,y.pendingProps,_);case 1:return j=y.type,V=df(j,y.pendingProps),pB(h,y,j,V,_);case 3:e:{if(F(y,y.stateNode.containerInfo),h===null)throw Error(r(387));j=y.pendingProps;var Z=y.memoizedState;V=Z.element,nj(h,y),lv(y,j,null,_);var Ne=y.memoizedState;if(j=Ne.cache,Xm(y,ui,j),j!==Z.cache&&YA(y,[ui],_,!0),ov(),j=Ne.element,Z.isDehydrated)if(Z={element:j,isDehydrated:!1,cache:Ne.cache},y.updateQueue.baseState=Z,y.memoizedState=Z,y.flags&256){y=fB(h,y,j,_);break e}else if(j!==V){V=Al(Error(r(424)),y),tv(V),y=fB(h,y,j,_);break e}else for(h=y.stateNode.containerInfo,h.nodeType===9?h=h.body:h=h.nodeName==="HTML"?h.ownerDocument.body:h,xa=Fl(h.firstChild),cs=y,Sr=!0,Wm=null,El=!0,_=dU(y,null,j,_),y.child=_;_;)_.flags=_.flags&-3|4096,_=_.sibling;else{if(tf(),j===V){y=gu(h,y,_);break e}us(h,y,j,_)}y=y.child}return y;case 26:return S_(h,y),h===null?(_=j8(y.type,null,y.pendingProps,null))?y.memoizedState=_:Sr||(_=y.type,h=y.pendingProps,j=z_(B.current).createElement(_),j[jt]=y,j[Jt]=h,ms(j,_,h),nt(j),y.stateNode=j):y.memoizedState=j8(y.type,h.memoizedProps,y.pendingProps,h.memoizedState),null;case 27:return U(y),h===null&&Sr&&(j=y.stateNode=P8(y.type,y.pendingProps,B.current),cs=y,El=!0,V=xa,lh(y.type)?(fM=V,xa=Fl(j.firstChild)):xa=V),us(h,y,y.pendingProps.children,_),S_(h,y),h===null&&(y.flags|=4194304),y.child;case 5:return h===null&&Sr&&((V=j=xa)&&(j=zde(j,y.type,y.pendingProps,El),j!==null?(y.stateNode=j,cs=y,xa=Fl(j.firstChild),El=!1,V=!0):V=!1),V||Km(y)),U(y),V=y.type,Z=y.pendingProps,Ne=h!==null?h.memoizedProps:null,j=Z.children,dM(V,Z)?j=null:Ne!==null&&dM(V,Ne)&&(y.flags|=32),y.memoizedState!==null&&(V=cj(h,y,ede,null,null,_),Tv._currentValue=V),S_(h,y),us(h,y,j,_),y.child;case 6:return h===null&&Sr&&((h=_=xa)&&(_=Ude(_,y.pendingProps,El),_!==null?(y.stateNode=_,cs=y,xa=null,h=!0):h=!1),h||Km(y)),null;case 13:return gB(h,y,_);case 4:return F(y,y.stateNode.containerInfo),j=y.pendingProps,h===null?y.child=lf(y,null,j,_):us(h,y,j,_),y.child;case 11:return oB(h,y,y.type,y.pendingProps,_);case 7:return us(h,y,y.pendingProps,_),y.child;case 8:return us(h,y,y.pendingProps.children,_),y.child;case 12:return us(h,y,y.pendingProps.children,_),y.child;case 10:return j=y.pendingProps,Xm(y,y.type,j.value),us(h,y,j.children,_),y.child;case 9:return V=y.type._context,j=y.pendingProps.children,rf(y),V=ds(V),j=j(V),y.flags|=1,us(h,y,j,_),y.child;case 14:return lB(h,y,y.type,y.pendingProps,_);case 15:return cB(h,y,y.type,y.pendingProps,_);case 19:return xB(h,y,_);case 31:return lde(h,y,_);case 22:return dB(h,y,_,y.pendingProps);case 24:return rf(y),j=ds(ui),h===null?(V=ZA(),V===null&&(V=oa,Z=QA(),V.pooledCache=Z,Z.refCount++,Z!==null&&(V.pooledCacheLanes|=_),V=Z),y.memoizedState={parent:j,cache:V},tj(y),Xm(y,ui,V)):((h.lanes&_)!==0&&(nj(h,y),lv(y,null,null,_),ov()),V=h.memoizedState,Z=y.memoizedState,V.parent!==j?(V={parent:j,cache:j},y.memoizedState=V,y.lanes===0&&(y.memoizedState=y.updateQueue.baseState=V),Xm(y,ui,j)):(j=Z.cache,Xm(y,ui,j),j!==V.cache&&YA(y,[ui],_,!0))),us(h,y,y.pendingProps.children,_),y.child;case 29:throw y.pendingProps}throw Error(r(156,y.tag))}function bu(h){h.flags|=4}function Ij(h,y,_,j,V){if((y=(h.mode&32)!==0)&&(y=!1),y){if(h.flags|=16777216,(V&335544128)===V)if(h.stateNode.complete)h.flags|=8192;else if(GB())h.flags|=8192;else throw of=o_,ej}else h.flags&=-16777217}function vB(h,y){if(y.type!=="stylesheet"||(y.state.loading&4)!==0)h.flags&=-16777217;else if(h.flags|=16777216,!R8(y))if(GB())h.flags|=8192;else throw of=o_,ej}function k_(h,y){y!==null&&(h.flags|=4),h.flags&16384&&(y=h.tag!==22?st():536870912,h.lanes|=y,vb|=y)}function pv(h,y){if(!Sr)switch(h.tailMode){case"hidden":y=h.tail;for(var _=null;y!==null;)y.alternate!==null&&(_=y),y=y.sibling;_===null?h.tail=null:_.sibling=null;break;case"collapsed":_=h.tail;for(var j=null;_!==null;)_.alternate!==null&&(j=_),_=_.sibling;j===null?y||h.tail===null?h.tail=null:h.tail.sibling=null:j.sibling=null}}function ya(h){var y=h.alternate!==null&&h.alternate.child===h.child,_=0,j=0;if(y)for(var V=h.child;V!==null;)_|=V.lanes|V.childLanes,j|=V.subtreeFlags&65011712,j|=V.flags&65011712,V.return=h,V=V.sibling;else for(V=h.child;V!==null;)_|=V.lanes|V.childLanes,j|=V.subtreeFlags,j|=V.flags,V.return=h,V=V.sibling;return h.subtreeFlags|=j,h.childLanes=_,y}function dde(h,y,_){var j=y.pendingProps;switch(VA(y),y.tag){case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return ya(y),null;case 1:return ya(y),null;case 3:return _=y.stateNode,j=null,h!==null&&(j=h.memoizedState.cache),y.memoizedState.cache!==j&&(y.flags|=2048),hu(ui),E(),_.pendingContext&&(_.context=_.pendingContext,_.pendingContext=null),(h===null||h.child===null)&&(ob(y)?bu(y):h===null||h.memoizedState.isDehydrated&&(y.flags&256)===0||(y.flags|=1024,WA())),ya(y),null;case 26:var V=y.type,Z=y.memoizedState;return h===null?(bu(y),Z!==null?(ya(y),vB(y,Z)):(ya(y),Ij(y,V,null,j,_))):Z?Z!==h.memoizedState?(bu(y),ya(y),vB(y,Z)):(ya(y),y.flags&=-16777217):(h=h.memoizedProps,h!==j&&bu(y),ya(y),Ij(y,V,h,j,_)),null;case 27:if(Q(y),_=B.current,V=y.type,h!==null&&y.stateNode!=null)h.memoizedProps!==j&&bu(y);else{if(!j){if(y.stateNode===null)throw Error(r(166));return ya(y),null}h=be.current,ob(y)?Z5(y):(h=P8(V,j,_),y.stateNode=h,bu(y))}return ya(y),null;case 5:if(Q(y),V=y.type,h!==null&&y.stateNode!=null)h.memoizedProps!==j&&bu(y);else{if(!j){if(y.stateNode===null)throw Error(r(166));return ya(y),null}if(Z=be.current,ob(y))Z5(y);else{var Ne=z_(B.current);switch(Z){case 1:Z=Ne.createElementNS("http://www.w3.org/2000/svg",V);break;case 2:Z=Ne.createElementNS("http://www.w3.org/1998/Math/MathML",V);break;default:switch(V){case"svg":Z=Ne.createElementNS("http://www.w3.org/2000/svg",V);break;case"math":Z=Ne.createElementNS("http://www.w3.org/1998/Math/MathML",V);break;case"script":Z=Ne.createElement("div"),Z.innerHTML=" +