From 3b9633a178fa8ce89b8aa3acac84aa99dd05c0c0 Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 24 May 2026 10:16:30 +0200 Subject: [PATCH] =?UTF-8?q?=E2=97=8F=20feat(support):=20include=20sanitize?= =?UTF-8?q?d=20connection=20/=20VP=20/=20log-health=20diagnostics=20in=20s?= =?UTF-8?q?upport=20bundle=20and=20bug=20report=20(#1506=20follow-up)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three diagnostic surfaces shipped earlier this month (6bc6a1d6 VP setup diagnostic, e222a0ef log-health scanner, ed31b8f4 connection diagnostic in the bug-report bubble) were only ever shown to the *user*. A bug report arriving in the maintainer's inbox carried raw logs but no diagnostic results — the user-visible "your X1C can't reach MQTT" finding never made it into the issue body, so the maintainer had to ask the user to re-run and paste. New `services/diagnostic_snapshot.collect_diagnostic_snapshot` runs all three concurrently with a per-probe 15 s wall-clock cap (so total ≈ max(per-cap), not sum — fleet size doesn't matter) and is fail-soft per probe: a crash inside one printer's check emits `{"printer_id": N, "error": "..."}` for that entry rather than nuking the whole snapshot. The snapshot is then added as a `diagnostics` top-level key by `_collect_support_info()`, so both flows (POST /support/bundle and POST /bug-report/submit via `support_info=...`) pick it up without their own changes. Private-data sanitization ------------------------- The diagnostic schemas embed raw IPv4 in five field shapes that must not land in a submitted GitHub issue or a shared support ZIP: - PrinterDiagnosticResult.ip_address (top-level) - DiagnosticCheck.params.printer_ip (network-mode check) - DiagnosticCheck.params.host_ip (network-mode check) - VPDiagnosticResult per-check params.bind_ip (VP setup) - IPs embedded in log-health sample lines The first two carry the printer's own IP (already in the existing `collect_sensitive_strings` table via the Printer rows); host_ip and bind_ip are NOT in the DB so a sensitive_strings-only pass missed them. Fix: `_sanitize_recursive` walks the full snapshot tree, masks DB-known values with the same `[PRINTER]/[IP]/[SERIAL]/[ACCESS_CODE]` labels the log sanitizer applies (via the shared `collect_sensitive_strings`), then an IPv4-regex pass catches any IP the DB didn't cover — most importantly the Bambuddy host IP returned by `_get_host_ip()` and the VP `bind_ip` the user picked at setup. Recursive walk so arbitrary nested dicts/lists don't slip through future schema additions. Live-DB smoke test against the dev fleet: zero raw IPv4 instances in the serialized snapshot output; all five field shapes plus the embedded log samples render as `[IP]`. Progress indicators ------------------- The bubble's "submitting" view and the System page's Download button now render a static four-line checklist showing what's running (printer connectivity → VP setup → log scan → submit / build ZIP). Static, not faked phase progress — we can't actually track server-side phases without SSE and the honest "here's what's happening" list communicates the longer wait without lying about percentage complete. 9 new i18n keys, real translations in all 9 locales (no English fallback). parity script clean at 4993 leaves per locale. Tests: 6 new in test_diagnostic_snapshot.py - empty-input shape stable (the three top-level keys always present) - per-printer / per-VP result coverage (lists match input lengths) - fail-soft on a single-probe crash (other entries + log-health still complete) - timed_out marker when a probe exceeds the per-probe cap (test patches the cap to 0.05 s) - end-to-end IP sanitization across all five field shapes plus log-sample IPs, with a final JSON-serialize-and-regex sweep asserting zero raw IPv4 escapes anywhere in the result - concurrent execution proof (4 × 0.2 s probes complete in < 0.5 s; sequential would be 0.8 s) --- CHANGELOG.md | 1 + backend/app/api/routes/support.py | 16 + backend/app/services/diagnostic_snapshot.py | 207 +++++++++++++ .../tests/unit/test_diagnostic_snapshot.py | 291 ++++++++++++++++++ frontend/src/components/BugReportBubble.tsx | 17 +- 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/SystemInfoPage.tsx | 23 +- .../{index-8qalZ11b.js => index-C-YgIh3u.js} | 24 +- ...{index-HqxudbTf.css => index-y4woBlMv.css} | 2 +- static/index.html | 4 +- 18 files changed, 648 insertions(+), 18 deletions(-) create mode 100644 backend/app/services/diagnostic_snapshot.py create mode 100644 backend/tests/unit/test_diagnostic_snapshot.py rename static/assets/{index-8qalZ11b.js => index-C-YgIh3u.js} (78%) rename static/assets/{index-HqxudbTf.css => index-y4woBlMv.css} (50%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5139b11b6..8786c580a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,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 +- **Support bundle + bug-report submission now include the live diagnostic snapshot** — Three diagnostics (Connection Diagnostic per printer, Virtual Printer Setup Diagnostic per enabled VP, Log Health Scanner) have shipped on the System page and inline in the bug-report bubble since 6bc6a1d6 / e222a0ef / ed31b8f4, but the results were only ever shown to the *user* — never persisted into the downloadable support ZIP or the submitted GitHub issue. A report saying "looks broken in Bambuddy" arrived with no actionable signal beyond raw logs. **Fix**: new `services/diagnostic_snapshot.collect_diagnostic_snapshot` runs all three concurrently with an outer per-probe 15 s wall-clock cap (so a hung interface adds at most ~15 s to bundle generation regardless of fleet size — `asyncio.gather`, total ≈ max(per-cap) not sum). Fail-soft per probe: a crash inside one printer's check emits `{"printer_id": N, "error": "..."}` for that entry rather than nuking the whole snapshot — partial result beats a 500. Wired into `_collect_support_info()` so both flows (`POST /support/bundle` and `POST /bug-report/submit` via `support_info=...`) pick up the new `diagnostics` top-level key without their own changes. **Private-data sanitization** — the diagnostic schemas embed raw IPv4 in three places (`PrinterDiagnosticResult.ip_address`, network-mode check's `params.{printer_ip, host_ip}`, VP diagnostic's `params.bind_ip`), and the snapshot adds printer names. None of those should leak. The snapshot now runs a recursive sanitizer on the full result tree before returning: known DB-listed values (printer name, IP, serial, access code) get the same `[PRINTER]/[IP]/[SERIAL]/[ACCESS_CODE]` labels the log sanitizer already applies (via the shared `collect_sensitive_strings`), and an IPv4-regex fallback catches IPs the DB doesn't know about — most importantly the Bambuddy host IP returned by `_get_host_ip()` and any VP `bind_ip` the user picked at setup. Live-DB smoke test confirms zero raw IPv4 instances in the serialized snapshot output. **Progress indicators**: the bubble's "submitting" view and the System page's Download button now render a static four-line checklist showing what's running (printer connectivity → VP setup → log scan → submit/build ZIP) — communicates the longer wait honestly without faking server-side phase progress we can't actually track. **Tests**: 6 new in `test_diagnostic_snapshot.py` — empty-input shape stable, per-printer / per-VP result coverage, fail-soft on a single-probe crash, `timed_out` marker when a probe exceeds the per-probe cap (test patches the cap to 0.05 s), end-to-end IP sanitization across all five field shapes (top-level `ip_address`, `printer_ip`, `host_ip`, `bind_ip`, plus IPs embedded in log-health sample lines) with a final regex sweep over the JSON-serialized result asserting zero raw IPv4 escapes, concurrent execution proof (4 × 0.2 s probes complete in < 0.5 s, would be 0.8 s sequential). Existing 27 BugReportBubble + SystemInfoPage frontend tests still pass; 9-locale i18n parity check clean (4993 leaves per locale, 9 new keys added with real translations everywhere — no English fallback). Backend ruff clean. - **"Prefer Lowest Remaining Filament" now uses Bambuddy's inventory weight, not just the printer's RFID counter (#1508, reported by @kleinwareio)** — Reporter has an inventory spool cloned to slot 1 and the original (much further used) in slot 4 of the same P1S AMS, with the preference enabled, and the dispatch consistently picked slot 1 (the fresh clone) instead of slot 4 (the original they wanted to finish first). Root cause is the `prefer_lowest` sort in `_match_filaments_to_slots` (`print_scheduler.py`): the sort key reads `f.get("remain", -1)` straight out of `_build_loaded_filaments`, which sources it from MQTT AMS `tray.remain` — the printer firmware's own RFID-decremented value. Two problems with that signal: (a) it's only populated for Bambu RFID spools, so every non-RFID / 3rd-party / user-loaded tray reports `-1` and gets clamped to a sentinel — multiple non-RFID spools then tie in the sort and Python's stable sort collapses to AMS-slot insertion order, so slot 1 always wins; (b) even when set, it's the *printer's* counter, not Bambuddy's `label_weight - weight_used` (internal mode) or Spoolman's `remaining_weight` (Spoolman mode) — the two diverge any time the user re-spools, swaps cardboard, or runs a print outside Bambuddy. The reporter is on internal-inventory mode with non-RFID spools — both failure modes apply, hence slot 1 every time. **Fix**: when a slot is bound to a Bambuddy / Spoolman spool, that inventory record's remaining weight becomes the sort signal. New async helper `_build_inventory_remain_overrides(db, printer_id, loaded)` returns `{global_tray_id: remaining_grams}` for slots with an assignment — internal mode joins `SpoolAssignment` → `Spool` once per dispatch, Spoolman mode joins `SpoolmanSlotAssignment` then fetches each spool through the existing `_spoolman_remaining_grams` (shared with `filament_deficit.py`, parity rule per [[feedback_inventory_modes_parity]]). The new `_prefer_lowest_sort_key` consumes that map alongside the legacy MQTT field with a **two-tier** comparison: inventory-tracked spools always sort BEFORE MQTT-only spools, then ascending by remaining within each tier, then ascending by `ams_id * 4 + tray_id` as the deterministic slot tie-breaker. The tier flag dominates so we never compare grams (inventory) against percent (MQTT) — no unit-conversion contortions. MQTT-only behaviour is preserved exactly: `remain = -1` still maps to the 101 sentinel and slot order still decides on ties, so users who haven't bound any spools see no change. External / VT tray slots are skipped (tracked separately from AMS bindings). Lookup runs only when `prefer_lowest_filament` is enabled — no extra DB hit for users who don't use the preference. **Tests**: 6 new in `TestPreferLowestInventoryOverride` in `test_scheduler_ams_mapping.py` (inventory override beats MQTT remain — the literal reporter scenario with 950 g clone vs 50 g original; zero-grams still sorts first within its tier; inventory tier beats MQTT tier regardless of value; tied inventory grams break to lower slot; no-override falls through to MQTT — regression guard for un-tracked spools; legacy `remain = -1` still sentinel-sorts last when override map is None) + 7 new in `test_scheduler_inventory_remain.py` covering `_build_inventory_remain_overrides` directly (internal mode returns label_weight − weight_used per bound slot; external slots skipped; empty loaded short-circuits; over-consumed spool clamps to 0 g; unbound slots absent from map; Spoolman mode uses `_spoolman_remaining_grams` for parity; Spoolman unreachability silently omits that slot). 102 scheduler + inventory tests green; backend ruff clean. - **X1/H2/P2 live camera no longer fails with `Address already in use` on transitional ffmpeg builds (#1504, reported by @rage03usa, confirmed by @PawseHaxor)** — On a native Ubuntu install with the Jammy-era system ffmpeg, the RTSP live-view path retried indefinitely with `Unable to open RTSP for listening … Address already in use`. Snapshots, the camera diagnostic, and OrcaSlicer all kept working — only live view was broken. Cause: the ffmpeg argv built in `backend/app/api/routes/camera.py` (added in 530a7a46 as part of an RTSP-stability bundle) passed `-timeout 30000000`. That ffmpeg version *deprecated* the original `-timeout` (socket I/O microseconds) and repurposed the name to mean the *RTSP listen-mode incoming-connection timeout* — any non-zero value **implies `-listen`**. ffmpeg then flipped into RTSP server mode and tried to bind the same localhost port Bambuddy's TLS proxy was already listening on, hence EADDRINUSE on every retry (the odd-port pattern @PawseHaxor noticed is coincidence — the ephemeral allocator just picked odd values that run). The reporter's own workaround (drop the option) works but silently loses the socket-level read timeout, so a hung TLS handshake would block past the OS TCP timeout instead of failing fast into the existing reconnect loop. **Why this can't be a one-line literal swap**: ffmpeg has shipped *three* arrangements of this option over time and Bambuddy supports the full range. Pre-deprecation builds: `-timeout` is the socket I/O timeout. Transitional builds (~late-4.x, what the reporter is on): `-timeout` is the broken listen-mode option, `-stimeout` is the replacement. **Modern ffmpeg (5.x / 6.x / 7.x — current Debian 13, Ubuntu 24.04, current Homebrew)**: `-stimeout` was removed entirely and `-timeout` is back to socket I/O. So both literals regress one half of the install base. **Fix**: a new `rtsp_socket_timeout_flag()` helper in `backend/app/services/camera.py` probes `ffmpeg -h demuxer=rtsp` once at first use and picks `-stimeout` when ffmpeg advertises it (transitional window) or `-timeout` otherwise (modern + very old). The result is cached for the process lifetime — ffmpeg won't swap mid-run. The function returns the option name without a leading dash so callers prepend it themselves (no empty-flag formatting bug). Wired into both RTSP ffmpeg call sites — `routes/camera.py` (printer camera) and `services/external_camera.py` (external RTSP) — in lockstep, same TLS-proxy + ffmpeg pattern, same regression. The reporter had tried `-listen_timeout` (doesn't help — we *don't* want listen mode) and `-rw_timeout` (AVIO-level, RTSP demuxer doesn't honour it on its control socket), but no manual swap could be correct for both transitional and modern installs simultaneously. **Tests**: 8 in `test_ffmpeg_rtsp_timeout_flag.py` — 6 unit tests for the probe (picks `-stimeout` when advertised, falls back to `-timeout` on modern, defaults to `-timeout` when ffmpeg missing or probe raises, caches across calls, substring-match guard against false-positives on `-listen_timeout`), 2 parametrised regression guards against either RTSP ffmpeg argv re-hard-coding a literal flag instead of consuming the probe. 37 (probe + existing external-camera) tests green; backend ruff clean. - **SliceModal: process / filament dropdowns now filter by nozzle diameter too, not just printer model (#1325 follow-up #2, reported by @IndividualGhost1905)** — With the @BBL name fallback in place, the reporter saw that an X2D 0.4 selection still mixed 0.2 / 0.6 / 0.8 nozzle process variants into the main list. The fallback's regex stripped any trailing ` nozzle` suffix from both sides before comparing, so `"Bambu Lab X2D 0.4 nozzle"` and `"0.40mm Strength @BBL X2D 0.8 nozzle"` both reduced to `"X2D"` and matched. The bundle path was already nozzle-correct (a `.bbscfg` is scoped to one printer-preset-name including its nozzle, so the bundle-side exact-match was nozzle-aware); only the name fallback needed fixing. **Fix**: `extractPrinterPresetModel` and `extractBblToken` now each return `{ model, nozzle }`. The nozzle is the parsed string ("0.4" / "0.6" / etc.) or `null` when the name has no suffix. `classifyByBambuName` treats a `null` process nozzle as `"0.4"` — Bambu's convention is to omit the suffix on 0.4 (the default) and include it for 0.2 / 0.6 / 0.8, exactly as the reporter described. Both `model` and `nozzle` must compare equal for a `'match'`; differing nozzles fall into the existing "Other printers" group, no new group label needed. If the selected printer preset name has no parseable nozzle (non-Bambu / hand-typed), the matcher degrades to model-only — Bambu printer presets always include nozzle in practice, so this is defensive. **Tests**: 9 new in `slicerPrinterMatch.test.ts` covering the matrix (0.4 printer vs no-suffix / 0.6 / 0.8 process; 0.6 printer vs 0.6 / no-suffix-=-0.4; explicit 0.4-suffix-on-process still matches 0.4 printer; same rule on filament presets; wrong-model dominates over matching-nozzle; no-nozzle printer name degrades to model-only); one existing test reframed (the case that previously asserted a 0.6-nozzle process matched a 0.4 printer — the exact bug — now asserts mismatch). 46 slicerPrinterMatch + 34 SliceModal tests green; frontend build clean. diff --git a/backend/app/api/routes/support.py b/backend/app/api/routes/support.py index 419f0d422..f5ea76a66 100644 --- a/backend/app/api/routes/support.py +++ b/backend/app/api/routes/support.py @@ -1099,6 +1099,22 @@ async def _collect_support_info() -> dict: except Exception: logger.debug("Failed to collect WebSocket info", exc_info=True) + # Active diagnostics — per-printer connection check, per-VP setup check, + # and the log-health scan. These all surface in the UI today (System page + + # bug-report bubble) but were never persisted into what the maintainer + # receives, so a "looks broken in bambuddy" report arrived with no + # actionable signal beyond raw logs. The snapshot helper is fail-soft per + # probe and bounded by a per-probe wall-clock cap, so a hung interface + # adds at most ~15 s to bundle generation regardless of fleet size (probes + # run concurrently). + try: + from backend.app.services.diagnostic_snapshot import collect_diagnostic_snapshot + + async with async_session() as db: + info["diagnostics"] = await collect_diagnostic_snapshot(db) + except Exception: + logger.warning("Failed to collect diagnostic snapshot", exc_info=True) + return info diff --git a/backend/app/services/diagnostic_snapshot.py b/backend/app/services/diagnostic_snapshot.py new file mode 100644 index 000000000..6e65bce08 --- /dev/null +++ b/backend/app/services/diagnostic_snapshot.py @@ -0,0 +1,207 @@ +"""Aggregate connection, virtual-printer, and log-health diagnostics into a +single snapshot for the support bundle and bug-report submission paths. + +Each user-triggered support artifact (the System-page support ZIP and the +bug-report bubble) already exposed these three checks inline in the UI but +omitted them from what landed in the maintainer's hands. This module is the +single entry point both flows call to capture all three at once. + +Designed around three constraints: + +- **Fail-soft per probe.** A crash inside one printer's check must not nuke the + whole snapshot — that's the whole point of including diagnostics in the + bundle: a partial result is more useful than a 500. +- **Bounded total runtime.** Each probe runs concurrently and is guarded by an + outer wall-clock cap; timeouts emit a marker entry rather than blocking. +- **No mutation.** Connection / VP diagnostics only probe TCP ports and read + state; log-health is a passive scanner. Safe to run on every bundle. +""" + +from __future__ import annotations + +import asyncio +import logging +import re +from typing import Any + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +logger = logging.getLogger(__name__) + +# Mirrors the IPv4 pattern in services.log_reader.sanitize_log_content. Kept as +# a literal here (not imported) so a refactor of that module's internals can't +# silently change snapshot sanitization. Skips firmware-version-shaped strings +# (leading-zero octets like "01.09.01.00") via the [1-9]\d|\d alternations. +_IPV4_RE = re.compile(r"\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\b") + +# Per-diagnostic wall-clock cap. Each underlying probe carries its own (smaller) +# TCP / HTTP timeouts; this is the outer guard so a hung interface or a wedged +# subprocess can't stall bundle generation past about this many seconds per +# printer/VP. Snapshot total runtime is bounded by max(per-cap) thanks to the +# concurrent gather, not the sum. +_PER_DIAGNOSTIC_TIMEOUT_SECONDS = 15.0 + + +def _serialize(result: Any) -> Any: + """Convert a Pydantic model to a dict; pass through plain dicts/lists.""" + if hasattr(result, "model_dump"): + return result.model_dump() + return result + + +async def _run_connection_for(printer) -> dict: + from backend.app.services.printer_diagnostic import run_connection_diagnostic + + base = {"printer_id": printer.id, "printer_name": printer.name} + try: + result = await asyncio.wait_for( + run_connection_diagnostic( + printer.ip_address, + printer=printer, + serial_number=printer.serial_number, + access_code=printer.access_code, + ), + timeout=_PER_DIAGNOSTIC_TIMEOUT_SECONDS, + ) + return {**base, "result": _serialize(result)} + except asyncio.TimeoutError: + return {**base, "error": "timed_out"} + except Exception as e: + # Log with traceback so the bundle generation isn't silent about + # a broken probe, but never propagate. + logger.warning("Connection diagnostic failed for printer %s: %s", printer.id, e, exc_info=True) + return {**base, "error": str(e)} + + +async def _run_vp_for(vp) -> dict: + from backend.app.services.virtual_printer import virtual_printer_manager + from backend.app.services.virtual_printer.diagnostic import run_vp_diagnostic + + base = {"vp_id": vp.id, "name": vp.name} + try: + instance = virtual_printer_manager.get_instance(vp.id) + result = await asyncio.wait_for( + run_vp_diagnostic(vp, instance), + timeout=_PER_DIAGNOSTIC_TIMEOUT_SECONDS, + ) + return {**base, "result": _serialize(result)} + except asyncio.TimeoutError: + return {**base, "error": "timed_out"} + except Exception as e: + logger.warning("VP diagnostic failed for VP %s: %s", vp.id, e, exc_info=True) + return {**base, "error": str(e)} + + +async def _run_log_health() -> Any: + from backend.app.services.log_health import scan_logs + + try: + # scan_logs is sync I/O-bound (file read + regex); push off the loop. + result = await asyncio.wait_for( + asyncio.to_thread(scan_logs), + timeout=_PER_DIAGNOSTIC_TIMEOUT_SECONDS, + ) + return _serialize(result) + except asyncio.TimeoutError: + return {"error": "timed_out"} + except Exception as e: + logger.warning("Log-health scan failed: %s", e, exc_info=True) + return {"error": str(e)} + + +async def collect_diagnostic_snapshot(db: AsyncSession) -> dict[str, Any]: + """Return the three-section diagnostic snapshot. + + Always returns a dict with keys ``connection_diagnostics`` (list, one entry + per active printer), ``vp_diagnostics`` (list, one entry per enabled VP — + empty if none), and ``log_health`` (the ``scan_logs`` result or an error + marker). Each list entry carries either ``result`` (success) or ``error`` + (timeout / exception) so the maintainer can tell at a glance whether a + given probe ran. + """ + from backend.app.models.printer import Printer + from backend.app.models.virtual_printer import VirtualPrinter + + printers_result = await db.execute(select(Printer).where(Printer.is_active.is_(True))) + printers = list(printers_result.scalars().all()) + + vps_result = await db.execute(select(VirtualPrinter).where(VirtualPrinter.enabled.is_(True))) + vps = list(vps_result.scalars().all()) + + # Concurrent: total wall-clock ≈ max(per-cap), not sum. + results = await asyncio.gather( + asyncio.gather(*(_run_connection_for(p) for p in printers)) if printers else _noop_list(), + asyncio.gather(*(_run_vp_for(vp) for vp in vps)) if vps else _noop_list(), + _run_log_health(), + return_exceptions=True, + ) + connection_results, vp_results, log_health = results + + def _coerce_list(r) -> list: + if isinstance(r, BaseException): + logger.warning("Diagnostic snapshot batch failed: %s", r) + return [] + return list(r) if r is not None else [] + + snapshot = { + "connection_diagnostics": _coerce_list(connection_results), + "vp_diagnostics": _coerce_list(vp_results), + "log_health": log_health if not isinstance(log_health, BaseException) else {"error": str(log_health)}, + } + + # Sanitize before returning. The diagnostic schemas embed printer/host IPs + # (`PrinterDiagnosticResult.ip_address`, network-mode check params, VP + # `bind_ip`) and the snapshot adds printer names — none of which should + # leak into a submitted GitHub issue or a shared support ZIP. Use the + # same `collect_sensitive_strings` table the log sanitizer already + # consults so the replacement labels stay consistent ([PRINTER], [SERIAL], + # [IP], [ACCESS_CODE]); the IPv4 regex fallback in `_mask_string` then + # catches host / bind IPs that aren't in the DB. + try: + from backend.app.services.log_reader import collect_sensitive_strings + + sensitive_strings = await collect_sensitive_strings(db) + except Exception: + logger.warning("Could not collect sensitive strings for snapshot sanitization", exc_info=True) + sensitive_strings = {} + return _sanitize_recursive(snapshot, sensitive_strings) + + +async def _noop_list() -> list: + return [] + + +def _mask_string(value: str, sensitive_strings: dict[str, str]) -> str: + """Apply known-value replacement + IPv4 regex masking to a single string. + + Known values are matched first (longest first so "My Printer 1" beats + "My Printer"); the regex pass then catches any IPs the sensitive_strings + table didn't already cover — most importantly the Bambuddy host's own + IP (returned by ``_get_host_ip`` inside the diagnostic, not in the DB) + and any virtual-printer ``bind_ip`` the user picked at setup. + """ + if not value: + return value + for raw, label in sorted(sensitive_strings.items(), key=lambda x: len(x[0]), reverse=True): + if len(raw) < 3: + continue + if raw in value: + value = value.replace(raw, label) + value = _IPV4_RE.sub("[IP]", value) + return value + + +def _sanitize_recursive(node: Any, sensitive_strings: dict[str, str]) -> Any: + """Walk the snapshot and redact strings in place — dicts, lists, scalars. + + Non-string scalars (ints, bools, None) pass through; we only need to + mask user-visible values. Keys are NOT renamed (those are structural). + """ + if isinstance(node, str): + return _mask_string(node, sensitive_strings) + if isinstance(node, dict): + return {k: _sanitize_recursive(v, sensitive_strings) for k, v in node.items()} + if isinstance(node, list): + return [_sanitize_recursive(item, sensitive_strings) for item in node] + return node diff --git a/backend/tests/unit/test_diagnostic_snapshot.py b/backend/tests/unit/test_diagnostic_snapshot.py new file mode 100644 index 000000000..b81311afd --- /dev/null +++ b/backend/tests/unit/test_diagnostic_snapshot.py @@ -0,0 +1,291 @@ +"""Tests for the diagnostic snapshot helper that aggregates connection, +virtual-printer, and log-health diagnostics for the support bundle and +bug-report submission paths (#1506 follow-up). + +The helper has three hard requirements: + +- Always returns the three top-level keys, even when sections are empty. +- Fail-soft per probe — a single crash doesn't break the snapshot. +- Bounded total runtime — concurrent gather caps wall-clock to the slowest probe. +""" + +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from backend.app.services.diagnostic_snapshot import collect_diagnostic_snapshot + + +def _make_db_with_printers_and_vps(printers: list, vps: list): + """Stub AsyncSession whose two .execute() calls return printers then VPs.""" + printers_result = MagicMock() + printers_result.scalars.return_value.all.return_value = printers + vps_result = MagicMock() + vps_result.scalars.return_value.all.return_value = vps + db = MagicMock() + # Two execute calls — printer query, then VP query (order matches the + # helper). side_effect cycles through the queue. + db.execute = AsyncMock(side_effect=[printers_result, vps_result]) + return db + + +@pytest.mark.asyncio +async def test_snapshot_always_returns_three_top_level_keys_when_empty(): + """No printers, no VPs — still get the three keys (empty lists, empty + log-health). Callers downstream rely on the shape being stable.""" + db = _make_db_with_printers_and_vps([], []) + with patch( + "backend.app.services.diagnostic_snapshot._run_log_health", + new=AsyncMock(return_value={"findings": []}), + ): + out = await collect_diagnostic_snapshot(db) + assert set(out.keys()) == {"connection_diagnostics", "vp_diagnostics", "log_health"} + assert out["connection_diagnostics"] == [] + assert out["vp_diagnostics"] == [] + assert out["log_health"] == {"findings": []} + + +@pytest.mark.asyncio +async def test_snapshot_runs_diagnostic_per_active_printer(): + """Each active printer gets a connection check; each enabled VP gets a + setup check. Result list length matches the input lists.""" + printers = [ + SimpleNamespace(id=1, name="P1S", ip_address="192.168.1.10", serial_number="01S00A", access_code="abc123"), + SimpleNamespace(id=2, name="X1C", ip_address="192.168.1.11", serial_number="C11Y00", access_code="xyz456"), + ] + vps = [SimpleNamespace(id=10, name="VP-1")] + db = _make_db_with_printers_and_vps(printers, vps) + + fake_conn = SimpleNamespace(model_dump=lambda: {"checks": []}) + fake_vp = SimpleNamespace(model_dump=lambda: {"checks": []}) + + with ( + patch( + "backend.app.services.printer_diagnostic.run_connection_diagnostic", + new=AsyncMock(return_value=fake_conn), + ), + patch( + "backend.app.services.virtual_printer.virtual_printer_manager.get_instance", + return_value=None, + ), + patch( + "backend.app.services.virtual_printer.diagnostic.run_vp_diagnostic", + new=AsyncMock(return_value=fake_vp), + ), + patch( + "backend.app.services.diagnostic_snapshot._run_log_health", + new=AsyncMock(return_value={"findings": []}), + ), + ): + out = await collect_diagnostic_snapshot(db) + + assert len(out["connection_diagnostics"]) == 2 + assert out["connection_diagnostics"][0]["printer_id"] == 1 + assert out["connection_diagnostics"][1]["printer_id"] == 2 + assert all("result" in entry for entry in out["connection_diagnostics"]) + assert len(out["vp_diagnostics"]) == 1 + assert out["vp_diagnostics"][0]["vp_id"] == 10 + assert "result" in out["vp_diagnostics"][0] + + +@pytest.mark.asyncio +async def test_snapshot_fails_soft_when_single_printer_diagnostic_raises(): + """A crash inside one printer's diagnostic emits an error marker for that + printer, but the snapshot's other sections still complete. This is the + whole point of including diagnostics in the bundle — a partial result + beats a 500.""" + printers = [ + SimpleNamespace(id=1, name="ok", ip_address="1.1.1.1", serial_number="s1", access_code="a"), + SimpleNamespace(id=2, name="bad", ip_address="2.2.2.2", serial_number="s2", access_code="b"), + ] + db = _make_db_with_printers_and_vps(printers, []) + + fake_ok = SimpleNamespace(model_dump=lambda: {"status": "ok"}) + + async def diag(ip_address, **_): + if ip_address == "2.2.2.2": + raise RuntimeError("simulated crash") + return fake_ok + + with ( + patch("backend.app.services.printer_diagnostic.run_connection_diagnostic", new=AsyncMock(side_effect=diag)), + patch( + "backend.app.services.diagnostic_snapshot._run_log_health", + new=AsyncMock(return_value={"findings": []}), + ), + ): + out = await collect_diagnostic_snapshot(db) + + # Both printers represented; the crashing one carries an `error` field. + assert len(out["connection_diagnostics"]) == 2 + ok_entry = next(e for e in out["connection_diagnostics"] if e["printer_id"] == 1) + bad_entry = next(e for e in out["connection_diagnostics"] if e["printer_id"] == 2) + assert "result" in ok_entry + assert "error" not in ok_entry + assert "error" in bad_entry + assert "simulated crash" in bad_entry["error"] + # Log-health still completes despite the per-printer crash. + assert out["log_health"] == {"findings": []} + + +@pytest.mark.asyncio +async def test_snapshot_emits_timed_out_marker_when_probe_exceeds_cap(): + """If a single probe stalls past the per-diagnostic timeout, the entry + is marked `timed_out` rather than blocking the whole snapshot. Patch + the timeout small so the test runs fast.""" + printers = [SimpleNamespace(id=1, name="slow", ip_address="1.1.1.1", serial_number="s", access_code="a")] + db = _make_db_with_printers_and_vps(printers, []) + + async def slow_diag(*a, **k): + import asyncio + + await asyncio.sleep(5) # well past the patched cap below + return SimpleNamespace(model_dump=lambda: {}) + + with ( + patch( + "backend.app.services.printer_diagnostic.run_connection_diagnostic", new=AsyncMock(side_effect=slow_diag) + ), + patch("backend.app.services.diagnostic_snapshot._PER_DIAGNOSTIC_TIMEOUT_SECONDS", 0.05), + patch( + "backend.app.services.diagnostic_snapshot._run_log_health", + new=AsyncMock(return_value={"findings": []}), + ), + ): + out = await collect_diagnostic_snapshot(db) + + assert len(out["connection_diagnostics"]) == 1 + assert out["connection_diagnostics"][0]["error"] == "timed_out" + + +@pytest.mark.asyncio +async def test_snapshot_masks_ip_addresses_in_all_diagnostic_fields(): + """The diagnostic schemas embed raw IPv4 in three places — the top-level + ``PrinterDiagnosticResult.ip_address``, the network-mode check's + ``params.{printer_ip, host_ip}``, and the VP diagnostic's + ``params.bind_ip``. None of those should leak into the submitted + snapshot. Sanitization runs after the per-probe gather; both DB-known + IPs (covered by sensitive_strings → "[IP]") and host / VP-bind IPs + (caught by the IPv4 regex fallback) end up redacted. + """ + printers = [ + SimpleNamespace( + id=1, name="Workshop", ip_address="192.168.255.131", serial_number="01S00ABC123", access_code="abcd1234" + ) + ] + vps = [SimpleNamespace(id=10, name="VP-Workshop")] + db = _make_db_with_printers_and_vps(printers, vps) + + fake_conn = SimpleNamespace( + model_dump=lambda: { + "ip_address": "192.168.255.131", + "overall": "ok", + "checks": [ + { + "id": "network_mode", + "status": "warn", + "params": {"printer_ip": "192.168.255.131", "host_ip": "192.168.255.16"}, + } + ], + } + ) + fake_vp = SimpleNamespace( + model_dump=lambda: { + "overall": "ok", + "checks": [{"id": "bind_interface", "status": "pass", "params": {"bind_ip": "192.168.254.2"}}], + } + ) + + with ( + patch( + "backend.app.services.log_reader.collect_sensitive_strings", + new=AsyncMock( + return_value={ + "Workshop": "[PRINTER]", + "192.168.255.131": "[IP]", + "01S00ABC123": "[SERIAL]", + "abcd1234": "[ACCESS_CODE]", + } + ), + ), + patch( + "backend.app.services.printer_diagnostic.run_connection_diagnostic", new=AsyncMock(return_value=fake_conn) + ), + patch("backend.app.services.virtual_printer.virtual_printer_manager.get_instance", return_value=None), + patch("backend.app.services.virtual_printer.diagnostic.run_vp_diagnostic", new=AsyncMock(return_value=fake_vp)), + patch( + "backend.app.services.diagnostic_snapshot._run_log_health", + new=AsyncMock(return_value={"findings": [{"sample": "Connecting to 10.0.0.5..."}]}), + ), + ): + out = await collect_diagnostic_snapshot(db) + + # Conection diagnostic — top-level ip_address and check params both masked. + conn_entry = out["connection_diagnostics"][0] + assert conn_entry["printer_name"] == "[PRINTER]" + assert conn_entry["result"]["ip_address"] == "[IP]" + check_params = conn_entry["result"]["checks"][0]["params"] + assert check_params["printer_ip"] == "[IP]" + assert check_params["host_ip"] == "[IP]" # not in DB; caught by regex fallback + + # VP diagnostic — bind_ip masked (regex fallback; never in DB). + vp_entry = out["vp_diagnostics"][0] + assert vp_entry["result"]["checks"][0]["params"]["bind_ip"] == "[IP]" + + # Log-health findings — IPs in log samples also masked (regex applies + # recursively through the dict, not just to known fields). + assert "10.0.0.5" not in str(out["log_health"]) + assert "[IP]" in out["log_health"]["findings"][0]["sample"] + + # Sanity: no raw IPv4 anywhere in the serialized snapshot. + import json + import re as _re + + serialized = json.dumps(out) + raw_ipv4 = _re.search( + r"\b(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}\b", serialized + ) + assert raw_ipv4 is None, f"raw IPv4 leaked into snapshot: {raw_ipv4.group()}" + + +@pytest.mark.asyncio +async def test_snapshot_runs_probes_concurrently_not_sequentially(): + """Total wall-clock for N printers should be O(slowest), not O(sum) — + this is what makes the feature usable on a fleet. Set each probe to + take 0.2 s; with 4 printers, sequential is 0.8 s, concurrent is 0.2 s. + Allow margin for scheduling and the test still catches a regression + to sequential execution. + """ + import time + + printers = [ + SimpleNamespace(id=i, name=f"P{i}", ip_address=f"1.1.1.{i}", serial_number=f"s{i}", access_code="a") + for i in range(4) + ] + db = _make_db_with_printers_and_vps(printers, []) + + async def slow_diag(*a, **k): + import asyncio + + await asyncio.sleep(0.2) + return SimpleNamespace(model_dump=lambda: {"ok": True}) + + with ( + patch( + "backend.app.services.printer_diagnostic.run_connection_diagnostic", new=AsyncMock(side_effect=slow_diag) + ), + patch( + "backend.app.services.diagnostic_snapshot._run_log_health", + new=AsyncMock(return_value={"findings": []}), + ), + ): + start = time.monotonic() + out = await collect_diagnostic_snapshot(db) + elapsed = time.monotonic() - start + + assert len(out["connection_diagnostics"]) == 4 + # Concurrent should be ~0.2 s; sequential would be ~0.8 s. Use 0.5 s + # as the threshold — slack enough for slow CI, tight enough to catch + # a regression to sequential execution. + assert elapsed < 0.5, f"snapshot ran sequentially: {elapsed:.2f}s for 4 x 0.2s probes" diff --git a/frontend/src/components/BugReportBubble.tsx b/frontend/src/components/BugReportBubble.tsx index 4b80bfeff..ebc3379eb 100644 --- a/frontend/src/components/BugReportBubble.tsx +++ b/frontend/src/components/BugReportBubble.tsx @@ -487,11 +487,24 @@ export function BugReportBubble() { )} {(viewState === 'stopping' || viewState === 'submitting') && ( -
+
-

+

{viewState === 'stopping' ? t('bugReport.stoppingLogs') : t('bugReport.submitting')}

+ {viewState === 'submitting' && ( + // Diagnostics are run server-side inside the submit call + // (#1506 follow-up): the bubble already displays current + // results inline, but the submitted report now also + // includes a snapshot. Wait is bounded but noticeable — + // list what's running so the user knows why. +
    +
  • {t('bugReport.submittingStepConnection')}
  • +
  • {t('bugReport.submittingStepVirtualPrinters')}
  • +
  • {t('bugReport.submittingStepLogScan')}
  • +
  • {t('bugReport.submittingStepSubmit')}
  • +
+ )}
)} diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 4393c0312..65dc08254 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -3054,6 +3054,11 @@ export default { collectItem10: 'Python-Paketversionen', collectItem11: 'Datenbankzustandsprüfungen', collectItem12: 'Docker-Umgebungsdetails', + bundleGenerating: 'Bundle wird erstellt...', + bundleStepConnection: 'Drucker-Verbindungsprüfungen werden ausgeführt', + bundleStepVirtualPrinters: 'Setup-Prüfungen für virtuelle Drucker werden ausgeführt', + bundleStepLogScan: 'Aktuelle Protokolle werden auf bekannte Probleme überprüft', + bundleStepBuild: 'Support-Bundle-ZIP wird erstellt', }, // File manager @@ -5680,6 +5685,10 @@ export default { maxDuration: 'Stoppt automatisch nach {{minutes}} Min.', stoppingLogs: 'Protokolle sammeln & senden...', submitting: 'Fehlerbericht wird gesendet...', + submittingStepConnection: 'Drucker-Verbindungsprüfungen werden ausgeführt', + submittingStepVirtualPrinters: 'Setup-Prüfungen für virtuelle Drucker werden ausgeführt', + submittingStepLogScan: 'Aktuelle Protokolle werden auf bekannte Probleme überprüft', + submittingStepSubmit: 'Bericht wird an GitHub gesendet', submitSuccess: 'Fehlerbericht erfolgreich gesendet!', submitFailed: 'Fehlerbericht konnte nicht gesendet werden', diagnosticChecking: 'Druckerverbindungen werden geprüft...', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 55420fb74..7c92fcc41 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -3057,6 +3057,11 @@ export default { collectItem10: 'Python package versions', collectItem11: 'Database health checks', collectItem12: 'Docker environment details', + bundleGenerating: 'Generating bundle...', + bundleStepConnection: 'Running printer connectivity checks', + bundleStepVirtualPrinters: 'Running virtual-printer setup checks', + bundleStepLogScan: 'Scanning recent logs for known issues', + bundleStepBuild: 'Building the support bundle ZIP', }, // File manager @@ -5691,6 +5696,10 @@ export default { maxDuration: 'Auto-stops after {{minutes}} min', stoppingLogs: 'Collecting logs & submitting...', submitting: 'Submitting bug report...', + submittingStepConnection: 'Running printer connectivity checks', + submittingStepVirtualPrinters: 'Running virtual-printer setup checks', + submittingStepLogScan: 'Scanning recent logs for known issues', + submittingStepSubmit: 'Submitting report to GitHub', submitSuccess: 'Bug report submitted successfully!', submitFailed: 'Failed to submit bug report', diagnosticChecking: 'Checking printer connections...', diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts index 02f5752f4..9e77d6805 100644 --- a/frontend/src/i18n/locales/es.ts +++ b/frontend/src/i18n/locales/es.ts @@ -3057,6 +3057,11 @@ export default { collectItem10: 'Versiones de los paquetes de Python', collectItem11: 'Comprobaciones de estado de la base de datos', collectItem12: 'Detalles del entorno de Docker', + bundleGenerating: 'Generando paquete...', + bundleStepConnection: 'Ejecutando comprobaciones de conectividad de impresoras', + bundleStepVirtualPrinters: 'Ejecutando comprobaciones de configuración de impresoras virtuales', + bundleStepLogScan: 'Analizando los registros recientes en busca de problemas conocidos', + bundleStepBuild: 'Creando el archivo ZIP del paquete de soporte', }, // File manager @@ -5689,6 +5694,10 @@ export default { maxDuration: 'Se detiene automáticamente tras {{minutes}} min', stoppingLogs: 'Recopilando registros y enviando...', submitting: 'Enviando el informe de error...', + submittingStepConnection: 'Ejecutando comprobaciones de conectividad de impresoras', + submittingStepVirtualPrinters: 'Ejecutando comprobaciones de configuración de impresoras virtuales', + submittingStepLogScan: 'Analizando los registros recientes en busca de problemas conocidos', + submittingStepSubmit: 'Enviando el informe a GitHub', submitSuccess: '¡Informe de error enviado correctamente!', submitFailed: 'Error al enviar el informe de error', diagnosticChecking: 'Comprobando las conexiones de las impresoras...', diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index eacbb5ceb..c1598ddc4 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -3043,6 +3043,11 @@ export default { collectItem10: 'Versions packages Python', collectItem11: 'Santé base de données', collectItem12: 'Détails environnement Docker', + bundleGenerating: 'Génération du paquet...', + bundleStepConnection: 'Vérification de la connectivité des imprimantes', + bundleStepVirtualPrinters: 'Vérification de la configuration des imprimantes virtuelles', + bundleStepLogScan: 'Analyse des journaux récents pour les problèmes connus', + bundleStepBuild: 'Création du ZIP du paquet de support', }, // File manager @@ -5670,6 +5675,10 @@ export default { maxDuration: 'Arrêt auto après {{minutes}} min', stoppingLogs: 'Collecte des journaux & envoi...', submitting: 'Envoi du rapport de bug...', + submittingStepConnection: 'Vérification de la connectivité des imprimantes', + submittingStepVirtualPrinters: 'Vérification de la configuration des imprimantes virtuelles', + submittingStepLogScan: 'Analyse des journaux récents pour les problèmes connus', + submittingStepSubmit: 'Envoi du rapport vers GitHub', submitSuccess: 'Rapport de bug envoyé avec succès !', submitFailed: 'Échec de l\'envoi du rapport de bug', diagnosticChecking: 'Vérification des connexions des imprimantes...', diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index ae322b354..db09bf2d9 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -3042,6 +3042,11 @@ export default { collectItem10: 'Versioni dei pacchetti Python', collectItem11: 'Controlli di integrità del database', collectItem12: 'Dettagli dell\'ambiente Docker', + bundleGenerating: 'Generazione del pacchetto...', + bundleStepConnection: 'Controlli di connettività delle stampanti in corso', + bundleStepVirtualPrinters: 'Controlli di configurazione delle stampanti virtuali in corso', + bundleStepLogScan: 'Analisi dei log recenti per problemi noti', + bundleStepBuild: 'Creazione dello ZIP del pacchetto di supporto', }, // File manager @@ -5669,6 +5674,10 @@ export default { maxDuration: 'Arresto automatico dopo {{minutes}} min', stoppingLogs: 'Raccolta log & invio...', submitting: 'Invio segnalazione bug...', + submittingStepConnection: 'Controlli di connettività delle stampanti in corso', + submittingStepVirtualPrinters: 'Controlli di configurazione delle stampanti virtuali in corso', + submittingStepLogScan: 'Analisi dei log recenti per problemi noti', + submittingStepSubmit: 'Invio del report a GitHub', submitSuccess: 'Segnalazione bug inviata con successo!', submitFailed: 'Impossibile inviare la segnalazione bug', diagnosticChecking: 'Verifica delle connessioni delle stampanti...', diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 7735f815c..136219249 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -3054,6 +3054,11 @@ export default { collectItem10: 'Pythonパッケージバージョン', collectItem11: 'データベース健全性チェック', collectItem12: 'Docker環境の詳細', + bundleGenerating: 'バンドルを生成中...', + bundleStepConnection: 'プリンターの接続確認を実行中', + bundleStepVirtualPrinters: '仮想プリンターのセットアップ確認を実行中', + bundleStepLogScan: '最近のログから既知の問題をスキャン中', + bundleStepBuild: 'サポートバンドル ZIP を作成中', }, // File manager @@ -5681,6 +5686,10 @@ export default { maxDuration: '{{minutes}}分後に自動停止', stoppingLogs: 'ログ収集・送信中...', submitting: 'バグレポートを送信中...', + submittingStepConnection: 'プリンターの接続確認を実行中', + submittingStepVirtualPrinters: '仮想プリンターのセットアップ確認を実行中', + submittingStepLogScan: '最近のログから既知の問題をスキャン中', + submittingStepSubmit: 'GitHub にレポートを送信中', submitSuccess: 'バグレポートが正常に送信されました!', submitFailed: 'バグレポートの送信に失敗しました', diagnosticChecking: 'プリンター接続を確認中...', diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index a1f684ccb..fd4493d5f 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/frontend/src/i18n/locales/pt-BR.ts @@ -3042,6 +3042,11 @@ export default { collectItem10: 'Versões de pacotes Python', collectItem11: 'Verificações de integridade do banco de dados', collectItem12: 'Detalhes do ambiente Docker', + bundleGenerating: 'Gerando pacote...', + bundleStepConnection: 'Executando verificações de conectividade das impressoras', + bundleStepVirtualPrinters: 'Executando verificações de configuração das impressoras virtuais', + bundleStepLogScan: 'Analisando registros recentes em busca de problemas conhecidos', + bundleStepBuild: 'Criando o ZIP do pacote de suporte', }, // File manager @@ -5669,6 +5674,10 @@ export default { maxDuration: 'Para automaticamente após {{minutes}} min', stoppingLogs: 'Coletando logs & enviando...', submitting: 'Enviando relatório de bug...', + submittingStepConnection: 'Executando verificações de conectividade das impressoras', + submittingStepVirtualPrinters: 'Executando verificações de configuração das impressoras virtuais', + submittingStepLogScan: 'Analisando registros recentes em busca de problemas conhecidos', + submittingStepSubmit: 'Enviando relatório para o GitHub', submitSuccess: 'Relatório de bug enviado com sucesso!', submitFailed: 'Falha ao enviar relatório de bug', diagnosticChecking: 'Verificando as conexões das impressoras...', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index c65a215d9..5bcf466d2 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -3042,6 +3042,11 @@ export default { collectItem10: 'Python 包版本', collectItem11: '数据库健康检查', collectItem12: 'Docker 环境详情', + bundleGenerating: '正在生成包...', + bundleStepConnection: '正在执行打印机连接性检查', + bundleStepVirtualPrinters: '正在执行虚拟打印机设置检查', + bundleStepLogScan: '正在扫描最近的日志以查找已知问题', + bundleStepBuild: '正在构建支持包 ZIP 文件', }, // File manager @@ -5668,6 +5673,10 @@ export default { maxDuration: '{{minutes}}分钟后自动停止', stoppingLogs: '正在收集日志并提交...', submitting: '正在提交错误报告...', + submittingStepConnection: '正在执行打印机连接性检查', + submittingStepVirtualPrinters: '正在执行虚拟打印机设置检查', + submittingStepLogScan: '正在扫描最近的日志以查找已知问题', + submittingStepSubmit: '正在将报告提交至 GitHub', submitSuccess: '错误报告提交成功!', submitFailed: '提交错误报告失败', diagnosticChecking: '正在检查打印机连接...', diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts index 76936deea..0ca288d12 100644 --- a/frontend/src/i18n/locales/zh-TW.ts +++ b/frontend/src/i18n/locales/zh-TW.ts @@ -3042,6 +3042,11 @@ export default { collectItem10: 'Python 套件版本', collectItem11: '資料庫健康檢查', collectItem12: 'Docker 環境詳情', + bundleGenerating: '正在產生套件...', + bundleStepConnection: '正在執行印表機連線檢查', + bundleStepVirtualPrinters: '正在執行虛擬印表機設定檢查', + bundleStepLogScan: '正在掃描最近的日誌以尋找已知問題', + bundleStepBuild: '正在建立支援套件 ZIP 檔案', }, // File manager @@ -5668,6 +5673,10 @@ export default { maxDuration: '{{minutes}} 分鐘後自動停止', stoppingLogs: '正在收集日誌並提交...', submitting: '正在提交錯誤報告...', + submittingStepConnection: '正在執行印表機連線檢查', + submittingStepVirtualPrinters: '正在執行虛擬印表機設定檢查', + submittingStepLogScan: '正在掃描最近的日誌以尋找已知問題', + submittingStepSubmit: '正在將報告提交至 GitHub', submitSuccess: '錯誤報告提交成功!', submitFailed: '提交錯誤報告失敗', diagnosticChecking: '正在檢查印表機連線...', diff --git a/frontend/src/pages/SystemInfoPage.tsx b/frontend/src/pages/SystemInfoPage.tsx index 147588f22..bd224a25d 100644 --- a/frontend/src/pages/SystemInfoPage.tsx +++ b/frontend/src/pages/SystemInfoPage.tsx @@ -303,10 +303,31 @@ export function SystemInfoPage() { title={!debugLoggingState?.enabled ? t('support.enableDebugFirst', 'Enable debug logging first') : undefined} > {bundleDownloading && } - {t('common.download', 'Download')} + {bundleDownloading + ? t('support.bundleGenerating', 'Generating...') + : t('common.download', 'Download')}
+ {/* Progress indicator — bundle generation now runs connection + + virtual-printer diagnostics and the log-health scan before + writing the ZIP (#1506 follow-up), so the wait is longer than + a pure file-export. List what's running so it's not opaque. */} + {bundleDownloading && ( +
+

+ + {t('support.bundleGenerating', 'Generating...')} +

+
    +
  • {t('support.bundleStepConnection', 'Running printer connectivity checks')}
  • +
  • {t('support.bundleStepVirtualPrinters', 'Running virtual-printer setup checks')}
  • +
  • {t('support.bundleStepLogScan', 'Scanning recent logs for known issues')}
  • +
  • {t('support.bundleStepBuild', 'Building the support bundle ZIP')}
  • +
+
+ )} + {/* Error message */} {bundleError && (
diff --git a/static/assets/index-8qalZ11b.js b/static/assets/index-C-YgIh3u.js similarity index 78% rename from static/assets/index-8qalZ11b.js rename to static/assets/index-C-YgIh3u.js index 7aebe2b65..869d99093 100644 --- a/static/assets/index-8qalZ11b.js +++ b/static/assets/index-C-YgIh3u.js @@ -6,36 +6,36 @@ function nme(t,e){for(var a=0;a")&&(qt=qt.replace("",h.displayName)),qt}while(1<=D&&0<=G);break}}}finally{V=!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 ie(h){try{var v="",_=null;do v+=Y(h,_),_=h,h=h.return;while(h);return v}catch(D){return` Error generating stack: `+D.message+` `+D.stack}}var se=Object.prototype.hasOwnProperty,pe=t.unstable_scheduleCallback,ce=t.unstable_cancelCallback,he=t.unstable_shouldYield,$=t.unstable_requestPaint,L=t.unstable_now,ae=t.unstable_getCurrentPriorityLevel,ge=t.unstable_ImmediatePriority,xe=t.unstable_UserBlockingPriority,Se=t.unstable_NormalPriority,Te=t.unstable_LowPriority,ve=t.unstable_IdlePriority,we=t.log,ze=t.unstable_setDisableYieldValue,De=null,Le=null;function Re(h){if(typeof we=="function"&&ze(h),Le&&typeof Le.setStrictMode=="function")try{Le.setStrictMode(De,h)}catch{}}var We=Math.clz32?Math.clz32:Ee,Be=Math.log,ot=Math.LN2;function Ee(h){return h>>>=0,h===0?32:31-(Be(h)/ot|0)|0}var it=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 Ae(h,v,_){var D=h.pendingLanes;if(D===0)return 0;var G=0,te=h.suspendedLanes,ke=h.pingedLanes;h=h.warmLanes;var He=D&134217727;return He!==0?(D=He&~te,D!==0?G=Ye(D):(ke&=He,ke!==0?G=Ye(ke):_||(_=He&~h,_!==0&&(G=Ye(_))))):(He=D&~te,He!==0?G=Ye(He):ke!==0?G=Ye(ke):_||(_=D&~h,_!==0&&(G=Ye(_)))),G===0?0:v!==0&&v!==G&&(v&te)===0&&(te=G&-G,_=v&-v,te>=_||te===32&&(_&4194048)!==0)?v:G}function Ie(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 Ke(){var h=Ze;return Ze<<=1,(Ze&62914560)===0&&(Ze=4194304),h}function Pe(h){for(var v=[],_=0;31>_;_++)v.push(h);return v}function at(h,v){h.pendingLanes|=v,v!==268435456&&(h.suspendedLanes=0,h.pingedLanes=0,h.warmLanes=0)}function rt(h,v,_,D,G,te){var ke=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(_=ke&~_;0<_;){var qt=31-We(_),Wt=1<"u")return null;try{return h.activeElement||h.body}catch{return h.body}}var In=/[\n"\\]/g;function $a(h){return h.replace(In,function(v){return"\\"+v.charCodeAt(0).toString(16)+" "})}function Br(h,v,_,D,G,te,ke,He){h.name="",ke!=null&&typeof ke!="function"&&typeof ke!="symbol"&&typeof ke!="boolean"?h.type=ke:h.removeAttribute("type"),v!=null?ke==="number"?(v===0&&h.value===""||h.value!=v)&&(h.value=""+ca(v)):h.value!==""+ca(v)&&(h.value=""+ca(v)):ke!=="submit"&&ke!=="reset"||h.removeAttribute("value"),v!=null?Vn(h,ke,ca(v)):_!=null?Vn(h,ke,ca(_)):D!=null&&h.removeAttribute("value"),G==null&&te!=null&&(h.defaultChecked=!!te),G!=null&&(h.checked=G&&typeof G!="function"&&typeof G!="symbol"),He!=null&&typeof He!="function"&&typeof He!="symbol"&&typeof He!="boolean"?h.name=""+ca(He):h.removeAttribute("name")}function Ra(h,v,_,D,G,te,ke,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??G,D=typeof D!="function"&&typeof D!="symbol"&&!!D,h.checked=He?h.checked:!!D,h.defaultChecked=!!D,ke!=null&&typeof ke!="function"&&typeof ke!="symbol"&&typeof ke!="boolean"&&(h.name=ke),pa(h)}function Vn(h,v,_){v==="number"&&tn(h.ownerDocument)===h||h.defaultValue===""+_||(h.defaultValue=""+_)}function br(h,v,_,D){if(h=h.options,v){v={};for(var G=0;G<_.length;G++)v["$"+_[G]]=!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 Kr=null,kd=null,Rc=null;function wi(){if(Rc)return Rc;var h,v=kd,_=v.length,D,G="value"in Kr?Kr.value:Kr.textContent,te=G.length;for(h=0;h<_&&v[h]===G[h];h++);var ke=_-h;for(D=1;D<=ke&&v[_-D]===G[te-D];D++);return Rc=G.slice(h,1=Qr),ye=" ",Ut=!1;function sa(h,v){switch(h){case"keyup":return wf.indexOf(v.keyCode)!==-1;case"keydown":return v.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Ta(h){return h=h.detail,typeof h=="object"&&"data"in h?h.data:null}var Ia=!1;function kr(h,v){switch(h){case"compositionend":return Ta(v);case"keypress":return v.which!==32?null:(Ut=!0,ye);case"textInput":return h=v.data,h===ye&&Ut?null:h;default:return null}}function Si(h,v){if(Ia)return h==="compositionend"||!cl&&sa(h,v)?(h=wi(),Rc=kd=Kr=null,Ia=!1,h):null;switch(h){case"paste":return null;case"keypress":if(!(v.ctrlKey||v.altKey||v.metaKey)||v.ctrlKey&&v.altKey){if(v.char&&1=v)return{node:_,offset:v-h};h=D}e:{for(;_;){if(_.nextSibling){_=_.nextSibling;break e}_=_.parentNode}_=void 0}_=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=tn(h.document);v instanceof h.HTMLIFrameElement;){try{var _=typeof v.contentWindow.location.href=="string"}catch{_=!1}if(_)h=v.contentWindow;else break;v=tn(h.document)}return v}function 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 Pd=vi&&"documentMode"in document&&11>=document.documentMode,pi=null,mn=null,jn=null,Xr=!1;function xp(h,v,_){var D=_.window===_?_.document:_.nodeType===9?_:_.ownerDocument;Xr||pi==null||pi!==tn(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}),jn&&_s(jn,D)||(jn=D,D=m_(mn,"onSelect"),0>=ke,G-=ke,Ad=1<<32-We(v)+G|_<Ja?(kn=Ca,Ca=null):kn=Ca.sibling;var Fn=Lt(St,Ca,jt[Ja],Vt);if(Fn===null){Ca===null&&(Ca=kn);break}h&&Ca&&Fn.alternate===null&&v(St,Ca),xt=te(Fn,xt,Ja),Dn===null?Ea=Fn:Dn.sibling=Fn,Dn=Fn,Ca=kn}if(Ja===jt.length)return _(St,Ca),Nn&&zu(St,Ja),Ea;if(Ca===null){for(;JaJa?(kn=Ca,Ca=null):kn=Ca.sibling;var Up=Lt(St,Ca,Fn.value,Vt);if(Up===null){Ca===null&&(Ca=kn);break}h&&Ca&&Up.alternate===null&&v(St,Ca),xt=te(Up,xt,Ja),Dn===null?Ea=Up:Dn.sibling=Up,Dn=Up,Ca=kn}if(Fn.done)return _(St,Ca),Nn&&zu(St,Ja),Ea;if(Ca===null){for(;!Fn.done;Ja++,Fn=jt.next())Fn=Wt(St,Fn.value,Vt),Fn!==null&&(xt=te(Fn,xt,Ja),Dn===null?Ea=Fn:Dn.sibling=Fn,Dn=Fn);return Nn&&zu(St,Ja),Ea}for(Ca=D(Ca);!Fn.done;Ja++,Fn=jt.next())Fn=zt(Ca,St,Ja,Fn.value,Vt),Fn!==null&&(h&&Fn.alternate!==null&&Ca.delete(Fn.key===null?Ja:Fn.key),xt=te(Fn,xt,Ja),Dn===null?Ea=Fn:Dn.sibling=Fn,Dn=Fn);return h&&Ca.forEach(function(ame){return v(St,ame)}),Nn&&zu(St,Ja),Ea}function ar(St,xt,jt,Vt){if(typeof jt=="object"&&jt!==null&&jt.type===w&&jt.key===null&&(jt=jt.props.children),typeof jt=="object"&&jt!==null){switch(jt.$$typeof){case f:e:{for(var Ea=jt.key;xt!==null;){if(xt.key===Ea){if(Ea=jt.type,Ea===w){if(xt.tag===7){_(St,xt.sibling),Vt=G(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=G(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 ke(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=G(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 ke(St);case F:return jt=Af(jt),ar(St,xt,jt,Vt)}if(X(jt))return wa(St,xt,jt,Vt);if(I(jt)){if(Ea=I(jt),typeof Ea!="function")throw Error(r(150));return jt=Ea.call(jt),za(St,xt,jt,Vt)}if(typeof jt.then=="function")return ar(St,xt,Rk(jt),Vt);if(jt.$$typeof===N)return ar(St,xt,Ek(St,jt),Vt);Ik(St,jt)}return typeof jt=="string"&&jt!==""||typeof jt=="number"||typeof jt=="bigint"?(jt=""+jt,xt!==null&&xt.tag===6?(_(St,xt.sibling),Vt=G(xt,jt),Vt.return=St,St=Vt):(_(St,xt),Vt=yj(jt,St.mode,Vt),Vt.return=St,St=Vt),ke(St)):_(St,xt)}return function(St,xt,jt,Vt){try{Pv=0;var Ea=ar(St,xt,jt,Vt);return Db=null,Ea}catch(Ca){if(Ca===Eb||Ca===Fk)throw Ca;var Dn=dl(29,Ca,null,St.mode);return Dn.lanes=Vt,Dn.return=St,Dn}}}var Mf=UU(!0),BU=UU(!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,(Un&2)!==0){var G=D.pending;return G===null?v.next=v:(v.next=G.next,G.next=v),D.pending=v,v=Tk(h),_U(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 G=null,te=null;if(_=_.firstBaseUpdate,_!==null){do{var ke={lane:_.lane,tag:_.tag,payload:_.payload,callback:null,next:null};te===null?G=te=ke:te=te.next=ke,_=_.next}while(_!==null);te===null?G=te=v:te=te.next=v}else G=te=v;_={baseState:D.baseState,firstBaseUpdate:G,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 G=h.updateQueue;kp=!1;var te=G.firstBaseUpdate,ke=G.lastBaseUpdate,He=G.shared.pending;if(He!==null){G.shared.pending=null;var mt=He,Et=mt.next;mt.next=null,ke===null?te=Et:ke.next=Et,ke=mt;var qt=h.alternate;qt!==null&&(qt=qt.updateQueue,He=qt.lastBaseUpdate,He!==ke&&(He===null?qt.firstBaseUpdate=Et:He.next=Et,qt.lastBaseUpdate=mt))}if(te!==null){var Wt=G.baseState;ke=0,qt=Et=mt=null,He=te;do{var Lt=He.lane&-536870913,zt=Lt!==He.lane;if(zt?(Sn&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 wa=h,za=He;Lt=v;var ar=_;switch(za.tag){case 1:if(wa=za.payload,typeof wa=="function"){Wt=wa.call(ar,Wt,Lt);break e}Wt=wa;break e;case 3:wa.flags=wa.flags&-65537|128;case 0:if(wa=za.payload,Lt=typeof wa=="function"?wa.call(ar,Wt,Lt):wa,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=G.callbacks,zt===null?G.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,ke|=Lt;if(He=He.next,He===null){if(He=G.shared.pending,He===null)break;zt=He,He=zt.next,zt.next=null,G.lastBaseUpdate=zt,G.shared.pending=null}}while(!0);qt===null&&(mt=Wt),G.baseState=mt,G.firstBaseUpdate=Et,G.lastBaseUpdate=qt,te===null&&(G.shared.lanes=0),jp|=ke,h.lanes=ke,h.memoizedState=Wt}}function qU(h,v){if(typeof h!="function")throw Error(r(191,h));h.call(v)}function HU(h,v){var _=h.callbacks;if(_!==null)for(h.callbacks=null,h=0;h<_.length;h++)qU(_[h],v)}var Fb=Z(null),Ok=Z(0);function $U(h,v){h=Yu,fe(Ok,h),fe(Fb,v),Yu=h|v.baseLanes}function Rj(){fe(Ok,Yu),fe(Fb,Fb.current)}function Ij(){Yu=Ok.current,oe(Fb),oe(Ok)}var ul=Z(null),Wl=null;function Cp(h){var v=h.alternate;fe(hi,hi.current&1),fe(ul,h),Wl===null&&(v===null||Fb.current!==null||v.memoizedState!==null)&&(Wl=h)}function Oj(h){fe(hi,hi.current),fe(ul,h),Wl===null&&(Wl=h)}function VU(h){h.tag===22?(fe(hi,hi.current),fe(ul,h),Wl===null&&(Wl=h)):Pp()}function Pp(){fe(hi,hi.current),fe(ul,ul.current)}function ml(h){oe(ul),Wl===h&&(Wl=null),oe(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,er=null,Ni=null,Uk=!1,Lb=!1,Ef=!1,Bk=0,Ev=0,Rb=null,Gde=0;function Zr(){throw Error(r(321))}function zj(h,v){if(v===null)return!1;for(var _=0;_te?te:8;var ke=ee.T,He={};ee.T=He,eM(h,!1,v,_);try{var mt=G(),Et=ee.S;if(Et!==null&&Et(He,mt),mt!==null&&typeof mt=="object"&&typeof mt.then=="function"){var qt=Vde(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,ke!==null&&He.types!==null&&(ke.types=He.types),ee.T=ke}}function Xde(){}function Zj(h,v,_,D){if(h.tag!==5)throw Error(r(476));var G=wB(h).queue;vB(h,G,v,le,_===null?Xde:function(){return SB(h),_(D)})}function wB(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 SB(h){var v=wB(h);v.next===null&&(v=h.alternate.memoizedState),Fv(h,v.next.queue,{},fl())}function Jj(){return Cs(Xv)}function kB(){return fi().memoizedState}function _B(){return fi().memoizedState}function Zde(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 Jde(h,v,_){var D=fl();_={lane:D,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null},Wk(h)?CB(v,_):(_=bj(h,v,_,D),_!==null&&(qo(_,h,D),PB(_,v,D)))}function NB(h,v,_){var D=fl();Fv(h,v,_,D)}function Fv(h,v,_,D){var G={lane:D,revertLane:0,gesture:null,action:_,hasEagerState:!1,eagerState:null,next:null};if(Wk(h))CB(v,G);else{var te=h.alternate;if(h.lanes===0&&(te===null||te.lanes===0)&&(te=v.lastRenderedReducer,te!==null))try{var ke=v.lastRenderedState,He=te(ke,_);if(G.hasEagerState=!0,G.eagerState=He,Mr(He,ke))return Pk(h,v,G,0),mr===null&&Ck(),!1}catch{}if(_=bj(h,v,G,D),_!==null)return qo(_,h,D),PB(_,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(r(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 CB(h,v){Lb=Uk=!0;var _=h.pending;_===null?v.next=v:(v.next=_.next,_.next=v),h.pending=v}function PB(h,v,_){if((_&4194048)!==0){var D=v.lanes;D&=h.pendingLanes,_|=D,v.lanes=_,Bt(h,_)}}var Lv={readContext:Cs,use:Hk,useCallback:Zr,useContext:Zr,useEffect:Zr,useImperativeHandle:Zr,useLayoutEffect:Zr,useInsertionEffect:Zr,useMemo:Zr,useReducer:Zr,useRef:Zr,useState:Zr,useDebugValue:Zr,useDeferredValue:Zr,useTransition:Zr,useSyncExternalStore:Zr,useId:Zr,useHostTransitionStatus:Zr,useFormState:Zr,useActionState:Zr,useOptimistic:Zr,useMemoCache:Zr,useCacheRefresh:Zr};Lv.useEffectEvent=Zr;var TB={readContext:Cs,use:Hk,useCallback:function(h,v){return go().memoizedState=[h,v===void 0?null:v],h},useContext:Cs,useEffect:uB,useImperativeHandle:function(h,v,_){_=_!=null?_.concat([h]):null,Vk(4194308,4,fB.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 G=_(v);if(Ef){Re(!0);try{_(v)}finally{Re(!1)}}}else G=v;return D.memoizedState=D.baseState=G,h={pending:null,lanes:0,dispatch:null,lastRenderedReducer:h,lastRenderedState:G},D.queue=h,h=h.dispatch=Jde.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,_=NB.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=vB.bind(null,Ya,h.queue,!0,!1),go().memoizedState=h,[!1,h]},useSyncExternalStore:function(h,v,_){var D=Ya,G=go();if(Nn){if(_===void 0)throw Error(r(407));_=_()}else{if(_=v(),mr===null)throw Error(r(349));(Sn&127)!==0||QU(D,v,_)}G.memoizedState=_;var te={value:_,getSnapshot:v};return G.queue=te,uB(XU.bind(null,D,te,h),[h]),D.flags|=2048,Ib(9,{destroy:void 0},YU.bind(null,D,te,_,v),null),_},useId:function(){var h=go(),v=mr.identifierPrefix;if(Nn){var _=jd,D=Ad;_=(D&~(1<<32-We(D)-1)).toString(32)+_,v="_"+v+"R_"+_,_=Bk++,0<_&&(v+="H"+_.toString(32)),v+="_"}else _=Gde++,v="_"+v+"r_"+_.toString(32)+"_";return h.memoizedState=v},useHostTransitionStatus:Jj,useFormState:sB,useActionState:sB,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=Zde.bind(null,Ya)},useEffectEvent:function(h){var v=go(),_={impl:h};return v.memoizedState=_,function(){if((Un&2)!==0)throw Error(r(440));return _.impl.apply(void 0,arguments)}}},tM={readContext:Cs,use:Hk,useCallback:bB,useContext:Cs,useEffect:Qj,useImperativeHandle:gB,useInsertionEffect:pB,useLayoutEffect:hB,useMemo:xB,useReducer:$k,useRef:dB,useState:function(){return $k(Hu)},useDebugValue:Yj,useDeferredValue:function(h,v){var _=fi();return yB(_,er.memoizedState,h,v)},useTransition:function(){var h=$k(Hu)[0],v=fi().memoizedState;return[typeof h=="boolean"?h:Dv(h),v]},useSyncExternalStore:KU,useId:kB,useHostTransitionStatus:Jj,useFormState:oB,useActionState:oB,useOptimistic:function(h,v){var _=fi();return eB(_,er,h,v)},useMemoCache:$j,useCacheRefresh:_B};tM.useEffectEvent=mB;var AB={readContext:Cs,use:Hk,useCallback:bB,useContext:Cs,useEffect:Qj,useImperativeHandle:gB,useInsertionEffect:pB,useLayoutEffect:hB,useMemo:xB,useReducer:Gj,useRef:dB,useState:function(){return Gj(Hu)},useDebugValue:Yj,useDeferredValue:function(h,v){var _=fi();return er===null?Xj(_,h,v):yB(_,er.memoizedState,h,v)},useTransition:function(){var h=Gj(Hu)[0],v=fi().memoizedState;return[typeof h=="boolean"?h:Dv(h),v]},useSyncExternalStore:KU,useId:kB,useHostTransitionStatus:Jj,useFormState:cB,useActionState:cB,useOptimistic:function(h,v){var _=fi();return er!==null?eB(_,er,h,v):(_.baseState=h,[h,_.queue.dispatch])},useMemoCache:$j,useCacheRefresh:_B};AB.useEffectEvent=mB;function aM(h,v,_,D){v=h.memoizedState,_=_(D,v),_=_==null?v:m({},v,_),h.memoizedState=_,h.lanes===0&&(h.updateQueue.baseState=_)}var nM={enqueueSetState:function(h,v,_){h=h._reactInternals;var D=fl(),G=_p(D);G.payload=v,_!=null&&(G.callback=_),v=Np(h,G,D),v!==null&&(qo(v,h,D),Av(v,h,D))},enqueueReplaceState:function(h,v,_){h=h._reactInternals;var D=fl(),G=_p(D);G.tag=1,G.payload=v,_!=null&&(G.callback=_),v=Np(h,G,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 jB(h,v,_,D,G,te,ke){return h=h.stateNode,typeof h.shouldComponentUpdate=="function"?h.shouldComponentUpdate(D,te,ke):v.prototype&&v.prototype.isPureReactComponent?!_s(_,D)||!_s(G,te):!0}function MB(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&&nM.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 G in h)_[G]===void 0&&(_[G]=h[G])}return _}function EB(h){Nk(h)}function DB(h){console.error(h)}function FB(h){Nk(h)}function Kk(h,v){try{var _=h.onUncaughtError;_(v.value,{componentStack:v.stack})}catch(D){setTimeout(function(){throw D})}}function LB(h,v,_){try{var D=h.onCaughtError;D(_.value,{componentStack:_.stack,errorBoundary:v.tag===1?v.stateNode:null})}catch(G){setTimeout(function(){throw G})}}function rM(h,v,_){return _=_p(_),_.tag=3,_.payload={element:null},_.callback=function(){Kk(h,v)},_}function RB(h){return h=_p(h),h.tag=3,h}function IB(h,v,_,D){var G=_.type.getDerivedStateFromError;if(typeof G=="function"){var te=D.value;h.payload=function(){return G(te)},h.callback=function(){LB(v,_,D)}}var ke=_.stateNode;ke!==null&&typeof ke.componentDidCatch=="function"&&(h.callback=function(){LB(v,_,D),typeof G!="function"&&(Mp===null?Mp=new Set([this]):Mp.add(this));var He=D.stack;this.componentDidCatch(D.value,{componentStack:He!==null?He:""})})}function eue(h,v,_,D,G){if(_.flags|=32768,D!==null&&typeof D=="object"&&typeof D.then=="function"){if(v=_.alternate,v!==null&&Ab(v,_,G,!0),_=ul.current,_!==null){switch(_.tag){case 31:case 13:return Wl===null?s_():_.alternate===null&&Jr===0&&(Jr=3),_.flags&=-257,_.flags|=65536,_.lanes=G,D===Lk?_.flags|=16384:(v=_.updateQueue,v===null?_.updateQueue=new Set([D]):v.add(D),AM(h,D,G)),!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,G)),!1}throw Error(r(435,_.tag))}return AM(h,D,G),s_(),!1}if(Nn)return v=ul.current,v!==null?((v.flags&65536)===0&&(v.flags|=256),v.flags|=65536,v.lanes=G,D!==kj&&(h=Error(r(422),{cause:D}),_v(Hl(h,_)))):(D!==kj&&(v=Error(r(423),{cause:D}),_v(Hl(v,_))),h=h.current.alternate,h.flags|=65536,G&=-G,h.lanes|=G,D=Hl(D,_),G=rM(h.stateNode,D,G),Fj(h,G),Jr!==4&&(Jr=2)),!1;var te=Error(r(520),{cause:D});if(te=Hl(te,_),Hv===null?Hv=[te]:Hv.push(te),Jr!==4&&(Jr=2),v===null)return!0;D=Hl(D,_),_=v;do{switch(_.tag){case 3:return _.flags|=65536,h=G&-G,_.lanes|=h,h=rM(_.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,G&=-G,_.lanes|=G,G=RB(G),IB(G,h,_,D),Fj(_,G),!1}_=_.return}while(_!==null);return!1}var iM=Error(r(461)),Ci=!1;function Ps(h,v,_,D){v.child=h===null?BU(v,null,_,D):Mf(v,h.child,_,D)}function OB(h,v,_,D,G){_=_.render;var te=v.ref;if("ref"in D){var ke={};for(var He in D)He!=="ref"&&(ke[He]=D[He])}else ke=D;return Pf(v),D=Uj(h,v,_,ke,te,G),He=Bj(),h!==null&&!Ci?(qj(h,v,G),$u(h,v,G)):(Nn&&He&&wj(v),v.flags|=1,Ps(h,v,D,G),v.child)}function zB(h,v,_,D,G){if(h===null){var te=_.type;return typeof te=="function"&&!xj(te)&&te.defaultProps===void 0&&_.compare===null?(v.tag=15,v.type=te,UB(h,v,te,D,G)):(h=Ak(_.type,null,D,v,v.mode,G),h.ref=v.ref,h.return=v,v.child=h)}if(te=h.child,!pM(h,G)){var ke=te.memoizedProps;if(_=_.compare,_=_!==null?_:_s,_(ke,D)&&h.ref===v.ref)return $u(h,v,G)}return v.flags|=1,h=Ou(te,D),h.ref=v.ref,h.return=v,v.child=h}function UB(h,v,_,D,G){if(h!==null){var te=h.memoizedProps;if(_s(te,D)&&h.ref===v.ref)if(Ci=!1,v.pendingProps=D=te,pM(h,G))(h.flags&131072)!==0&&(Ci=!0);else return v.lanes=h.lanes,$u(h,v,G)}return sM(h,v,_,D,G)}function BB(h,v,_,D){var G=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,G=0;D!==null;)G=G|D.lanes|D.childLanes,D=D.sibling;D=G&~te}else D=0,v.child=null;return qB(h,v,te,_,D)}if((_&536870912)!==0)v.memoizedState={baseLanes:0,cachePool:null},h!==null&&Dk(v,te!==null?te.cachePool:null),te!==null?$U(v,te):Rj(),VU(v);else return D=v.lanes=536870912,qB(h,v,te!==null?te.baseLanes|_:_,_,D)}else te!==null?(Dk(v,te.cachePool),$U(v,te),Pp(),v.memoizedState=null):(h!==null&&Dk(v,null),Rj(),Pp());return Ps(h,v,G,_),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 qB(h,v,_,D,G){var te=jj();return te=te===null?null:{parent:_i._currentValue,pool:te},v.memoizedState={baseLanes:_,cachePool:te},h!==null&&Dk(v,null),Rj(),VU(v),h!==null&&Ab(h,v,D,!0),v.childLanes=G,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 HB(h,v,_){return Mf(v,h.child,null,_),h=Qk(v,v.pendingProps),h.flags|=2,ml(v),v.memoizedState=null,h}function tue(h,v,_){var D=v.pendingProps,G=(v.flags&128)!==0;if(v.flags&=-129,h===null){if(Nn){if(D.mode==="hidden")return h=Qk(v,D),v.lanes=536870912,Rv(null,h);if(Oj(v),(h=_r)?(h=tH(h,Gl),h=h!==null&&h.data==="&"?h:null,h!==null&&(v.memoizedState={dehydrated:h,treeContext:yp!==null?{id:Ad,overflow:jd}:null,retryLane:536870912,hydrationErrors:null},_=CU(h),_.return=v,v.child=_,Ns=v,_r=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 ke=te.dehydrated;if(Oj(v),G)if(v.flags&256)v.flags&=-257,v=HB(h,v,_);else if(v.memoizedState!==null)v.child=h.child,v.flags|=128,v=null;else throw Error(r(558));else if(Ci||Ab(h,v,_,!1),G=(_&h.childLanes)!==0,Ci||G){if(D=mr,D!==null&&(ke=gt(D,_),ke!==0&&ke!==te.retryLane))throw te.retryLane=ke,kf(h,ke),qo(D,h,ke),iM;s_(),v=HB(h,v,_)}else h=te.treeContext,_r=Kl(ke.nextSibling),Ns=v,Nn=!0,vp=null,Gl=!1,h!==null&&AU(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(r(284));(h===null||h.ref!==_)&&(v.flags|=4194816)}}function sM(h,v,_,D,G){return Pf(v),_=Uj(h,v,_,D,void 0,G),D=Bj(),h!==null&&!Ci?(qj(h,v,G),$u(h,v,G)):(Nn&&D&&wj(v),v.flags|=1,Ps(h,v,_,G),v.child)}function $B(h,v,_,D,G,te){return Pf(v),v.updateQueue=null,_=WU(v,D,_,G),GU(h),D=Bj(),h!==null&&!Ci?(qj(h,v,te),$u(h,v,te)):(Nn&&D&&wj(v),v.flags|=1,Ps(h,v,_,te),v.child)}function VB(h,v,_,D,G){if(Pf(v),v.stateNode===null){var te=Nb,ke=_.contextType;typeof ke=="object"&&ke!==null&&(te=Cs(ke)),te=new _(D,te),v.memoizedState=te.state!==null&&te.state!==void 0?te.state:null,te.updater=nM,v.stateNode=te,te._reactInternals=v,te=v.stateNode,te.props=D,te.state=v.memoizedState,te.refs={},Ej(v),ke=_.contextType,te.context=typeof ke=="object"&&ke!==null?Cs(ke):Nb,te.state=v.memoizedState,ke=_.getDerivedStateFromProps,typeof ke=="function"&&(aM(v,_,ke,D),te.state=v.memoizedState),typeof _.getDerivedStateFromProps=="function"||typeof te.getSnapshotBeforeUpdate=="function"||typeof te.UNSAFE_componentWillMount!="function"&&typeof te.componentWillMount!="function"||(ke=te.state,typeof te.componentWillMount=="function"&&te.componentWillMount(),typeof te.UNSAFE_componentWillMount=="function"&&te.UNSAFE_componentWillMount(),ke!==te.state&&nM.enqueueReplaceState(te,te.state,null),Mv(v,D,te,G),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;ke=Nb,typeof qt=="object"&&qt!==null&&(ke=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!==ke)&&MB(v,te,D,ke),kp=!1;var Lt=v.memoizedState;te.state=Lt,Mv(v,D,te,G),jv(),Et=v.memoizedState,He||Lt!==Et||kp?(typeof Wt=="function"&&(aM(v,_,Wt,D),Et=v.memoizedState),(mt=kp||jB(v,_,mt,D,Lt,Et,ke))?(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=ke,D=mt):(typeof te.componentDidMount=="function"&&(v.flags|=4194308),D=!1)}else{te=v.stateNode,Dj(h,v),ke=v.memoizedProps,qt=Df(_,ke),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"||(ke!==Wt||Lt!==mt)&&MB(v,te,D,mt),kp=!1,Lt=v.memoizedState,te.state=Lt,Mv(v,D,te,G),jv();var zt=v.memoizedState;ke!==Wt||Lt!==zt||kp||h!==null&&h.dependencies!==null&&Mk(h.dependencies)?(typeof He=="function"&&(aM(v,_,He,D),zt=v.memoizedState),(qt=kp||jB(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"||ke===h.memoizedProps&&Lt===h.memoizedState||(v.flags|=4),typeof te.getSnapshotBeforeUpdate!="function"||ke===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"||ke===h.memoizedProps&&Lt===h.memoizedState||(v.flags|=4),typeof te.getSnapshotBeforeUpdate!="function"||ke===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,G),v.child=Mf(v,null,_,G)):Ps(h,v,_,G),v.memoizedState=te.state,h=v.child):h=$u(h,v,G),h}function GB(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:LU()}}function cM(h,v,_){return h=h!==null?h.childLanes&~_:0,v&&(h|=hl),h}function WB(h,v,_){var D=v.pendingProps,G=!1,te=(v.flags&128)!==0,ke;if((ke=te)||(ke=h!==null&&h.memoizedState===null?!1:(hi.current&2)!==0),ke&&(G=!0,v.flags&=-129),ke=(v.flags&32)!==0,v.flags&=-33,h===null){if(Nn){if(G?Cp(v):Pp(),(h=_r)?(h=tH(h,Gl),h=h!==null&&h.data!=="&"?h:null,h!==null&&(v.memoizedState={dehydrated:h,treeContext:yp!==null?{id:Ad,overflow:jd}:null,retryLane:536870912,hydrationErrors:null},_=CU(h),_.return=v,v.child=_,Ns=v,_r=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,G?(Pp(),G=v.mode,He=Xk({mode:"hidden",children:He},G),D=_f(D,G,_,null),He.return=v,D.return=v,He.sibling=D,v.child=He,D=v.child,D.memoizedState=lM(_),D.childLanes=cM(h,ke,_),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,G=v.mode,D=Xk({mode:"visible",children:D.children},G),He=_f(He,G,_,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,ke,_),v.memoizedState=oM,v=Rv(null,D));else if(Cp(v),VM(He)){if(ke=He.nextSibling&&He.nextSibling.dataset,ke)var Et=ke.dgst;ke=Et,D=Error(r(419)),D.stack="",D.digest=ke,_v({value:D,source:null,stack:null}),v=uM(h,v,_)}else if(Ci||Ab(h,v,_,!1),ke=(_&h.childLanes)!==0,Ci||ke){if(ke=mr,ke!==null&&(D=gt(ke,_),D!==0&&D!==mt.retryLane))throw mt.retryLane=D,kf(h,D),qo(ke,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,_r=Kl(He.nextSibling),Ns=v,Nn=!0,vp=null,Gl=!1,h!==null&&AU(v,h),v=dM(v,D.children),v.flags|=4096);return v}return G?(Pp(),He=D.fallback,G=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,G,_,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(_):(G=He.cachePool,G!==null?(mt=_i._currentValue,G=G.parent!==mt?{parent:mt,pool:mt}:G):G=LU(),He={baseLanes:He.baseLanes|_,cachePool:G}),D.memoizedState=He,D.childLanes=cM(h,ke,_),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&&(ke=v.deletions,ke===null?(v.deletions=[h],v.flags|=16):ke.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 KB(h,v,_){h.lanes|=v;var D=h.alternate;D!==null&&(D.lanes|=v),Cj(h.return,v,_)}function mM(h,v,_,D,G,te){var ke=h.memoizedState;ke===null?h.memoizedState={isBackwards:v,rendering:null,renderingStartTime:0,last:D,tail:_,tailMode:G,treeForkCount:te}:(ke.isBackwards=v,ke.rendering=null,ke.renderingStartTime=0,ke.last=D,ke.tail=_,ke.tailMode=G,ke.treeForkCount=te)}function QB(h,v,_){var D=v.pendingProps,G=D.revealOrder,te=D.tail;D=D.children;var ke=hi.current,He=(ke&2)!==0;if(He?(ke=ke&1|2,v.flags|=128):ke&=1,fe(hi,ke),Ps(h,v,D,_),D=Nn?kv:0,!He&&h!==null&&(h.flags&128)!==0)e:for(h=v.child;h!==null;){if(h.tag===13)h.memoizedState!==null&&KB(h,_,v);else if(h.tag===19)KB(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(G){case"forwards":for(_=v.child,G=null;_!==null;)h=_.alternate,h!==null&&zk(h)===null&&(G=_),_=_.sibling;_=G,_===null?(G=v.child,v.child=null):(G=_.sibling,_.sibling=null),mM(v,!1,G,_,te,D);break;case"backwards":case"unstable_legacy-backwards":for(_=null,G=v.child,v.child=null;G!==null;){if(h=G.alternate,h!==null&&zk(h)===null){v.child=G;break}h=G.sibling,G.sibling=_,_=G,G=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(r(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 aue(h,v,_){switch(v.tag){case 3:A(v,v.stateNode.containerInfo),Sp(v,_i,h.memoizedState.cache),Nf();break;case 27:case 5:z(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?WB(h,v,_):(Cp(v),h=$u(h,v,_),h!==null?h.sibling:null);Cp(v);break;case 19:var G=(h.flags&128)!==0;if(D=(_&v.childLanes)!==0,D||(Ab(h,v,_,!1),D=(_&v.childLanes)!==0),G){if(D)return QB(h,v,_);v.flags|=128}if(G=v.memoizedState,G!==null&&(G.rendering=null,G.tail=null,G.lastEffect=null),fe(hi,hi.current),D)break;return null;case 22:return v.lanes=0,BB(h,v,_,v.pendingProps);case 24:Sp(v,_i,h.memoizedState.cache)}return $u(h,v,_)}function YB(h,v,_){if(h!==null)if(h.memoizedProps!==v.pendingProps)Ci=!0;else{if(!pM(h,_)&&(v.flags&128)===0)return Ci=!1,aue(h,v,_);Ci=(h.flags&131072)!==0}else Ci=!1,Nn&&(v.flags&1048576)!==0&&TU(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=VB(null,v,h,D,_)):(v.tag=0,v=sM(null,v,h,D,_));else{if(h!=null){var G=h.$$typeof;if(G===C){v.tag=11,v=OB(null,v,h,D,_);break e}else if(G===j){v.tag=14,v=zB(null,v,h,D,_);break e}}throw v=R(h)||h,Error(r(306,v,""))}}return v;case 0:return sM(h,v,v.type,v.pendingProps,_);case 1:return D=v.type,G=Df(D,v.pendingProps),VB(h,v,D,G,_);case 3:e:{if(A(v,v.stateNode.containerInfo),h===null)throw Error(r(387));D=v.pendingProps;var te=v.memoizedState;G=te.element,Dj(h,v),Mv(v,D,null,_);var ke=v.memoizedState;if(D=ke.cache,Sp(v,_i,D),D!==te.cache&&Pj(v,[_i],_,!0),jv(),D=ke.element,te.isDehydrated)if(te={element:D,isDehydrated:!1,cache:ke.cache},v.updateQueue.baseState=te,v.memoizedState=te,v.flags&256){v=GB(h,v,D,_);break e}else if(D!==G){G=Hl(Error(r(424)),v),_v(G),v=GB(h,v,D,_);break e}else for(h=v.stateNode.containerInfo,h.nodeType===9?h=h.body:h=h.nodeName==="HTML"?h.ownerDocument.body:h,_r=Kl(h.firstChild),Ns=v,Nn=!0,vp=null,Gl=!0,_=BU(v,null,D,_),v.child=_;_;)_.flags=_.flags&-3|4096,_=_.sibling;else{if(Nf(),D===G){v=$u(h,v,_);break e}Ps(h,v,D,_)}v=v.child}return v;case 26:return Yk(h,v),h===null?(_=oH(v.type,null,v.pendingProps,null))?v.memoizedState=_:Nn||(_=v.type,h=v.pendingProps,D=p_(H.current).createElement(_),D[_t]=v,D[Ct]=h,Ts(D,_,h),st(D),v.stateNode=D):v.memoizedState=oH(v.type,h.memoizedProps,v.pendingProps,h.memoizedState),null;case 27:return z(v),h===null&&Nn&&(D=v.stateNode=rH(v.type,v.pendingProps,H.current),Ns=v,Gl=!0,G=_r,Lp(v.type)?(GM=G,_r=Kl(D.firstChild)):_r=G),Ps(h,v,v.pendingProps.children,_),Yk(h,v),h===null&&(v.flags|=4194304),v.child;case 5:return h===null&&Nn&&((G=D=_r)&&(D=Eue(D,v.type,v.pendingProps,Gl),D!==null?(v.stateNode=D,Ns=v,_r=Kl(D.firstChild),Gl=!1,G=!0):G=!1),G||wp(v)),z(v),G=v.type,te=v.pendingProps,ke=h!==null?h.memoizedProps:null,D=te.children,BM(G,te)?D=null:ke!==null&&BM(G,ke)&&(v.flags|=32),v.memoizedState!==null&&(G=Uj(h,v,Wde,null,null,_),Xv._currentValue=G),Yk(h,v),Ps(h,v,D,_),v.child;case 6:return h===null&&Nn&&((h=_=_r)&&(_=Due(_,v.pendingProps,Gl),_!==null?(v.stateNode=_,Ns=v,_r=null,h=!0):h=!1),h||wp(v)),null;case 13:return WB(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 OB(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 G=v.type._context,D=v.pendingProps.children,Pf(v),G=Cs(G),D=D(G),v.flags|=1,Ps(h,v,D,_),v.child;case 14:return zB(h,v,v.type,v.pendingProps,_);case 15:return UB(h,v,v.type,v.pendingProps,_);case 19:return QB(h,v,_);case 31:return tue(h,v,_);case 22:return BB(h,v,_,v.pendingProps);case 24:return Pf(v),D=Cs(_i),h===null?(G=jj(),G===null&&(G=mr,te=Tj(),G.pooledCache=te,te.refCount++,te!==null&&(G.pooledCacheLanes|=_),G=te),v.memoizedState={parent:D,cache:G},Ej(v),Sp(v,_i,G)):((h.lanes&_)!==0&&(Dj(h,v),Mv(v,null,null,_),jv()),G=h.memoizedState,te=v.memoizedState,G.parent!==D?(G={parent:D,cache:D},v.memoizedState=G,v.lanes===0&&(v.memoizedState=v.updateQueue.baseState=G),Sp(v,_i,D)):(D=te.cache,Sp(v,_i,D),D!==G.cache&&Pj(v,[_i],_,!0))),Ps(h,v,v.pendingProps.children,_),v.child;case 29:throw v.pendingProps}throw Error(r(156,v.tag))}function Vu(h){h.flags|=4}function hM(h,v,_,D,G){if((v=(h.mode&32)!==0)&&(v=!1),v){if(h.flags|=16777216,(G&335544128)===G)if(h.stateNode.complete)h.flags|=8192;else if(kq())h.flags|=8192;else throw jf=Lk,Mj}else h.flags&=-16777217}function XB(h,v){if(v.type!=="stylesheet"||(v.state.loading&4)!==0)h.flags&=-16777217;else if(h.flags|=16777216,!mH(v))if(kq())h.flags|=8192;else throw jf=Lk,Mj}function Zk(h,v){v!==null&&(h.flags|=4),h.flags&16384&&(v=h.tag!==22?Ke():536870912,h.lanes|=v,Bb|=v)}function Iv(h,v){if(!Nn)switch(h.tailMode){case"hidden":v=h.tail;for(var _=null;v!==null;)v.alternate!==null&&(_=v),v=v.sibling;_===null?h.tail=null:_.sibling=null;break;case"collapsed":_=h.tail;for(var D=null;_!==null;)_.alternate!==null&&(D=_),_=_.sibling;D===null?v||h.tail===null?h.tail=null:h.tail.sibling=null:D.sibling=null}}function Nr(h){var v=h.alternate!==null&&h.alternate.child===h.child,_=0,D=0;if(v)for(var G=h.child;G!==null;)_|=G.lanes|G.childLanes,D|=G.subtreeFlags&65011712,D|=G.flags&65011712,G.return=h,G=G.sibling;else for(G=h.child;G!==null;)_|=G.lanes|G.childLanes,D|=G.subtreeFlags,D|=G.flags,G.return=h,G=G.sibling;return h.subtreeFlags|=D,h.childLanes=_,v}function nue(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 Nr(v),null;case 1:return Nr(v),null;case 3:return _=v.stateNode,D=null,h!==null&&(D=h.memoizedState.cache),v.memoizedState.cache!==D&&(v.flags|=2048),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())),Nr(v),null;case 26:var G=v.type,te=v.memoizedState;return h===null?(Vu(v),te!==null?(Nr(v),XB(v,te)):(Nr(v),hM(v,G,null,D,_))):te?te!==h.memoizedState?(Vu(v),Nr(v),XB(v,te)):(Nr(v),v.flags&=-16777217):(h=h.memoizedProps,h!==D&&Vu(v),Nr(v),hM(v,G,h,D,_)),null;case 27:if(W(v),_=H.current,G=v.type,h!==null&&v.stateNode!=null)h.memoizedProps!==D&&Vu(v);else{if(!D){if(v.stateNode===null)throw Error(r(166));return Nr(v),null}h=me.current,Tb(v)?jU(v):(h=rH(G,D,_),v.stateNode=h,Vu(v))}return Nr(v),null;case 5:if(W(v),G=v.type,h!==null&&v.stateNode!=null)h.memoizedProps!==D&&Vu(v);else{if(!D){if(v.stateNode===null)throw Error(r(166));return Nr(v),null}if(te=me.current,Tb(v))jU(v);else{var ke=p_(H.current);switch(te){case 1:te=ke.createElementNS("http://www.w3.org/2000/svg",G);break;case 2:te=ke.createElementNS("http://www.w3.org/1998/Math/MathML",G);break;default:switch(G){case"svg":te=ke.createElementNS("http://www.w3.org/2000/svg",G);break;case"math":te=ke.createElementNS("http://www.w3.org/1998/Math/MathML",G);break;case"script":te=ke.createElement("div"),te.innerHTML=" - + +