From 7ea4410b211dce4afca7dc4295c5011e02383164 Mon Sep 17 00:00:00 2001
From: maziggy
Date: Sat, 23 May 2026 08:47:46 +0200
Subject: [PATCH] fix(queue): insufficient-filament warning now fires on
every dispatch path (#1496)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The pre-print deficit warning from #720 only ran inside the PrintModal
submit flow. Both the green ▶ button on a staged queue row (POST
/queue/{id}/start) and the Virtual Printer queue-mode intake bypassed
it — auto_dispatch=True VP intakes would dispatch unsupervised onto
spools that physically can't complete the print.
Extracted the deficit check into backend/app/services/filament_deficit.py
(single source of truth, both internal inventory and Spoolman modes).
POST /queue/{id}/start returns 409 with a structured deficit payload
unless ?skip_filament_check=true. The dispatch scheduler runs the same
check before each _start_print; a deficit promotes the item to
manual_start + sets a new filament_short flag (idempotent migration on
print_queue). The flag clears automatically on the next tick when the
operator swaps a spool to one with enough material.
Frontend ▶ catches the 409 and opens a confirm modal showing each
shorted slot's required vs remaining grams; the row now renders a
yellow "Insufficient filament" badge when filament_short is set.
Translated across all 9 locales.
---
CHANGELOG.md | 1 +
backend/app/api/routes/print_queue.py | 35 ++-
backend/app/core/database.py | 9 +
backend/app/models/print_queue.py | 7 +
backend/app/schemas/print_queue.py | 5 +
backend/app/services/filament_deficit.py | 287 ++++++++++++++++++
backend/app/services/print_scheduler.py | 61 ++++
.../tests/integration/test_print_queue_api.py | 87 ++++++
.../unit/services/test_filament_deficit.py | 267 ++++++++++++++++
.../unit/test_scheduler_filament_deficit.py | 119 ++++++++
.../src/__tests__/pages/QueuePage.test.tsx | 79 +++++
frontend/src/api/client.ts | 38 ++-
frontend/src/i18n/locales/de.ts | 9 +
frontend/src/i18n/locales/en.ts | 9 +
frontend/src/i18n/locales/es.ts | 9 +
frontend/src/i18n/locales/fr.ts | 9 +
frontend/src/i18n/locales/it.ts | 9 +
frontend/src/i18n/locales/ja.ts | 9 +
frontend/src/i18n/locales/pt-BR.ts | 9 +
frontend/src/i18n/locales/zh-CN.ts | 9 +
frontend/src/i18n/locales/zh-TW.ts | 9 +
frontend/src/pages/QueuePage.tsx | 74 ++++-
.../{index-B5qvYtQq.js => index-vzamAN0i.js} | 171 ++++++-----
static/index.html | 2 +-
24 files changed, 1224 insertions(+), 99 deletions(-)
create mode 100644 backend/app/services/filament_deficit.py
create mode 100644 backend/tests/unit/services/test_filament_deficit.py
create mode 100644 backend/tests/unit/test_scheduler_filament_deficit.py
rename static/assets/{index-B5qvYtQq.js => index-vzamAN0i.js} (73%)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff2c361c6..c7048abbf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ All notable changes to Bambuddy will be documented in this file.
- **PyJWT CVE-2025-45768 (PYSEC-2025-183 / GHSA-65pc-fj4g-8rjx): permanently ignored in pip-audit** — Advisory is disputed by the PyJWT maintainers, with the advisory description literally noting *"this is disputed by the Supplier because the key length is chosen by the application that uses the library."* `fix_versions=[]` on the advisory confirms no PyJWT patch exists or will exist. Bambuddy is not affected: `backend/app/core/auth.py:184` auto-generates secrets via `secrets.token_urlsafe(64)` (~86 chars of entropy, far above any sane minimum) and the file-loaded path at `:177` rejects secrets shorter than 32 chars. Added a permanent `--ignore-vuln CVE-2025-45768` to `.github/workflows/security.yml` with an inline comment citing the file:line evidence so a future maintainer reviewing the ignore list sees why it's load-bearing. Also dropped the stale `--ignore-vuln CVE-2026-4539` for Pygments — Pygments has since shipped a patched version and the ignore is no longer load-bearing (verified: `pip-audit --ignore-vuln CVE-2025-45768` alone reports clean).
### Fixed
+- **Insufficient-filament pre-print warning now fires on every dispatch path (#1496, reported by @needo37)** — The "Pre-print checks now also warn when the spool has insufficient material" guard from #720 only fired on the `PrintModal` submit path. Two other queue-dispatch paths bypassed it entirely: the green ▶ Play button on a staged (`manual_start`) queue row called `POST /queue/{id}/start`, which only flipped the manual_start flag with no filament check; and the Virtual Printer queue-mode intake (`virtual_printer/manager._add_to_print_queue`) parsed per-slot requirements for *type* matching only — never weight. With `auto_dispatch=True` the scheduler would then dispatch unsupervised onto a doomed-to-fail spool. **Fix**: extracted the per-slot deficit calculation into a single backend helper (`backend/app/services/filament_deficit.py`) that both the route and the dispatch scheduler call against live spool state. Works for internal-inventory mode (`SpoolAssignment` → `Spool.label_weight - weight_used`) and Spoolman mode (`SpoolmanSlotAssignment` → `SpoolmanClient.get_spool`); Spoolman unreachability returns no deficit rather than wedging the queue. The `disable_filament_warnings` setting is honoured at the service boundary. `POST /queue/{id}/start` now returns `409 {detail: {code: 'insufficient_filament', deficit: [...]}}` when short; the `?skip_filament_check=true` query param is the "Print Anyway" bypass. The dispatch scheduler runs the same check just before each `_start_print` call: a deficit promotes the item to `manual_start=True` + `filament_short=True` (so the user must consciously click ▶) and a previously-flagged item whose spool was swapped to one with enough material clears the flag automatically on the next tick. A new `filament_short` boolean column on `print_queue` carries the flag; the queue row now renders a yellow "Insufficient filament for the assigned spool" badge when set, and the ▶ button catches the 409, opens an `Insufficient Filament / Print Anyway` confirm modal showing each shorted slot's required-vs-remaining grams, and on confirm re-issues the start with the skip flag. Migration is idempotent and branches on `is_sqlite()` for the `BOOLEAN DEFAULT` syntax. **Tests**: 8 in `test_filament_deficit.py` (deficit + sufficient + missing mapping + no printer + disabled-warnings + no-assignment + missing 3MF + multi-slot only-shorted-returned), 4 in `test_scheduler_filament_deficit.py` (block-on-deficit, clear-stale-flag, no-deficit no-op, helper-exception doesn't wedge), 2 new in `test_print_queue_api.py` (`/start` returns 409 + structured payload, `?skip_filament_check=true` bypasses), and 2 frontend tests in `QueuePage.test.tsx` (badge renders on flagged row, ▶ click → 409 → modal → retry with `skip_filament_check=true`). 3392 unit + 63 print-queue integration green; backend ruff clean; frontend build + i18n parity (9 locales × 4979 keys) clean.
- **File Manager "All Files" view showed nothing when every file lived in a subfolder (#1499)** — The sidebar entry was meant to list every file across the library but instead returned only files at the library root, so a library with two folders and three files (all nested) appeared empty. Cause was an inverted boolean on the React Query call: `getLibraryFiles(selectedFolderId, selectedFolderId === null)` passed `include_root=true` for the "All Files" selection, which on the backend (`library.py` `list_files`) means *root files only* — the opposite of what the UI wanted. **Fix**: pass `include_root=false` for "All Files" so the backend returns every active file across folders (it remains a no-op when a specific folder is selected — `folder_id` takes precedence). A new vitest regression case renders the page with one root file and one nested file and asserts both appear, and that the request goes out with `include_root=false`. 48/48 FileManagerPage tests green; frontend build clean.
- **Archive filament colour now reflects the assigned inventory spool, not the slicer's 3MF (#1494, reported by @IndividualGhost1905)** — A user added a `#000000` black filament to the built-in inventory, assigned it to the printer, and printed from the desktop slicer; the print, AMS and inventory all showed it as black and the correct spool's weight decremented — but the resulting archive (and the Color Distribution graph) showed `#161616`. Root cause is two independent colour sources: an archive's `filament_color` is parsed verbatim from the print job's 3MF (`archive.py` `_extract_filament_info` reads `filament_colour` from `project_settings.config`), which carries the *slicer's* filament-slot colour — a value the user picks separately from the exact hex they curate on the Bambuddy inventory spool. The two are "close but not equal" (slicer near-black `#161616` vs inventory `#000000`), which is exactly the "always a similar colour, never an unrelated one" pattern the report describes. **Fix**: once usage tracking has resolved the print's filament slots to inventory spools, the spool colours are authoritative — `_track_from_3mf` (built-in inventory) and `report_usage` (Spoolman mode) now overwrite the archive's `filament_color` with the slot-ordered, de-duplicated colours of the matched spools. The rewrite is **all-or-nothing**: it only applies when *every* used slot resolved to a spool that carries a colour, so a partially-mapped multi-colour print never silently loses the unmatched slots' colours (the 3MF value is kept). Shipped for both inventory modes in the same drop — built-in spools read `Spool.rgba`, Spoolman spools read the spool's `filament.color_hex` (fetched for tag-less slot-assignment matches). New helpers `_spool_color_to_hex` / `_archive_colors_from_spools` in `usage_tracker.py`, reused by `spoolman_tracking.py` via `_apply_spool_colors_to_archive`. **Tests**: 12 new in `test_usage_tracker.py` (hex normalisation, the all-or-nothing slot-colour rule across single/multi/partial/no-colour/AMS-fallback cases, and end-to-end that a `#000000` spool rewrites a `#161616` archive) + 4 in `test_spoolman_tracking.py` (the Spoolman-mode rewrite, empty/partial/missing-archive no-ops). 70 usage + Spoolman tracking tests green; backend ruff clean.
- **Re-slicing across the single-nozzle / dual-nozzle boundary now fails fast with a clear message** — Re-slicing a model that was sliced for a single-nozzle printer (X1C, P1S, A1, …) onto a dual-nozzle printer (H2D / H2D Pro), or vice versa, produced a cryptic BambuStudio rejection — *"temperature difference of the filaments used is too large"* or *"G-code in unprintable area of multi-extruder printers"* — and an attempt at automatically converting the embedded single-nozzle layout segfaults the slicer CLI. Full cross-nozzle-class re-slicing isn't supported yet (it needs a proper dual-nozzle `project_settings` reconciliation — tracked separately). Until then, both slice routes (`POST /archives/{id}/slice`, `POST /library/files/{id}/slice`) now detect a nozzle-class mismatch up front and reject it synchronously with a `400` and a plain explanation, instead of letting the user wait for a confusing slicer failure. The guard is fail-open — it only fires when both the source's `sliced_for_model` and the target printer resolve to known models of *different* nozzle class; an un-sliced source (first-time slice) or an undeterminable target is never blocked. As part of this, the dual-nozzle model classification — previously an inline `("H2D", "H2D PRO", …)` tuple **duplicated across three call sites** (`start_print`, the two K-profile routes) — is centralized into `DUAL_NOZZLE_MODELS` + `is_dual_nozzle_model()` in `printer_models.py`, and all three sites plus the new guard now consume that single source of truth. **Tests**: `is_dual_nozzle_model` (H2D/Pro/internal-codes dual, X1C/P1S/A1/H2S single, None safe); `_canonical_printer_model` (strips the `"# "` clone prefix and `" 0.4 nozzle"` suffix); `guard_nozzle_class_reslice` (blocks single→dual and dual→single, allows same-class, no-ops on an un-sliced source); and an end-to-end test that an X1C archive re-sliced for an H2D preset returns `400` before any job is enqueued. 305 slicer/MQTT/K-profile tests + 31 slice-API + the printer-model suite all green; backend ruff clean.
diff --git a/backend/app/api/routes/print_queue.py b/backend/app/api/routes/print_queue.py
index 3888ea7ea..1d6b3ac62 100644
--- a/backend/app/api/routes/print_queue.py
+++ b/backend/app/api/routes/print_queue.py
@@ -32,6 +32,7 @@ from backend.app.schemas.print_queue import (
PrintQueueItemUpdate,
PrintQueueReorder,
)
+from backend.app.services.filament_deficit import compute_deficit_for_queue_item
from backend.app.services.notification_service import notification_service
from backend.app.utils.printer_models import normalize_printer_model, normalize_printer_model_id
from backend.app.utils.threemf_tools import extract_filament_usage_from_3mf
@@ -196,6 +197,7 @@ def _enrich_response(item: PrintQueueItem) -> PrintQueueItemResponse:
"require_previous_success": item.require_previous_success,
"auto_off_after": item.auto_off_after,
"manual_start": item.manual_start,
+ "filament_short": bool(item.filament_short),
"ams_mapping": ams_mapping_parsed,
"plate_id": item.plate_id,
"bed_levelling": item.bed_levelling,
@@ -1028,19 +1030,25 @@ async def stop_queue_item(
@router.post("/{item_id}/start")
async def start_queue_item(
item_id: int,
+ skip_filament_check: bool = Query(default=False),
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_UPDATE_OWN),
):
"""Manually start a staged (manual_start) queue item.
- This clears the manual_start flag so the scheduler will pick it up,
- or starts immediately if the printer is ready.
+ Clears the manual_start flag so the scheduler picks it up. When
+ ``skip_filament_check`` is false (the default) the live filament
+ deficit (#1496) is checked first — if the assigned spool can't satisfy
+ a slot's required grams, the route returns ``409`` with the deficit
+ payload so the caller can show a confirm dialog and retry with
+ ``skip_filament_check=true``.
"""
result = await db.execute(
select(PrintQueueItem)
.options(
selectinload(PrintQueueItem.archive),
selectinload(PrintQueueItem.printer),
+ selectinload(PrintQueueItem.library_file),
selectinload(PrintQueueItem.batch),
)
.where(PrintQueueItem.id == item_id)
@@ -1052,10 +1060,29 @@ async def start_queue_item(
if item.status != "pending":
raise HTTPException(400, f"Can only start pending items, current status: '{item.status}'")
- # Clear manual_start flag so scheduler picks it up
+ # Live deficit check — re-evaluated against current spool state, so a
+ # spool swap between scheduler flagging and the user clicking ▶ clears
+ # the block automatically.
+ if not skip_filament_check:
+ deficit = await compute_deficit_for_queue_item(db, item)
+ if deficit:
+ raise HTTPException(
+ status_code=409,
+ detail={
+ "code": "insufficient_filament",
+ "deficit": [d.to_dict() for d in deficit],
+ },
+ )
+
+ # Print Anyway / no deficit: clear the flags and let the scheduler dispatch.
item.manual_start = False
+ item.filament_short = False
await db.commit()
await db.refresh(item, ["archive", "printer", "library_file", "created_by", "batch"])
- logger.info("Manually started queue item %s (cleared manual_start flag)", item_id)
+ logger.info(
+ "Manually started queue item %s (cleared manual_start; skip_filament_check=%s)",
+ item_id,
+ skip_filament_check,
+ )
return _enrich_response(item)
diff --git a/backend/app/core/database.py b/backend/app/core/database.py
index 4814ceed1..9b98776bc 100644
--- a/backend/app/core/database.py
+++ b/backend/app/core/database.py
@@ -906,6 +906,15 @@ 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: filament_short flag on print_queue (#1496). Set by the
+ # dispatch scheduler when the assigned spool can't satisfy the print's
+ # per-slot weight; surfaced as a "filament short" badge on the queue row.
+ # Postgres rejects `DEFAULT 0` for BOOLEAN — branch on dialect.
+ if is_sqlite():
+ await _safe_execute(conn, "ALTER TABLE print_queue ADD COLUMN filament_short BOOLEAN DEFAULT 0")
+ else:
+ await _safe_execute(conn, "ALTER TABLE print_queue ADD COLUMN filament_short BOOLEAN DEFAULT false")
+
# 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
diff --git a/backend/app/models/print_queue.py b/backend/app/models/print_queue.py
index 76c3f1824..e0628ef24 100644
--- a/backend/app/models/print_queue.py
+++ b/backend/app/models/print_queue.py
@@ -76,6 +76,13 @@ class PrintQueueItem(Base):
# Status: pending, printing, completed, failed, skipped, cancelled
status: Mapped[str] = mapped_column(String(20), default="pending")
+ # Set by the dispatch scheduler when the assigned spool can't satisfy
+ # this print's per-slot filament weight (#1496). Display-only flag — the
+ # actual deficit is recomputed live every time the user clicks ▶, so
+ # swapping a spool to a fuller one between flag and dispatch clears the
+ # block automatically.
+ filament_short: Mapped[bool] = mapped_column(Boolean, default=False)
+
# Tracking
started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
diff --git a/backend/app/schemas/print_queue.py b/backend/app/schemas/print_queue.py
index a672bfa35..715099253 100644
--- a/backend/app/schemas/print_queue.py
+++ b/backend/app/schemas/print_queue.py
@@ -86,6 +86,11 @@ class PrintQueueItemResponse(BaseModel):
require_previous_success: bool
auto_off_after: bool
manual_start: bool
+ # True when the dispatch scheduler last evaluated this item and the
+ # assigned spool could not satisfy at least one slot's required grams
+ # (#1496). Display-only — the ▶ click recomputes deficit against live
+ # spool state.
+ filament_short: bool = False
ams_mapping: list[int] | None = None
plate_id: int | None = None # Plate ID for multi-plate 3MF files
# Print options
diff --git a/backend/app/services/filament_deficit.py b/backend/app/services/filament_deficit.py
new file mode 100644
index 000000000..d55b0b1df
--- /dev/null
+++ b/backend/app/services/filament_deficit.py
@@ -0,0 +1,287 @@
+"""Filament-deficit check used by every queue dispatch path.
+
+The PrintModal warns when an assigned spool can't satisfy a print's per-slot
+filament weight (``Pre-print checks now also warn when the spool has
+insufficient material`` — #720). That check only runs when the user clicks
+"Print" inside PrintModal; ``QueuePage`` Play button, ``start_queue_item``
+route, and the VP intake + scheduler auto-dispatch path all skip it (#1496).
+
+This module is the single source of truth for the check. Both the route
+handler (``POST /print-queue/{id}/start``) and the dispatch scheduler call
+``compute_deficit_for_queue_item`` against live spool state.
+
+Design notes:
+* The 3MF parser is the same one used by PrintModal: per-slot ``used_grams``
+ comes from ``extract_filament_requirements`` (#1188's filament-overrides
+ pipeline) or — when the item points at an unsliced library file — falls
+ through to the file's archive copy. Anything that yields no requirements
+ is treated as "no deficit" so a malformed or stripped 3MF never blocks.
+* Both internal-inventory and Spoolman modes are covered. Internal mode
+ resolves via ``SpoolAssignment`` joined to ``Spool`` (``label_weight``
+ minus ``weight_used``). Spoolman mode resolves via
+ ``SpoolmanSlotAssignment`` then ``SpoolmanClient.get_spool`` for the live
+ remaining weight; if Spoolman is unreachable we return no deficit rather
+ than wedge the queue on a flaky network call.
+* The ``disable_filament_warnings`` user setting is respected at the
+ service boundary — callers do not have to know about it.
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+from dataclasses import dataclass
+from pathlib import Path
+
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy.orm import selectinload
+
+from backend.app.core.config import settings as app_settings
+from backend.app.models.print_queue import PrintQueueItem
+from backend.app.models.spool_assignment import SpoolAssignment
+from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
+from backend.app.services.filament_requirements import extract_filament_requirements
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class FilamentDeficit:
+ """One slot's filament shortfall."""
+
+ slot_id: int
+ ams_id: int | None
+ tray_id: int | None
+ filament_type: str
+ required_grams: float
+ remaining_grams: float | None # None = could not determine
+
+ def to_dict(self) -> dict:
+ return {
+ "slot_id": self.slot_id,
+ "ams_id": self.ams_id,
+ "tray_id": self.tray_id,
+ "filament_type": self.filament_type,
+ "required_grams": self.required_grams,
+ "remaining_grams": self.remaining_grams,
+ }
+
+
+def _global_to_ams_key(global_tray_id: int) -> tuple[int, int]:
+ """Inverse of ``ams_id * 4 + tray_id`` — matches ``usage_tracker``."""
+ if global_tray_id >= 254:
+ return (255, global_tray_id - 254)
+ if global_tray_id >= 128:
+ return (global_tray_id, 0)
+ return (global_tray_id // 4, global_tray_id % 4)
+
+
+def _resolve_source_3mf(item: PrintQueueItem) -> Path | None:
+ """Locate the 3MF file backing this queue item (archive or library)."""
+ if item.archive is not None and item.archive.file_path:
+ return app_settings.base_dir / item.archive.file_path
+ if item.library_file is not None and item.library_file.file_path:
+ return Path(item.library_file.file_path)
+ return None
+
+
+async def _spoolman_remaining_grams(spoolman_spool_id: int) -> float | None:
+ """Live remaining grams for a Spoolman spool, or None if unavailable."""
+ try:
+ from backend.app.services.spoolman import (
+ SpoolmanClientError,
+ SpoolmanNotFoundError,
+ get_spoolman_client,
+ )
+ except ImportError:
+ return None
+ try:
+ client = await get_spoolman_client()
+ if client is None:
+ return None
+ spool = await client.get_spool(spoolman_spool_id)
+ except (SpoolmanNotFoundError, SpoolmanClientError):
+ return None
+ except Exception as e:
+ logger.debug("Spoolman fetch failed for spool %s: %s", spoolman_spool_id, e)
+ return None
+
+ if not spool:
+ return None
+
+ # Spoolman exposes either an absolute remaining_weight, or used_weight +
+ # filament.weight. Either is sufficient — prefer remaining_weight when
+ # present (the user may have overridden it).
+ remaining = spool.get("remaining_weight")
+ if isinstance(remaining, (int, float)) and remaining >= 0:
+ return float(remaining)
+
+ used = spool.get("used_weight")
+ filament = spool.get("filament") or {}
+ total = filament.get("weight")
+ if isinstance(used, (int, float)) and isinstance(total, (int, float)) and total > 0:
+ return max(0.0, float(total) - float(used))
+
+ return None
+
+
+async def _is_spoolman_mode(db: AsyncSession) -> bool:
+ """Check whether the user has opted in to Spoolman inventory mode."""
+ try:
+ from backend.app.api.routes.settings import get_setting
+
+ spoolman_enabled = await get_setting(db, "spoolman_enabled")
+ return bool(spoolman_enabled) and spoolman_enabled.lower() == "true"
+ except Exception:
+ return False
+
+
+async def _warnings_disabled(db: AsyncSession) -> bool:
+ """Honour the ``disable_filament_warnings`` setting (#720)."""
+ try:
+ from backend.app.api.routes.settings import get_setting
+
+ disabled = await get_setting(db, "disable_filament_warnings")
+ return bool(disabled) and disabled.lower() == "true"
+ except Exception:
+ return False
+
+
+def _parse_ams_mapping(raw: str | None) -> list[int] | None:
+ if not raw:
+ return None
+ try:
+ parsed = json.loads(raw)
+ except (json.JSONDecodeError, TypeError):
+ return None
+ if not isinstance(parsed, list):
+ return None
+ return [v for v in parsed if isinstance(v, int)]
+
+
+async def compute_deficit_for_queue_item(
+ db: AsyncSession,
+ item: PrintQueueItem,
+) -> list[FilamentDeficit]:
+ """Return per-slot filament shortfalls for ``item``, or [] when it's safe to dispatch.
+
+ Returns an empty list whenever any of the following hold:
+
+ * The ``disable_filament_warnings`` setting is on.
+ * The item has no resolved ``printer_id`` (model-based assignment not
+ yet picked a printer — the scheduler re-runs the check after it does).
+ * No source 3MF is available, or the 3MF carries no per-slot
+ requirements (treated as "nothing to verify" rather than an error,
+ matching the PrintModal behaviour).
+ * No AMS mapping is set yet — the scheduler computes the mapping just
+ before dispatch; until it does we cannot map slot → tray.
+ * Spoolman mode is on but the Spoolman server is unreachable. We do not
+ wedge the queue on a network blip.
+ """
+ if await _warnings_disabled(db):
+ return []
+ if item.printer_id is None:
+ return []
+
+ # Refresh the relationships we need without assuming the caller eagerly
+ # loaded them — both the route and the scheduler call this from contexts
+ # with different loading strategies.
+ refreshed = await db.execute(
+ select(PrintQueueItem)
+ .options(
+ selectinload(PrintQueueItem.archive),
+ selectinload(PrintQueueItem.library_file),
+ )
+ .where(PrintQueueItem.id == item.id)
+ )
+ item = refreshed.scalar_one_or_none() or item
+
+ source_path = _resolve_source_3mf(item)
+ if source_path is None or not source_path.exists():
+ return []
+
+ requirements = extract_filament_requirements(source_path, item.plate_id)
+ if not requirements:
+ return []
+
+ mapping = _parse_ams_mapping(item.ams_mapping)
+ if not mapping:
+ return []
+
+ spoolman_mode = await _is_spoolman_mode(db)
+
+ deficits: list[FilamentDeficit] = []
+ for req in requirements:
+ slot_id = req.get("slot_id")
+ used_grams = req.get("used_grams")
+ if not isinstance(slot_id, int) or slot_id <= 0:
+ continue
+ if not isinstance(used_grams, (int, float)) or used_grams <= 0:
+ continue
+ idx = slot_id - 1
+ if idx >= len(mapping):
+ continue
+ global_tray_id = mapping[idx]
+ if global_tray_id is None or global_tray_id < 0:
+ continue
+ ams_id, tray_id = _global_to_ams_key(global_tray_id)
+
+ remaining: float | None = None
+ if spoolman_mode:
+ sm_result = await db.execute(
+ select(SpoolmanSlotAssignment).where(
+ SpoolmanSlotAssignment.printer_id == item.printer_id,
+ SpoolmanSlotAssignment.ams_id == ams_id,
+ SpoolmanSlotAssignment.tray_id == tray_id,
+ )
+ )
+ sm_assignment = sm_result.scalar_one_or_none()
+ if sm_assignment is None:
+ continue
+ remaining = await _spoolman_remaining_grams(sm_assignment.spoolman_spool_id)
+ else:
+ internal_result = await db.execute(
+ select(SpoolAssignment)
+ .options(selectinload(SpoolAssignment.spool))
+ .where(
+ SpoolAssignment.printer_id == item.printer_id,
+ SpoolAssignment.ams_id == ams_id,
+ SpoolAssignment.tray_id == tray_id,
+ )
+ )
+ assignment = internal_result.scalar_one_or_none()
+ if assignment is None or assignment.spool is None:
+ continue
+ spool = assignment.spool
+ label_weight = float(spool.label_weight or 0)
+ weight_used = float(spool.weight_used or 0)
+ if label_weight <= 0:
+ continue
+ remaining = max(0.0, label_weight - weight_used)
+
+ if remaining is None:
+ # Spoolman unreachable for this spool — skip rather than block.
+ continue
+ if remaining >= float(used_grams):
+ continue
+
+ deficits.append(
+ FilamentDeficit(
+ slot_id=slot_id,
+ ams_id=ams_id,
+ tray_id=tray_id,
+ filament_type=str(req.get("type", "")),
+ required_grams=float(used_grams),
+ remaining_grams=remaining,
+ )
+ )
+
+ return deficits
+
+
+# Re-export the most useful pieces for callers that just want the data.
+__all__ = [
+ "FilamentDeficit",
+ "compute_deficit_for_queue_item",
+]
diff --git a/backend/app/services/print_scheduler.py b/backend/app/services/print_scheduler.py
index c58672214..41784552e 100644
--- a/backend/app/services/print_scheduler.py
+++ b/backend/app/services/print_scheduler.py
@@ -25,6 +25,7 @@ from backend.app.services.bambu_ftp import (
upload_file_async,
with_ftp_retry,
)
+from backend.app.services.filament_deficit import compute_deficit_for_queue_item
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
@@ -300,6 +301,13 @@ class PrintScheduler:
)
await db.commit()
+ # Filament-deficit pre-dispatch check (#1496). If the
+ # assigned spool can't satisfy any required slot grams,
+ # promote the item to manual_start so the user must
+ # acknowledge via the ▶ button (which re-checks live).
+ if await self._block_on_filament_deficit(db, item):
+ continue
+
# Start the print
await self._start_print(db, item)
busy_printers.add(item.printer_id)
@@ -423,6 +431,10 @@ class PrintScheduler:
)
await db.commit()
+ # Filament-deficit pre-dispatch check (#1496).
+ if await self._block_on_filament_deficit(db, item):
+ continue
+
await self._start_print(db, item)
busy_printers.add(printer_id)
@@ -1648,6 +1660,55 @@ class PrintScheduler:
result = await db.execute(select(Printer).where(Printer.id == printer_id))
return result.scalar_one_or_none()
+ async def _block_on_filament_deficit(
+ self,
+ db: AsyncSession,
+ item: PrintQueueItem,
+ ) -> bool:
+ """Promote the item to manual_start when the assigned spool is short (#1496).
+
+ Returns True when this dispatch attempt was blocked, False when the
+ item is clear to start. A previously-flagged item whose spool has
+ since been swapped to one with enough material clears the flag here
+ so the next scheduler tick dispatches it.
+ """
+ try:
+ deficit = await compute_deficit_for_queue_item(db, item)
+ except Exception as e:
+ # Never let a flaky deficit check wedge the queue — log and let
+ # dispatch proceed. The PrintModal-side check still runs on the
+ # manual paths.
+ logger.warning("Filament deficit check failed for item %s: %s", item.id, e)
+ return False
+
+ if deficit:
+ item.filament_short = True
+ item.manual_start = True
+ await db.commit()
+ job_name = await self._get_job_name(db, item)
+ printer = await self._get_printer(db, item.printer_id) if item.printer_id else None
+ logger.info(
+ "Queue item %s blocked on filament deficit (%d slot(s)) — promoted to manual_start",
+ item.id,
+ len(deficit),
+ )
+ try:
+ await notification_service.on_queue_job_waiting(
+ job_name=job_name,
+ target_model=(printer.model if printer else "") or "",
+ waiting_reason="filament_short",
+ db=db,
+ )
+ except Exception as e:
+ logger.debug("filament_short notification failed for item %s: %s", item.id, e)
+ return True
+
+ # No deficit — clear any stale flag from a previous tick.
+ if item.filament_short:
+ item.filament_short = False
+ await db.commit()
+ return False
+
async def _start_print(self, db: AsyncSession, item: PrintQueueItem):
"""Upload file and start print for a queue item.
diff --git a/backend/tests/integration/test_print_queue_api.py b/backend/tests/integration/test_print_queue_api.py
index 2f23d19a2..d5e3454c4 100644
--- a/backend/tests/integration/test_print_queue_api.py
+++ b/backend/tests/integration/test_print_queue_api.py
@@ -496,6 +496,93 @@ class TestQueueStartEndpoint:
response = await async_client.post(f"/api/v1/queue/{item.id}/start")
assert response.status_code == 400
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_start_returns_409_on_filament_deficit(
+ self,
+ async_client: AsyncClient,
+ queue_item_factory,
+ db_session,
+ monkeypatch,
+ ):
+ """Filament deficit must surface as 409 + structured payload (#1496)."""
+ from backend.app.services import filament_deficit as fd_module
+
+ item = await queue_item_factory(manual_start=True)
+
+ async def _fake_deficit(_db, _item):
+ return [
+ fd_module.FilamentDeficit(
+ slot_id=1,
+ ams_id=0,
+ tray_id=0,
+ filament_type="PLA",
+ required_grams=270.0,
+ remaining_grams=200.0,
+ ),
+ ]
+
+ monkeypatch.setattr(
+ "backend.app.api.routes.print_queue.compute_deficit_for_queue_item",
+ _fake_deficit,
+ )
+
+ response = await async_client.post(f"/api/v1/queue/{item.id}/start")
+ assert response.status_code == 409
+ body = response.json()
+ assert body["detail"]["code"] == "insufficient_filament"
+ assert len(body["detail"]["deficit"]) == 1
+ assert body["detail"]["deficit"][0]["slot_id"] == 1
+ assert body["detail"]["deficit"][0]["required_grams"] == 270.0
+ assert body["detail"]["deficit"][0]["remaining_grams"] == 200.0
+
+ # Item still pending, manual_start unchanged.
+ await db_session.refresh(item)
+ assert item.status == "pending"
+ assert item.manual_start is True
+
+ @pytest.mark.asyncio
+ @pytest.mark.integration
+ async def test_start_with_skip_flag_bypasses_deficit_check(
+ self,
+ async_client: AsyncClient,
+ queue_item_factory,
+ db_session,
+ monkeypatch,
+ ):
+ """With skip_filament_check=true the route dispatches even when short (#1496)."""
+ from backend.app.services import filament_deficit as fd_module
+
+ item = await queue_item_factory(manual_start=True, filament_short=True)
+ called_with = {}
+
+ async def _fake_deficit(_db, _item):
+ called_with["called"] = True
+ return [
+ fd_module.FilamentDeficit(
+ slot_id=1,
+ ams_id=0,
+ tray_id=0,
+ filament_type="PLA",
+ required_grams=270.0,
+ remaining_grams=200.0,
+ ),
+ ]
+
+ monkeypatch.setattr(
+ "backend.app.api.routes.print_queue.compute_deficit_for_queue_item",
+ _fake_deficit,
+ )
+
+ response = await async_client.post(f"/api/v1/queue/{item.id}/start?skip_filament_check=true")
+ assert response.status_code == 200
+ body = response.json()
+ assert body["manual_start"] is False
+ assert body["filament_short"] is False
+ # Helper not called on the bypass path — we trust the operator's
+ # decision to print anyway.
+ assert called_with == {}
+
class TestQueueCancelEndpoint:
"""Tests for the /queue/{item_id}/cancel endpoint."""
diff --git a/backend/tests/unit/services/test_filament_deficit.py b/backend/tests/unit/services/test_filament_deficit.py
new file mode 100644
index 000000000..0df1d8d57
--- /dev/null
+++ b/backend/tests/unit/services/test_filament_deficit.py
@@ -0,0 +1,267 @@
+"""Unit tests for the filament-deficit pre-dispatch check (#1496).
+
+The check is the single source of truth that both ``POST /queue/{id}/start``
+and the dispatch scheduler call before sending a print to the printer. Pin
+the contract for the cases that matter:
+
+* Internal-inventory mode: shortfall + sufficient + no assignment.
+* AMS-mapping gating: a missing mapping means "not yet decided, skip".
+* Disabled-warnings setting + missing printer (model-based item) + no
+ source 3MF all short-circuit to "no deficit".
+"""
+
+from __future__ import annotations
+
+import json
+import zipfile
+from pathlib import Path
+from unittest.mock import patch
+
+import pytest
+
+from backend.app.models.archive import PrintArchive
+from backend.app.models.print_queue import PrintQueueItem
+from backend.app.models.settings import Settings
+from backend.app.models.spool import Spool
+from backend.app.models.spool_assignment import SpoolAssignment
+from backend.app.services.filament_deficit import (
+ FilamentDeficit,
+ compute_deficit_for_queue_item,
+)
+
+
+def _write_3mf(file_path: Path, filaments: list[dict]) -> None:
+ """Minimal 3MF that ``extract_filament_requirements`` can parse (flat shape)."""
+ body = "".join(
+ f''
+ for f in filaments
+ )
+ config = f'{body}'
+ with zipfile.ZipFile(file_path, "w") as zf:
+ zf.writestr("Metadata/slice_info.config", config)
+
+
+async def _setup_archive_3mf(db_session, tmp_path: Path, filaments: list[dict]) -> PrintArchive:
+ """Create a 3MF on disk and a PrintArchive row pointing at it."""
+ file_name = "model.3mf"
+ file_path = tmp_path / file_name
+ _write_3mf(file_path, filaments)
+ archive = PrintArchive(
+ filename=file_name,
+ print_name="Test",
+ # The helper resolves via app_settings.base_dir / file_path, but
+ # storing the absolute path on the model also works because
+ # ``Path / abs`` collapses to the absolute side.
+ file_path=str(file_path),
+ file_size=file_path.stat().st_size,
+ status="completed",
+ )
+ db_session.add(archive)
+ await db_session.commit()
+ await db_session.refresh(archive)
+ return archive
+
+
+async def _spool(db_session, *, label_weight: int, weight_used: float, color: str = "#000000") -> Spool:
+ spool = Spool(
+ material="PLA",
+ label_weight=label_weight,
+ weight_used=weight_used,
+ rgba=color,
+ )
+ db_session.add(spool)
+ await db_session.commit()
+ await db_session.refresh(spool)
+ return spool
+
+
+async def _assign(db_session, *, printer_id: int, spool_id: int, ams_id: int = 0, tray_id: int = 0) -> None:
+ db_session.add(
+ SpoolAssignment(
+ spool_id=spool_id,
+ printer_id=printer_id,
+ ams_id=ams_id,
+ tray_id=tray_id,
+ )
+ )
+ await db_session.commit()
+
+
+async def _queue_item(
+ db_session,
+ *,
+ printer_id: int | None,
+ archive: PrintArchive | None,
+ ams_mapping: list[int] | None,
+ plate_id: int | None = None,
+) -> PrintQueueItem:
+ item = PrintQueueItem(
+ printer_id=printer_id,
+ archive_id=archive.id if archive else None,
+ ams_mapping=json.dumps(ams_mapping) if ams_mapping is not None else None,
+ plate_id=plate_id,
+ status="pending",
+ manual_start=True,
+ )
+ db_session.add(item)
+ await db_session.commit()
+ await db_session.refresh(item, ["archive", "library_file"])
+ return item
+
+
+class TestFilamentDeficit:
+ @pytest.mark.asyncio
+ async def test_returns_deficit_when_spool_too_light(self, db_session, printer_factory, tmp_path):
+ """Spool with 30g remaining for a 100g print → one deficit row."""
+ printer = await printer_factory()
+ archive = await _setup_archive_3mf(
+ db_session,
+ tmp_path,
+ [{"id": "1", "type": "PLA", "color": "#FFFFFF", "used_g": "100.0"}],
+ )
+ spool = await _spool(db_session, label_weight=1000, weight_used=970.0) # 30g left
+ await _assign(db_session, printer_id=printer.id, spool_id=spool.id, ams_id=0, tray_id=0)
+ item = await _queue_item(db_session, printer_id=printer.id, archive=archive, ams_mapping=[0])
+
+ with patch("backend.app.services.filament_deficit.app_settings.base_dir", Path("/")):
+ deficit = await compute_deficit_for_queue_item(db_session, item)
+
+ assert len(deficit) == 1
+ assert isinstance(deficit[0], FilamentDeficit)
+ assert deficit[0].slot_id == 1
+ assert deficit[0].required_grams == 100.0
+ assert deficit[0].remaining_grams == 30.0
+ assert deficit[0].filament_type == "PLA"
+
+ @pytest.mark.asyncio
+ async def test_returns_empty_when_spool_has_enough(self, db_session, printer_factory, tmp_path):
+ printer = await printer_factory()
+ archive = await _setup_archive_3mf(
+ db_session,
+ tmp_path,
+ [{"id": "1", "type": "PLA", "color": "#FFFFFF", "used_g": "100.0"}],
+ )
+ spool = await _spool(db_session, label_weight=1000, weight_used=200.0) # 800g left
+ await _assign(db_session, printer_id=printer.id, spool_id=spool.id, ams_id=0, tray_id=0)
+ item = await _queue_item(db_session, printer_id=printer.id, archive=archive, ams_mapping=[0])
+
+ with patch("backend.app.services.filament_deficit.app_settings.base_dir", Path("/")):
+ deficit = await compute_deficit_for_queue_item(db_session, item)
+
+ assert deficit == []
+
+ @pytest.mark.asyncio
+ async def test_returns_empty_when_ams_mapping_missing(self, db_session, printer_factory, tmp_path):
+ """No mapping yet = scheduler hasn't decided which slot maps where."""
+ printer = await printer_factory()
+ archive = await _setup_archive_3mf(
+ db_session,
+ tmp_path,
+ [{"id": "1", "type": "PLA", "color": "#FFFFFF", "used_g": "100.0"}],
+ )
+ item = await _queue_item(db_session, printer_id=printer.id, archive=archive, ams_mapping=None)
+
+ with patch("backend.app.services.filament_deficit.app_settings.base_dir", Path("/")):
+ deficit = await compute_deficit_for_queue_item(db_session, item)
+
+ assert deficit == []
+
+ @pytest.mark.asyncio
+ async def test_returns_empty_when_no_printer_assigned(self, db_session, tmp_path):
+ """Model-based queue items with no resolved printer_id can't be checked."""
+ archive = await _setup_archive_3mf(
+ db_session,
+ tmp_path,
+ [{"id": "1", "type": "PLA", "color": "#FFFFFF", "used_g": "100.0"}],
+ )
+ item = await _queue_item(db_session, printer_id=None, archive=archive, ams_mapping=[0])
+
+ with patch("backend.app.services.filament_deficit.app_settings.base_dir", Path("/")):
+ deficit = await compute_deficit_for_queue_item(db_session, item)
+
+ assert deficit == []
+
+ @pytest.mark.asyncio
+ async def test_returns_empty_when_warnings_disabled(self, db_session, printer_factory, tmp_path):
+ """Honour the disable_filament_warnings setting (#720 toggle)."""
+ printer = await printer_factory()
+ archive = await _setup_archive_3mf(
+ db_session,
+ tmp_path,
+ [{"id": "1", "type": "PLA", "color": "#FFFFFF", "used_g": "100.0"}],
+ )
+ spool = await _spool(db_session, label_weight=1000, weight_used=970.0)
+ await _assign(db_session, printer_id=printer.id, spool_id=spool.id)
+ item = await _queue_item(db_session, printer_id=printer.id, archive=archive, ams_mapping=[0])
+ db_session.add(Settings(key="disable_filament_warnings", value="true"))
+ await db_session.commit()
+
+ with patch("backend.app.services.filament_deficit.app_settings.base_dir", Path("/")):
+ deficit = await compute_deficit_for_queue_item(db_session, item)
+
+ assert deficit == []
+
+ @pytest.mark.asyncio
+ async def test_returns_empty_when_no_assignment(self, db_session, printer_factory, tmp_path):
+ """Mapping points at a slot with no spool assigned → silent, not blocked."""
+ printer = await printer_factory()
+ archive = await _setup_archive_3mf(
+ db_session,
+ tmp_path,
+ [{"id": "1", "type": "PLA", "color": "#FFFFFF", "used_g": "100.0"}],
+ )
+ item = await _queue_item(db_session, printer_id=printer.id, archive=archive, ams_mapping=[0])
+
+ with patch("backend.app.services.filament_deficit.app_settings.base_dir", Path("/")):
+ deficit = await compute_deficit_for_queue_item(db_session, item)
+
+ assert deficit == []
+
+ @pytest.mark.asyncio
+ async def test_returns_empty_when_3mf_missing(self, db_session, printer_factory):
+ printer = await printer_factory()
+ archive = PrintArchive(
+ filename="ghost.3mf",
+ file_path="/tmp/nope-does-not-exist.3mf",
+ file_size=0,
+ status="completed",
+ )
+ db_session.add(archive)
+ await db_session.commit()
+ await db_session.refresh(archive)
+ item = await _queue_item(db_session, printer_id=printer.id, archive=archive, ams_mapping=[0])
+
+ deficit = await compute_deficit_for_queue_item(db_session, item)
+
+ assert deficit == []
+
+ @pytest.mark.asyncio
+ async def test_multi_slot_only_shorted_slot_returned(self, db_session, printer_factory, tmp_path):
+ """One slot fine, one short — only the short slot is in the result."""
+ printer = await printer_factory()
+ archive = await _setup_archive_3mf(
+ db_session,
+ tmp_path,
+ [
+ {"id": "1", "type": "PLA", "color": "#FFFFFF", "used_g": "100.0"},
+ {"id": "2", "type": "PETG", "color": "#000000", "used_g": "80.0"},
+ ],
+ )
+ plenty = await _spool(db_session, label_weight=1000, weight_used=100.0) # 900g
+ shorted = await _spool(db_session, label_weight=1000, weight_used=950.0) # 50g
+ await _assign(db_session, printer_id=printer.id, spool_id=plenty.id, ams_id=0, tray_id=0)
+ await _assign(db_session, printer_id=printer.id, spool_id=shorted.id, ams_id=0, tray_id=1)
+ item = await _queue_item(
+ db_session,
+ printer_id=printer.id,
+ archive=archive,
+ ams_mapping=[0, 1], # slot 1 -> tray 0, slot 2 -> tray 1
+ )
+
+ with patch("backend.app.services.filament_deficit.app_settings.base_dir", Path("/")):
+ deficit = await compute_deficit_for_queue_item(db_session, item)
+
+ assert [d.slot_id for d in deficit] == [2]
+ assert deficit[0].remaining_grams == 50.0
+ assert deficit[0].required_grams == 80.0
diff --git a/backend/tests/unit/test_scheduler_filament_deficit.py b/backend/tests/unit/test_scheduler_filament_deficit.py
new file mode 100644
index 000000000..d198ba955
--- /dev/null
+++ b/backend/tests/unit/test_scheduler_filament_deficit.py
@@ -0,0 +1,119 @@
+"""Scheduler pre-dispatch filament-deficit guard tests (#1496).
+
+``PrintScheduler._block_on_filament_deficit`` is the gate that keeps an
+auto_dispatch=True VP intake (or any other scheduler-driven dispatch) from
+sending a print onto a spool that can't satisfy it. On a deficit it
+promotes the item to manual_start; when a previously-flagged item's spool
+is now adequate it clears the flag so the next tick dispatches.
+"""
+
+from __future__ import annotations
+
+from unittest.mock import AsyncMock, patch
+
+import pytest
+
+from backend.app.models.print_queue import PrintQueueItem
+from backend.app.services.filament_deficit import FilamentDeficit
+from backend.app.services.print_scheduler import PrintScheduler
+
+
+@pytest.fixture
+def scheduler():
+ """A fresh scheduler instance — internal state is not exercised."""
+ return PrintScheduler()
+
+
+@pytest.fixture
+def queue_item(db_session, printer_factory):
+ """Helper to drop a queue item the helper can mutate."""
+
+ async def _make(**overrides):
+ printer = await printer_factory()
+ defaults = {
+ "printer_id": printer.id,
+ "status": "pending",
+ "manual_start": False,
+ "filament_short": False,
+ }
+ defaults.update(overrides)
+ item = PrintQueueItem(**defaults)
+ db_session.add(item)
+ await db_session.commit()
+ await db_session.refresh(item)
+ return item
+
+ return _make
+
+
+@pytest.mark.asyncio
+async def test_blocks_on_deficit_promotes_to_manual_start(scheduler, db_session, queue_item):
+ item = await queue_item()
+ with patch(
+ "backend.app.services.print_scheduler.compute_deficit_for_queue_item",
+ AsyncMock(
+ return_value=[
+ FilamentDeficit(
+ slot_id=1,
+ ams_id=0,
+ tray_id=0,
+ filament_type="PLA",
+ required_grams=270.0,
+ remaining_grams=200.0,
+ ),
+ ]
+ ),
+ ):
+ blocked = await scheduler._block_on_filament_deficit(db_session, item)
+
+ assert blocked is True
+ await db_session.refresh(item)
+ assert item.manual_start is True
+ assert item.filament_short is True
+
+
+@pytest.mark.asyncio
+async def test_clears_stale_flag_when_deficit_resolves(scheduler, db_session, queue_item):
+ """Previously-flagged item whose spool was swapped is unblocked."""
+ item = await queue_item(filament_short=True, manual_start=False)
+ with patch(
+ "backend.app.services.print_scheduler.compute_deficit_for_queue_item",
+ AsyncMock(return_value=[]),
+ ):
+ blocked = await scheduler._block_on_filament_deficit(db_session, item)
+
+ assert blocked is False
+ await db_session.refresh(item)
+ assert item.filament_short is False
+ assert item.manual_start is False
+
+
+@pytest.mark.asyncio
+async def test_no_deficit_no_op(scheduler, db_session, queue_item):
+ """Happy path — no deficit, no flag changes, dispatch proceeds."""
+ item = await queue_item()
+ with patch(
+ "backend.app.services.print_scheduler.compute_deficit_for_queue_item",
+ AsyncMock(return_value=[]),
+ ):
+ blocked = await scheduler._block_on_filament_deficit(db_session, item)
+
+ assert blocked is False
+ await db_session.refresh(item)
+ assert item.filament_short is False
+ assert item.manual_start is False
+
+
+@pytest.mark.asyncio
+async def test_helper_exception_does_not_wedge_dispatch(scheduler, db_session, queue_item):
+ """A flaky deficit check (e.g. Spoolman timeout) must not block dispatch."""
+ item = await queue_item()
+ with patch(
+ "backend.app.services.print_scheduler.compute_deficit_for_queue_item",
+ AsyncMock(side_effect=RuntimeError("network down")),
+ ):
+ blocked = await scheduler._block_on_filament_deficit(db_session, item)
+
+ assert blocked is False
+ await db_session.refresh(item)
+ assert item.filament_short is False
diff --git a/frontend/src/__tests__/pages/QueuePage.test.tsx b/frontend/src/__tests__/pages/QueuePage.test.tsx
index 0da2d9a6e..be9e00745 100644
--- a/frontend/src/__tests__/pages/QueuePage.test.tsx
+++ b/frontend/src/__tests__/pages/QueuePage.test.tsx
@@ -442,4 +442,83 @@ describe('QueuePage', () => {
expect(screen.queryByText('G-code')).not.toBeInTheDocument();
});
});
+
+ describe('filament-short ▶ flow (#1496)', () => {
+ /**
+ * The dispatch pre-flight flags a queue item as filament_short. The user
+ * clicks ▶, the backend re-checks live and either dispatches (no deficit
+ * anymore — clear flag) or returns 409 with the per-slot deficit so the
+ * frontend can render the "Print Anyway" confirm modal.
+ */
+ const shortItem = {
+ ...mockQueueItems[0],
+ manual_start: true,
+ filament_short: true,
+ };
+
+ it('renders the filament-short badge on a flagged pending row', async () => {
+ server.use(
+ http.get('/api/v1/queue/', () => HttpResponse.json([shortItem])),
+ );
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText(/Insufficient filament for the assigned spool/i)).toBeInTheDocument();
+ });
+ });
+
+ it('opens the Print Anyway modal when ▶ returns 409 and retries with skip_filament_check', async () => {
+ let secondCallSkippedCheck: boolean | null = null;
+ let attempts = 0;
+ server.use(
+ http.get('/api/v1/queue/', () => HttpResponse.json([shortItem])),
+ http.post('/api/v1/queue/:id/start', ({ request }) => {
+ attempts += 1;
+ const url = new URL(request.url);
+ const skip = url.searchParams.get('skip_filament_check') === 'true';
+ if (attempts === 1) {
+ return HttpResponse.json(
+ {
+ detail: {
+ code: 'insufficient_filament',
+ deficit: [
+ {
+ slot_id: 1,
+ ams_id: 0,
+ tray_id: 0,
+ filament_type: 'PLA',
+ required_grams: 270,
+ remaining_grams: 200,
+ },
+ ],
+ },
+ },
+ { status: 409 },
+ );
+ }
+ secondCallSkippedCheck = skip;
+ return HttpResponse.json({ ...shortItem, manual_start: false, filament_short: false });
+ }),
+ );
+
+ render();
+
+ const playButton = await screen.findByTitle(/Start Print|do not have permission to start prints/i);
+ await userEvent.click(playButton);
+
+ // Wait for the start endpoint to be hit (the 409 path returns to onError).
+ await waitFor(() => expect(attempts).toBe(1));
+ // Modal shows the deficit detail
+ await screen.findByRole('button', { name: /Print Anyway/i });
+ expect(
+ screen.getByText(/Slot 1: needs 270 g, 200 g remaining/i),
+ ).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole('button', { name: /Print Anyway/i }));
+
+ await waitFor(() => expect(secondCallSkippedCheck).toBe(true));
+ expect(attempts).toBe(2);
+ });
+ });
});
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts
index 7ff367ebe..d7f2be046 100644
--- a/frontend/src/api/client.ts
+++ b/frontend/src/api/client.ts
@@ -8,11 +8,21 @@ export class ApiError extends Error {
* Frontend uses this to look up an i18n key instead of showing the raw
* English fallback. Null when the backend returned a plain-string detail. */
code: string | null;
- constructor(message: string, status: number, code: string | null = null) {
+ /** Full structured detail object when the backend returned `{code, ...}`
+ * with additional fields (e.g. the deficit list for 409s on queue
+ * start, #1496). Null for plain-string or array-shaped details. */
+ detail: Record | null;
+ constructor(
+ message: string,
+ status: number,
+ code: string | null = null,
+ detail: Record | null = null,
+ ) {
super(message);
this.name = 'ApiError';
this.status = status;
this.code = code;
+ this.detail = detail;
}
}
@@ -129,13 +139,17 @@ async function request(
.join('; ');
message = joined || JSON.stringify(detail) || `HTTP ${response.status}`;
} else if (detail && typeof detail === 'object') {
- // Structured detail `{code, message}` — frontend uses the code to
- // pick an i18n key, message is the English fallback.
+ // Structured detail `{code, message, ...}` — frontend uses the code
+ // to pick an i18n key, message is the English fallback, any extra
+ // fields land on ApiError.detail (e.g. `deficit` for #1496).
code = typeof detail.code === 'string' ? detail.code : null;
message = typeof detail.message === 'string' ? detail.message : `HTTP ${response.status}`;
} else {
message = `HTTP ${response.status}`;
}
+ const structuredDetail = detail && typeof detail === 'object' && !Array.isArray(detail)
+ ? (detail as Record)
+ : null;
// Handle 401 Unauthorized - only clear token if it's actually invalid
// Don't clear on "Authentication required" which might be a timing issue
@@ -152,7 +166,7 @@ async function request(
}
}
- throw new ApiError(message, response.status, code);
+ throw new ApiError(message, response.status, code, structuredDetail);
}
// Handle empty responses (204 No Content, etc.)
@@ -1783,6 +1797,10 @@ export interface PrintQueueItem {
require_previous_success: boolean;
auto_off_after: boolean;
manual_start: boolean; // Requires manual trigger to start (staged)
+ // Set by the dispatch scheduler when the assigned spool can't satisfy
+ // any required slot's grams (#1496). Surfaced on the queue row as a
+ // "filament short" badge; cleared on a successful ▶ click (live recheck).
+ filament_short: boolean;
ams_mapping: number[] | null; // AMS slot mapping for multi-color prints
filament_overrides: Array<{ slot_id: number; type: string; color: string; color_name?: string; force_color_match?: boolean }> | null; // Filament overrides for model-based assignment
plate_id: number | null; // Plate ID for multi-plate 3MF files
@@ -4458,8 +4476,16 @@ export const api = {
request<{ message: string }>(`/queue/${id}/cancel`, { method: 'POST' }),
stopQueueItem: (id: number) =>
request<{ message: string }>(`/queue/${id}/stop`, { method: 'POST' }),
- startQueueItem: (id: number) =>
- request(`/queue/${id}/start`, { method: 'POST' }),
+ /**
+ * Start a staged queue item. The backend re-checks live filament deficit
+ * for the assigned spool and, when short, returns 409 with a structured
+ * payload so the caller can confirm and retry. Pass `skipFilamentCheck`
+ * after the user confirms "Print Anyway" (#1496).
+ */
+ startQueueItem: (id: number, opts?: { skipFilamentCheck?: boolean }) => {
+ const qs = opts?.skipFilamentCheck ? '?skip_filament_check=true' : '';
+ return request(`/queue/${id}/start${qs}`, { method: 'POST' });
+ },
bulkUpdateQueue: (data: PrintQueueBulkUpdate) =>
request('/queue/bulk', {
method: 'PATCH',
diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts
index 301cc2c98..c7415ff3b 100644
--- a/frontend/src/i18n/locales/de.ts
+++ b/frontend/src/i18n/locales/de.ts
@@ -927,6 +927,15 @@ export default {
// Queue page
queue: {
+ filamentShort: {
+ rowBadge: 'Filament fuer die zugewiesene Spule reicht nicht',
+ rowTooltip: 'Der Dispatcher hat diese Position markiert. Klicke auf Play, um den Pro-Slot-Fehlbestand zu sehen und zu entscheiden, ob trotzdem gedruckt werden soll.',
+ confirmTitle: 'Filament reicht nicht',
+ confirmIntro: 'Die zugewiesene Spule kann mindestens einen Slot nicht versorgen. Trotzdem drucken?',
+ lineItem: 'Slot {{slot}}: benoetigt {{required}} g, {{remaining}} g verbleibend',
+ unknown: 'unbekannt',
+ printAnyway: 'Trotzdem drucken',
+ },
title: 'Druckwarteschlange',
subtitle: 'Planen und verwalten Sie Ihre Druckaufträge',
addToQueue: 'Zur Warteschlange hinzufügen',
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index 3acc37874..727fe0e60 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -930,6 +930,15 @@ export default {
title: 'Print Queue',
subtitle: 'Schedule and manage your print jobs',
addToQueue: 'Add to Queue',
+ filamentShort: {
+ rowBadge: 'Insufficient filament for the assigned spool',
+ rowTooltip: 'The dispatch scheduler flagged this item. Click Play to see the per-slot deficit and decide whether to print anyway.',
+ confirmTitle: 'Insufficient filament',
+ confirmIntro: 'The assigned spool cannot satisfy at least one slot. Print anyway?',
+ lineItem: 'Slot {{slot}}: needs {{required}} g, {{remaining}} g remaining',
+ unknown: 'unknown',
+ printAnyway: 'Print Anyway',
+ },
// Print modal
print: 'Print',
reprint: 'Re-print',
diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts
index 52f588125..621f089da 100644
--- a/frontend/src/i18n/locales/es.ts
+++ b/frontend/src/i18n/locales/es.ts
@@ -927,6 +927,15 @@ export default {
// Queue page
queue: {
+ filamentShort: {
+ rowBadge: 'Filamento insuficiente para la bobina asignada',
+ rowTooltip: 'El planificador marco este elemento. Pulsa Reproducir para ver el deficit por slot y decidir si imprimir de todos modos.',
+ confirmTitle: 'Filamento insuficiente',
+ confirmIntro: 'La bobina asignada no puede cubrir al menos un slot. Imprimir de todos modos?',
+ lineItem: 'Slot {{slot}}: necesita {{required}} g, quedan {{remaining}} g',
+ unknown: 'desconocido',
+ printAnyway: 'Imprimir de todos modos',
+ },
title: 'Cola de impresión',
subtitle: 'Programe y gestione sus trabajos de impresión',
addToQueue: 'Añadir a la cola',
diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts
index e5ab9c952..facf27b1e 100644
--- a/frontend/src/i18n/locales/fr.ts
+++ b/frontend/src/i18n/locales/fr.ts
@@ -927,6 +927,15 @@ export default {
// Queue page
queue: {
+ filamentShort: {
+ rowBadge: 'Filament insuffisant pour la bobine assignee',
+ rowTooltip: "Le planificateur a signale cet element. Cliquez sur Lecture pour voir le deficit par emplacement et decider de lancer quand meme l'impression.",
+ confirmTitle: 'Filament insuffisant',
+ confirmIntro: 'La bobine assignee ne peut pas couvrir au moins un emplacement. Imprimer quand meme ?',
+ lineItem: 'Emplacement {{slot}} : besoin de {{required}} g, {{remaining}} g restants',
+ unknown: 'inconnu',
+ printAnyway: 'Imprimer quand meme',
+ },
title: 'File d\'attente',
subtitle: 'Gérez vos travaux d\'impression',
addToQueue: 'Ajouter à la file',
diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts
index 592b3be5a..de4ebf867 100644
--- a/frontend/src/i18n/locales/it.ts
+++ b/frontend/src/i18n/locales/it.ts
@@ -927,6 +927,15 @@ export default {
// Queue page
queue: {
+ filamentShort: {
+ rowBadge: 'Filamento insufficiente per la bobina assegnata',
+ rowTooltip: 'Lo scheduler ha segnalato questo elemento. Premi Play per vedere il deficit per slot e decidere se stampare comunque.',
+ confirmTitle: 'Filamento insufficiente',
+ confirmIntro: 'La bobina assegnata non puo coprire almeno uno slot. Stampare comunque?',
+ lineItem: 'Slot {{slot}}: servono {{required}} g, rimangono {{remaining}} g',
+ unknown: 'sconosciuto',
+ printAnyway: 'Stampa comunque',
+ },
title: 'Coda di stampa',
subtitle: 'Programma e gestisci i tuoi lavori di stampa',
addToQueue: 'Aggiungi alla coda',
diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts
index 317dae8e0..328673ed0 100644
--- a/frontend/src/i18n/locales/ja.ts
+++ b/frontend/src/i18n/locales/ja.ts
@@ -926,6 +926,15 @@ export default {
// Queue page
queue: {
+ filamentShort: {
+ rowBadge: '割り当てられたスプールのフィラメントが不足しています',
+ rowTooltip: 'スケジューラーがこのアイテムをフラグしました。再生ボタンをクリックしてスロットごとの不足量を確認し、それでも印刷するか判断してください。',
+ confirmTitle: 'フィラメント不足',
+ confirmIntro: '割り当てられたスプールでは少なくとも 1 つのスロットを賄えません。それでも印刷しますか?',
+ lineItem: 'スロット {{slot}}:必要 {{required}} g、残り {{remaining}} g',
+ unknown: '不明',
+ printAnyway: 'それでも印刷',
+ },
title: '印刷キュー',
subtitle: '印刷ジョブのスケジュールと管理',
addToQueue: 'キューに追加',
diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts
index b0e942900..690c1029e 100644
--- a/frontend/src/i18n/locales/pt-BR.ts
+++ b/frontend/src/i18n/locales/pt-BR.ts
@@ -927,6 +927,15 @@ export default {
// Queue page
queue: {
+ filamentShort: {
+ rowBadge: 'Filamento insuficiente para a bobina atribuida',
+ rowTooltip: 'O escalonador sinalizou este item. Clique em Play para ver o deficit por slot e decidir se quer imprimir mesmo assim.',
+ confirmTitle: 'Filamento insuficiente',
+ confirmIntro: 'A bobina atribuida nao pode cobrir pelo menos um slot. Imprimir mesmo assim?',
+ lineItem: 'Slot {{slot}}: precisa de {{required}} g, restam {{remaining}} g',
+ unknown: 'desconhecido',
+ printAnyway: 'Imprimir mesmo assim',
+ },
title: 'Fila de Impressão',
subtitle: 'Agende e gerencie seus trabalhos de impressão',
addToQueue: 'Adicionar à Fila',
diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts
index 4c2e51372..c4229792b 100644
--- a/frontend/src/i18n/locales/zh-CN.ts
+++ b/frontend/src/i18n/locales/zh-CN.ts
@@ -927,6 +927,15 @@ export default {
// Queue page
queue: {
+ filamentShort: {
+ rowBadge: '所分配料盘的耀丝不足',
+ rowTooltip: '调度程序已标记该项。点击播放查看各插槽的赤字并决定是否仍要打印。',
+ confirmTitle: '耀丝不足',
+ confirmIntro: '所分配的料盘至少有一个插槽无法满足。仍要打印吗?',
+ lineItem: '插槽 {{slot}}:需要 {{required}} g,剩余 {{remaining}} g',
+ unknown: '未知',
+ printAnyway: '仍要打印',
+ },
title: '打印队列',
subtitle: '排程和管理您的打印任务',
addToQueue: '添加到队列',
diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts
index 5614cbee7..bc3fe7b6e 100644
--- a/frontend/src/i18n/locales/zh-TW.ts
+++ b/frontend/src/i18n/locales/zh-TW.ts
@@ -927,6 +927,15 @@ export default {
// Queue page
queue: {
+ filamentShort: {
+ rowBadge: '所分配料盤的線材不足',
+ rowTooltip: '調度程式已標記此項目。點擊播放查看各槽位的不足量,並決定是否仍要列印。',
+ confirmTitle: '線材不足',
+ confirmIntro: '所分配的料盤至少有一個槽位無法滿足。仍要列印嗎?',
+ lineItem: '槽位 {{slot}}:需要 {{required}} g,剩餘 {{remaining}} g',
+ unknown: '不明',
+ printAnyway: '仍要列印',
+ },
title: '列印佇列',
subtitle: '排程和管理您的列印任務',
addToQueue: '新增到佇列',
diff --git a/frontend/src/pages/QueuePage.tsx b/frontend/src/pages/QueuePage.tsx
index a106d6d6c..87013ace2 100644
--- a/frontend/src/pages/QueuePage.tsx
+++ b/frontend/src/pages/QueuePage.tsx
@@ -55,7 +55,7 @@ import {
Code,
Snail,
} from 'lucide-react';
-import { api } from '../api/client';
+import { api, ApiError } from '../api/client';
import { type TimeFormat, formatETA, formatDuration, formatRelativeTime, parseUTCDate } from '../utils/date';
import type { PrintQueueItem, PrintQueueBulkUpdate, Permission } from '../api/client';
import { Card } from '../components/Card';
@@ -612,6 +612,17 @@ function SortableQueueItem({
)}
+ {/* Filament-short flag from the dispatch pre-flight (#1496). */}
+ {item.filament_short && item.status === 'pending' && (
+
+
+ {t('queue.filamentShort.rowBadge')}
+
+ )}
+
{/* Error message */}
{item.error_message && (
@@ -827,13 +838,41 @@ export function QueuePage() {
onError: () => showToast(t('queue.toast.stopFailed'), 'error'),
});
+ // Filament-deficit confirmation state (#1496). When the backend returns
+ // 409 with `code=insufficient_filament` we stash the deficit + item id
+ // here; the modal at the bottom of the page reads it and the "Print
+ // Anyway" path re-issues the start with `skipFilamentCheck=true`.
+ const [filamentShortConfirm, setFilamentShortConfirm] = useState<{
+ itemId: number;
+ deficit: Array<{
+ slot_id: number;
+ required_grams: number;
+ remaining_grams: number | null;
+ filament_type?: string | null;
+ }>;
+ } | null>(null);
+
const startMutation = useMutation({
- mutationFn: (id: number) => api.startQueueItem(id),
+ mutationFn: ({ id, skipFilamentCheck }: { id: number; skipFilamentCheck?: boolean }) =>
+ api.startQueueItem(id, { skipFilamentCheck }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['queue'] });
showToast(t('queue.toast.released'));
+ setFilamentShortConfirm(null);
+ },
+ onError: (error: unknown, variables) => {
+ if (error instanceof ApiError && error.status === 409 && error.code === 'insufficient_filament') {
+ const deficitRaw = (error.detail?.deficit ?? []) as Array<{
+ slot_id: number;
+ required_grams: number;
+ remaining_grams: number | null;
+ filament_type?: string | null;
+ }>;
+ setFilamentShortConfirm({ itemId: variables.id, deficit: deficitRaw });
+ return;
+ }
+ showToast(t('queue.toast.startFailed'), 'error');
},
- onError: () => showToast(t('queue.toast.startFailed'), 'error'),
});
const reorderMutation = useMutation({
@@ -1367,7 +1406,7 @@ export function QueuePage() {
onRemove={() => {}}
onStop={() => {}}
onRequeue={() => {}}
- onStart={() => startMutation.mutate(item.id)}
+ onStart={() => startMutation.mutate({ id: item.id })}
timeFormat={timeFormat}
isSelected={selectedItems.includes(item.id)}
onToggleSelect={() => handleToggleSelect(item.id)}
@@ -1464,6 +1503,33 @@ export function QueuePage() {
)}
{/* Confirm Action Modal */}
+ {filamentShortConfirm && (
+
+ t('queue.filamentShort.lineItem', {
+ slot: d.slot_id,
+ required: Math.round(d.required_grams),
+ remaining:
+ d.remaining_grams == null
+ ? t('queue.filamentShort.unknown')
+ : Math.round(d.remaining_grams),
+ }),
+ )
+ .join('\n')
+ }
+ confirmText={t('queue.filamentShort.printAnyway')}
+ variant="warning"
+ onConfirm={() => {
+ startMutation.mutate({ id: filamentShortConfirm.itemId, skipFilamentCheck: true });
+ }}
+ onCancel={() => setFilamentShortConfirm(null)}
+ />
+ )}
+
{confirmAction && (
n[i]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))n(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function a(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function n(i){if(i.ep)return;i.ep=!0;const s=a(i);fetch(i.href,s)}})();var P_=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Ec(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var aE={exports:{}},t0={};var NH;function ame(){if(NH)return t0;NH=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function a(n,i,s){var o=null;if(s!==void 0&&(o=""+s),i.key!==void 0&&(o=""+i.key),"key"in i){s={};for(var l in i)l!=="key"&&(s[l]=i[l])}else s=i;return i=s.ref,{$$typeof:t,type:n,key:o,ref:i!==void 0?i:null,props:s}}return t0.Fragment=e,t0.jsx=a,t0.jsxs=a,t0}var CH;function rme(){return CH||(CH=1,aE.exports=ame()),aE.exports}var r=rme(),rE={exports:{}},Ka={};var PH;function nme(){if(PH)return Ka;PH=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),a=Symbol.for("react.fragment"),n=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),s=Symbol.for("react.consumer"),o=Symbol.for("react.context"),l=Symbol.for("react.forward_ref"),c=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),u=Symbol.for("react.lazy"),m=Symbol.for("react.activity"),p=Symbol.iterator;function f(Z){return Z===null||typeof Z!="object"?null:(Z=p&&Z[p]||Z["@@iterator"],typeof Z=="function"?Z:null)}var g={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},w=Object.assign,b={};function x(Z,ce,he){this.props=Z,this.context=ce,this.refs=b,this.updater=he||g}x.prototype.isReactComponent={},x.prototype.setState=function(Z,ce){if(typeof Z!="object"&&typeof Z!="function"&&Z!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,Z,ce,"setState")},x.prototype.forceUpdate=function(Z){this.updater.enqueueForceUpdate(this,Z,"forceUpdate")};function S(){}S.prototype=x.prototype;function N(Z,ce,he){this.props=Z,this.context=ce,this.refs=b,this.updater=he||g}var C=N.prototype=new S;C.constructor=N,w(C,x.prototype),C.isPureReactComponent=!0;var P=Array.isArray;function T(){}var j={H:null,A:null,T:null,S:null},F=Object.prototype.hasOwnProperty;function k(Z,ce,he){var ue=he.ref;return{$$typeof:t,type:Z,key:ce,ref:ue!==void 0?ue:null,props:he}}function M(Z,ce){return k(Z.type,ce,Z.props)}function q(Z){return typeof Z=="object"&&Z!==null&&Z.$$typeof===t}function I(Z){var ce={"=":"=0",":":"=2"};return"$"+Z.replace(/[=:]/g,function(he){return ce[he]})}var O=/\/+/g;function L(Z,ce){return typeof Z=="object"&&Z!==null&&Z.key!=null?I(""+Z.key):ce.toString(36)}function X(Z){switch(Z.status){case"fulfilled":return Z.value;case"rejected":throw Z.reason;default:switch(typeof Z.status=="string"?Z.then(T,T):(Z.status="pending",Z.then(function(ce){Z.status==="pending"&&(Z.status="fulfilled",Z.value=ce)},function(ce){Z.status==="pending"&&(Z.status="rejected",Z.reason=ce)})),Z.status){case"fulfilled":return Z.value;case"rejected":throw Z.reason}}throw Z}function ee(Z,ce,he,ue,xe){var H=typeof Z;(H==="undefined"||H==="boolean")&&(Z=null);var J=!1;if(Z===null)J=!0;else switch(H){case"bigint":case"string":case"number":J=!0;break;case"object":switch(Z.$$typeof){case t:case e:J=!0;break;case u:return J=Z._init,ee(J(Z._payload),ce,he,ue,xe)}}if(J)return xe=xe(Z),J=ue===""?"."+L(Z,0):ue,P(xe)?(he="",J!=null&&(he=J.replace(O,"$&/")+"/"),ee(xe,ce,he,"",function(B){return B})):xe!=null&&(q(xe)&&(xe=M(xe,he+(xe.key==null||Z&&Z.key===xe.key?"":(""+xe.key).replace(O,"$&/")+"/")+J)),ce.push(xe)),1;J=0;var A=ue===""?".":ue+":";if(P(Z))for(var E=0;E>>1,me=ee[de];if(0>>1;dei(he,le))uei(xe,he)?(ee[de]=xe,ee[ue]=le,de=ue):(ee[de]=he,ee[ce]=le,de=ce);else if(uei(xe,le))ee[de]=xe,ee[ue]=le,de=ue;else break e}}return Q}function i(ee,Q){var le=ee.sortIndex-Q.sortIndex;return le!==0?le:ee.id-Q.id}if(t.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var s=performance;t.unstable_now=function(){return s.now()}}else{var o=Date,l=o.now();t.unstable_now=function(){return o.now()-l}}var c=[],d=[],u=1,m=null,p=3,f=!1,g=!1,w=!1,b=!1,x=typeof setTimeout=="function"?setTimeout:null,S=typeof clearTimeout=="function"?clearTimeout:null,N=typeof setImmediate<"u"?setImmediate:null;function C(ee){for(var Q=a(d);Q!==null;){if(Q.callback===null)n(d);else if(Q.startTime<=ee)n(d),Q.sortIndex=Q.expirationTime,e(c,Q);else break;Q=a(d)}}function P(ee){if(w=!1,C(ee),!g)if(a(c)!==null)g=!0,T||(T=!0,I());else{var Q=a(d);Q!==null&&X(P,Q.startTime-ee)}}var T=!1,j=-1,F=5,k=-1;function M(){return b?!0:!(t.unstable_now()-kee&&M());){var de=m.callback;if(typeof de=="function"){m.callback=null,p=m.priorityLevel;var me=de(m.expirationTime<=ee);if(ee=t.unstable_now(),typeof me=="function"){m.callback=me,C(ee),Q=!0;break t}m===a(c)&&n(c),C(ee)}else n(c);m=a(c)}if(m!==null)Q=!0;else{var Z=a(d);Z!==null&&X(P,Z.startTime-ee),Q=!1}}break e}finally{m=null,p=le,f=!1}Q=void 0}}finally{Q?I():T=!1}}}var I;if(typeof N=="function")I=function(){N(q)};else if(typeof MessageChannel<"u"){var O=new MessageChannel,L=O.port2;O.port1.onmessage=q,I=function(){L.postMessage(null)}}else I=function(){x(q,0)};function X(ee,Q){j=x(function(){ee(t.unstable_now())},Q)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(ee){ee.callback=null},t.unstable_forceFrameRate=function(ee){0>ee||125de?(ee.sortIndex=le,e(d,ee),a(c)===null&&ee===a(d)&&(w?(S(j),j=-1):w=!0,X(P,le-de))):(ee.sortIndex=me,e(c,ee),g||f||(g=!0,T||(T=!0,I()))),ee},t.unstable_shouldYield=M,t.unstable_wrapCallback=function(ee){var Q=p;return function(){var le=p;p=Q;try{return ee.apply(this,arguments)}finally{p=le}}}})(sE)),sE}var jH;function ome(){return jH||(jH=1,iE.exports=sme()),iE.exports}var oE={exports:{}},Gs={};var MH;function lme(){if(MH)return Gs;MH=1;var t=ob();function e(c){var d="https://react.dev/errors/"+c;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(e){console.error(e)}}return t(),oE.exports=lme(),oE.exports}var DH;function cme(){if(DH)return a0;DH=1;var t=ome(),e=ob(),a=YX();function n(h){var v="https://react.dev/errors/"+h;if(1me||(h.current=de[me],de[me]=null,me--)}function he(h,v){me++,de[me]=h.current,h.current=v}var ue=Z(null),xe=Z(null),H=Z(null),J=Z(null);function A(h,v){switch(he(H,v),he(xe,h),he(ue,null),v.nodeType){case 9:case 11:h=(h=v.documentElement)&&(h=h.namespaceURI)?Kq(h):0;break;default:if(h=v.tagName,v=v.namespaceURI)v=Kq(v),h=Qq(v,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}ce(ue),he(ue,h)}function E(){ce(ue),ce(xe),ce(H)}function B(h){h.memoizedState!==null&&he(J,h);var v=ue.current,_=Qq(v,h.type);v!==_&&(he(xe,h),he(ue,_))}function W(h){xe.current===h&&(ce(ue),ce(xe)),J.current===h&&(ce(J),Xv._currentValue=le)}var z,ie;function U(h){if(z===void 0)try{throw Error()}catch(_){var v=_.stack.trim().match(/\n( *(at )?)/);z=v&&v[1]||"",ie=-1<_.stack.indexOf(`
+function tme(t,e){for(var a=0;an[i]})}}}return Object.freeze(Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}))}(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))n(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function a(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function n(i){if(i.ep)return;i.ep=!0;const s=a(i);fetch(i.href,s)}})();var P_=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Ec(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var aE={exports:{}},t0={};var NH;function ame(){if(NH)return t0;NH=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function a(n,i,s){var o=null;if(s!==void 0&&(o=""+s),i.key!==void 0&&(o=""+i.key),"key"in i){s={};for(var l in i)l!=="key"&&(s[l]=i[l])}else s=i;return i=s.ref,{$$typeof:t,type:n,key:o,ref:i!==void 0?i:null,props:s}}return t0.Fragment=e,t0.jsx=a,t0.jsxs=a,t0}var CH;function rme(){return CH||(CH=1,aE.exports=ame()),aE.exports}var r=rme(),rE={exports:{}},Ka={};var PH;function nme(){if(PH)return Ka;PH=1;var t=Symbol.for("react.transitional.element"),e=Symbol.for("react.portal"),a=Symbol.for("react.fragment"),n=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),s=Symbol.for("react.consumer"),o=Symbol.for("react.context"),l=Symbol.for("react.forward_ref"),c=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),u=Symbol.for("react.lazy"),m=Symbol.for("react.activity"),p=Symbol.iterator;function f(X){return X===null||typeof X!="object"?null:(X=p&&X[p]||X["@@iterator"],typeof X=="function"?X:null)}var g={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},w=Object.assign,b={};function x(X,ce,he){this.props=X,this.context=ce,this.refs=b,this.updater=he||g}x.prototype.isReactComponent={},x.prototype.setState=function(X,ce){if(typeof X!="object"&&typeof X!="function"&&X!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,X,ce,"setState")},x.prototype.forceUpdate=function(X){this.updater.enqueueForceUpdate(this,X,"forceUpdate")};function S(){}S.prototype=x.prototype;function N(X,ce,he){this.props=X,this.context=ce,this.refs=b,this.updater=he||g}var C=N.prototype=new S;C.constructor=N,w(C,x.prototype),C.isPureReactComponent=!0;var P=Array.isArray;function T(){}var j={H:null,A:null,T:null,S:null},F=Object.prototype.hasOwnProperty;function k(X,ce,he){var ue=he.ref;return{$$typeof:t,type:X,key:ce,ref:ue!==void 0?ue:null,props:he}}function M(X,ce){return k(X.type,ce,X.props)}function q(X){return typeof X=="object"&&X!==null&&X.$$typeof===t}function I(X){var ce={"=":"=0",":":"=2"};return"$"+X.replace(/[=:]/g,function(he){return ce[he]})}var O=/\/+/g;function L(X,ce){return typeof X=="object"&&X!==null&&X.key!=null?I(""+X.key):ce.toString(36)}function Y(X){switch(X.status){case"fulfilled":return X.value;case"rejected":throw X.reason;default:switch(typeof X.status=="string"?X.then(T,T):(X.status="pending",X.then(function(ce){X.status==="pending"&&(X.status="fulfilled",X.value=ce)},function(ce){X.status==="pending"&&(X.status="rejected",X.reason=ce)})),X.status){case"fulfilled":return X.value;case"rejected":throw X.reason}}throw X}function ee(X,ce,he,ue,be){var H=typeof X;(H==="undefined"||H==="boolean")&&(X=null);var J=!1;if(X===null)J=!0;else switch(H){case"bigint":case"string":case"number":J=!0;break;case"object":switch(X.$$typeof){case t:case e:J=!0;break;case u:return J=X._init,ee(J(X._payload),ce,he,ue,be)}}if(J)return be=be(X),J=ue===""?"."+L(X,0):ue,P(be)?(he="",J!=null&&(he=J.replace(O,"$&/")+"/"),ee(be,ce,he,"",function(B){return B})):be!=null&&(q(be)&&(be=M(be,he+(be.key==null||X&&X.key===be.key?"":(""+be.key).replace(O,"$&/")+"/")+J)),ce.push(be)),1;J=0;var A=ue===""?".":ue+":";if(P(X))for(var E=0;E>>1,me=ee[de];if(0>>1;dei(he,le))uei(be,he)?(ee[de]=be,ee[ue]=le,de=ue):(ee[de]=he,ee[ce]=le,de=ce);else if(uei(be,le))ee[de]=be,ee[ue]=le,de=ue;else break e}}return Q}function i(ee,Q){var le=ee.sortIndex-Q.sortIndex;return le!==0?le:ee.id-Q.id}if(t.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var s=performance;t.unstable_now=function(){return s.now()}}else{var o=Date,l=o.now();t.unstable_now=function(){return o.now()-l}}var c=[],d=[],u=1,m=null,p=3,f=!1,g=!1,w=!1,b=!1,x=typeof setTimeout=="function"?setTimeout:null,S=typeof clearTimeout=="function"?clearTimeout:null,N=typeof setImmediate<"u"?setImmediate:null;function C(ee){for(var Q=a(d);Q!==null;){if(Q.callback===null)n(d);else if(Q.startTime<=ee)n(d),Q.sortIndex=Q.expirationTime,e(c,Q);else break;Q=a(d)}}function P(ee){if(w=!1,C(ee),!g)if(a(c)!==null)g=!0,T||(T=!0,I());else{var Q=a(d);Q!==null&&Y(P,Q.startTime-ee)}}var T=!1,j=-1,F=5,k=-1;function M(){return b?!0:!(t.unstable_now()-kee&&M());){var de=m.callback;if(typeof de=="function"){m.callback=null,p=m.priorityLevel;var me=de(m.expirationTime<=ee);if(ee=t.unstable_now(),typeof me=="function"){m.callback=me,C(ee),Q=!0;break t}m===a(c)&&n(c),C(ee)}else n(c);m=a(c)}if(m!==null)Q=!0;else{var X=a(d);X!==null&&Y(P,X.startTime-ee),Q=!1}}break e}finally{m=null,p=le,f=!1}Q=void 0}}finally{Q?I():T=!1}}}var I;if(typeof N=="function")I=function(){N(q)};else if(typeof MessageChannel<"u"){var O=new MessageChannel,L=O.port2;O.port1.onmessage=q,I=function(){L.postMessage(null)}}else I=function(){x(q,0)};function Y(ee,Q){j=x(function(){ee(t.unstable_now())},Q)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(ee){ee.callback=null},t.unstable_forceFrameRate=function(ee){0>ee||125de?(ee.sortIndex=le,e(d,ee),a(c)===null&&ee===a(d)&&(w?(S(j),j=-1):w=!0,Y(P,le-de))):(ee.sortIndex=me,e(c,ee),g||f||(g=!0,T||(T=!0,I()))),ee},t.unstable_shouldYield=M,t.unstable_wrapCallback=function(ee){var Q=p;return function(){var le=p;p=Q;try{return ee.apply(this,arguments)}finally{p=le}}}})(sE)),sE}var jH;function ome(){return jH||(jH=1,iE.exports=sme()),iE.exports}var oE={exports:{}},Gs={};var MH;function lme(){if(MH)return Gs;MH=1;var t=ob();function e(c){var d="https://react.dev/errors/"+c;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(t)}catch(e){console.error(e)}}return t(),oE.exports=lme(),oE.exports}var DH;function cme(){if(DH)return a0;DH=1;var t=ome(),e=ob(),a=YX();function n(h){var v="https://react.dev/errors/"+h;if(1me||(h.current=de[me],de[me]=null,me--)}function he(h,v){me++,de[me]=h.current,h.current=v}var ue=X(null),be=X(null),H=X(null),J=X(null);function A(h,v){switch(he(H,v),he(be,h),he(ue,null),v.nodeType){case 9:case 11:h=(h=v.documentElement)&&(h=h.namespaceURI)?Kq(h):0;break;default:if(h=v.tagName,v=v.namespaceURI)v=Kq(v),h=Qq(v,h);else switch(h){case"svg":h=1;break;case"math":h=2;break;default:h=0}}ce(ue),he(ue,h)}function E(){ce(ue),ce(be),ce(H)}function B(h){h.memoizedState!==null&&he(J,h);var v=ue.current,_=Qq(v,h.type);v!==_&&(he(be,h),he(ue,_))}function W(h){be.current===h&&(ce(ue),ce(be)),J.current===h&&(ce(J),Xv._currentValue=le)}var z,ie;function U(h){if(z===void 0)try{throw Error()}catch(_){var v=_.stack.trim().match(/\n( *(at )?)/);z=v&&v[1]||"",ie=-1<_.stack.indexOf(`
at`)?" ()":-1<_.stack.indexOf("@")?"@unknown:0:0":""}return`
`+z+h+ie}var G=!1;function K(h,v){if(!h||G)return"";G=!0;var _=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var D={DetermineComponentFrameRoot:function(){try{if(v){var Wt=function(){throw Error()};if(Object.defineProperty(Wt.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(Wt,[])}catch(zt){var Lt=zt}Reflect.construct(h,[],Wt)}else{try{Wt.call()}catch(zt){Lt=zt}h.call(Wt.prototype)}}else{try{throw Error()}catch(zt){Lt=zt}(Wt=h())&&typeof Wt.catch=="function"&&Wt.catch(function(){})}}catch(zt){if(zt&&Lt&&typeof zt.stack=="string")return[zt.stack,Lt.stack]}return[null,null]}};D.DetermineComponentFrameRoot.displayName="DetermineComponentFrameRoot";var V=Object.getOwnPropertyDescriptor(D.DetermineComponentFrameRoot,"name");V&&V.configurable&&Object.defineProperty(D.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var te=D.DetermineComponentFrameRoot(),Se=te[0],He=te[1];if(Se&&He){var mt=Se.split(`
`),Et=He.split(`
`);for(V=D=0;DV||mt[D]!==Et[V]){var qt=`
-`+mt[D].replace(" at new "," at ");return h.displayName&&qt.includes("")&&(qt=qt.replace("",h.displayName)),qt}while(1<=D&&0<=V);break}}}finally{G=!1,Error.prepareStackTrace=_}return(_=h?h.displayName||h.name:"")?U(_):""}function Y(h,v){switch(h.tag){case 26:case 27:case 5:return U(h.type);case 16:return U("Lazy");case 13:return h.child!==v&&v!==null?U("Suspense Fallback"):U("Suspense");case 19:return U("SuspenseList");case 0:case 15:return K(h.type,!1);case 11:return K(h.type.render,!1);case 1:return K(h.type,!0);case 31:return U("Activity");default:return""}}function se(h){try{var v="",_=null;do v+=Y(h,_),_=h,h=h.return;while(h);return v}catch(D){return`
+`+mt[D].replace(" at new "," at ");return h.displayName&&qt.includes("")&&(qt=qt.replace("",h.displayName)),qt}while(1<=D&&0<=V);break}}}finally{G=!1,Error.prepareStackTrace=_}return(_=h?h.displayName||h.name:"")?U(_):""}function Z(h,v){switch(h.tag){case 26:case 27:case 5:return U(h.type);case 16:return U("Lazy");case 13:return h.child!==v&&v!==null?U("Suspense Fallback"):U("Suspense");case 19:return U("SuspenseList");case 0:case 15:return K(h.type,!1);case 11:return K(h.type.render,!1);case 1:return K(h.type,!0);case 31:return U("Activity");default:return""}}function oe(h){try{var v="",_=null;do v+=Z(h,_),_=h,h=h.return;while(h);return v}catch(D){return`
Error generating stack: `+D.message+`
-`+D.stack}}var oe=Object.prototype.hasOwnProperty,fe=t.unstable_scheduleCallback,ae=t.unstable_cancelCallback,pe=t.unstable_shouldYield,$=t.unstable_requestPaint,R=t.unstable_now,ne=t.unstable_getCurrentPriorityLevel,be=t.unstable_ImmediatePriority,ve=t.unstable_UserBlockingPriority,ke=t.unstable_NormalPriority,ye=t.unstable_LowPriority,ge=t.unstable_IdlePriority,Me=t.log,Be=t.unstable_setDisableYieldValue,Le=null,Oe=null;function Re(h){if(typeof Me=="function"&&Be(h),Oe&&typeof Oe.setStrictMode=="function")try{Oe.setStrictMode(Le,h)}catch{}}var Ke=Math.clz32?Math.clz32:Ee,Ue=Math.log,lt=Math.LN2;function Ee(h){return h>>>=0,h===0?32:31-(Ue(h)/lt|0)|0}var st=256,Ne=262144,Ze=4194304;function Ye(h){var v=h&42;if(v!==0)return v;switch(h&-h){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return h&261888;case 262144:case 524288:case 1048576:case 2097152:return h&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return h&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return h}}function Te(h,v,_){var D=h.pendingLanes;if(D===0)return 0;var V=0,te=h.suspendedLanes,Se=h.pingedLanes;h=h.warmLanes;var He=D&134217727;return He!==0?(D=He&~te,D!==0?V=Ye(D):(Se&=He,Se!==0?V=Ye(Se):_||(_=He&~h,_!==0&&(V=Ye(_))))):(He=D&~te,He!==0?V=Ye(He):Se!==0?V=Ye(Se):_||(_=D&~h,_!==0&&(V=Ye(_)))),V===0?0:v!==0&&v!==V&&(v&te)===0&&(te=V&-V,_=v&-v,te>=_||te===32&&(_&4194048)!==0)?v:V}function Fe(h,v){return(h.pendingLanes&~(h.suspendedLanes&~h.pingedLanes)&v)===0}function et(h,v){switch(h){case 1:case 2:case 4:case 8:case 64:return v+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return v+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function We(){var h=Ze;return Ze<<=1,(Ze&62914560)===0&&(Ze=4194304),h}function Pe(h){for(var v=[],_=0;31>_;_++)v.push(h);return v}function at(h,v){h.pendingLanes|=v,v!==268435456&&(h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0)}function nt(h,v,_,D,V,te){var Se=h.pendingLanes;h.pendingLanes=_,h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0,h.expiredLanes&=_,h.entangledLanes&=_,h.errorRecoveryDisabledLanes&=_,h.shellSuspendCounter=0;var He=h.entanglements,mt=h.expirationTimes,Et=h.hiddenUpdates;for(_=Se&~_;0<_;){var qt=31-Ke(_),Wt=1<"u")return null;try{return h.activeElement||h.body}catch{return h.body}}var Rr=/[\n"\\]/g;function $a(h){return h.replace(Rr,function(v){return"\\"+v.charCodeAt(0).toString(16)+" "})}function Un(h,v,_,D,V,te,Se,He){h.name="",Se!=null&&typeof Se!="function"&&typeof Se!="symbol"&&typeof Se!="boolean"?h.type=Se:h.removeAttribute("type"),v!=null?Se==="number"?(v===0&&h.value===""||h.value!=v)&&(h.value=""+ca(v)):h.value!==""+ca(v)&&(h.value=""+ca(v)):Se!=="submit"&&Se!=="reset"||h.removeAttribute("value"),v!=null?$r(h,Se,ca(v)):_!=null?$r(h,Se,ca(_)):D!=null&&h.removeAttribute("value"),V==null&&te!=null&&(h.defaultChecked=!!te),V!=null&&(h.checked=V&&typeof V!="function"&&typeof V!="symbol"),He!=null&&typeof He!="function"&&typeof He!="symbol"&&typeof He!="boolean"?h.name=""+ca(He):h.removeAttribute("name")}function Ra(h,v,_,D,V,te,Se,He){if(te!=null&&typeof te!="function"&&typeof te!="symbol"&&typeof te!="boolean"&&(h.type=te),v!=null||_!=null){if(!(te!=="submit"&&te!=="reset"||v!=null)){pa(h);return}_=_!=null?""+ca(_):"",v=v!=null?""+ca(v):_,He||v===h.value||(h.value=v),h.defaultValue=v}D=D??V,D=typeof D!="function"&&typeof D!="symbol"&&!!D,h.checked=He?h.checked:!!D,h.defaultChecked=!!D,Se!=null&&typeof Se!="function"&&typeof Se!="symbol"&&typeof Se!="boolean"&&(h.name=Se),pa(h)}function $r(h,v,_){v==="number"&&tr(h.ownerDocument)===h||h.defaultValue===""+_||(h.defaultValue=""+_)}function bn(h,v,_,D){if(h=h.options,v){v={};for(var V=0;V<_.length;V++)v["$"+_[V]]=!0;for(_=0;_"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),$s=!1;if(vi)try{var ju={};Object.defineProperty(ju,"passive",{get:function(){$s=!0}}),window.addEventListener("test",ju,ju),window.removeEventListener("test",ju,ju)}catch{$s=!1}var Kn=null,Sd=null,Rc=null;function wi(){if(Rc)return Rc;var h,v=Sd,_=v.length,D,V="value"in Kn?Kn.value:Kn.textContent,te=V.length;for(h=0;h<_&&v[h]===V[h];h++);var Se=_-h;for(D=1;D<=Se&&v[_-D]===V[te-D];D++);return Rc=V.slice(h,1=Qn),we=" ",Ut=!1;function sa(h,v){switch(h){case"keyup":return wf.indexOf(v.keyCode)!==-1;case"keydown":return v.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Ta(h){return h=h.detail,typeof h=="object"&&"data"in h?h.data:null}var Ia=!1;function kn(h,v){switch(h){case"compositionend":return Ta(v);case"keypress":return v.which!==32?null:(Ut=!0,we);case"textInput":return h=v.data,h===we&&Ut?null:h;default:return null}}function Si(h,v){if(Ia)return h==="compositionend"||!cl&&sa(h,v)?(h=wi(),Rc=Sd=Kn=null,Ia=!1,h):null;switch(h){case"paste":return null;case"keypress":if(!(v.ctrlKey||v.altKey||v.metaKey)||v.ctrlKey&&v.altKey){if(v.char&&1=v)return{node:_,offset:v-h};h=D}e:{for(;_;){if(_.nextSibling){_=_.nextSibling;break e}_=_.parentNode}_=void 0}_=Ro(_)}}function Io(h,v){return h&&v?h===v?!0:h&&h.nodeType===3?!1:v&&v.nodeType===3?Io(h,v.parentNode):"contains"in h?h.contains(v):h.compareDocumentPosition?!!(h.compareDocumentPosition(v)&16):!1:!1}function fo(h){h=h!=null&&h.ownerDocument!=null&&h.ownerDocument.defaultView!=null?h.ownerDocument.defaultView:window;for(var v=tr(h.document);v instanceof h.HTMLIFrameElement;){try{var _=typeof v.contentWindow.location.href=="string"}catch{_=!1}if(_)h=v.contentWindow;else break;v=tr(h.document)}return v}function ki(h){var v=h&&h.nodeName&&h.nodeName.toLowerCase();return v&&(v==="input"&&(h.type==="text"||h.type==="search"||h.type==="tel"||h.type==="url"||h.type==="password")||v==="textarea"||h.contentEditable==="true")}var Cd=vi&&"documentMode"in document&&11>=document.documentMode,pi=null,ur=null,Ar=null,Xn=!1;function xp(h,v,_){var D=_.window===_?_.document:_.nodeType===9?_:_.ownerDocument;Xn||pi==null||pi!==tr(D)||(D=pi,"selectionStart"in D&&ki(D)?D={start:D.selectionStart,end:D.selectionEnd}:(D=(D.ownerDocument&&D.ownerDocument.defaultView||window).getSelection(),D={anchorNode:D.anchorNode,anchorOffset:D.anchorOffset,focusNode:D.focusNode,focusOffset:D.focusOffset}),Ar&&_s(Ar,D)||(Ar=D,D=m_(ur,"onSelect"),0>=Se,V-=Se,Td=1<<32-Ke(v)+V|_<Ja?(Sr=Ca,Ca=null):Sr=Ca.sibling;var Dr=Lt(St,Ca,jt[Ja],Vt);if(Dr===null){Ca===null&&(Ca=Sr);break}h&&Ca&&Dr.alternate===null&&v(St,Ca),xt=te(Dr,xt,Ja),Er===null?Ea=Dr:Er.sibling=Dr,Er=Dr,Ca=Sr}if(Ja===jt.length)return _(St,Ca),_r&&zu(St,Ja),Ea;if(Ca===null){for(;JaJa?(Sr=Ca,Ca=null):Sr=Ca.sibling;var Up=Lt(St,Ca,Dr.value,Vt);if(Up===null){Ca===null&&(Ca=Sr);break}h&&Ca&&Up.alternate===null&&v(St,Ca),xt=te(Up,xt,Ja),Er===null?Ea=Up:Er.sibling=Up,Er=Up,Ca=Sr}if(Dr.done)return _(St,Ca),_r&&zu(St,Ja),Ea;if(Ca===null){for(;!Dr.done;Ja++,Dr=jt.next())Dr=Wt(St,Dr.value,Vt),Dr!==null&&(xt=te(Dr,xt,Ja),Er===null?Ea=Dr:Er.sibling=Dr,Er=Dr);return _r&&zu(St,Ja),Ea}for(Ca=D(Ca);!Dr.done;Ja++,Dr=jt.next())Dr=zt(Ca,St,Ja,Dr.value,Vt),Dr!==null&&(h&&Dr.alternate!==null&&Ca.delete(Dr.key===null?Ja:Dr.key),xt=te(Dr,xt,Ja),Er===null?Ea=Dr:Er.sibling=Dr,Er=Dr);return h&&Ca.forEach(function(eme){return v(St,eme)}),_r&&zu(St,Ja),Ea}function tn(St,xt,jt,Vt){if(typeof jt=="object"&&jt!==null&&jt.type===w&&jt.key===null&&(jt=jt.props.children),typeof jt=="object"&&jt!==null){switch(jt.$$typeof){case f:e:{for(var Ea=jt.key;xt!==null;){if(xt.key===Ea){if(Ea=jt.type,Ea===w){if(xt.tag===7){_(St,xt.sibling),Vt=V(xt,jt.props.children),Vt.return=St,St=Vt;break e}}else if(xt.elementType===Ea||typeof Ea=="object"&&Ea!==null&&Ea.$$typeof===F&&Af(Ea)===xt.type){_(St,xt.sibling),Vt=V(xt,jt.props),Tv(Vt,jt),Vt.return=St,St=Vt;break e}_(St,xt);break}else v(St,xt);xt=xt.sibling}jt.type===w?(Vt=_f(jt.props.children,St.mode,Vt,jt.key),Vt.return=St,St=Vt):(Vt=Ak(jt.type,jt.key,jt.props,null,St.mode,Vt),Tv(Vt,jt),Vt.return=St,St=Vt)}return Se(St);case g:e:{for(Ea=jt.key;xt!==null;){if(xt.key===Ea)if(xt.tag===4&&xt.stateNode.containerInfo===jt.containerInfo&&xt.stateNode.implementation===jt.implementation){_(St,xt.sibling),Vt=V(xt,jt.children||[]),Vt.return=St,St=Vt;break e}else{_(St,xt);break}else v(St,xt);xt=xt.sibling}Vt=vj(jt,St.mode,Vt),Vt.return=St,St=Vt}return Se(St);case F:return jt=Af(jt),tn(St,xt,jt,Vt)}if(X(jt))return va(St,xt,jt,Vt);if(I(jt)){if(Ea=I(jt),typeof Ea!="function")throw Error(n(150));return jt=Ea.call(jt),za(St,xt,jt,Vt)}if(typeof jt.then=="function")return tn(St,xt,Rk(jt),Vt);if(jt.$$typeof===N)return tn(St,xt,Ek(St,jt),Vt);Ik(St,jt)}return typeof jt=="string"&&jt!==""||typeof jt=="number"||typeof jt=="bigint"?(jt=""+jt,xt!==null&&xt.tag===6?(_(St,xt.sibling),Vt=V(xt,jt),Vt.return=St,St=Vt):(_(St,xt),Vt=yj(jt,St.mode,Vt),Vt.return=St,St=Vt),Se(St)):_(St,xt)}return function(St,xt,jt,Vt){try{Pv=0;var Ea=tn(St,xt,jt,Vt);return Db=null,Ea}catch(Ca){if(Ca===Eb||Ca===Fk)throw Ca;var Er=dl(29,Ca,null,St.mode);return Er.lanes=Vt,Er.return=St,Er}}}var Mf=zU(!0),UU=zU(!1),kp=!1;function Ej(h){h.updateQueue={baseState:h.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Dj(h,v){h=h.updateQueue,v.updateQueue===h&&(v.updateQueue={baseState:h.baseState,firstBaseUpdate:h.firstBaseUpdate,lastBaseUpdate:h.lastBaseUpdate,shared:h.shared,callbacks:null})}function _p(h){return{lane:h,tag:0,payload:null,callback:null,next:null}}function Np(h,v,_){var D=h.updateQueue;if(D===null)return null;if(D=D.shared,(zr&2)!==0){var V=D.pending;return V===null?v.next=v:(v.next=V.next,V.next=v),D.pending=v,v=Tk(h),kU(h,null,_),v}return Pk(h,D,v,_),Tk(h)}function Av(h,v,_){if(v=v.updateQueue,v!==null&&(v=v.shared,(_&4194048)!==0)){var D=v.lanes;D&=h.pendingLanes,_|=D,v.lanes=_,Bt(h,_)}}function Fj(h,v){var _=h.updateQueue,D=h.alternate;if(D!==null&&(D=D.updateQueue,_===D)){var V=null,te=null;if(_=_.firstBaseUpdate,_!==null){do{var Se={lane:_.lane,tag:_.tag,payload:_.payload,callback:null,next:null};te===null?V=te=Se:te=te.next=Se,_=_.next}while(_!==null);te===null?V=te=v:te=te.next=v}else V=te=v;_={baseState:D.baseState,firstBaseUpdate:V,lastBaseUpdate:te,shared:D.shared,callbacks:D.callbacks},h.updateQueue=_;return}h=_.lastBaseUpdate,h===null?_.firstBaseUpdate=v:h.next=v,_.lastBaseUpdate=v}var Lj=!1;function jv(){if(Lj){var h=Mb;if(h!==null)throw h}}function Mv(h,v,_,D){Lj=!1;var V=h.updateQueue;kp=!1;var te=V.firstBaseUpdate,Se=V.lastBaseUpdate,He=V.shared.pending;if(He!==null){V.shared.pending=null;var mt=He,Et=mt.next;mt.next=null,Se===null?te=Et:Se.next=Et,Se=mt;var qt=h.alternate;qt!==null&&(qt=qt.updateQueue,He=qt.lastBaseUpdate,He!==Se&&(He===null?qt.firstBaseUpdate=Et:He.next=Et,qt.lastBaseUpdate=mt))}if(te!==null){var Wt=V.baseState;Se=0,qt=Et=mt=null,He=te;do{var Lt=He.lane&-536870913,zt=Lt!==He.lane;if(zt?(wr&Lt)===Lt:(D&Lt)===Lt){Lt!==0&&Lt===jb&&(Lj=!0),qt!==null&&(qt=qt.next={lane:0,tag:He.tag,payload:He.payload,callback:null,next:null});e:{var va=h,za=He;Lt=v;var tn=_;switch(za.tag){case 1:if(va=za.payload,typeof va=="function"){Wt=va.call(tn,Wt,Lt);break e}Wt=va;break e;case 3:va.flags=va.flags&-65537|128;case 0:if(va=za.payload,Lt=typeof va=="function"?va.call(tn,Wt,Lt):va,Lt==null)break e;Wt=m({},Wt,Lt);break e;case 2:kp=!0}}Lt=He.callback,Lt!==null&&(h.flags|=64,zt&&(h.flags|=8192),zt=V.callbacks,zt===null?V.callbacks=[Lt]:zt.push(Lt))}else zt={lane:Lt,tag:He.tag,payload:He.payload,callback:He.callback,next:null},qt===null?(Et=qt=zt,mt=Wt):qt=qt.next=zt,Se|=Lt;if(He=He.next,He===null){if(He=V.shared.pending,He===null)break;zt=He,He=zt.next,zt.next=null,V.lastBaseUpdate=zt,V.shared.pending=null}}while(!0);qt===null&&(mt=Wt),V.baseState=mt,V.firstBaseUpdate=Et,V.lastBaseUpdate=qt,te===null&&(V.shared.lanes=0),jp|=Se,h.lanes=Se,h.memoizedState=Wt}}function BU(h,v){if(typeof h!="function")throw Error(n(191,h));h.call(v)}function qU(h,v){var _=h.callbacks;if(_!==null)for(h.callbacks=null,h=0;h<_.length;h++)BU(_[h],v)}var Fb=Z(null),Ok=Z(0);function HU(h,v){h=Yu,he(Ok,h),he(Fb,v),Yu=h|v.baseLanes}function Rj(){he(Ok,Yu),he(Fb,Fb.current)}function Ij(){Yu=Ok.current,ce(Fb),ce(Ok)}var ul=Z(null),Wl=null;function Cp(h){var v=h.alternate;he(hi,hi.current&1),he(ul,h),Wl===null&&(v===null||Fb.current!==null||v.memoizedState!==null)&&(Wl=h)}function Oj(h){he(hi,hi.current),he(ul,h),Wl===null&&(Wl=h)}function $U(h){h.tag===22?(he(hi,hi.current),he(ul,h),Wl===null&&(Wl=h)):Pp()}function Pp(){he(hi,hi.current),he(ul,ul.current)}function ml(h){ce(ul),Wl===h&&(Wl=null),ce(hi)}var hi=Z(0);function zk(h){for(var v=h;v!==null;){if(v.tag===13){var _=v.memoizedState;if(_!==null&&(_=_.dehydrated,_===null||$M(_)||VM(_)))return v}else if(v.tag===19&&(v.memoizedProps.revealOrder==="forwards"||v.memoizedProps.revealOrder==="backwards"||v.memoizedProps.revealOrder==="unstable_legacy-backwards"||v.memoizedProps.revealOrder==="together")){if((v.flags&128)!==0)return v}else if(v.child!==null){v.child.return=v,v=v.child;continue}if(v===h)break;for(;v.sibling===null;){if(v.return===null||v.return===h)return null;v=v.return}v.sibling.return=v.return,v=v.sibling}return null}var qu=0,Ya=null,Jr=null,Ni=null,Uk=!1,Lb=!1,Ef=!1,Bk=0,Ev=0,Rb=null,$de=0;function Zn(){throw Error(n(321))}function zj(h,v){if(v===null)return!1;for(var _=0;_te?te:8;var Se=ee.T,He={};ee.T=He,eM(h,!1,v,_);try{var mt=V(),Et=ee.S;if(Et!==null&&Et(He,mt),mt!==null&&typeof mt=="object"&&typeof mt.then=="function"){var qt=Hde(mt,D);Fv(h,v,qt,fl(h))}else Fv(h,v,D,fl(h))}catch(Wt){Fv(h,v,{then:function(){},status:"rejected",reason:Wt},fl())}finally{Q.p=te,Se!==null&&He.types!==null&&(Se.types=He.types),ee.T=Se}}function Qde(){}function Zj(h,v,_,D){if(h.tag!==5)throw Error(n(476));var V=vB(h).queue;yB(h,V,v,le,_===null?Qde:function(){return wB(h),_(D)})}function vB(h){var v=h.memoizedState;if(v!==null)return v;v={memoizedState:le,baseState:le,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Hu,lastRenderedState:le},next:null};var _={};return v.next={memoizedState:_,baseState:_,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Hu,lastRenderedState:_},next:null},h.memoizedState=v,h=h.alternate,h!==null&&(h.memoizedState=v),v}function wB(h){var v=vB(h);v.next===null&&(v=h.alternate.memoizedState),Fv(h,v.next.queue,{},fl())}function Jj(){return Cs(Xv)}function SB(){return fi().memoizedState}function kB(){return fi().memoizedState}function Yde(h){for(var v=h.return;v!==null;){switch(v.tag){case 24:case 3:var _=fl();h=_p(_);var D=Np(v,h,_);D!==null&&(qo(D,v,_),Av(D,v,_)),v={cache:Tj()},h.payload=v;return}v=v.return}}function Xde(h,v,_){var D=fl();_={lane:D,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null},Wk(h)?NB(v,_):(_=bj(h,v,_,D),_!==null&&(qo(_,h,D),CB(_,v,D)))}function _B(h,v,_){var D=fl();Fv(h,v,_,D)}function Fv(h,v,_,D){var V={lane:D,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null};if(Wk(h))NB(v,V);else{var te=h.alternate;if(h.lanes===0&&(te===null||te.lanes===0)&&(te=v.lastRenderedReducer,te!==null))try{var Se=v.lastRenderedState,He=te(Se,_);if(V.hasEagerState=!0,V.eagerState=He,Mn(He,Se))return Pk(h,v,V,0),mn===null&&Ck(),!1}catch{}if(_=bj(h,v,V,D),_!==null)return qo(_,h,D),CB(_,v,D),!0}return!1}function eM(h,v,_,D){if(D={lane:2,revertLane:EM(),gesture:null,action:D,hasEagerState:!1,eagerState:null,next:null},Wk(h)){if(v)throw Error(n(479))}else v=bj(h,_,D,2),v!==null&&qo(v,h,2)}function Wk(h){var v=h.alternate;return h===Ya||v!==null&&v===Ya}function NB(h,v){Lb=Uk=!0;var _=h.pending;_===null?v.next=v:(v.next=_.next,_.next=v),h.pending=v}function CB(h,v,_){if((_&4194048)!==0){var D=v.lanes;D&=h.pendingLanes,_|=D,v.lanes=_,Bt(h,_)}}var Lv={readContext:Cs,use:Hk,useCallback:Zn,useContext:Zn,useEffect:Zn,useImperativeHandle:Zn,useLayoutEffect:Zn,useInsertionEffect:Zn,useMemo:Zn,useReducer:Zn,useRef:Zn,useState:Zn,useDebugValue:Zn,useDeferredValue:Zn,useTransition:Zn,useSyncExternalStore:Zn,useId:Zn,useHostTransitionStatus:Zn,useFormState:Zn,useActionState:Zn,useOptimistic:Zn,useMemoCache:Zn,useCacheRefresh:Zn};Lv.useEffectEvent=Zn;var PB={readContext:Cs,use:Hk,useCallback:function(h,v){return go().memoizedState=[h,v===void 0?null:v],h},useContext:Cs,useEffect:dB,useImperativeHandle:function(h,v,_){_=_!=null?_.concat([h]):null,Vk(4194308,4,hB.bind(null,v,h),_)},useLayoutEffect:function(h,v){return Vk(4194308,4,h,v)},useInsertionEffect:function(h,v){Vk(4,2,h,v)},useMemo:function(h,v){var _=go();v=v===void 0?null:v;var D=h();if(Ef){Re(!0);try{h()}finally{Re(!1)}}return _.memoizedState=[D,v],D},useReducer:function(h,v,_){var D=go();if(_!==void 0){var V=_(v);if(Ef){Re(!0);try{_(v)}finally{Re(!1)}}}else V=v;return D.memoizedState=D.baseState=V,h={pending:null,lanes:0,dispatch:null,lastRenderedReducer:h,lastRenderedState:V},D.queue=h,h=h.dispatch=Xde.bind(null,Ya,h),[D.memoizedState,h]},useRef:function(h){var v=go();return h={current:h},v.memoizedState=h},useState:function(h){h=Wj(h);var v=h.queue,_=_B.bind(null,Ya,v);return v.dispatch=_,[h.memoizedState,_]},useDebugValue:Yj,useDeferredValue:function(h,v){var _=go();return Xj(_,h,v)},useTransition:function(){var h=Wj(!1);return h=yB.bind(null,Ya,h.queue,!0,!1),go().memoizedState=h,[!1,h]},useSyncExternalStore:function(h,v,_){var D=Ya,V=go();if(_r){if(_===void 0)throw Error(n(407));_=_()}else{if(_=v(),mn===null)throw Error(n(349));(wr&127)!==0||KU(D,v,_)}V.memoizedState=_;var te={value:_,getSnapshot:v};return V.queue=te,dB(YU.bind(null,D,te,h),[h]),D.flags|=2048,Ib(9,{destroy:void 0},QU.bind(null,D,te,_,v),null),_},useId:function(){var h=go(),v=mn.identifierPrefix;if(_r){var _=Ad,D=Td;_=(D&~(1<<32-Ke(D)-1)).toString(32)+_,v="_"+v+"R_"+_,_=Bk++,0<_&&(v+="H"+_.toString(32)),v+="_"}else _=$de++,v="_"+v+"r_"+_.toString(32)+"_";return h.memoizedState=v},useHostTransitionStatus:Jj,useFormState:iB,useActionState:iB,useOptimistic:function(h){var v=go();v.memoizedState=v.baseState=h;var _={pending:null,lanes:0,dispatch:null,lastRenderedReducer:null,lastRenderedState:null};return v.queue=_,v=eM.bind(null,Ya,!0,_),_.dispatch=v,[h,v]},useMemoCache:$j,useCacheRefresh:function(){return go().memoizedState=Yde.bind(null,Ya)},useEffectEvent:function(h){var v=go(),_={impl:h};return v.memoizedState=_,function(){if((zr&2)!==0)throw Error(n(440));return _.impl.apply(void 0,arguments)}}},tM={readContext:Cs,use:Hk,useCallback:gB,useContext:Cs,useEffect:Qj,useImperativeHandle:fB,useInsertionEffect:mB,useLayoutEffect:pB,useMemo:bB,useReducer:$k,useRef:cB,useState:function(){return $k(Hu)},useDebugValue:Yj,useDeferredValue:function(h,v){var _=fi();return xB(_,Jr.memoizedState,h,v)},useTransition:function(){var h=$k(Hu)[0],v=fi().memoizedState;return[typeof h=="boolean"?h:Dv(h),v]},useSyncExternalStore:WU,useId:SB,useHostTransitionStatus:Jj,useFormState:sB,useActionState:sB,useOptimistic:function(h,v){var _=fi();return JU(_,Jr,h,v)},useMemoCache:$j,useCacheRefresh:kB};tM.useEffectEvent=uB;var TB={readContext:Cs,use:Hk,useCallback:gB,useContext:Cs,useEffect:Qj,useImperativeHandle:fB,useInsertionEffect:mB,useLayoutEffect:pB,useMemo:bB,useReducer:Gj,useRef:cB,useState:function(){return Gj(Hu)},useDebugValue:Yj,useDeferredValue:function(h,v){var _=fi();return Jr===null?Xj(_,h,v):xB(_,Jr.memoizedState,h,v)},useTransition:function(){var h=Gj(Hu)[0],v=fi().memoizedState;return[typeof h=="boolean"?h:Dv(h),v]},useSyncExternalStore:WU,useId:SB,useHostTransitionStatus:Jj,useFormState:lB,useActionState:lB,useOptimistic:function(h,v){var _=fi();return Jr!==null?JU(_,Jr,h,v):(_.baseState=h,[h,_.queue.dispatch])},useMemoCache:$j,useCacheRefresh:kB};TB.useEffectEvent=uB;function aM(h,v,_,D){v=h.memoizedState,_=_(D,v),_=_==null?v:m({},v,_),h.memoizedState=_,h.lanes===0&&(h.updateQueue.baseState=_)}var rM={enqueueSetState:function(h,v,_){h=h._reactInternals;var D=fl(),V=_p(D);V.payload=v,_!=null&&(V.callback=_),v=Np(h,V,D),v!==null&&(qo(v,h,D),Av(v,h,D))},enqueueReplaceState:function(h,v,_){h=h._reactInternals;var D=fl(),V=_p(D);V.tag=1,V.payload=v,_!=null&&(V.callback=_),v=Np(h,V,D),v!==null&&(qo(v,h,D),Av(v,h,D))},enqueueForceUpdate:function(h,v){h=h._reactInternals;var _=fl(),D=_p(_);D.tag=2,v!=null&&(D.callback=v),v=Np(h,D,_),v!==null&&(qo(v,h,_),Av(v,h,_))}};function AB(h,v,_,D,V,te,Se){return h=h.stateNode,typeof h.shouldComponentUpdate=="function"?h.shouldComponentUpdate(D,te,Se):v.prototype&&v.prototype.isPureReactComponent?!_s(_,D)||!_s(V,te):!0}function jB(h,v,_,D){h=v.state,typeof v.componentWillReceiveProps=="function"&&v.componentWillReceiveProps(_,D),typeof v.UNSAFE_componentWillReceiveProps=="function"&&v.UNSAFE_componentWillReceiveProps(_,D),v.state!==h&&rM.enqueueReplaceState(v,v.state,null)}function Df(h,v){var _=v;if("ref"in v){_={};for(var D in v)D!=="ref"&&(_[D]=v[D])}if(h=h.defaultProps){_===v&&(_=m({},_));for(var V in h)_[V]===void 0&&(_[V]=h[V])}return _}function MB(h){Nk(h)}function EB(h){console.error(h)}function DB(h){Nk(h)}function Kk(h,v){try{var _=h.onUncaughtError;_(v.value,{componentStack:v.stack})}catch(D){setTimeout(function(){throw D})}}function FB(h,v,_){try{var D=h.onCaughtError;D(_.value,{componentStack:_.stack,errorBoundary:v.tag===1?v.stateNode:null})}catch(V){setTimeout(function(){throw V})}}function nM(h,v,_){return _=_p(_),_.tag=3,_.payload={element:null},_.callback=function(){Kk(h,v)},_}function LB(h){return h=_p(h),h.tag=3,h}function RB(h,v,_,D){var V=_.type.getDerivedStateFromError;if(typeof V=="function"){var te=D.value;h.payload=function(){return V(te)},h.callback=function(){FB(v,_,D)}}var Se=_.stateNode;Se!==null&&typeof Se.componentDidCatch=="function"&&(h.callback=function(){FB(v,_,D),typeof V!="function"&&(Mp===null?Mp=new Set([this]):Mp.add(this));var He=D.stack;this.componentDidCatch(D.value,{componentStack:He!==null?He:""})})}function Zde(h,v,_,D,V){if(_.flags|=32768,D!==null&&typeof D=="object"&&typeof D.then=="function"){if(v=_.alternate,v!==null&&Ab(v,_,V,!0),_=ul.current,_!==null){switch(_.tag){case 31:case 13:return Wl===null?s_():_.alternate===null&&Jn===0&&(Jn=3),_.flags&=-257,_.flags|=65536,_.lanes=V,D===Lk?_.flags|=16384:(v=_.updateQueue,v===null?_.updateQueue=new Set([D]):v.add(D),AM(h,D,V)),!1;case 22:return _.flags|=65536,D===Lk?_.flags|=16384:(v=_.updateQueue,v===null?(v={transitions:null,markerInstances:null,retryQueue:new Set([D])},_.updateQueue=v):(_=v.retryQueue,_===null?v.retryQueue=new Set([D]):_.add(D)),AM(h,D,V)),!1}throw Error(n(435,_.tag))}return AM(h,D,V),s_(),!1}if(_r)return v=ul.current,v!==null?((v.flags&65536)===0&&(v.flags|=256),v.flags|=65536,v.lanes=V,D!==kj&&(h=Error(n(422),{cause:D}),_v(Hl(h,_)))):(D!==kj&&(v=Error(n(423),{cause:D}),_v(Hl(v,_))),h=h.current.alternate,h.flags|=65536,V&=-V,h.lanes|=V,D=Hl(D,_),V=nM(h.stateNode,D,V),Fj(h,V),Jn!==4&&(Jn=2)),!1;var te=Error(n(520),{cause:D});if(te=Hl(te,_),Hv===null?Hv=[te]:Hv.push(te),Jn!==4&&(Jn=2),v===null)return!0;D=Hl(D,_),_=v;do{switch(_.tag){case 3:return _.flags|=65536,h=V&-V,_.lanes|=h,h=nM(_.stateNode,D,h),Fj(_,h),!1;case 1:if(v=_.type,te=_.stateNode,(_.flags&128)===0&&(typeof v.getDerivedStateFromError=="function"||te!==null&&typeof te.componentDidCatch=="function"&&(Mp===null||!Mp.has(te))))return _.flags|=65536,V&=-V,_.lanes|=V,V=LB(V),RB(V,h,_,D),Fj(_,V),!1}_=_.return}while(_!==null);return!1}var iM=Error(n(461)),Ci=!1;function Ps(h,v,_,D){v.child=h===null?UU(v,null,_,D):Mf(v,h.child,_,D)}function IB(h,v,_,D,V){_=_.render;var te=v.ref;if("ref"in D){var Se={};for(var He in D)He!=="ref"&&(Se[He]=D[He])}else Se=D;return Pf(v),D=Uj(h,v,_,Se,te,V),He=Bj(),h!==null&&!Ci?(qj(h,v,V),$u(h,v,V)):(_r&&He&&wj(v),v.flags|=1,Ps(h,v,D,V),v.child)}function OB(h,v,_,D,V){if(h===null){var te=_.type;return typeof te=="function"&&!xj(te)&&te.defaultProps===void 0&&_.compare===null?(v.tag=15,v.type=te,zB(h,v,te,D,V)):(h=Ak(_.type,null,D,v,v.mode,V),h.ref=v.ref,h.return=v,v.child=h)}if(te=h.child,!pM(h,V)){var Se=te.memoizedProps;if(_=_.compare,_=_!==null?_:_s,_(Se,D)&&h.ref===v.ref)return $u(h,v,V)}return v.flags|=1,h=Ou(te,D),h.ref=v.ref,h.return=v,v.child=h}function zB(h,v,_,D,V){if(h!==null){var te=h.memoizedProps;if(_s(te,D)&&h.ref===v.ref)if(Ci=!1,v.pendingProps=D=te,pM(h,V))(h.flags&131072)!==0&&(Ci=!0);else return v.lanes=h.lanes,$u(h,v,V)}return sM(h,v,_,D,V)}function UB(h,v,_,D){var V=D.children,te=h!==null?h.memoizedState:null;if(h===null&&v.stateNode===null&&(v.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),D.mode==="hidden"){if((v.flags&128)!==0){if(te=te!==null?te.baseLanes|_:_,h!==null){for(D=v.child=h.child,V=0;D!==null;)V=V|D.lanes|D.childLanes,D=D.sibling;D=V&~te}else D=0,v.child=null;return BB(h,v,te,_,D)}if((_&536870912)!==0)v.memoizedState={baseLanes:0,cachePool:null},h!==null&&Dk(v,te!==null?te.cachePool:null),te!==null?HU(v,te):Rj(),$U(v);else return D=v.lanes=536870912,BB(h,v,te!==null?te.baseLanes|_:_,_,D)}else te!==null?(Dk(v,te.cachePool),HU(v,te),Pp(),v.memoizedState=null):(h!==null&&Dk(v,null),Rj(),Pp());return Ps(h,v,V,_),v.child}function Rv(h,v){return h!==null&&h.tag===22||v.stateNode!==null||(v.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),v.sibling}function BB(h,v,_,D,V){var te=jj();return te=te===null?null:{parent:_i._currentValue,pool:te},v.memoizedState={baseLanes:_,cachePool:te},h!==null&&Dk(v,null),Rj(),$U(v),h!==null&&Ab(h,v,D,!0),v.childLanes=V,null}function Qk(h,v){return v=Xk({mode:v.mode,children:v.children},h.mode),v.ref=h.ref,h.child=v,v.return=h,v}function qB(h,v,_){return Mf(v,h.child,null,_),h=Qk(v,v.pendingProps),h.flags|=2,ml(v),v.memoizedState=null,h}function Jde(h,v,_){var D=v.pendingProps,V=(v.flags&128)!==0;if(v.flags&=-129,h===null){if(_r){if(D.mode==="hidden")return h=Qk(v,D),v.lanes=536870912,Rv(null,h);if(Oj(v),(h=_n)?(h=eH(h,Gl),h=h!==null&&h.data==="&"?h:null,h!==null&&(v.memoizedState={dehydrated:h,treeContext:yp!==null?{id:Td,overflow:Ad}:null,retryLane:536870912,hydrationErrors:null},_=NU(h),_.return=v,v.child=_,Ns=v,_n=null)):h=null,h===null)throw wp(v);return v.lanes=536870912,null}return Qk(v,D)}var te=h.memoizedState;if(te!==null){var Se=te.dehydrated;if(Oj(v),V)if(v.flags&256)v.flags&=-257,v=qB(h,v,_);else if(v.memoizedState!==null)v.child=h.child,v.flags|=128,v=null;else throw Error(n(558));else if(Ci||Ab(h,v,_,!1),V=(_&h.childLanes)!==0,Ci||V){if(D=mn,D!==null&&(Se=gt(D,_),Se!==0&&Se!==te.retryLane))throw te.retryLane=Se,kf(h,Se),qo(D,h,Se),iM;s_(),v=qB(h,v,_)}else h=te.treeContext,_n=Kl(Se.nextSibling),Ns=v,_r=!0,vp=null,Gl=!1,h!==null&&TU(v,h),v=Qk(v,D),v.flags|=4096;return v}return h=Ou(h.child,{mode:D.mode,children:D.children}),h.ref=v.ref,v.child=h,h.return=v,h}function Yk(h,v){var _=v.ref;if(_===null)h!==null&&h.ref!==null&&(v.flags|=4194816);else{if(typeof _!="function"&&typeof _!="object")throw Error(n(284));(h===null||h.ref!==_)&&(v.flags|=4194816)}}function sM(h,v,_,D,V){return Pf(v),_=Uj(h,v,_,D,void 0,V),D=Bj(),h!==null&&!Ci?(qj(h,v,V),$u(h,v,V)):(_r&&D&&wj(v),v.flags|=1,Ps(h,v,_,V),v.child)}function HB(h,v,_,D,V,te){return Pf(v),v.updateQueue=null,_=GU(v,D,_,V),VU(h),D=Bj(),h!==null&&!Ci?(qj(h,v,te),$u(h,v,te)):(_r&&D&&wj(v),v.flags|=1,Ps(h,v,_,te),v.child)}function $B(h,v,_,D,V){if(Pf(v),v.stateNode===null){var te=Nb,Se=_.contextType;typeof Se=="object"&&Se!==null&&(te=Cs(Se)),te=new _(D,te),v.memoizedState=te.state!==null&&te.state!==void 0?te.state:null,te.updater=rM,v.stateNode=te,te._reactInternals=v,te=v.stateNode,te.props=D,te.state=v.memoizedState,te.refs={},Ej(v),Se=_.contextType,te.context=typeof Se=="object"&&Se!==null?Cs(Se):Nb,te.state=v.memoizedState,Se=_.getDerivedStateFromProps,typeof Se=="function"&&(aM(v,_,Se,D),te.state=v.memoizedState),typeof _.getDerivedStateFromProps=="function"||typeof te.getSnapshotBeforeUpdate=="function"||typeof te.UNSAFE_componentWillMount!="function"&&typeof te.componentWillMount!="function"||(Se=te.state,typeof te.componentWillMount=="function"&&te.componentWillMount(),typeof te.UNSAFE_componentWillMount=="function"&&te.UNSAFE_componentWillMount(),Se!==te.state&&rM.enqueueReplaceState(te,te.state,null),Mv(v,D,te,V),jv(),te.state=v.memoizedState),typeof te.componentDidMount=="function"&&(v.flags|=4194308),D=!0}else if(h===null){te=v.stateNode;var He=v.memoizedProps,mt=Df(_,He);te.props=mt;var Et=te.context,qt=_.contextType;Se=Nb,typeof qt=="object"&&qt!==null&&(Se=Cs(qt));var Wt=_.getDerivedStateFromProps;qt=typeof Wt=="function"||typeof te.getSnapshotBeforeUpdate=="function",He=v.pendingProps!==He,qt||typeof te.UNSAFE_componentWillReceiveProps!="function"&&typeof te.componentWillReceiveProps!="function"||(He||Et!==Se)&&jB(v,te,D,Se),kp=!1;var Lt=v.memoizedState;te.state=Lt,Mv(v,D,te,V),jv(),Et=v.memoizedState,He||Lt!==Et||kp?(typeof Wt=="function"&&(aM(v,_,Wt,D),Et=v.memoizedState),(mt=kp||AB(v,_,mt,D,Lt,Et,Se))?(qt||typeof te.UNSAFE_componentWillMount!="function"&&typeof te.componentWillMount!="function"||(typeof te.componentWillMount=="function"&&te.componentWillMount(),typeof te.UNSAFE_componentWillMount=="function"&&te.UNSAFE_componentWillMount()),typeof te.componentDidMount=="function"&&(v.flags|=4194308)):(typeof te.componentDidMount=="function"&&(v.flags|=4194308),v.memoizedProps=D,v.memoizedState=Et),te.props=D,te.state=Et,te.context=Se,D=mt):(typeof te.componentDidMount=="function"&&(v.flags|=4194308),D=!1)}else{te=v.stateNode,Dj(h,v),Se=v.memoizedProps,qt=Df(_,Se),te.props=qt,Wt=v.pendingProps,Lt=te.context,Et=_.contextType,mt=Nb,typeof Et=="object"&&Et!==null&&(mt=Cs(Et)),He=_.getDerivedStateFromProps,(Et=typeof He=="function"||typeof te.getSnapshotBeforeUpdate=="function")||typeof te.UNSAFE_componentWillReceiveProps!="function"&&typeof te.componentWillReceiveProps!="function"||(Se!==Wt||Lt!==mt)&&jB(v,te,D,mt),kp=!1,Lt=v.memoizedState,te.state=Lt,Mv(v,D,te,V),jv();var zt=v.memoizedState;Se!==Wt||Lt!==zt||kp||h!==null&&h.dependencies!==null&&Mk(h.dependencies)?(typeof He=="function"&&(aM(v,_,He,D),zt=v.memoizedState),(qt=kp||AB(v,_,qt,D,Lt,zt,mt)||h!==null&&h.dependencies!==null&&Mk(h.dependencies))?(Et||typeof te.UNSAFE_componentWillUpdate!="function"&&typeof te.componentWillUpdate!="function"||(typeof te.componentWillUpdate=="function"&&te.componentWillUpdate(D,zt,mt),typeof te.UNSAFE_componentWillUpdate=="function"&&te.UNSAFE_componentWillUpdate(D,zt,mt)),typeof te.componentDidUpdate=="function"&&(v.flags|=4),typeof te.getSnapshotBeforeUpdate=="function"&&(v.flags|=1024)):(typeof te.componentDidUpdate!="function"||Se===h.memoizedProps&&Lt===h.memoizedState||(v.flags|=4),typeof te.getSnapshotBeforeUpdate!="function"||Se===h.memoizedProps&&Lt===h.memoizedState||(v.flags|=1024),v.memoizedProps=D,v.memoizedState=zt),te.props=D,te.state=zt,te.context=mt,D=qt):(typeof te.componentDidUpdate!="function"||Se===h.memoizedProps&&Lt===h.memoizedState||(v.flags|=4),typeof te.getSnapshotBeforeUpdate!="function"||Se===h.memoizedProps&&Lt===h.memoizedState||(v.flags|=1024),D=!1)}return te=D,Yk(h,v),D=(v.flags&128)!==0,te||D?(te=v.stateNode,_=D&&typeof _.getDerivedStateFromError!="function"?null:te.render(),v.flags|=1,h!==null&&D?(v.child=Mf(v,h.child,null,V),v.child=Mf(v,null,_,V)):Ps(h,v,_,V),v.memoizedState=te.state,h=v.child):h=$u(h,v,V),h}function VB(h,v,_,D){return Nf(),v.flags|=256,Ps(h,v,_,D),v.child}var oM={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function lM(h){return{baseLanes:h,cachePool:FU()}}function cM(h,v,_){return h=h!==null?h.childLanes&~_:0,v&&(h|=hl),h}function GB(h,v,_){var D=v.pendingProps,V=!1,te=(v.flags&128)!==0,Se;if((Se=te)||(Se=h!==null&&h.memoizedState===null?!1:(hi.current&2)!==0),Se&&(V=!0,v.flags&=-129),Se=(v.flags&32)!==0,v.flags&=-33,h===null){if(_r){if(V?Cp(v):Pp(),(h=_n)?(h=eH(h,Gl),h=h!==null&&h.data!=="&"?h:null,h!==null&&(v.memoizedState={dehydrated:h,treeContext:yp!==null?{id:Td,overflow:Ad}:null,retryLane:536870912,hydrationErrors:null},_=NU(h),_.return=v,v.child=_,Ns=v,_n=null)):h=null,h===null)throw wp(v);return VM(h)?v.lanes=32:v.lanes=536870912,null}var He=D.children;return D=D.fallback,V?(Pp(),V=v.mode,He=Xk({mode:"hidden",children:He},V),D=_f(D,V,_,null),He.return=v,D.return=v,He.sibling=D,v.child=He,D=v.child,D.memoizedState=lM(_),D.childLanes=cM(h,Se,_),v.memoizedState=oM,Rv(null,D)):(Cp(v),dM(v,He))}var mt=h.memoizedState;if(mt!==null&&(He=mt.dehydrated,He!==null)){if(te)v.flags&256?(Cp(v),v.flags&=-257,v=uM(h,v,_)):v.memoizedState!==null?(Pp(),v.child=h.child,v.flags|=128,v=null):(Pp(),He=D.fallback,V=v.mode,D=Xk({mode:"visible",children:D.children},V),He=_f(He,V,_,null),He.flags|=2,D.return=v,He.return=v,D.sibling=He,v.child=D,Mf(v,h.child,null,_),D=v.child,D.memoizedState=lM(_),D.childLanes=cM(h,Se,_),v.memoizedState=oM,v=Rv(null,D));else if(Cp(v),VM(He)){if(Se=He.nextSibling&&He.nextSibling.dataset,Se)var Et=Se.dgst;Se=Et,D=Error(n(419)),D.stack="",D.digest=Se,_v({value:D,source:null,stack:null}),v=uM(h,v,_)}else if(Ci||Ab(h,v,_,!1),Se=(_&h.childLanes)!==0,Ci||Se){if(Se=mn,Se!==null&&(D=gt(Se,_),D!==0&&D!==mt.retryLane))throw mt.retryLane=D,kf(h,D),qo(Se,h,D),iM;$M(He)||s_(),v=uM(h,v,_)}else $M(He)?(v.flags|=192,v.child=h.child,v=null):(h=mt.treeContext,_n=Kl(He.nextSibling),Ns=v,_r=!0,vp=null,Gl=!1,h!==null&&TU(v,h),v=dM(v,D.children),v.flags|=4096);return v}return V?(Pp(),He=D.fallback,V=v.mode,mt=h.child,Et=mt.sibling,D=Ou(mt,{mode:"hidden",children:D.children}),D.subtreeFlags=mt.subtreeFlags&65011712,Et!==null?He=Ou(Et,He):(He=_f(He,V,_,null),He.flags|=2),He.return=v,D.return=v,D.sibling=He,v.child=D,Rv(null,D),D=v.child,He=h.child.memoizedState,He===null?He=lM(_):(V=He.cachePool,V!==null?(mt=_i._currentValue,V=V.parent!==mt?{parent:mt,pool:mt}:V):V=FU(),He={baseLanes:He.baseLanes|_,cachePool:V}),D.memoizedState=He,D.childLanes=cM(h,Se,_),v.memoizedState=oM,Rv(h.child,D)):(Cp(v),_=h.child,h=_.sibling,_=Ou(_,{mode:"visible",children:D.children}),_.return=v,_.sibling=null,h!==null&&(Se=v.deletions,Se===null?(v.deletions=[h],v.flags|=16):Se.push(h)),v.child=_,v.memoizedState=null,_)}function dM(h,v){return v=Xk({mode:"visible",children:v},h.mode),v.return=h,h.child=v}function Xk(h,v){return h=dl(22,h,null,v),h.lanes=0,h}function uM(h,v,_){return Mf(v,h.child,null,_),h=dM(v,v.pendingProps.children),h.flags|=2,v.memoizedState=null,h}function WB(h,v,_){h.lanes|=v;var D=h.alternate;D!==null&&(D.lanes|=v),Cj(h.return,v,_)}function mM(h,v,_,D,V,te){var Se=h.memoizedState;Se===null?h.memoizedState={isBackwards:v,rendering:null,renderingStartTime:0,last:D,tail:_,tailMode:V,treeForkCount:te}:(Se.isBackwards=v,Se.rendering=null,Se.renderingStartTime=0,Se.last=D,Se.tail=_,Se.tailMode=V,Se.treeForkCount=te)}function KB(h,v,_){var D=v.pendingProps,V=D.revealOrder,te=D.tail;D=D.children;var Se=hi.current,He=(Se&2)!==0;if(He?(Se=Se&1|2,v.flags|=128):Se&=1,he(hi,Se),Ps(h,v,D,_),D=_r?kv:0,!He&&h!==null&&(h.flags&128)!==0)e:for(h=v.child;h!==null;){if(h.tag===13)h.memoizedState!==null&&WB(h,_,v);else if(h.tag===19)WB(h,_,v);else if(h.child!==null){h.child.return=h,h=h.child;continue}if(h===v)break e;for(;h.sibling===null;){if(h.return===null||h.return===v)break e;h=h.return}h.sibling.return=h.return,h=h.sibling}switch(V){case"forwards":for(_=v.child,V=null;_!==null;)h=_.alternate,h!==null&&zk(h)===null&&(V=_),_=_.sibling;_=V,_===null?(V=v.child,v.child=null):(V=_.sibling,_.sibling=null),mM(v,!1,V,_,te,D);break;case"backwards":case"unstable_legacy-backwards":for(_=null,V=v.child,v.child=null;V!==null;){if(h=V.alternate,h!==null&&zk(h)===null){v.child=V;break}h=V.sibling,V.sibling=_,_=V,V=h}mM(v,!0,_,null,te,D);break;case"together":mM(v,!1,null,null,void 0,D);break;default:v.memoizedState=null}return v.child}function $u(h,v,_){if(h!==null&&(v.dependencies=h.dependencies),jp|=v.lanes,(_&v.childLanes)===0)if(h!==null){if(Ab(h,v,_,!1),(_&v.childLanes)===0)return null}else return null;if(h!==null&&v.child!==h.child)throw Error(n(153));if(v.child!==null){for(h=v.child,_=Ou(h,h.pendingProps),v.child=_,_.return=v;h.sibling!==null;)h=h.sibling,_=_.sibling=Ou(h,h.pendingProps),_.return=v;_.sibling=null}return v.child}function pM(h,v){return(h.lanes&v)!==0?!0:(h=h.dependencies,!!(h!==null&&Mk(h)))}function eue(h,v,_){switch(v.tag){case 3:A(v,v.stateNode.containerInfo),Sp(v,_i,h.memoizedState.cache),Nf();break;case 27:case 5:B(v);break;case 4:A(v,v.stateNode.containerInfo);break;case 10:Sp(v,v.type,v.memoizedProps.value);break;case 31:if(v.memoizedState!==null)return v.flags|=128,Oj(v),null;break;case 13:var D=v.memoizedState;if(D!==null)return D.dehydrated!==null?(Cp(v),v.flags|=128,null):(_&v.child.childLanes)!==0?GB(h,v,_):(Cp(v),h=$u(h,v,_),h!==null?h.sibling:null);Cp(v);break;case 19:var V=(h.flags&128)!==0;if(D=(_&v.childLanes)!==0,D||(Ab(h,v,_,!1),D=(_&v.childLanes)!==0),V){if(D)return KB(h,v,_);v.flags|=128}if(V=v.memoizedState,V!==null&&(V.rendering=null,V.tail=null,V.lastEffect=null),he(hi,hi.current),D)break;return null;case 22:return v.lanes=0,UB(h,v,_,v.pendingProps);case 24:Sp(v,_i,h.memoizedState.cache)}return $u(h,v,_)}function QB(h,v,_){if(h!==null)if(h.memoizedProps!==v.pendingProps)Ci=!0;else{if(!pM(h,_)&&(v.flags&128)===0)return Ci=!1,eue(h,v,_);Ci=(h.flags&131072)!==0}else Ci=!1,_r&&(v.flags&1048576)!==0&&PU(v,kv,v.index);switch(v.lanes=0,v.tag){case 16:e:{var D=v.pendingProps;if(h=Af(v.elementType),v.type=h,typeof h=="function")xj(h)?(D=Df(h,D),v.tag=1,v=$B(null,v,h,D,_)):(v.tag=0,v=sM(null,v,h,D,_));else{if(h!=null){var V=h.$$typeof;if(V===C){v.tag=11,v=IB(null,v,h,D,_);break e}else if(V===j){v.tag=14,v=OB(null,v,h,D,_);break e}}throw v=L(h)||h,Error(n(306,v,""))}}return v;case 0:return sM(h,v,v.type,v.pendingProps,_);case 1:return D=v.type,V=Df(D,v.pendingProps),$B(h,v,D,V,_);case 3:e:{if(A(v,v.stateNode.containerInfo),h===null)throw Error(n(387));D=v.pendingProps;var te=v.memoizedState;V=te.element,Dj(h,v),Mv(v,D,null,_);var Se=v.memoizedState;if(D=Se.cache,Sp(v,_i,D),D!==te.cache&&Pj(v,[_i],_,!0),jv(),D=Se.element,te.isDehydrated)if(te={element:D,isDehydrated:!1,cache:Se.cache},v.updateQueue.baseState=te,v.memoizedState=te,v.flags&256){v=VB(h,v,D,_);break e}else if(D!==V){V=Hl(Error(n(424)),v),_v(V),v=VB(h,v,D,_);break e}else for(h=v.stateNode.containerInfo,h.nodeType===9?h=h.body:h=h.nodeName==="HTML"?h.ownerDocument.body:h,_n=Kl(h.firstChild),Ns=v,_r=!0,vp=null,Gl=!0,_=UU(v,null,D,_),v.child=_;_;)_.flags=_.flags&-3|4096,_=_.sibling;else{if(Nf(),D===V){v=$u(h,v,_);break e}Ps(h,v,D,_)}v=v.child}return v;case 26:return Yk(h,v),h===null?(_=sH(v.type,null,v.pendingProps,null))?v.memoizedState=_:_r||(_=v.type,h=v.pendingProps,D=p_(H.current).createElement(_),D[_t]=v,D[Ct]=h,Ts(D,_,h),it(D),v.stateNode=D):v.memoizedState=sH(v.type,h.memoizedProps,v.pendingProps,h.memoizedState),null;case 27:return B(v),h===null&&_r&&(D=v.stateNode=rH(v.type,v.pendingProps,H.current),Ns=v,Gl=!0,V=_n,Lp(v.type)?(GM=V,_n=Kl(D.firstChild)):_n=V),Ps(h,v,v.pendingProps.children,_),Yk(h,v),h===null&&(v.flags|=4194304),v.child;case 5:return h===null&&_r&&((V=D=_n)&&(D=jue(D,v.type,v.pendingProps,Gl),D!==null?(v.stateNode=D,Ns=v,_n=Kl(D.firstChild),Gl=!1,V=!0):V=!1),V||wp(v)),B(v),V=v.type,te=v.pendingProps,Se=h!==null?h.memoizedProps:null,D=te.children,BM(V,te)?D=null:Se!==null&&BM(V,Se)&&(v.flags|=32),v.memoizedState!==null&&(V=Uj(h,v,Vde,null,null,_),Xv._currentValue=V),Yk(h,v),Ps(h,v,D,_),v.child;case 6:return h===null&&_r&&((h=_=_n)&&(_=Mue(_,v.pendingProps,Gl),_!==null?(v.stateNode=_,Ns=v,_n=null,h=!0):h=!1),h||wp(v)),null;case 13:return GB(h,v,_);case 4:return A(v,v.stateNode.containerInfo),D=v.pendingProps,h===null?v.child=Mf(v,null,D,_):Ps(h,v,D,_),v.child;case 11:return IB(h,v,v.type,v.pendingProps,_);case 7:return Ps(h,v,v.pendingProps,_),v.child;case 8:return Ps(h,v,v.pendingProps.children,_),v.child;case 12:return Ps(h,v,v.pendingProps.children,_),v.child;case 10:return D=v.pendingProps,Sp(v,v.type,D.value),Ps(h,v,D.children,_),v.child;case 9:return V=v.type._context,D=v.pendingProps.children,Pf(v),V=Cs(V),D=D(V),v.flags|=1,Ps(h,v,D,_),v.child;case 14:return OB(h,v,v.type,v.pendingProps,_);case 15:return zB(h,v,v.type,v.pendingProps,_);case 19:return KB(h,v,_);case 31:return Jde(h,v,_);case 22:return UB(h,v,_,v.pendingProps);case 24:return Pf(v),D=Cs(_i),h===null?(V=jj(),V===null&&(V=mn,te=Tj(),V.pooledCache=te,te.refCount++,te!==null&&(V.pooledCacheLanes|=_),V=te),v.memoizedState={parent:D,cache:V},Ej(v),Sp(v,_i,V)):((h.lanes&_)!==0&&(Dj(h,v),Mv(v,null,null,_),jv()),V=h.memoizedState,te=v.memoizedState,V.parent!==D?(V={parent:D,cache:D},v.memoizedState=V,v.lanes===0&&(v.memoizedState=v.updateQueue.baseState=V),Sp(v,_i,D)):(D=te.cache,Sp(v,_i,D),D!==V.cache&&Pj(v,[_i],_,!0))),Ps(h,v,v.pendingProps.children,_),v.child;case 29:throw v.pendingProps}throw Error(n(156,v.tag))}function Vu(h){h.flags|=4}function hM(h,v,_,D,V){if((v=(h.mode&32)!==0)&&(v=!1),v){if(h.flags|=16777216,(V&335544128)===V)if(h.stateNode.complete)h.flags|=8192;else if(Sq())h.flags|=8192;else throw jf=Lk,Mj}else h.flags&=-16777217}function YB(h,v){if(v.type!=="stylesheet"||(v.state.loading&4)!==0)h.flags&=-16777217;else if(h.flags|=16777216,!uH(v))if(Sq())h.flags|=8192;else throw jf=Lk,Mj}function Zk(h,v){v!==null&&(h.flags|=4),h.flags&16384&&(v=h.tag!==22?We():536870912,h.lanes|=v,Bb|=v)}function Iv(h,v){if(!_r)switch(h.tailMode){case"hidden":v=h.tail;for(var _=null;v!==null;)v.alternate!==null&&(_=v),v=v.sibling;_===null?h.tail=null:_.sibling=null;break;case"collapsed":_=h.tail;for(var D=null;_!==null;)_.alternate!==null&&(D=_),_=_.sibling;D===null?v||h.tail===null?h.tail=null:h.tail.sibling=null:D.sibling=null}}function Nn(h){var v=h.alternate!==null&&h.alternate.child===h.child,_=0,D=0;if(v)for(var V=h.child;V!==null;)_|=V.lanes|V.childLanes,D|=V.subtreeFlags&65011712,D|=V.flags&65011712,V.return=h,V=V.sibling;else for(V=h.child;V!==null;)_|=V.lanes|V.childLanes,D|=V.subtreeFlags,D|=V.flags,V.return=h,V=V.sibling;return h.subtreeFlags|=D,h.childLanes=_,v}function tue(h,v,_){var D=v.pendingProps;switch(Sj(v),v.tag){case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Nn(v),null;case 1:return Nn(v),null;case 3:return _=v.stateNode,D=null,h!==null&&(D=h.memoizedState.cache),v.memoizedState.cache!==D&&(v.flags|=2048),Bu(_i),E(),_.pendingContext&&(_.context=_.pendingContext,_.pendingContext=null),(h===null||h.child===null)&&(Tb(v)?Vu(v):h===null||h.memoizedState.isDehydrated&&(v.flags&256)===0||(v.flags|=1024,_j())),Nn(v),null;case 26:var V=v.type,te=v.memoizedState;return h===null?(Vu(v),te!==null?(Nn(v),YB(v,te)):(Nn(v),hM(v,V,null,D,_))):te?te!==h.memoizedState?(Vu(v),Nn(v),YB(v,te)):(Nn(v),v.flags&=-16777217):(h=h.memoizedProps,h!==D&&Vu(v),Nn(v),hM(v,V,h,D,_)),null;case 27:if(W(v),_=H.current,V=v.type,h!==null&&v.stateNode!=null)h.memoizedProps!==D&&Vu(v);else{if(!D){if(v.stateNode===null)throw Error(n(166));return Nn(v),null}h=ue.current,Tb(v)?AU(v):(h=rH(V,D,_),v.stateNode=h,Vu(v))}return Nn(v),null;case 5:if(W(v),V=v.type,h!==null&&v.stateNode!=null)h.memoizedProps!==D&&Vu(v);else{if(!D){if(v.stateNode===null)throw Error(n(166));return Nn(v),null}if(te=ue.current,Tb(v))AU(v);else{var Se=p_(H.current);switch(te){case 1:te=Se.createElementNS("http://www.w3.org/2000/svg",V);break;case 2:te=Se.createElementNS("http://www.w3.org/1998/Math/MathML",V);break;default:switch(V){case"svg":te=Se.createElementNS("http://www.w3.org/2000/svg",V);break;case"math":te=Se.createElementNS("http://www.w3.org/1998/Math/MathML",V);break;case"script":te=Se.createElement("div"),te.innerHTML="