diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 015b060..d704393 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,6 @@ jobs: - name: Playwright E2E (approval workspace) env: FD_E2E_FORCE_APPROVAL: "1" - PW_WEBSERVER_APPROVAL: "1" run: | cd web npx playwright install chromium @@ -127,7 +126,6 @@ jobs: shell: bash env: FD_E2E_FORCE_APPROVAL: "1" - PW_WEBSERVER_APPROVAL: "1" run: | cd web npx playwright install chromium diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d4c8e2..c4c4733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,19 +8,16 @@ This project follows [Semantic Versioning](https://semver.org/). From **v1.0.0** ### Changed -- **Web Runs:** forensics UX — empty / offset / truncation messaging, export copy aligned to server limits, trace band rows, **View** drawer with structured fields and full event JSON, extra table columns (trace, status). -- **Web Diff:** scannable sections (policy, evidence window, pricing/catalog/hints, rollups), pre-query hint, `evaluated_at` when present; **examples** index and **integration** README link **`/#/diff`** and **`POST /v1/diff`** to the end-to-end loop. -- **Web Actions:** workspace loading skeleton; numbered steps when approval is on; pending table **Use for confirm** and **Refresh list**; clearer browser confirm copy and approval-reason placeholder. -- **Web shell / Overview:** skeleton loading instead of plain “Loading…”; **Refresh** disabled while loading; ledger metrics line with links to **Diff** and **Runs**; per-metric hint lines; Diff query card **`aria-busy`** while computing. -- **Web Runs:** optional **Group by trace_id** (collapsible `
` per trace); **View** uses **`aria-haspopup="dialog"`**. -- **Web Diff:** warn when imported **pricing table versions** or **providers** differ between baseline and candidate (same `pricing` block as before). -- **Web security strip:** loading state with **`aria-busy`** while **`/health`** is fetched. -- **Web Actions:** **Rollback** uses danger-styled button (still same confirm + API). -- **Examples / deploy / SECURITY:** [examples/README.md](examples/README.md) step 7 and readiness row mention **`/#/runs`** grouping; [examples/deploy/README.md](examples/deploy/README.md) operator checklist; Compose **`restart: unless-stopped`** on the reference service; **[SECURITY.md](SECURITY.md)** links the deploy guide for operational hardening. -- **Playwright:** `e2e-server.mjs` enables **`promotion_requires_approval`** only when **`PW_FORCE_APPROVAL_WORKSPACE=1`** (set from the CLI target or **`PW_WEBSERVER_APPROVAL`**); default suite no longer breaks on a stray **`FD_E2E_FORCE_APPROVAL`** shell export; **`reuseExistingServer: false`** for a clean workspace each run; **[web/README.md](web/README.md)** documents approval vs default runs. +- **Web Runs:** forensics — empty / offset / truncation messaging, export copy, trace band rows or **Group by trace_id**, **View** drawer (structured fields + full JSON, **session_id** / **span_id**, focus trap + return focus, **`aria-haspopup="dialog"`**), trace/status columns; **run-query** failures show a typed error card with **Retry**. +- **Web Diff:** scannable sections (policy, evidence window, pricing/catalog/hints, rollups), pre-query hint, `evaluated_at` when present; warn when imported **pricing table versions** or **providers** differ baseline vs candidate. +- **Web Actions:** workspace loading skeleton; numbered approval steps; pending **Refresh list** / **Use for confirm**; clearer confirms; approval-reason placeholder; **Rollback** danger-styled; **Actions** shows whether **`VITE_FLIGHTDECK_LOCAL_API_TOKEN`** is set (no value) and an inline hint when the server uses **Bearer** and the UI token is missing. +- **Web shell / Overview / CSS:** **Langfuse-style** left sidebar + main column (stacks on narrow viewports); skeleton loading on first load; **Overview** auto-polls timeline + metrics every **30s** when the tab is visible (silent refresh; no manual **Refresh** button); updates after **Actions** mutations via context; ledger metrics hints + links to **Diff** / **Runs**; Diff query **`aria-busy`**; **Security strip** `/health` loading + **Bearer** + client-token reassurance line; shared **focus-visible** / type scale / narrow breakpoints; **skip to main** (HashRouter-safe); **[ROADMAP.md](ROADMAP.md)** adds **Visual system** backlog item and theme deferral. +- **Examples / deploy / SECURITY / web README:** [examples/README.md](examples/README.md) end-to-end loop + **UI polish / operator flow** blurb; deploy checklist + **`restart: unless-stopped`**; **[SECURITY.md](SECURITY.md)** deploy pointer; **[web/README.md](web/README.md)** Playwright approval vs default runs. +- **Playwright:** `e2e-server.mjs` gates approval workspace on **`PW_FORCE_APPROVAL_WORKSPACE`** (set from config); **`reuseExistingServer: false`**; config sets approval workspace only when the CLI lists **exactly one** `e2e/*.spec.ts` path and it is **`actions-approval.spec.ts`** (avoids multi-spec argv; **`PW_WEBSERVER_APPROVAL`** no longer toggles the server so a stale value cannot break **`npm run test:e2e`**); **`actions-approval.spec.ts`** skips when **`GET /v1/workspace`** shows approval off (e.g. full suite with **`FD_E2E_FORCE_APPROVAL=1`**). ### Added +- **PostgreSQL ledger:** optional **`database_url`** in **`flightdeck.yaml`** (`postgresql://` or `postgres://`); install **`psycopg`** with **`uv sync --extra postgres`** (or **`pip install 'flightdeck-ai[postgres]'`**). Same schema migrations and API behavior as SQLite; run filters use **`::json`** predicates on **`event_json`**. **`flightdeck doctor --backup`** stays SQLite-only (use **`pg_dump`** for Postgres). Optional integration tests: **`FLIGHTDECK_TEST_POSTGRES_URL`** with the **`postgres`** extra. - **`GET /v1/runs/export`** — NDJSON stream of the same filtered slice as **`GET /v1/runs`** (optional response headers when truncated). - **`session_id`** / **`span_id`** query filters on **`GET /v1/runs`**, matching CLI/SDK, and **`offset`** pagination on run listings (with **`runs list`** / **`runs export`**). - **Web Runs** page — query **`GET /v1/runs`** from the bundled UI. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 191273c..6001138 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -18,7 +18,10 @@ uv sync --extra dev This creates **`.venv/`** (gitignored), installs **`flightdeck`** editable plus **pytest** and **ruff**, and pins versions from **`uv.lock`**. -Optional extras (telemetry, SDK helpers): e.g. **`uv sync --extra dev --extra telemetry`**. +Optional extras (telemetry, SDK helpers, PostgreSQL driver): e.g. +**`uv sync --extra dev --extra telemetry`** or **`uv sync --extra dev --extra postgres`** +for **`database_url`** / optional **`tests/test_storage_postgres.py`** runs (**`FLIGHTDECK_TEST_POSTGRES_URL`**). +Local driver test helper (Docker optional): **`scripts/run_postgres_tests.ps1`** (see script header). ### Package extras diff --git a/ROADMAP.md b/ROADMAP.md index 69b3961..6a2c47e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -53,10 +53,11 @@ These map to **What is next** items **1**, **2**, and **5**; ship notes stay in 4. **Overview and trust** — Metrics **context** (what a counter means), light cross-links to Diff/Runs—not a metrics dashboard product. 5. **Shell and quality bar** — **Loading** states, consistent spacing and type rhythm, keyboard **focus** and labels, layouts that tolerate narrow viewports where cheap. 6. **Security ergonomics (UI)** — Token/env/mutation visibility, read-only build behavior, cautious affordances for destructive actions. +7. **Visual system** — Shared typography scale, spacing rhythm, **focus-visible** affordances, and narrow-layout breakpoints so the operator surfaces stay legible without a separate design system product. **Explicit UI deferrals** -Out of scope for the near-term web app: theme marketplaces; embedded arbitrary log viewers; full observability or fleet consoles in the browser; multi-workspace UI (follows conditional **Fleet / cross-workspace** in **What is next**). +Out of scope for the near-term web app: custom themes or theme marketplaces; embedded arbitrary log viewers; full observability or fleet consoles in the browser; multi-workspace UI (follows conditional **Fleet / cross-workspace** in **What is next**). --- diff --git a/docs/cli.md b/docs/cli.md index 6365039..402f8be 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -62,21 +62,26 @@ Wrote flightdeck.yaml ``` The generated file uses all defaults. Edit `diff.*` thresholds or `db_path` before using -in a shared repo. See [release-artifact.md § Workspace config](release-artifact.md). +in a shared repo. For **PostgreSQL**, set **`database_url`** to a `postgresql://…` (or +`postgres://…`) DSN and install **`psycopg`** (`uv sync --extra postgres`); **`db_path`** +is ignored when **`database_url`** is set. **`flightdeck doctor --backup`** remains +SQLite-only. See [release-artifact.md § Workspace config](release-artifact.md). --- ## `flightdeck doctor` -Run read-only health checks on the local SQLite ledger. +Run read-only health checks on the workspace ledger (SQLite file or PostgreSQL when +**`database_url`** is configured). ```bash flightdeck doctor [--backup PATH] ``` Calls `Storage.migrate()` at start (idempotent). With **`--backup PATH`**, runs an SQLite -online backup of the workspace database to **`PATH`** (parent directories are created; -an existing file is overwritten), then runs the checks below. +online backup of the workspace database to **`PATH`** when the workspace uses SQLite +(**`--backup`** is rejected for PostgreSQL-ledgers; use **`pg_dump`** instead). Parent +directories are created; an existing file is overwritten, then the checks below run. Without **`--backup`**, only the checks run. In both cases **`migrate()`** runs first. diff --git a/docs/operations-and-policy.md b/docs/operations-and-policy.md index 94d8b03..e5697a3 100644 --- a/docs/operations-and-policy.md +++ b/docs/operations-and-policy.md @@ -39,8 +39,10 @@ maps these to `click.ClickException`; the HTTP layer maps them to HTTP 400. `server/app.py` registers a FastAPI **lifespan** handler that runs at startup: ```python +from flightdeck.storage import storage_from_config + cfg = load_config() # reads flightdeck.yaml from cwd -storage = Storage(cfg.db_path) +storage = storage_from_config(cfg) storage.migrate() app.state.cfg = cfg app.state.storage = storage diff --git a/docs/release-artifact.md b/docs/release-artifact.md index 5f330fe..149be81 100644 --- a/docs/release-artifact.md +++ b/docs/release-artifact.md @@ -142,7 +142,9 @@ look for this file in the current working directory. ```yaml api_version: v1 kind: WorkspaceConfig -db_path: .flightdeck/flightdeck.db # SQLite database path +db_path: .flightdeck/flightdeck.db # SQLite database path (default when database_url unset) +# Optional: ledger on PostgreSQL (requires psycopg; install flightdeck-ai[postgres]) +# database_url: postgresql://user:pass@localhost:5432/flightdeck default_environment: local # default environment for register/diff/promote diff: min_candidate_runs: 500 # HIGH confidence threshold (candidate side) @@ -154,8 +156,11 @@ diff: # promotion_requires_approval: false ``` -All fields have defaults; an empty `flightdeck.yaml` is valid. `db_path` accepts any -relative or absolute path — the parent directory is created automatically on first use. +All fields have defaults; an empty `flightdeck.yaml` is valid. **`db_path`** accepts any +relative or absolute SQLite path — the parent directory is created automatically on first use. +When **`database_url`** is a `postgresql://` (or `postgres://`) DSN, the ledger uses that +database instead and **`db_path`** is ignored for storage (keep **`flightdeck doctor --backup`** +on SQLite, or use **`pg_dump`** for Postgres). **`pricing_catalog_path`** — optional path to a [`PricingCatalog`](../schemas/v1/pricing_catalog.schema.json) YAML (relative to the workspace cwd or absolute). When set, diffs include additive `pricing.catalog` / `pricing.hints`. diff --git a/docs/web-ui.md b/docs/web-ui.md index 6d7e971..0f940b4 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -18,9 +18,9 @@ The app uses **HashRouter** (`react-router-dom`) so all navigation stays within | Hash path | Component | HTTP calls | Notes | |-----------|-----------|-----------|-------| -| `#/` | `OverviewPage` | `GET /v1/releases`, `GET /v1/promoted`, `GET /v1/actions`, `GET /v1/metrics` (parallel where applicable) | Ledger metrics (read-only); short per-counter hints; skeleton while loading; links to Diff/Runs | +| `#/` | `OverviewPage` | `GET /v1/releases`, `GET /v1/promoted`, `GET /v1/actions`, `GET /v1/metrics` (parallel where applicable) | Ledger metrics (read-only); short per-counter hints; skeleton on first load; **auto-refresh** every 30s when the tab is visible + on timeline **`generation`** bump; links to Diff/Runs | | `#/diff` | `DiffPage` | `POST /v1/diff` | Sections: policy gate (incl. `evaluated_at`), evidence window, pricing/catalog/hints (incl. provider/version skew callout when sides differ), per-1k prices when present, cost/quality rollups; raw JSON panel | -| `#/runs` | `RunsPage` | `GET /v1/releases` (for datalist), `GET /v1/runs`, `GET /v1/runs/export` | Forensics: filters, table (trace/status, trace band rows or **Group by trace_id**), **View** drawer, empty/offset/truncation hints, NDJSON download | +| `#/runs` | `RunsPage` | `GET /v1/releases` (for datalist), `GET /v1/runs`, `GET /v1/runs/export` | Forensics: filters, table (trace/status, trace band rows or **Group by trace_id**), **View** drawer (focus trap, session/span ids), typed **run-query error** card with **Retry**, empty/offset/truncation hints, NDJSON download | | `#/actions` | `ActionsPage` | `GET /v1/workspace`, `GET /v1/promotion-requests` (when `promotion_requires_approval`), `POST /v1/promote` **or** `POST /v1/promote/request` + `POST /v1/promote/confirm`, `POST /v1/rollback` | Workspace skeleton then strip; approval path: numbered steps, pending **Refresh list** / **Use for confirm**; **Rollback** danger-styled; see **ActionsPage** below | | `#/*` (any other) | — | Redirects to `#/` | | @@ -37,23 +37,28 @@ promote/rollback capability should be unavailable regardless of network placemen ``` App (HashRouter) -└── AppShell (layout: header + nav) +└── AppShell (layout: left sidebar + main column) └── TimelineRefreshProvider (context) - ├── SecurityStatusBar (below header, above main content) - ├── OverviewPage (route: #/) - ├── DiffPage (route: #/diff) - ├── RunsPage (route: #/runs) - └── ActionsPage (route: #/actions; redirects → #/ when UI_READ_ONLY) + └── div.fd-shell + ├── aside.fd-sidebar (brand + primary nav) + └── div.fd-shell__content + ├── SecurityStatusBar + └── main#main-content → OverviewPage | DiffPage | RunsPage | ActionsPage ``` --- ## `AppShell` (`web/src/components/AppShell.tsx`) -Renders the top header with brand name and primary nav links, then an `` for the -active page. Wraps the entire subtree in `TimelineRefreshProvider` so any descendant can -access the refresh context. Mounts `SecurityStatusBar` between the header and the main -content area. +Renders a fixed-width **left sidebar** (`aside.fd-sidebar`) with brand and vertical primary +nav (Langfuse-style rail), then a **`fd-shell__content`** column with `SecurityStatusBar` and +`
` wrapping an `` for the active page. On narrow viewports the sidebar stacks +above the content with a horizontal nav row. Wraps the subtree in `TimelineRefreshProvider` +so any descendant can access the refresh context. + +A **Skip to main content** link (class `fd-skip-link`) appears first in the shell; it uses +`preventDefault` + `focus()` on `#main-content` so **HashRouter** hash URLs (`#/…`) are not +replaced by a fragment-only `href`. Nav links use `NavLink` from `react-router-dom` with an `fd-nav__link--active` class applied when the route is active. The **Promote** nav link is suppressed when `UI_READ_ONLY` is @@ -105,7 +110,8 @@ Build-time configuration helpers read from `import.meta.env`: ## `SecurityStatusBar` (`web/src/components/SecurityStatusBar.tsx`) -Mounted by `AppShell` between the header and the main content area. Fetches `GET /health` +Mounted by `AppShell` at the top of the main content column (below the sidebar on wide +layouts). Fetches `GET /health` on mount to read `mutation_auth` (`"bearer"` or `"loopback"`), then renders an info or warning strip: @@ -141,9 +147,10 @@ Read-only dashboard. Renders a **Ledger metrics** card from `fetchMetrics()` plu Long IDs are abbreviated with `shortId(id, keepStart, keepEnd)` and shown in full on hover via the HTML `title` attribute. -**Refresh:** a manual **Refresh** button in the page header calls `loadTimeline()` directly. -The `generation` counter from `TimelineRefreshContext` also triggers automatic refreshes -after mutations from `ActionsPage`. +**Refresh:** while the document tab is visible, the page **auto-polls** metrics and the +timeline on an interval and uses **silent** fetches after the first load. The `generation` +counter from `TimelineRefreshContext` triggers an immediate refresh after mutations from +`ActionsPage`. --- @@ -357,8 +364,9 @@ All tokens are CSS custom properties on `:root`: | Token | Purpose | |-------|---------| -| `--fd-bg` | Page background | -| `--fd-surface` | Card / header background | +| `--fd-bg` | Main column page background | +| `--fd-surface` | Card / sidebar rail background | +| `--fd-sidebar-width` | Width of the left navigation rail (wide layouts) | | `--fd-surface-2` | Secondary surface (hover, code blocks) | | `--fd-border` | Standard border | | `--fd-border-strong` | Input and button borders | @@ -378,9 +386,10 @@ All tokens are CSS custom properties on `:root`: | Class | Description | |-------|-------------| -| `fd-shell` | Full-height flex container for header + main | -| `fd-header` | Sticky top bar | -| `fd-nav__link` | Navigation link; `--active` modifier for current route | +| `fd-shell` | Full-height row: sidebar + main column | +| `fd-sidebar` | Left rail: brand block + `fd-sidebar__nav` primary links | +| `fd-shell__content` | Flex column: security strip + `fd-main` | +| `fd-nav__link` | Sidebar nav link; `--active` modifier (accent left border) | | `fd-main` | Page content area with max-width and padding | | `fd-page-head` | Flex row with title/subtitle and optional action button | | `fd-card` | White surface card with border and shadow | @@ -393,7 +402,7 @@ All tokens are CSS custom properties on `:root`: | `fd-field` | Label + input pair; `--full` modifier spans both grid columns | | `fd-input` | Styled text input | | `fd-alert` | Inline alert box; `--error`, `--info`, `--warn` modifiers | -| `fd-security-strip` | Full-width strip below the header; wraps `SecurityStatusBar` output | +| `fd-security-strip` | Strip at top of main column; wraps `SecurityStatusBar` output | | `fd-security-strip__msg` | Message paragraph inside the security strip (zero margin) | | `fd-json-panel` | Collapsible JSON viewer container | | `fd-metric-grid` | Grid of metric cards for diff output | diff --git a/examples/README.md b/examples/README.md index e67aa26..5635299 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,6 +12,8 @@ This folder holds **copy-pasteable** references for wiring FlightDeck into a rea 6. **Run the server** in a container or compose stack — see [deploy/](deploy/README.md). The bundled UI calls **`GET /v1/workspace`** to choose direct promote vs request/confirm. 7. **Triage runs** with **`flightdeck runs list`** / **`runs export`** or **`GET /v1/runs`**, and **observe** aggregate ledger size with **`GET /v1/metrics`** (JSON counters; read-only, same access tier as other `GET /v1/*` routes). With **`flightdeck serve`**, **`/#/runs`** adds optional **Group by trace_id** (collapsible sections) on top of the same API slice. +**UI polish / operator flow:** See [docs/web-ui.md](../docs/web-ui.md) for routing and surfaces. In the bundled app, prefer **Diff** for policy and pricing conclusions, **Runs** for trace-scoped triage, and **Actions** for promote and rollback so operators rarely need raw JSON first. + ## Readiness checklist (quick pass) Use this as a **discoverability** pass for the **[ROADMAP.md](../ROADMAP.md)** success and readiness signals (not a product guarantee): diff --git a/pyproject.toml b/pyproject.toml index 4a9acb4..ddd9578 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,9 @@ dev = [ "pytest>=7.0", "ruff==0.15.12", ] +postgres = [ + "psycopg[binary]>=3.2", +] [project.urls] Homepage = "https://github.com/flightdeckdev/flightdeck" diff --git a/scripts/run_postgres_tests.ps1 b/scripts/run_postgres_tests.ps1 new file mode 100644 index 0000000..3e46f99 --- /dev/null +++ b/scripts/run_postgres_tests.ps1 @@ -0,0 +1,68 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Run optional PostgreSQL storage tests (tests/test_storage_postgres.py). + +.DESCRIPTION + 1. Install the driver: close any process using flightdeck.exe, then from repo root: + uv sync --extra dev --extra postgres + (Or: uv pip install "psycopg[binary]>=3.2" into your venv if sync cannot replace the CLI shim.) + + 2. Start PostgreSQL with an empty database, e.g. Docker: + docker run -d --rm --name fd-pg-test -e POSTGRES_PASSWORD=test -e POSTGRES_DB=flightdeck_test -p 5432:5432 postgres:16 + Wait ~5s for ready, then: + $env:FLIGHTDECK_TEST_POSTGRES_URL = 'postgresql://postgres:test@127.0.0.1:5432/flightdeck_test' + + 3. From repo root: + .\scripts\run_postgres_tests.ps1 + + With -Docker, this script tries to start/stop the container above (requires Docker on PATH). +#> +param( + [switch] $Docker, + [string] $PostgresUrl = "postgresql://postgres:test@127.0.0.1:5432/flightdeck_test" +) + +$ErrorActionPreference = "Stop" +# PSScriptRoot = .../flightdeck/scripts -> repo root is parent +$root = Split-Path $PSScriptRoot -Parent + +Push-Location $root +try { + $py = Join-Path $root ".venv\Scripts\python.exe" + if (-not (Test-Path $py)) { + Write-Error "No .venv\Scripts\python.exe — run: uv sync --extra dev --extra postgres" + } + + & $py -c "import psycopg" 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Error "psycopg not installed. Close apps using flightdeck.exe, then run: uv sync --extra dev --extra postgres" + } + + $startedDocker = $false + if ($Docker) { + $docker = Get-Command docker -ErrorAction SilentlyContinue + if (-not $docker) { + Write-Error "Docker not on PATH; start Postgres yourself or install Docker Desktop." + } + docker rm -f fd-pg-test 2>$null | Out-Null + docker run -d --name fd-pg-test -e POSTGRES_PASSWORD=test -e POSTGRES_DB=flightdeck_test -p 5432:5432 postgres:16 + $startedDocker = $true + Write-Host "Waiting for Postgres..." -ForegroundColor Cyan + Start-Sleep -Seconds 6 + } + + $env:FLIGHTDECK_TEST_POSTGRES_URL = $PostgresUrl + & $py -m pytest tests/test_storage_postgres.py -v --tb=short + $code = $LASTEXITCODE + + if ($startedDocker) { + docker rm -f fd-pg-test 2>$null | Out-Null + Write-Host "Stopped container fd-pg-test." -ForegroundColor Cyan + } + + if ($code -ne 0) { exit $code } +} +finally { + Pop-Location +} diff --git a/scripts/run_postgres_tests.sh b/scripts/run_postgres_tests.sh new file mode 100644 index 0000000..abfbc17 --- /dev/null +++ b/scripts/run_postgres_tests.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Run optional PostgreSQL storage tests. See scripts/run_postgres_tests.ps1 for full notes. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" +export FLIGHTDECK_TEST_POSTGRES_URL="${FLIGHTDECK_TEST_POSTGRES_URL:-postgresql://postgres:test@127.0.0.1:5432/flightdeck_test}" +if [[ "${1:-}" == "--docker" ]]; then + docker rm -f fd-pg-test 2>/dev/null || true + docker run -d --name fd-pg-test -e POSTGRES_PASSWORD=test -e POSTGRES_DB=flightdeck_test -p 5432:5432 postgres:16 + sleep 6 + trap 'docker rm -f fd-pg-test 2>/dev/null || true' EXIT +fi +uv run python -c "import psycopg" +uv run python -m pytest tests/test_storage_postgres.py -v --tb=short diff --git a/src/flightdeck/cli/main.py b/src/flightdeck/cli/main.py index 9a3a3de..0382779 100644 --- a/src/flightdeck/cli/main.py +++ b/src/flightdeck/cli/main.py @@ -35,7 +35,7 @@ request_promotion, rollback_release, ) -from flightdeck.storage import Storage +from flightdeck.storage import storage_from_config def read_release_artifact(path: Path) -> ReleaseArtifact: @@ -94,9 +94,12 @@ def init(path_: str) -> None: def doctor_cmd(backup_path: Path | None) -> None: """Run read-only health checks on the local ledger (migrations, promoted pointers).""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) if backup_path is not None: - storage.backup_to(backup_path) + try: + storage.backup_to(backup_path) + except ValueError as exc: + raise click.ClickException(str(exc)) from exc click.echo(f"Backed up database to {backup_path}") checks = run_doctor(storage) failed = False @@ -149,7 +152,7 @@ def release() -> None: def release_register(path: Path, environment: str | None) -> None: """Register an immutable Release artifact (file or bundle directory).""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() env = environment or cfg.default_environment @@ -179,7 +182,7 @@ def release_register(path: Path, environment: str | None) -> None: def release_list() -> None: """List registered releases.""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() for r in storage.list_releases(): click.echo(f"{r.release_id}\t{r.agent_id}\t{r.version}\t{r.environment}\t{r.created_at.isoformat()}") @@ -190,7 +193,7 @@ def release_list() -> None: def release_show(release_id: str) -> None: """Show a registered release record as JSON.""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() record = storage.get_release(release_id) @@ -212,7 +215,7 @@ def release_show(release_id: str) -> None: def release_verify(release_id: str, artifact_path: Path) -> None: """Verify on-disk artifact bytes match the checksum stored at registration (exit 2 on mismatch).""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() record = storage.get_release(release_id) @@ -252,7 +255,7 @@ def pricing() -> None: def pricing_import(path: Path, replace: bool, reason: str | None) -> None: """Import a pricing table YAML.""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() data = yaml.safe_load(path.read_text(encoding="utf-8")) @@ -272,7 +275,7 @@ def pricing_import(path: Path, replace: bool, reason: str | None) -> None: def pricing_show(provider: str, pricing_version: str) -> None: """Show a pricing table by provider/version.""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() table = storage.get_pricing_table(provider, pricing_version) @@ -291,7 +294,7 @@ def policy() -> None: def policy_set(path: Path) -> None: """Set the active promotion policy from YAML.""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} @@ -304,7 +307,7 @@ def policy_set(path: Path) -> None: def policy_show() -> None: """Show the active promotion policy.""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() p = storage.get_active_policy() or default_policy() @@ -349,7 +352,7 @@ def runs_list( ) -> None: """List ingested run events for a release (newest first; truncated to --limit).""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() try: payload = query_run_events_page( @@ -413,7 +416,7 @@ def runs_export( ) -> None: """Export run events as JSONL (newest first), same filters as ``runs list`` (truncated to --limit, max 500).""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() try: payload = query_run_events_page( @@ -453,7 +456,7 @@ def runs_export( def runs_ingest(path: Path) -> None: """Ingest RunEvent JSONL (or JSON array) into local storage.""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() events = parse_events_file(path) @@ -494,7 +497,7 @@ def release_diff( ) -> None: """Compare two releases over a time window and print a confidence-labeled safety diff.""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() try: result = compute_diff( @@ -598,7 +601,7 @@ def release_diff( def release_promote(release_id: str, environment: str, window: str, reason: str) -> None: """Promote a release after evaluating active policy against the current baseline.""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() try: outcome = promote_release( @@ -633,7 +636,7 @@ def release_promote(release_id: str, environment: str, window: str, reason: str) def release_promote_request(release_id: str, environment: str, window: str, reason: str) -> None: """Create a pending promotion request (requires promotion_requires_approval in flightdeck.yaml).""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() try: record = request_promotion( @@ -657,7 +660,7 @@ def release_promote_request(release_id: str, environment: str, window: str, reas def release_promote_confirm(request_id: str, approval_reason: str) -> None: """Confirm a pending promotion request and perform the promotion.""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() try: outcome = confirm_promotion_request( @@ -691,7 +694,7 @@ def release_promote_confirm(request_id: str, approval_reason: str) -> None: def release_rollback(release_id: str, environment: str, window: str, reason: str) -> None: """Roll back to a prior release (audit record + promoted pointer update).""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() try: outcome = rollback_release( @@ -724,7 +727,7 @@ def release_rollback(release_id: str, environment: str, window: str, reason: str def release_history(agent_id: str | None, environment: str | None) -> None: """Show release promotion decision history.""" cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() records = storage.list_release_actions(agent_id=agent_id, environment=environment) diff --git a/src/flightdeck/db_connect.py b/src/flightdeck/db_connect.py new file mode 100644 index 0000000..22692c4 --- /dev/null +++ b/src/flightdeck/db_connect.py @@ -0,0 +1,212 @@ +"""SQLite and PostgreSQL connection helpers for :class:`flightdeck.storage.Storage`.""" + +from __future__ import annotations + +import sqlite3 +from collections.abc import Iterator, Mapping +from contextlib import contextmanager +from typing import Any, Literal + +Backend = Literal["sqlite", "postgresql"] + + +def detect_backend(dsn: str) -> Backend: + s = dsn.strip() + if s.startswith(("postgresql://", "postgres://")): + return "postgresql" + return "sqlite" + + +def adapt_placeholders(sql: str, backend: Backend) -> str: + """SQLite uses ``?``; PostgreSQL (psycopg) uses ``%s`` for parameters.""" + if backend == "sqlite": + return sql + return sql.replace("?", "%s") + + +def json_request_field_predicate(column: str, field: str, backend: Backend) -> str: + """Equality predicate on ``event_json`` for ``request.`` (parameterized separately).""" + if backend == "sqlite": + return f"json_extract({column}, '$.request.{field}') = ?" + # ``field`` is fixed by callers (trace_id | session_id | span_id); not user-controlled SQL. + return f"({column}::json->'request'->>'{field}') = ?" + + +def table_column_names_sqlite(conn: sqlite3.Connection, table: str) -> set[str]: + rows = conn.execute(f"PRAGMA table_info({table})").fetchall() + return {str(r["name"]) for r in rows} + + +def table_column_names_postgres(cur: Any, table: str) -> set[str]: + cur.execute( + """ + SELECT column_name AS name + FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = %s + """, + (table,), + ) + rows = cur.fetchall() + return {str(r["name"]) for r in rows} + + +def is_unique_violation(backend: Backend, exc: BaseException) -> bool: + if backend == "sqlite": + return isinstance(exc, sqlite3.IntegrityError) + try: + from psycopg.errors import UniqueViolation + except ImportError: + return False + return isinstance(exc, UniqueViolation) + + +def require_psycopg() -> Any: + try: + import psycopg + except ImportError as e: + msg = ( + "PostgreSQL requires the psycopg package. " + "Install with: uv sync --extra postgres (or pip install 'flightdeck-ai[postgres]')." + ) + raise ImportError(msg) from e + return psycopg + + +class DbCursor: + """Row cursor for the last ``execute`` (sqlite3 / psycopg).""" + + __slots__ = ("_backend", "_sqlite_cur", "_pg_cur") + + def __init__(self, backend: Backend, sqlite_cur: sqlite3.Cursor | None, pg_cur: Any | None) -> None: + self._backend = backend + self._sqlite_cur = sqlite_cur + self._pg_cur = pg_cur + + def fetchone(self) -> Mapping[str, Any] | None: + if self._backend == "sqlite": + assert self._sqlite_cur is not None + return self._sqlite_cur.fetchone() + assert self._pg_cur is not None + return self._pg_cur.fetchone() + + def fetchall(self) -> list[Mapping[str, Any]]: + if self._backend == "sqlite": + assert self._sqlite_cur is not None + return self._sqlite_cur.fetchall() + assert self._pg_cur is not None + return self._pg_cur.fetchall() + + +class DbConn: + """Unified execute/fetch surface over sqlite3 or psycopg.""" + + __slots__ = ("_backend", "_sqlite", "_pg", "_last", "_pg_cur") + + def __init__(self, backend: Backend, sqlite_conn: sqlite3.Connection | None, pg_conn: Any | None) -> None: + self._backend = backend + self._sqlite = sqlite_conn + self._pg = pg_conn + self._last = DbCursor(backend, None, None) + self._pg_cur: Any | None = None + + @property + def backend(self) -> Backend: + return self._backend + + @property + def raw_sqlite(self) -> sqlite3.Connection | None: + return self._sqlite + + @property + def raw_pg(self) -> Any | None: + return self._pg + + def execute(self, sql: str, params: tuple[Any, ...] | list[Any] | None = None) -> DbCursor: + sql2 = adapt_placeholders(sql, self._backend) + p = tuple(params) if params is not None else () + if self._backend == "sqlite": + assert self._sqlite is not None + cur = self._sqlite.execute(sql2, p) + self._last = DbCursor(self._backend, cur, None) + return self._last + assert self._pg is not None + require_psycopg() + from psycopg.rows import dict_row + + if self._pg_cur is not None: + self._pg_cur.close() + self._pg_cur = None + self._pg_cur = self._pg.cursor(row_factory=dict_row) + self._pg_cur.execute(sql2, p) + self._last = DbCursor(self._backend, None, self._pg_cur) + return self._last + + def close_last_pg_cursor(self) -> None: + if self._backend == "postgresql" and self._pg_cur is not None: + self._pg_cur.close() + self._pg_cur = None + + def table_columns(self, table: str) -> set[str]: + if self._backend == "sqlite": + assert self._sqlite is not None + return table_column_names_sqlite(self._sqlite, table) + assert self._pg is not None + require_psycopg() + from psycopg.rows import dict_row + + with self._pg.cursor(row_factory=dict_row) as cur: + return table_column_names_postgres(cur, table) + + def fetchone(self) -> Mapping[str, Any] | None: + return self._last.fetchone() + + def fetchall(self) -> list[Mapping[str, Any]]: + return self._last.fetchall() + + +def configure_sqlite(conn: sqlite3.Connection) -> None: + conn.execute("PRAGMA foreign_keys=ON;") + conn.execute("PRAGMA journal_mode=WAL;") + conn.execute("PRAGMA synchronous=NORMAL;") + conn.execute("PRAGMA busy_timeout=5000;") + + +@contextmanager +def open_sqlite(dsn: str) -> Iterator[DbConn]: + with sqlite3.connect(dsn) as conn: + conn.row_factory = sqlite3.Row + configure_sqlite(conn) + yield DbConn("sqlite", conn, None) + + +@contextmanager +def open_postgres(dsn: str) -> Iterator[DbConn]: + psycopg = require_psycopg() + conn = psycopg.connect(dsn, autocommit=False) + wrapper: DbConn | None = None + try: + wrapper = DbConn("postgresql", None, conn) + yield wrapper + conn.commit() + except Exception: + conn.rollback() + raise + finally: + if wrapper is not None: + wrapper.close_last_pg_cursor() + conn.close() + + +@contextmanager +def open_postgres_transaction(dsn: str) -> Iterator[DbConn]: + psycopg = require_psycopg() + conn = psycopg.connect(dsn, autocommit=False) + wrapper: DbConn | None = None + try: + wrapper = DbConn("postgresql", None, conn) + with conn.transaction(): + yield wrapper + finally: + if wrapper is not None: + wrapper.close_last_pg_cursor() + conn.close() diff --git a/src/flightdeck/models.py b/src/flightdeck/models.py index ef27555..39f4643 100644 --- a/src/flightdeck/models.py +++ b/src/flightdeck/models.py @@ -17,6 +17,9 @@ class WorkspaceConfig(BaseModel): kind: Literal["WorkspaceConfig"] = "WorkspaceConfig" db_path: str = ".flightdeck/flightdeck.db" + # When set (``postgresql://`` or ``postgres://``), the ledger uses PostgreSQL instead of + # the SQLite file at ``db_path``. Requires optional dependency ``psycopg`` (``--extra postgres``). + database_url: str | None = None default_environment: str = "local" diff: DiffConfig = Field(default_factory=DiffConfig) diff --git a/src/flightdeck/server/app.py b/src/flightdeck/server/app.py index e4e63f3..63d16c8 100644 --- a/src/flightdeck/server/app.py +++ b/src/flightdeck/server/app.py @@ -10,14 +10,14 @@ from flightdeck.config import load_config from flightdeck.server.routes import include_routes -from flightdeck.storage import Storage +from flightdeck.storage import storage_from_config def create_app() -> FastAPI: @asynccontextmanager async def lifespan(app: FastAPI): cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() app.state.cfg = cfg app.state.storage = storage diff --git a/src/flightdeck/server/routes/common.py b/src/flightdeck/server/routes/common.py index a12ccc1..cef1a80 100644 --- a/src/flightdeck/server/routes/common.py +++ b/src/flightdeck/server/routes/common.py @@ -5,7 +5,7 @@ from flightdeck.config import load_config from flightdeck.models import WorkspaceConfig -from flightdeck.storage import Storage +from flightdeck.storage import Storage, storage_from_config def ensure_app_state(request: Request) -> tuple[WorkspaceConfig, Storage]: @@ -15,7 +15,7 @@ def ensure_app_state(request: Request) -> tuple[WorkspaceConfig, Storage]: return cfg, storage cfg = load_config() - storage = Storage(cfg.db_path) + storage = storage_from_config(cfg) storage.migrate() request.app.state.cfg = cfg request.app.state.storage = storage diff --git a/src/flightdeck/server/static/assets/index-9f3RLHKo.css b/src/flightdeck/server/static/assets/index-9f3RLHKo.css deleted file mode 100644 index ab832bc..0000000 --- a/src/flightdeck/server/static/assets/index-9f3RLHKo.css +++ /dev/null @@ -1 +0,0 @@ -:root{color-scheme:light;--fd-bg: #f4f5f7;--fd-surface: #ffffff;--fd-surface-2: #fafbfc;--fd-border: #e1e4e8;--fd-border-strong: #c9ccd1;--fd-text: #1a1d21;--fd-muted: #5c6570;--fd-accent: #0d6efd;--fd-accent-hover: #0b5ed7;--fd-pass-bg: #e8f5e9;--fd-pass-fg: #1b5e20;--fd-fail-bg: #ffebee;--fd-fail-fg: #b71c1c;--fd-radius: 10px;--fd-radius-sm: 6px;--fd-shadow: 0 1px 2px rgba(15, 23, 42, .06);--fd-font: "Segoe UI Variable", "Segoe UI", system-ui, -apple-system, sans-serif;--fd-mono: ui-monospace, "Cascadia Code", "Consolas", monospace;font-family:var(--fd-font);line-height:1.5;color:var(--fd-text);background:var(--fd-bg)}*,*:before,*:after{box-sizing:border-box}body{margin:0;min-height:100vh}.fd-shell{min-height:100vh;display:flex;flex-direction:column}.fd-header{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:1rem 2rem;padding:1rem 1.5rem;background:var(--fd-surface);border-bottom:1px solid var(--fd-border);box-shadow:var(--fd-shadow)}.fd-header__brand{min-width:0}.fd-header__title{margin:0;font-size:1.35rem;font-weight:650;letter-spacing:-.02em}.fd-header__tagline{margin:.15rem 0 0;font-size:.875rem;color:var(--fd-muted)}.fd-nav{display:flex;gap:.25rem;flex-wrap:wrap}.fd-nav__link{padding:.45rem .85rem;border-radius:var(--fd-radius-sm);font-size:.9rem;font-weight:500;color:var(--fd-muted);text-decoration:none;border:1px solid transparent;transition:background .12s,color .12s,border-color .12s}.fd-nav__link:hover{color:var(--fd-text);background:var(--fd-surface-2)}.fd-nav__link--active{color:var(--fd-text);background:var(--fd-surface-2);border-color:var(--fd-border)}.fd-main{flex:1;width:100%;max-width:1120px;margin:0 auto;padding:1.25rem 1.5rem 2.5rem}.fd-page-head{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:1rem;margin-bottom:1.25rem}.fd-page-title{margin:0;font-size:1.25rem;font-weight:650}.fd-page-sub{margin:.35rem 0 0;max-width:52ch;font-size:.9rem;color:var(--fd-muted)}.fd-card{background:var(--fd-surface);border:1px solid var(--fd-border);border-radius:var(--fd-radius);padding:1rem 1.15rem;margin-bottom:1rem;box-shadow:var(--fd-shadow)}.fd-card__head{margin-bottom:.75rem}.fd-card__head--row{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:.75rem 1rem}.fd-card__head--row>.fd-btn{flex-shrink:0;align-self:flex-start}.fd-card__title{margin:0;font-size:1.05rem;font-weight:600}.fd-card__subtitle{margin:0 0 .5rem;font-size:1rem;font-weight:600}.fd-card__desc{margin:.35rem 0 0;font-size:.85rem;color:var(--fd-muted)}.fd-table-wrap{overflow-x:auto;margin:0 -.15rem}.fd-table{width:100%;border-collapse:collapse;font-size:.875rem}.fd-table th,.fd-table td{text-align:left;padding:.55rem .65rem;border-bottom:1px solid var(--fd-border);vertical-align:top}.fd-table th{font-weight:600;color:var(--fd-muted);font-size:.78rem;text-transform:uppercase;letter-spacing:.04em}.fd-table tbody tr:last-child td{border-bottom:none}.fd-table tbody tr:hover td{background:var(--fd-surface-2)}.fd-mono{font-family:var(--fd-mono);font-size:.82rem}.fd-mono--sm{font-size:.78rem}.fd-nowrap{white-space:nowrap}.fd-reason{max-width:18rem;word-break:break-word}.fd-empty{margin:0;padding:.75rem 0;color:var(--fd-muted);font-size:.9rem}.fd-empty-cell{padding:.85rem .65rem;color:var(--fd-muted);font-size:.9rem;font-style:italic}.fd-muted{color:var(--fd-muted);font-size:.9rem}.fd-samples{margin:.5rem 0 1rem}.fd-badge{display:inline-block;padding:.15rem .45rem;border-radius:999px;font-size:.72rem;font-weight:700;letter-spacing:.04em}.fd-badge--pass{background:var(--fd-pass-bg);color:var(--fd-pass-fg)}.fd-badge--fail{background:var(--fd-fail-bg);color:var(--fd-fail-fg)}.fd-badge--neutral{background:var(--fd-surface-2);color:var(--fd-muted)}.fd-badge--warn{background:#fff8e1;color:#6d4c00}.fd-btn{font:inherit;cursor:pointer;border-radius:var(--fd-radius-sm);border:1px solid var(--fd-border-strong);background:var(--fd-surface);color:var(--fd-text);padding:.45rem .95rem;font-size:.9rem;font-weight:500}.fd-btn:hover:not(:disabled){background:var(--fd-surface-2)}.fd-btn:disabled{opacity:.55;cursor:not-allowed}.fd-btn--primary{background:var(--fd-accent);border-color:var(--fd-accent);color:#fff}.fd-btn--primary:hover:not(:disabled){background:var(--fd-accent-hover);border-color:var(--fd-accent-hover)}.fd-btn--ghost{background:transparent}.fd-btn--danger{border-color:#b71c1c73;color:#8b1a1a;background:#b71c1c0f}.fd-btn--danger:hover:not(:disabled){background:#b71c1c1f;border-color:#b71c1c8c}.fd-form-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));gap:.85rem 1rem}.fd-field{display:flex;flex-direction:column;gap:.3rem}.fd-field--full{grid-column:1 / -1}.fd-field__label{font-size:.8rem;font-weight:600;color:var(--fd-muted)}.fd-input{font:inherit;padding:.45rem .55rem;border:1px solid var(--fd-border-strong);border-radius:var(--fd-radius-sm);background:var(--fd-surface);color:var(--fd-text)}.fd-input:focus{outline:2px solid rgba(13,110,253,.35);outline-offset:1px}.fd-actions{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1rem}.fd-alert{padding:.65rem .85rem;border-radius:var(--fd-radius-sm);font-size:.9rem}.fd-alert--error{background:var(--fd-fail-bg);color:var(--fd-fail-fg);border:1px solid rgba(183,28,28,.25)}.fd-alert--info{background:#e7f1ff;color:#0d3a66;border:1px solid rgba(13,110,253,.2)}.fd-alert--warn{background:#fff8e1;color:#6d4c00;border:1px solid rgba(180,140,0,.28)}.fd-security-strip{padding:0 1.5rem;margin-top:.5rem}.fd-security-strip__msg{margin:0}.fd-json-panel{margin-top:.5rem}.fd-json-panel__toggle{font:inherit;display:inline-flex;align-items:center;gap:.35rem;padding:.35rem .5rem;margin:0;border:none;background:transparent;color:var(--fd-accent);cursor:pointer;font-size:.88rem;font-weight:500}.fd-json-panel__toggle:hover{text-decoration:underline}.fd-json-panel__chevron{font-size:.7rem;opacity:.85}.fd-json-panel__pre{margin:.5rem 0 0;padding:.75rem;border-radius:var(--fd-radius-sm);border:1px solid var(--fd-border);background:#0d1117;color:#e6edf3;font-family:var(--fd-mono);font-size:.78rem;line-height:1.45;overflow-x:auto;white-space:pre-wrap;word-break:break-word}.fd-metric-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(15rem,1fr));gap:1rem}.fd-metric{padding:.75rem;border:1px solid var(--fd-border);border-radius:var(--fd-radius-sm);background:var(--fd-surface-2)}.fd-metric__label{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--fd-muted);margin-bottom:.45rem}.fd-metric__row{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem .5rem;font-size:.88rem}.fd-metric__tag{display:inline-block;min-width:1.25rem;text-align:center;font-size:.68rem;font-weight:700;border-radius:4px;background:var(--fd-border);color:var(--fd-muted)}.fd-metric__arrow{color:var(--fd-muted)}.fd-metric__delta{margin-top:.4rem;font-size:.82rem;color:var(--fd-muted)}.fd-metric__hint{margin:.5rem 0 0;font-size:.78rem;line-height:1.35;color:var(--fd-muted);font-weight:400;text-transform:none;letter-spacing:normal}.fd-checkbox-label{display:inline-flex;align-items:center;gap:.45rem;font-size:.88rem;color:var(--fd-text);cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap}.fd-checkbox-label input{width:1rem;height:1rem;accent-color:var(--fd-accent)}.fd-trace-groups{display:flex;flex-direction:column;gap:.65rem}.fd-trace-group{border:1px solid var(--fd-border);border-radius:var(--fd-radius-sm);background:var(--fd-surface);padding:.15rem .5rem .5rem}.fd-trace-group__summary{cursor:pointer;font-size:.9rem;padding:.45rem .35rem;list-style-position:outside}.fd-trace-group__summary::-webkit-details-marker{color:var(--fd-muted)}.fd-trace-group .fd-table-wrap{margin:0 -.15rem}.fd-inline{margin-top:.25rem}.fd-reasons{margin:0 0 .75rem;padding-left:1.2rem;font-size:.88rem;color:var(--fd-text)}.fd-reasons li{margin-bottom:.25rem}.fd-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.fd-btn--sm{padding:.28rem .55rem;font-size:.82rem}.fd-card--hint{background:var(--fd-surface-2);border-style:dashed}.fd-empty-state{padding:.5rem 0 1rem}.fd-empty-state__title{margin:0 0 .35rem;font-size:1rem;font-weight:650;color:var(--fd-text)}.fd-empty-state__body{margin:0;font-size:.9rem;color:var(--fd-muted);max-width:40rem;line-height:1.5}.fd-table__trace-sep td{padding-top:.85rem;padding-bottom:.35rem;font-size:.78rem;color:var(--fd-muted);border-bottom:1px solid var(--fd-border-strong);background:var(--fd-surface-2)}.fd-table__trace-sep-label{font-weight:700;text-transform:uppercase;letter-spacing:.05em;font-size:.68rem}.fd-dl{margin:0 0 1rem;display:grid;gap:.65rem 1rem}.fd-dl>div{display:grid;grid-template-columns:7rem 1fr;gap:.35rem .75rem;align-items:baseline}.fd-dl dt{margin:0;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--fd-muted)}.fd-dl dd{margin:0;min-width:0;word-break:break-word}.fd-drawer-root{position:fixed;inset:0;z-index:50;display:flex;justify-content:flex-end;align-items:stretch}.fd-drawer-backdrop{position:absolute;inset:0;border:none;padding:0;margin:0;background:#0f172a73;cursor:pointer}.fd-drawer{position:relative;width:min(28rem,100vw);max-width:100%;background:var(--fd-surface);border-left:1px solid var(--fd-border-strong);box-shadow:-4px 0 24px #0f172a1f;display:flex;flex-direction:column;min-height:0}.fd-drawer__head{display:flex;align-items:center;justify-content:space-between;gap:.75rem;padding:.85rem 1rem;border-bottom:1px solid var(--fd-border);flex-shrink:0}.fd-drawer__title{margin:0;font-size:1rem;font-weight:650}.fd-drawer__body{padding:1rem;overflow:auto;flex:1;min-height:0}.fd-diff-stack{display:flex;flex-direction:column}.fd-diff-section{padding:.9rem 0;border-top:1px solid var(--fd-border)}.fd-diff-stack>.fd-diff-section:first-child{border-top:none;padding-top:0}.fd-diff-section__title{margin:0 0 .5rem;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--fd-muted)}.fd-diff-section__body{margin:0;font-size:.9rem;line-height:1.5}.fd-dl--inline dt{font-size:.75rem}.fd-dl--inline>div{grid-template-columns:9.5rem 1fr}.fd-loading-panel{padding:.5rem 0 1rem}.fd-skeleton{display:block;height:.82rem;max-width:100%;border-radius:4px;background:linear-gradient(90deg,var(--fd-surface-2) 0%,var(--fd-border) 45%,var(--fd-surface-2) 90%);background-size:200% 100%;animation:fd-skeleton-shimmer 1.1s ease-in-out infinite}.fd-skeleton--w40{width:40%}.fd-skeleton--w60{width:60%}.fd-skeleton--w75{width:75%}.fd-skeleton--mt{margin-top:.55rem}@keyframes fd-skeleton-shimmer{0%{background-position:100% 0}to{background-position:-100% 0}}.fd-inline-nav{margin:.65rem 0 0;font-size:.85rem;color:var(--fd-muted)}.fd-inline-nav a{color:var(--fd-accent);font-weight:600;text-decoration:none}.fd-inline-nav a:hover{text-decoration:underline} diff --git a/src/flightdeck/server/static/assets/index-BKz6lCHf.css b/src/flightdeck/server/static/assets/index-BKz6lCHf.css new file mode 100644 index 0000000..39eecb6 --- /dev/null +++ b/src/flightdeck/server/static/assets/index-BKz6lCHf.css @@ -0,0 +1 @@ +:root{color-scheme:light;--fd-bg: #f3f4f6;--fd-surface: #ffffff;--fd-surface-2: #f8f9fb;--fd-border: #dfe3e8;--fd-border-strong: #c4c9d1;--fd-text: #171a1f;--fd-muted: #525a63;--fd-accent: #0d6efd;--fd-accent-hover: #0b5ed7;--fd-pass-bg: #e8f5e9;--fd-pass-fg: #1b5e20;--fd-fail-bg: #ffebee;--fd-fail-fg: #b71c1c;--fd-radius: 10px;--fd-radius-sm: 6px;--fd-shadow: 0 1px 2px rgba(15, 23, 42, .06);--fd-font: "Segoe UI Variable", "Segoe UI", system-ui, -apple-system, sans-serif;--fd-mono: ui-monospace, "Cascadia Code", "Consolas", monospace;--fd-focus-ring: 0 0 0 2px var(--fd-surface), 0 0 0 4px rgba(13, 110, 253, .45);--fd-type-page-title: 1.375rem;--fd-type-page-title--lh: 1.25;--fd-type-page-sub: .9375rem;--fd-type-page-sub--lh: 1.45;--fd-type-card-title: 1.0625rem;--fd-type-card-title--lh: 1.3;--fd-type-table: .875rem;--fd-type-table--lh: 1.45;--fd-type-table-head: .75rem;--fd-sidebar-width: 15.5rem;font-family:var(--fd-font);line-height:1.5;color:var(--fd-text);background:var(--fd-bg)}*,*:before,*:after{box-sizing:border-box}body{margin:0;min-height:100vh}.fd-shell{position:relative;min-height:100vh;display:flex;flex-direction:row;align-items:stretch}.fd-sidebar{position:sticky;top:0;align-self:flex-start;flex:0 0 var(--fd-sidebar-width);width:var(--fd-sidebar-width);max-width:100%;height:100vh;max-height:100vh;overflow-x:hidden;overflow-y:auto;display:flex;flex-direction:column;gap:1.15rem;padding:1.1rem .75rem 1.5rem;background:var(--fd-surface);border-right:1px solid var(--fd-border);box-shadow:1px 0 #0f172a0a}.fd-sidebar__brand{min-width:0;padding:0 .35rem}.fd-sidebar__title{margin:0;font-size:1.125rem;font-weight:700;letter-spacing:-.02em;line-height:1.2}.fd-sidebar__tagline{margin:.35rem 0 0;font-size:.75rem;line-height:1.35;color:var(--fd-muted)}.fd-sidebar__nav{display:flex;flex-direction:column;gap:.12rem;flex:1;min-height:0}.fd-shell__content{flex:1;min-width:0;display:flex;flex-direction:column;background:var(--fd-bg)}.fd-nav__link{display:block;width:100%;padding:.5rem .75rem;border-radius:var(--fd-radius-sm);font-size:.9rem;font-weight:500;color:var(--fd-muted);text-decoration:none;border:1px solid transparent;border-left:3px solid transparent}@media(prefers-reduced-motion:no-preference){.fd-nav__link{transition:background .14s ease,color .14s ease,border-color .14s ease}}.fd-nav__link:hover{color:var(--fd-text);background:var(--fd-surface-2)}.fd-nav__link:focus{outline:none}.fd-nav__link:focus-visible{box-shadow:var(--fd-focus-ring)}.fd-nav__link--active{color:var(--fd-text);background:var(--fd-surface-2);border-color:var(--fd-border);border-left-color:var(--fd-accent)}.fd-main{flex:1;width:100%;max-width:min(72rem,calc(100vw - 1.5rem));margin:0 auto;padding:1.35rem 1.25rem 2.75rem;outline:none}.fd-page-head{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:.85rem 1rem;margin-bottom:1.5rem}.fd-page-title{margin:0;font-size:var(--fd-type-page-title);line-height:var(--fd-type-page-title--lh);font-weight:650;letter-spacing:-.015em}.fd-page-sub{margin:.4rem 0 0;max-width:52ch;font-size:var(--fd-type-page-sub);line-height:var(--fd-type-page-sub--lh);color:var(--fd-muted)}.fd-card{background:var(--fd-surface);border:1px solid var(--fd-border);border-radius:var(--fd-radius);padding:1.05rem 1.2rem;margin-bottom:1.125rem;box-shadow:var(--fd-shadow)}.fd-card__head{margin-bottom:.8rem}.fd-card__head--row{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:.75rem 1rem}.fd-card__head--row>.fd-btn{flex-shrink:0;align-self:flex-start}.fd-card__title{margin:0;font-size:var(--fd-type-card-title);line-height:var(--fd-type-card-title--lh);font-weight:600;letter-spacing:-.01em}.fd-card__subtitle{margin:0 0 .5rem;font-size:1rem;font-weight:600}.fd-card__desc{margin:.35rem 0 0;font-size:.85rem;color:var(--fd-muted)}.fd-table-wrap{overflow-x:auto;margin:0 -.15rem;-webkit-overflow-scrolling:touch;overscroll-behavior-x:contain;scrollbar-gutter:stable}.fd-table{width:100%;border-collapse:collapse;font-size:var(--fd-type-table);line-height:var(--fd-type-table--lh)}.fd-table th,.fd-table td{text-align:left;padding:.55rem .65rem;border-bottom:1px solid var(--fd-border);vertical-align:top}.fd-table th{font-weight:600;color:var(--fd-muted);font-size:var(--fd-type-table-head);text-transform:uppercase;letter-spacing:.045em}.fd-table tbody tr:last-child td{border-bottom:none}.fd-table tbody tr:hover td{background:var(--fd-surface-2)}@media(prefers-reduced-motion:no-preference){.fd-table tbody tr:hover td{transition:background .12s ease}}.fd-mono{font-family:var(--fd-mono);font-size:.82rem}.fd-mono--sm{font-size:.78rem}.fd-nowrap{white-space:nowrap}.fd-reason{max-width:18rem;word-break:break-word}.fd-empty{margin:0;padding:.75rem 0;color:var(--fd-muted);font-size:.9rem}.fd-empty-cell{padding:.85rem .65rem;color:var(--fd-muted);font-size:.9rem;font-style:italic}.fd-muted{color:var(--fd-muted);font-size:.9rem}.fd-samples{margin:.5rem 0 1rem}.fd-badge{display:inline-block;padding:.15rem .45rem;border-radius:999px;font-size:.72rem;font-weight:700;letter-spacing:.04em}.fd-badge--pass{background:var(--fd-pass-bg);color:var(--fd-pass-fg)}.fd-badge--fail{background:var(--fd-fail-bg);color:var(--fd-fail-fg)}.fd-badge--neutral{background:var(--fd-surface-2);color:var(--fd-muted)}.fd-badge--warn{background:#fff8e1;color:#6d4c00}.fd-btn{font:inherit;cursor:pointer;border-radius:var(--fd-radius-sm);border:1px solid var(--fd-border-strong);background:var(--fd-surface);color:var(--fd-text);padding:.45rem .95rem;font-size:.9rem;font-weight:500}@media(prefers-reduced-motion:no-preference){.fd-btn{transition:background .12s ease,border-color .12s ease,color .12s ease}}.fd-btn:focus{outline:none}.fd-btn:focus-visible{box-shadow:var(--fd-focus-ring)}.fd-btn:hover:not(:disabled){background:var(--fd-surface-2)}.fd-btn:disabled{opacity:.55;cursor:not-allowed}.fd-btn--primary{background:var(--fd-accent);border-color:var(--fd-accent);color:#fff}.fd-btn--primary:hover:not(:disabled){background:var(--fd-accent-hover);border-color:var(--fd-accent-hover)}.fd-btn--primary:focus-visible{box-shadow:var(--fd-focus-ring)}.fd-btn--ghost{background:transparent}.fd-btn--danger{border-color:#b71c1c73;color:#8b1a1a;background:#b71c1c0f}.fd-btn--danger:hover:not(:disabled){background:#b71c1c1f;border-color:#b71c1c8c}.fd-form-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));gap:.85rem 1rem}.fd-field{display:flex;flex-direction:column;gap:.3rem}.fd-field--full{grid-column:1 / -1}.fd-field__label{font-size:.8rem;font-weight:600;color:var(--fd-muted)}.fd-input{font:inherit;padding:.45rem .55rem;border:1px solid var(--fd-border-strong);border-radius:var(--fd-radius-sm);background:var(--fd-surface);color:var(--fd-text)}@media(prefers-reduced-motion:no-preference){.fd-input{transition:border-color .12s ease,box-shadow .12s ease}}.fd-input:focus{outline:none}.fd-input:focus-visible{border-color:var(--fd-accent);box-shadow:0 0 0 1px var(--fd-surface),0 0 0 3px #0d6efd47}.fd-actions{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1rem}.fd-alert{padding:.65rem .85rem;border-radius:var(--fd-radius-sm);font-size:.9rem}.fd-alert--error{background:var(--fd-fail-bg);color:var(--fd-fail-fg);border:1px solid rgba(183,28,28,.25)}.fd-alert--info{background:#e7f1ff;color:#0d3a66;border:1px solid rgba(13,110,253,.2)}.fd-alert--warn{background:#fff8e1;color:#6d4c00;border:1px solid rgba(180,140,0,.28)}.fd-security-strip{flex-shrink:0;padding:.5rem 1.25rem 0;margin-top:0}.fd-security-strip__msg{margin:0}.fd-json-panel{margin-top:.5rem}.fd-json-panel__toggle{font:inherit;display:inline-flex;align-items:center;gap:.35rem;padding:.35rem .5rem;margin:0;border:none;background:transparent;color:var(--fd-accent);cursor:pointer;font-size:.88rem;font-weight:500}.fd-json-panel__toggle:hover{text-decoration:underline}.fd-json-panel__chevron{font-size:.7rem;opacity:.85}.fd-json-panel__pre{margin:.5rem 0 0;padding:.75rem;border-radius:var(--fd-radius-sm);border:1px solid var(--fd-border);background:#0d1117;color:#e6edf3;font-family:var(--fd-mono);font-size:.78rem;line-height:1.45;overflow-x:auto;white-space:pre-wrap;word-break:break-word}.fd-metric-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(15rem,1fr));gap:1rem}.fd-metric{padding:.75rem;border:1px solid var(--fd-border);border-radius:var(--fd-radius-sm);background:var(--fd-surface-2)}.fd-metric__label{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--fd-muted);margin-bottom:.45rem}.fd-metric__row{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem .5rem;font-size:.88rem}.fd-metric__tag{display:inline-block;min-width:1.25rem;text-align:center;font-size:.68rem;font-weight:700;border-radius:4px;background:var(--fd-border);color:var(--fd-muted)}.fd-metric__arrow{color:var(--fd-muted)}.fd-metric__delta{margin-top:.4rem;font-size:.82rem;color:var(--fd-muted)}.fd-metric__hint{margin:.5rem 0 0;font-size:.78rem;line-height:1.35;color:var(--fd-muted);font-weight:400;text-transform:none;letter-spacing:normal}.fd-checkbox-label{display:inline-flex;align-items:center;gap:.45rem;font-size:.88rem;color:var(--fd-text);cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap}.fd-checkbox-label input{width:1rem;height:1rem;accent-color:var(--fd-accent)}.fd-checkbox-label input:focus{outline:none}.fd-checkbox-label input:focus-visible{outline:2px solid rgba(13,110,253,.5);outline-offset:3px}.fd-trace-groups{display:flex;flex-direction:column;gap:.65rem}.fd-trace-group{border:1px solid var(--fd-border);border-radius:var(--fd-radius-sm);background:var(--fd-surface);padding:.15rem .5rem .5rem}.fd-trace-group__summary{cursor:pointer;font-size:.9rem;padding:.45rem .35rem;list-style-position:outside;border-radius:var(--fd-radius-sm)}.fd-trace-group__summary:focus{outline:none}.fd-trace-group__summary:focus-visible{box-shadow:var(--fd-focus-ring)}.fd-trace-group__summary::-webkit-details-marker{color:var(--fd-muted)}.fd-trace-group .fd-table-wrap{margin:0 -.15rem}.fd-inline{margin-top:.25rem}.fd-reasons{margin:0 0 .75rem;padding-left:1.2rem;font-size:.88rem;color:var(--fd-text)}.fd-reasons li{margin-bottom:.25rem}.fd-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.fd-skip-link{position:absolute;left:.75rem;top:.55rem;z-index:200;padding:.45rem .75rem;font-size:.875rem;font-weight:600;font-family:inherit;line-height:1.3;color:var(--fd-text);background:var(--fd-surface);border:1px solid var(--fd-border-strong);border-radius:var(--fd-radius-sm);box-shadow:var(--fd-shadow);text-decoration:none;transform:translateY(-160%);opacity:0;pointer-events:none}.fd-skip-link:focus{outline:none}.fd-skip-link:focus-visible{transform:translateY(0);opacity:1;pointer-events:auto;box-shadow:var(--fd-focus-ring)}@media(prefers-reduced-motion:reduce){.fd-skip-link{transition:none}}@media(prefers-reduced-motion:no-preference){.fd-skip-link{transition:transform .16s ease,opacity .16s ease}}.fd-btn--sm{padding:.28rem .55rem;font-size:.82rem}.fd-card--hint{background:var(--fd-surface-2);border-style:dashed}.fd-empty-state{padding:.5rem 0 1rem}.fd-empty-state__title{margin:0 0 .35rem;font-size:1rem;font-weight:650;color:var(--fd-text)}.fd-empty-state__body{margin:0;font-size:.9rem;color:var(--fd-muted);max-width:40rem;line-height:1.5}.fd-table__trace-sep td{padding-top:.85rem;padding-bottom:.35rem;font-size:.78rem;color:var(--fd-muted);border-bottom:1px solid var(--fd-border-strong);background:var(--fd-surface-2)}.fd-table__trace-sep-label{font-weight:700;text-transform:uppercase;letter-spacing:.05em;font-size:.68rem}.fd-dl{margin:0 0 1rem;display:grid;gap:.65rem 1rem}.fd-dl>div{display:grid;grid-template-columns:7rem 1fr;gap:.35rem .75rem;align-items:baseline}.fd-dl dt{margin:0;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--fd-muted)}.fd-dl dd{margin:0;min-width:0;word-break:break-word}.fd-drawer-root{position:fixed;inset:0;z-index:50;display:flex;justify-content:flex-end;align-items:stretch}.fd-drawer-backdrop{position:absolute;inset:0;border:none;padding:0;margin:0;background:#0f172a73;cursor:pointer}.fd-drawer{position:relative;width:min(28rem,100vw);max-width:100%;background:var(--fd-surface);border-left:1px solid var(--fd-border-strong);box-shadow:-4px 0 24px #0f172a1f;display:flex;flex-direction:column;min-height:0}.fd-drawer__head{display:flex;align-items:center;justify-content:space-between;gap:.75rem;padding:.85rem 1rem;border-bottom:1px solid var(--fd-border);flex-shrink:0}.fd-drawer__title{margin:0;font-size:1rem;font-weight:650}.fd-drawer__body{padding:1rem;overflow:auto;flex:1;min-height:0}.fd-diff-stack{display:flex;flex-direction:column}.fd-diff-section{padding:.9rem 0;border-top:1px solid var(--fd-border)}.fd-diff-stack>.fd-diff-section:first-child{border-top:none;padding-top:0}.fd-diff-section__title{margin:0 0 .5rem;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--fd-muted)}.fd-diff-section__body{margin:0;font-size:.9rem;line-height:1.5}.fd-dl--inline dt{font-size:.75rem}.fd-dl--inline>div{grid-template-columns:9.5rem 1fr}.fd-loading-panel{padding:.5rem 0 1rem}.fd-skeleton{display:block;height:.82rem;max-width:100%;border-radius:4px;background:linear-gradient(90deg,var(--fd-surface-2) 0%,var(--fd-border) 45%,var(--fd-surface-2) 90%);background-size:200% 100%;animation:fd-skeleton-shimmer 1.1s ease-in-out infinite}.fd-skeleton--w40{width:40%}.fd-skeleton--w60{width:60%}.fd-skeleton--w75{width:75%}.fd-skeleton--mt{margin-top:.55rem}@keyframes fd-skeleton-shimmer{0%{background-position:100% 0}to{background-position:-100% 0}}@media(prefers-reduced-motion:reduce){.fd-skeleton{animation:none;background:var(--fd-surface-2)}}.fd-inline-nav{margin:.65rem 0 0;font-size:.85rem;color:var(--fd-muted)}.fd-inline-nav a{color:var(--fd-accent);font-weight:600;text-decoration:none}.fd-inline-nav a:hover{text-decoration:underline}@media(max-width:42rem){.fd-shell{flex-direction:column}.fd-sidebar{position:relative;top:auto;align-self:stretch;flex:none;width:100%;height:auto;max-height:none;overflow:visible;border-right:none;border-bottom:1px solid var(--fd-border);box-shadow:none;padding:.75rem 1rem .85rem;gap:.65rem}.fd-sidebar__nav{flex-direction:row;flex-wrap:wrap;gap:.25rem}.fd-sidebar__nav .fd-nav__link{width:auto;flex:0 1 auto}.fd-sidebar__nav .fd-nav__link--active{border-color:transparent;border-left-color:var(--fd-accent)}.fd-security-strip{padding-inline:1rem}.fd-main{padding-inline:1rem;max-width:100%}.fd-table th,.fd-table td{overflow-wrap:anywhere;word-break:break-word}.fd-checkbox-label{white-space:normal}} diff --git a/src/flightdeck/server/static/assets/index-BZgyAhPX.js b/src/flightdeck/server/static/assets/index-BZgyAhPX.js deleted file mode 100644 index fccbd44..0000000 --- a/src/flightdeck/server/static/assets/index-BZgyAhPX.js +++ /dev/null @@ -1,11 +0,0 @@ -(function(){const r=document.createElement("link").relList;if(r&&r.supports&&r.supports("modulepreload"))return;for(const m of document.querySelectorAll('link[rel="modulepreload"]'))f(m);new MutationObserver(m=>{for(const h of m)if(h.type==="childList")for(const _ of h.addedNodes)_.tagName==="LINK"&&_.rel==="modulepreload"&&f(_)}).observe(document,{childList:!0,subtree:!0});function d(m){const h={};return m.integrity&&(h.integrity=m.integrity),m.referrerPolicy&&(h.referrerPolicy=m.referrerPolicy),m.crossOrigin==="use-credentials"?h.credentials="include":m.crossOrigin==="anonymous"?h.credentials="omit":h.credentials="same-origin",h}function f(m){if(m.ep)return;m.ep=!0;const h=d(m);fetch(m.href,h)}})();var ws={exports:{}},wn={};var dm;function Ey(){if(dm)return wn;dm=1;var i=Symbol.for("react.transitional.element"),r=Symbol.for("react.fragment");function d(f,m,h){var _=null;if(h!==void 0&&(_=""+h),m.key!==void 0&&(_=""+m.key),"key"in m){h={};for(var R in m)R!=="key"&&(h[R]=m[R])}else h=m;return m=h.ref,{$$typeof:i,type:f,key:_,ref:m!==void 0?m:null,props:h}}return wn.Fragment=r,wn.jsx=d,wn.jsxs=d,wn}var mm;function Ty(){return mm||(mm=1,ws.exports=Ey()),ws.exports}var c=Ty(),Ys={exports:{}},ne={};var hm;function Ry(){if(hm)return ne;hm=1;var i=Symbol.for("react.transitional.element"),r=Symbol.for("react.portal"),d=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),m=Symbol.for("react.profiler"),h=Symbol.for("react.consumer"),_=Symbol.for("react.context"),R=Symbol.for("react.forward_ref"),S=Symbol.for("react.suspense"),y=Symbol.for("react.memo"),O=Symbol.for("react.lazy"),E=Symbol.for("react.activity"),Y=Symbol.iterator;function L(b){return b===null||typeof b!="object"?null:(b=Y&&b[Y]||b["@@iterator"],typeof b=="function"?b:null)}var Z={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},V=Object.assign,B={};function J(b,M,Q){this.props=b,this.context=M,this.refs=B,this.updater=Q||Z}J.prototype.isReactComponent={},J.prototype.setState=function(b,M){if(typeof b!="object"&&typeof b!="function"&&b!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,b,M,"setState")},J.prototype.forceUpdate=function(b){this.updater.enqueueForceUpdate(this,b,"forceUpdate")};function I(){}I.prototype=J.prototype;function W(b,M,Q){this.props=b,this.context=M,this.refs=B,this.updater=Q||Z}var X=W.prototype=new I;X.constructor=W,V(X,J.prototype),X.isPureReactComponent=!0;var ue=Array.isArray;function me(){}var q={H:null,A:null,T:null,S:null},le=Object.prototype.hasOwnProperty;function Ae(b,M,Q){var K=Q.ref;return{$$typeof:i,type:b,key:M,ref:K!==void 0?K:null,props:Q}}function qe(b,M){return Ae(b.type,M,b.props)}function je(b){return typeof b=="object"&&b!==null&&b.$$typeof===i}function ve(b){var M={"=":"=0",":":"=2"};return"$"+b.replace(/[=:]/g,function(Q){return M[Q]})}var De=/\/+/g;function Be(b,M){return typeof b=="object"&&b!==null&&b.key!=null?ve(""+b.key):M.toString(36)}function ze(b){switch(b.status){case"fulfilled":return b.value;case"rejected":throw b.reason;default:switch(typeof b.status=="string"?b.then(me,me):(b.status="pending",b.then(function(M){b.status==="pending"&&(b.status="fulfilled",b.value=M)},function(M){b.status==="pending"&&(b.status="rejected",b.reason=M)})),b.status){case"fulfilled":return b.value;case"rejected":throw b.reason}}throw b}function A(b,M,Q,K,U){var k=typeof b;(k==="undefined"||k==="boolean")&&(b=null);var w=!1;if(b===null)w=!0;else switch(k){case"bigint":case"string":case"number":w=!0;break;case"object":switch(b.$$typeof){case i:case r:w=!0;break;case O:return w=b._init,A(w(b._payload),M,Q,K,U)}}if(w)return U=U(b),w=K===""?"."+Be(b,0):K,ue(U)?(Q="",w!=null&&(Q=w.replace(De,"$&/")+"/"),A(U,M,Q,"",function(Me){return Me})):U!=null&&(je(U)&&(U=qe(U,Q+(U.key==null||b&&b.key===U.key?"":(""+U.key).replace(De,"$&/")+"/")+w)),M.push(U)),1;w=0;var ae=K===""?".":K+":";if(ue(b))for(var ce=0;ce>>1,pe=A[ye];if(0>>1;yem(Q,ee))Km(U,Q)?(A[ye]=U,A[K]=ee,ye=K):(A[ye]=Q,A[M]=ee,ye=M);else if(Km(U,ee))A[ye]=U,A[K]=ee,ye=K;else break e}}return G}function m(A,G){var ee=A.sortIndex-G.sortIndex;return ee!==0?ee:A.id-G.id}if(i.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var h=performance;i.unstable_now=function(){return h.now()}}else{var _=Date,R=_.now();i.unstable_now=function(){return _.now()-R}}var S=[],y=[],O=1,E=null,Y=3,L=!1,Z=!1,V=!1,B=!1,J=typeof setTimeout=="function"?setTimeout:null,I=typeof clearTimeout=="function"?clearTimeout:null,W=typeof setImmediate<"u"?setImmediate:null;function X(A){for(var G=d(y);G!==null;){if(G.callback===null)f(y);else if(G.startTime<=A)f(y),G.sortIndex=G.expirationTime,r(S,G);else break;G=d(y)}}function ue(A){if(V=!1,X(A),!Z)if(d(S)!==null)Z=!0,me||(me=!0,ve());else{var G=d(y);G!==null&&ze(ue,G.startTime-A)}}var me=!1,q=-1,le=5,Ae=-1;function qe(){return B?!0:!(i.unstable_now()-AeA&&qe());){var ye=E.callback;if(typeof ye=="function"){E.callback=null,Y=E.priorityLevel;var pe=ye(E.expirationTime<=A);if(A=i.unstable_now(),typeof pe=="function"){E.callback=pe,X(A),G=!0;break t}E===d(S)&&f(S),X(A)}else f(S);E=d(S)}if(E!==null)G=!0;else{var b=d(y);b!==null&&ze(ue,b.startTime-A),G=!1}}break e}finally{E=null,Y=ee,L=!1}G=void 0}}finally{G?ve():me=!1}}}var ve;if(typeof W=="function")ve=function(){W(je)};else if(typeof MessageChannel<"u"){var De=new MessageChannel,Be=De.port2;De.port1.onmessage=je,ve=function(){Be.postMessage(null)}}else ve=function(){J(je,0)};function ze(A,G){q=J(function(){A(i.unstable_now())},G)}i.unstable_IdlePriority=5,i.unstable_ImmediatePriority=1,i.unstable_LowPriority=4,i.unstable_NormalPriority=3,i.unstable_Profiling=null,i.unstable_UserBlockingPriority=2,i.unstable_cancelCallback=function(A){A.callback=null},i.unstable_forceFrameRate=function(A){0>A||125ye?(A.sortIndex=ee,r(y,A),d(S)===null&&A===d(y)&&(V?(I(q),q=-1):V=!0,ze(ue,ee-ye))):(A.sortIndex=pe,r(S,A),Z||L||(Z=!0,me||(me=!0,ve()))),A},i.unstable_shouldYield=qe,i.unstable_wrapCallback=function(A){var G=Y;return function(){var ee=Y;Y=G;try{return A.apply(this,arguments)}finally{Y=ee}}}})(Qs)),Qs}var pm;function zy(){return pm||(pm=1,Xs.exports=Ay()),Xs.exports}var Zs={exports:{}},et={};var gm;function Oy(){if(gm)return et;gm=1;var i=ef();function r(S){var y="https://react.dev/errors/"+S;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(r){console.error(r)}}return i(),Zs.exports=Oy(),Zs.exports}var _m;function Dy(){if(_m)return Yn;_m=1;var i=zy(),r=ef(),d=Cy();function f(e){var t="https://react.dev/errors/"+e;if(1pe||(e.current=ye[pe],ye[pe]=null,pe--)}function Q(e,t){pe++,ye[pe]=e.current,e.current=t}var K=b(null),U=b(null),k=b(null),w=b(null);function ae(e,t){switch(Q(k,t),Q(U,e),Q(K,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Hd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Hd(t),e=qd(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}M(K),Q(K,e)}function ce(){M(K),M(U),M(k)}function Me(e){e.memoizedState!==null&&Q(w,e);var t=K.current,l=qd(t,e.type);t!==l&&(Q(U,e),Q(K,l))}function St(e){U.current===e&&(M(K),M(U)),w.current===e&&(M(w),Hn._currentValue=ee)}var lt,Zt;function Hl(e){if(lt===void 0)try{throw Error()}catch(l){var t=l.stack.trim().match(/\n( *(at )?)/);lt=t&&t[1]||"",Zt=-1)":-1n||v[a]!==N[n]){var C=` -`+v[a].replace(" at new "," at ");return e.displayName&&C.includes("")&&(C=C.replace("",e.displayName)),C}while(1<=a&&0<=n);break}}}finally{xi=!1,Error.prepareStackTrace=l}return(l=e?e.displayName||e.name:"")?Hl(l):""}function lh(e,t){switch(e.tag){case 26:case 27:case 5:return Hl(e.type);case 16:return Hl("Lazy");case 13:return e.child!==t&&t!==null?Hl("Suspense Fallback"):Hl("Suspense");case 19:return Hl("SuspenseList");case 0:case 15:return ji(e.type,!1);case 11:return ji(e.type.render,!1);case 1:return ji(e.type,!0);case 31:return Hl("Activity");default:return""}}function of(e){try{var t="",l=null;do t+=lh(e,l),l=e,e=e.return;while(e);return t}catch(a){return` -Error generating stack: `+a.message+` -`+a.stack}}var Ni=Object.prototype.hasOwnProperty,Ei=i.unstable_scheduleCallback,Ti=i.unstable_cancelCallback,ah=i.unstable_shouldYield,nh=i.unstable_requestPaint,rt=i.unstable_now,uh=i.unstable_getCurrentPriorityLevel,df=i.unstable_ImmediatePriority,mf=i.unstable_UserBlockingPriority,Jn=i.unstable_NormalPriority,ih=i.unstable_LowPriority,hf=i.unstable_IdlePriority,ch=i.log,sh=i.unstable_setDisableYieldValue,Ja=null,ot=null;function rl(e){if(typeof ch=="function"&&sh(e),ot&&typeof ot.setStrictMode=="function")try{ot.setStrictMode(Ja,e)}catch{}}var dt=Math.clz32?Math.clz32:oh,fh=Math.log,rh=Math.LN2;function oh(e){return e>>>=0,e===0?32:31-(fh(e)/rh|0)|0}var Kn=256,kn=262144,$n=4194304;function ql(e){var t=e&42;if(t!==0)return t;switch(e&-e){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 e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Wn(e,t,l){var a=e.pendingLanes;if(a===0)return 0;var n=0,u=e.suspendedLanes,s=e.pingedLanes;e=e.warmLanes;var o=a&134217727;return o!==0?(a=o&~u,a!==0?n=ql(a):(s&=o,s!==0?n=ql(s):l||(l=o&~e,l!==0&&(n=ql(l))))):(o=a&~u,o!==0?n=ql(o):s!==0?n=ql(s):l||(l=a&~e,l!==0&&(n=ql(l)))),n===0?0:t!==0&&t!==n&&(t&u)===0&&(u=n&-n,l=t&-t,u>=l||u===32&&(l&4194048)!==0)?t:n}function Ka(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function dh(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+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 t+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 vf(){var e=$n;return $n<<=1,($n&62914560)===0&&($n=4194304),e}function Ri(e){for(var t=[],l=0;31>l;l++)t.push(e);return t}function ka(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function mh(e,t,l,a,n,u){var s=e.pendingLanes;e.pendingLanes=l,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=l,e.entangledLanes&=l,e.errorRecoveryDisabledLanes&=l,e.shellSuspendCounter=0;var o=e.entanglements,v=e.expirationTimes,N=e.hiddenUpdates;for(l=s&~l;0"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var bh=/[\n"\\]/g;function jt(e){return e.replace(bh,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function Mi(e,t,l,a,n,u,s,o){e.name="",s!=null&&typeof s!="function"&&typeof s!="symbol"&&typeof s!="boolean"?e.type=s:e.removeAttribute("type"),t!=null?s==="number"?(t===0&&e.value===""||e.value!=t)&&(e.value=""+xt(t)):e.value!==""+xt(t)&&(e.value=""+xt(t)):s!=="submit"&&s!=="reset"||e.removeAttribute("value"),t!=null?Ui(e,s,xt(t)):l!=null?Ui(e,s,xt(l)):a!=null&&e.removeAttribute("value"),n==null&&u!=null&&(e.defaultChecked=!!u),n!=null&&(e.checked=n&&typeof n!="function"&&typeof n!="symbol"),o!=null&&typeof o!="function"&&typeof o!="symbol"&&typeof o!="boolean"?e.name=""+xt(o):e.removeAttribute("name")}function Af(e,t,l,a,n,u,s,o){if(u!=null&&typeof u!="function"&&typeof u!="symbol"&&typeof u!="boolean"&&(e.type=u),t!=null||l!=null){if(!(u!=="submit"&&u!=="reset"||t!=null)){Di(e);return}l=l!=null?""+xt(l):"",t=t!=null?""+xt(t):l,o||t===e.value||(e.value=t),e.defaultValue=t}a=a??n,a=typeof a!="function"&&typeof a!="symbol"&&!!a,e.checked=o?e.checked:!!a,e.defaultChecked=!!a,s!=null&&typeof s!="function"&&typeof s!="symbol"&&typeof s!="boolean"&&(e.name=s),Di(e)}function Ui(e,t,l){t==="number"&&Pn(e.ownerDocument)===e||e.defaultValue===""+l||(e.defaultValue=""+l)}function sa(e,t,l,a){if(e=e.options,t){t={};for(var n=0;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),wi=!1;if(Kt)try{var Ia={};Object.defineProperty(Ia,"passive",{get:function(){wi=!0}}),window.addEventListener("test",Ia,Ia),window.removeEventListener("test",Ia,Ia)}catch{wi=!1}var dl=null,Yi=null,tu=null;function Hf(){if(tu)return tu;var e,t=Yi,l=t.length,a,n="value"in dl?dl.value:dl.textContent,u=n.length;for(e=0;e=tn),Gf=" ",Xf=!1;function Qf(e,t){switch(e){case"keyup":return Kh.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Zf(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var da=!1;function $h(e,t){switch(e){case"compositionend":return Zf(t);case"keypress":return t.which!==32?null:(Xf=!0,Gf);case"textInput":return e=t.data,e===Gf&&Xf?null:e;default:return null}}function Wh(e,t){if(da)return e==="compositionend"||!Vi&&Qf(e,t)?(e=Hf(),tu=Yi=dl=null,da=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:l,offset:t-e};e=a}e:{for(;l;){if(l.nextSibling){l=l.nextSibling;break e}l=l.parentNode}l=void 0}l=If(l)}}function er(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?er(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function tr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Pn(e.document);t instanceof e.HTMLIFrameElement;){try{var l=typeof t.contentWindow.location.href=="string"}catch{l=!1}if(l)e=t.contentWindow;else break;t=Pn(e.document)}return t}function ki(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}var nv=Kt&&"documentMode"in document&&11>=document.documentMode,ma=null,$i=null,un=null,Wi=!1;function lr(e,t,l){var a=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;Wi||ma==null||ma!==Pn(a)||(a=ma,"selectionStart"in a&&ki(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),un&&nn(un,a)||(un=a,a=ku($i,"onSelect"),0>=s,n-=s,wt=1<<32-dt(t)+n|l<se?(de=F,F=null):de=F.sibling;var be=T(x,F,j[se],D);if(be===null){F===null&&(F=de);break}e&&F&&be.alternate===null&&t(x,F),p=u(be,p,se),ge===null?P=be:ge.sibling=be,ge=be,F=de}if(se===j.length)return l(x,F),he&&$t(x,se),P;if(F===null){for(;sese?(de=F,F=null):de=F.sibling;var Ul=T(x,F,be.value,D);if(Ul===null){F===null&&(F=de);break}e&&F&&Ul.alternate===null&&t(x,F),p=u(Ul,p,se),ge===null?P=Ul:ge.sibling=Ul,ge=Ul,F=de}if(be.done)return l(x,F),he&&$t(x,se),P;if(F===null){for(;!be.done;se++,be=j.next())be=H(x,be.value,D),be!==null&&(p=u(be,p,se),ge===null?P=be:ge.sibling=be,ge=be);return he&&$t(x,se),P}for(F=a(F);!be.done;se++,be=j.next())be=z(F,x,se,be.value,D),be!==null&&(e&&be.alternate!==null&&F.delete(be.key===null?se:be.key),p=u(be,p,se),ge===null?P=be:ge.sibling=be,ge=be);return e&&F.forEach(function(Ny){return t(x,Ny)}),he&&$t(x,se),P}function Te(x,p,j,D){if(typeof j=="object"&&j!==null&&j.type===V&&j.key===null&&(j=j.props.children),typeof j=="object"&&j!==null){switch(j.$$typeof){case L:e:{for(var P=j.key;p!==null;){if(p.key===P){if(P=j.type,P===V){if(p.tag===7){l(x,p.sibling),D=n(p,j.props.children),D.return=x,x=D;break e}}else if(p.elementType===P||typeof P=="object"&&P!==null&&P.$$typeof===le&&Kl(P)===p.type){l(x,p.sibling),D=n(p,j.props),dn(D,j),D.return=x,x=D;break e}l(x,p);break}else t(x,p);p=p.sibling}j.type===V?(D=Xl(j.props.children,x.mode,D,j.key),D.return=x,x=D):(D=ou(j.type,j.key,j.props,null,x.mode,D),dn(D,j),D.return=x,x=D)}return s(x);case Z:e:{for(P=j.key;p!==null;){if(p.key===P)if(p.tag===4&&p.stateNode.containerInfo===j.containerInfo&&p.stateNode.implementation===j.implementation){l(x,p.sibling),D=n(p,j.children||[]),D.return=x,x=D;break e}else{l(x,p);break}else t(x,p);p=p.sibling}D=ac(j,x.mode,D),D.return=x,x=D}return s(x);case le:return j=Kl(j),Te(x,p,j,D)}if(ze(j))return $(x,p,j,D);if(ve(j)){if(P=ve(j),typeof P!="function")throw Error(f(150));return j=P.call(j),te(x,p,j,D)}if(typeof j.then=="function")return Te(x,p,gu(j),D);if(j.$$typeof===W)return Te(x,p,hu(x,j),D);bu(x,j)}return typeof j=="string"&&j!==""||typeof j=="number"||typeof j=="bigint"?(j=""+j,p!==null&&p.tag===6?(l(x,p.sibling),D=n(p,j),D.return=x,x=D):(l(x,p),D=lc(j,x.mode,D),D.return=x,x=D),s(x)):l(x,p)}return function(x,p,j,D){try{on=0;var P=Te(x,p,j,D);return Na=null,P}catch(F){if(F===ja||F===yu)throw F;var ge=ht(29,F,null,x.mode);return ge.lanes=D,ge.return=x,ge}}}var $l=Er(!0),Tr=Er(!1),pl=!1;function vc(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function yc(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function gl(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function bl(e,t,l){var a=e.updateQueue;if(a===null)return null;if(a=a.shared,(_e&2)!==0){var n=a.pending;return n===null?t.next=t:(t.next=n.next,n.next=t),a.pending=t,t=ru(e),fr(e,null,l),t}return fu(e,a,t,l),ru(e)}function mn(e,t,l){if(t=t.updateQueue,t!==null&&(t=t.shared,(l&4194048)!==0)){var a=t.lanes;a&=e.pendingLanes,l|=a,t.lanes=l,pf(e,l)}}function pc(e,t){var l=e.updateQueue,a=e.alternate;if(a!==null&&(a=a.updateQueue,l===a)){var n=null,u=null;if(l=l.firstBaseUpdate,l!==null){do{var s={lane:l.lane,tag:l.tag,payload:l.payload,callback:null,next:null};u===null?n=u=s:u=u.next=s,l=l.next}while(l!==null);u===null?n=u=t:u=u.next=t}else n=u=t;l={baseState:a.baseState,firstBaseUpdate:n,lastBaseUpdate:u,shared:a.shared,callbacks:a.callbacks},e.updateQueue=l;return}e=l.lastBaseUpdate,e===null?l.firstBaseUpdate=t:e.next=t,l.lastBaseUpdate=t}var gc=!1;function hn(){if(gc){var e=xa;if(e!==null)throw e}}function vn(e,t,l,a){gc=!1;var n=e.updateQueue;pl=!1;var u=n.firstBaseUpdate,s=n.lastBaseUpdate,o=n.shared.pending;if(o!==null){n.shared.pending=null;var v=o,N=v.next;v.next=null,s===null?u=N:s.next=N,s=v;var C=e.alternate;C!==null&&(C=C.updateQueue,o=C.lastBaseUpdate,o!==s&&(o===null?C.firstBaseUpdate=N:o.next=N,C.lastBaseUpdate=v))}if(u!==null){var H=n.baseState;s=0,C=N=v=null,o=u;do{var T=o.lane&-536870913,z=T!==o.lane;if(z?(oe&T)===T:(a&T)===T){T!==0&&T===Sa&&(gc=!0),C!==null&&(C=C.next={lane:0,tag:o.tag,payload:o.payload,callback:null,next:null});e:{var $=e,te=o;T=t;var Te=l;switch(te.tag){case 1:if($=te.payload,typeof $=="function"){H=$.call(Te,H,T);break e}H=$;break e;case 3:$.flags=$.flags&-65537|128;case 0:if($=te.payload,T=typeof $=="function"?$.call(Te,H,T):$,T==null)break e;H=E({},H,T);break e;case 2:pl=!0}}T=o.callback,T!==null&&(e.flags|=64,z&&(e.flags|=8192),z=n.callbacks,z===null?n.callbacks=[T]:z.push(T))}else z={lane:T,tag:o.tag,payload:o.payload,callback:o.callback,next:null},C===null?(N=C=z,v=H):C=C.next=z,s|=T;if(o=o.next,o===null){if(o=n.shared.pending,o===null)break;z=o,o=z.next,z.next=null,n.lastBaseUpdate=z,n.shared.pending=null}}while(!0);C===null&&(v=H),n.baseState=v,n.firstBaseUpdate=N,n.lastBaseUpdate=C,u===null&&(n.shared.lanes=0),Nl|=s,e.lanes=s,e.memoizedState=H}}function Rr(e,t){if(typeof e!="function")throw Error(f(191,e));e.call(t)}function Ar(e,t){var l=e.callbacks;if(l!==null)for(e.callbacks=null,e=0;eu?u:8;var s=A.T,o={};A.T=o,Bc(e,!1,t,l);try{var v=n(),N=A.S;if(N!==null&&N(o,v),v!==null&&typeof v=="object"&&typeof v.then=="function"){var C=mv(v,a);gn(e,t,C,bt(e))}else gn(e,t,a,bt(e))}catch(H){gn(e,t,{then:function(){},status:"rejected",reason:H},bt())}finally{G.p=u,s!==null&&o.types!==null&&(s.types=o.types),A.T=s}}function bv(){}function Hc(e,t,l,a){if(e.tag!==5)throw Error(f(476));var n=io(e).queue;uo(e,n,t,ee,l===null?bv:function(){return co(e),l(a)})}function io(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:ee,baseState:ee,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Pt,lastRenderedState:ee},next:null};var l={};return t.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Pt,lastRenderedState:l},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function co(e){var t=io(e);t.next===null&&(t=e.alternate.memoizedState),gn(e,t.next.queue,{},bt())}function qc(){return Fe(Hn)}function so(){return Ge().memoizedState}function fo(){return Ge().memoizedState}function _v(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var l=bt();e=gl(l);var a=bl(t,e,l);a!==null&&(ft(a,t,l),mn(a,t,l)),t={cache:oc()},e.payload=t;return}t=t.return}}function Sv(e,t,l){var a=bt();l={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},zu(e)?oo(t,l):(l=ec(e,t,l,a),l!==null&&(ft(l,e,a),mo(l,t,a)))}function ro(e,t,l){var a=bt();gn(e,t,l,a)}function gn(e,t,l,a){var n={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null};if(zu(e))oo(t,n);else{var u=e.alternate;if(e.lanes===0&&(u===null||u.lanes===0)&&(u=t.lastRenderedReducer,u!==null))try{var s=t.lastRenderedState,o=u(s,l);if(n.hasEagerState=!0,n.eagerState=o,mt(o,s))return fu(e,t,n,0),Re===null&&su(),!1}catch{}if(l=ec(e,t,n,a),l!==null)return ft(l,e,a),mo(l,t,a),!0}return!1}function Bc(e,t,l,a){if(a={lane:2,revertLane:vs(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},zu(e)){if(t)throw Error(f(479))}else t=ec(e,l,a,2),t!==null&&ft(t,e,2)}function zu(e){var t=e.alternate;return e===ie||t!==null&&t===ie}function oo(e,t){Ta=xu=!0;var l=e.pending;l===null?t.next=t:(t.next=l.next,l.next=t),e.pending=t}function mo(e,t,l){if((l&4194048)!==0){var a=t.lanes;a&=e.pendingLanes,l|=a,t.lanes=l,pf(e,l)}}var bn={readContext:Fe,use:Eu,useCallback:Le,useContext:Le,useEffect:Le,useImperativeHandle:Le,useLayoutEffect:Le,useInsertionEffect:Le,useMemo:Le,useReducer:Le,useRef:Le,useState:Le,useDebugValue:Le,useDeferredValue:Le,useTransition:Le,useSyncExternalStore:Le,useId:Le,useHostTransitionStatus:Le,useFormState:Le,useActionState:Le,useOptimistic:Le,useMemoCache:Le,useCacheRefresh:Le};bn.useEffectEvent=Le;var ho={readContext:Fe,use:Eu,useCallback:function(e,t){return tt().memoizedState=[e,t===void 0?null:t],e},useContext:Fe,useEffect:Wr,useImperativeHandle:function(e,t,l){l=l!=null?l.concat([e]):null,Ru(4194308,4,eo.bind(null,t,e),l)},useLayoutEffect:function(e,t){return Ru(4194308,4,e,t)},useInsertionEffect:function(e,t){Ru(4,2,e,t)},useMemo:function(e,t){var l=tt();t=t===void 0?null:t;var a=e();if(Wl){rl(!0);try{e()}finally{rl(!1)}}return l.memoizedState=[a,t],a},useReducer:function(e,t,l){var a=tt();if(l!==void 0){var n=l(t);if(Wl){rl(!0);try{l(t)}finally{rl(!1)}}}else n=t;return a.memoizedState=a.baseState=n,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:n},a.queue=e,e=e.dispatch=Sv.bind(null,ie,e),[a.memoizedState,e]},useRef:function(e){var t=tt();return e={current:e},t.memoizedState=e},useState:function(e){e=Oc(e);var t=e.queue,l=ro.bind(null,ie,t);return t.dispatch=l,[e.memoizedState,l]},useDebugValue:Mc,useDeferredValue:function(e,t){var l=tt();return Uc(l,e,t)},useTransition:function(){var e=Oc(!1);return e=uo.bind(null,ie,e.queue,!0,!1),tt().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,l){var a=ie,n=tt();if(he){if(l===void 0)throw Error(f(407));l=l()}else{if(l=t(),Re===null)throw Error(f(349));(oe&127)!==0||Ur(a,t,l)}n.memoizedState=l;var u={value:l,getSnapshot:t};return n.queue=u,Wr(qr.bind(null,a,u,e),[e]),a.flags|=2048,Aa(9,{destroy:void 0},Hr.bind(null,a,u,l,t),null),l},useId:function(){var e=tt(),t=Re.identifierPrefix;if(he){var l=Yt,a=wt;l=(a&~(1<<32-dt(a)-1)).toString(32)+l,t="_"+t+"R_"+l,l=ju++,0<\/script>",u=u.removeChild(u.firstChild);break;case"select":u=typeof a.is=="string"?s.createElement("select",{is:a.is}):s.createElement("select"),a.multiple?u.multiple=!0:a.size&&(u.size=a.size);break;default:u=typeof a.is=="string"?s.createElement(n,{is:a.is}):s.createElement(n)}}u[$e]=t,u[at]=a;e:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)u.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break e;for(;s.sibling===null;){if(s.return===null||s.return===t)break e;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=u;e:switch(Pe(u,n,a),n){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break e;case"img":a=!0;break e;default:a=!1}a&&tl(t)}}return Ce(t),Fc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,l),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==a&&tl(t);else{if(typeof a!="string"&&t.stateNode===null)throw Error(f(166));if(e=k.current,ba(t)){if(e=t.stateNode,l=t.memoizedProps,a=null,n=We,n!==null)switch(n.tag){case 27:case 5:a=n.memoizedProps}e[$e]=t,e=!!(e.nodeValue===l||a!==null&&a.suppressHydrationWarning===!0||Md(e.nodeValue,l)),e||vl(t,!0)}else e=$u(e).createTextNode(a),e[$e]=t,t.stateNode=e}return Ce(t),null;case 31:if(l=t.memoizedState,e===null||e.memoizedState!==null){if(a=ba(t),l!==null){if(e===null){if(!a)throw Error(f(318));if(e=t.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(f(557));e[$e]=t}else Ql(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;Ce(t),e=!1}else l=cc(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=l),e=!0;if(!e)return t.flags&256?(yt(t),t):(yt(t),null);if((t.flags&128)!==0)throw Error(f(558))}return Ce(t),null;case 13:if(a=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(n=ba(t),a!==null&&a.dehydrated!==null){if(e===null){if(!n)throw Error(f(318));if(n=t.memoizedState,n=n!==null?n.dehydrated:null,!n)throw Error(f(317));n[$e]=t}else Ql(),(t.flags&128)===0&&(t.memoizedState=null),t.flags|=4;Ce(t),n=!1}else n=cc(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),n=!0;if(!n)return t.flags&256?(yt(t),t):(yt(t),null)}return yt(t),(t.flags&128)!==0?(t.lanes=l,t):(l=a!==null,e=e!==null&&e.memoizedState!==null,l&&(a=t.child,n=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(n=a.alternate.memoizedState.cachePool.pool),u=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(u=a.memoizedState.cachePool.pool),u!==n&&(a.flags|=2048)),l!==e&&l&&(t.child.flags|=8192),Uu(t,t.updateQueue),Ce(t),null);case 4:return ce(),e===null&&bs(t.stateNode.containerInfo),Ce(t),null;case 10:return Ft(t.type),Ce(t),null;case 19:if(M(Ye),a=t.memoizedState,a===null)return Ce(t),null;if(n=(t.flags&128)!==0,u=a.rendering,u===null)if(n)Sn(a,!1);else{if(we!==0||e!==null&&(e.flags&128)!==0)for(e=t.child;e!==null;){if(u=Su(e),u!==null){for(t.flags|=128,Sn(a,!1),e=u.updateQueue,t.updateQueue=e,Uu(t,e),t.subtreeFlags=0,e=l,l=t.child;l!==null;)rr(l,e),l=l.sibling;return Q(Ye,Ye.current&1|2),he&&$t(t,a.treeForkCount),t.child}e=e.sibling}a.tail!==null&&rt()>wu&&(t.flags|=128,n=!0,Sn(a,!1),t.lanes=4194304)}else{if(!n)if(e=Su(u),e!==null){if(t.flags|=128,n=!0,e=e.updateQueue,t.updateQueue=e,Uu(t,e),Sn(a,!0),a.tail===null&&a.tailMode==="hidden"&&!u.alternate&&!he)return Ce(t),null}else 2*rt()-a.renderingStartTime>wu&&l!==536870912&&(t.flags|=128,n=!0,Sn(a,!1),t.lanes=4194304);a.isBackwards?(u.sibling=t.child,t.child=u):(e=a.last,e!==null?e.sibling=u:t.child=u,a.last=u)}return a.tail!==null?(e=a.tail,a.rendering=e,a.tail=e.sibling,a.renderingStartTime=rt(),e.sibling=null,l=Ye.current,Q(Ye,n?l&1|2:l&1),he&&$t(t,a.treeForkCount),e):(Ce(t),null);case 22:case 23:return yt(t),_c(),a=t.memoizedState!==null,e!==null?e.memoizedState!==null!==a&&(t.flags|=8192):a&&(t.flags|=8192),a?(l&536870912)!==0&&(t.flags&128)===0&&(Ce(t),t.subtreeFlags&6&&(t.flags|=8192)):Ce(t),l=t.updateQueue,l!==null&&Uu(t,l.retryQueue),l=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(l=e.memoizedState.cachePool.pool),a=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(a=t.memoizedState.cachePool.pool),a!==l&&(t.flags|=2048),e!==null&&M(Jl),null;case 24:return l=null,e!==null&&(l=e.memoizedState.cache),t.memoizedState.cache!==l&&(t.flags|=2048),Ft(Xe),Ce(t),null;case 25:return null;case 30:return null}throw Error(f(156,t.tag))}function Tv(e,t){switch(uc(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Ft(Xe),ce(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return St(t),null;case 31:if(t.memoizedState!==null){if(yt(t),t.alternate===null)throw Error(f(340));Ql()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(yt(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(f(340));Ql()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return M(Ye),null;case 4:return ce(),null;case 10:return Ft(t.type),null;case 22:case 23:return yt(t),_c(),e!==null&&M(Jl),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Ft(Xe),null;case 25:return null;default:return null}}function Lo(e,t){switch(uc(t),t.tag){case 3:Ft(Xe),ce();break;case 26:case 27:case 5:St(t);break;case 4:ce();break;case 31:t.memoizedState!==null&&yt(t);break;case 13:yt(t);break;case 19:M(Ye);break;case 10:Ft(t.type);break;case 22:case 23:yt(t),_c(),e!==null&&M(Jl);break;case 24:Ft(Xe)}}function xn(e,t){try{var l=t.updateQueue,a=l!==null?l.lastEffect:null;if(a!==null){var n=a.next;l=n;do{if((l.tag&e)===e){a=void 0;var u=l.create,s=l.inst;a=u(),s.destroy=a}l=l.next}while(l!==n)}}catch(o){xe(t,t.return,o)}}function xl(e,t,l){try{var a=t.updateQueue,n=a!==null?a.lastEffect:null;if(n!==null){var u=n.next;a=u;do{if((a.tag&e)===e){var s=a.inst,o=s.destroy;if(o!==void 0){s.destroy=void 0,n=t;var v=l,N=o;try{N()}catch(C){xe(n,v,C)}}}a=a.next}while(a!==u)}}catch(C){xe(t,t.return,C)}}function wo(e){var t=e.updateQueue;if(t!==null){var l=e.stateNode;try{Ar(t,l)}catch(a){xe(e,e.return,a)}}}function Yo(e,t,l){l.props=Fl(e.type,e.memoizedProps),l.state=e.memoizedState;try{l.componentWillUnmount()}catch(a){xe(e,t,a)}}function jn(e,t){try{var l=e.ref;if(l!==null){switch(e.tag){case 26:case 27:case 5:var a=e.stateNode;break;case 30:a=e.stateNode;break;default:a=e.stateNode}typeof l=="function"?e.refCleanup=l(a):l.current=a}}catch(n){xe(e,t,n)}}function Gt(e,t){var l=e.ref,a=e.refCleanup;if(l!==null)if(typeof a=="function")try{a()}catch(n){xe(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof l=="function")try{l(null)}catch(n){xe(e,t,n)}else l.current=null}function Go(e){var t=e.type,l=e.memoizedProps,a=e.stateNode;try{e:switch(t){case"button":case"input":case"select":case"textarea":l.autoFocus&&a.focus();break e;case"img":l.src?a.src=l.src:l.srcSet&&(a.srcset=l.srcSet)}}catch(n){xe(e,e.return,n)}}function Ic(e,t,l){try{var a=e.stateNode;kv(a,e.type,l,t),a[at]=t}catch(n){xe(e,e.return,n)}}function Xo(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&zl(e.type)||e.tag===4}function Pc(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Xo(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&zl(e.type)||e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function es(e,t,l){var a=e.tag;if(a===5||a===6)e=e.stateNode,t?(l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l).insertBefore(e,t):(t=l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l,t.appendChild(e),l=l._reactRootContainer,l!=null||t.onclick!==null||(t.onclick=Jt));else if(a!==4&&(a===27&&zl(e.type)&&(l=e.stateNode,t=null),e=e.child,e!==null))for(es(e,t,l),e=e.sibling;e!==null;)es(e,t,l),e=e.sibling}function Hu(e,t,l){var a=e.tag;if(a===5||a===6)e=e.stateNode,t?l.insertBefore(e,t):l.appendChild(e);else if(a!==4&&(a===27&&zl(e.type)&&(l=e.stateNode),e=e.child,e!==null))for(Hu(e,t,l),e=e.sibling;e!==null;)Hu(e,t,l),e=e.sibling}function Qo(e){var t=e.stateNode,l=e.memoizedProps;try{for(var a=e.type,n=t.attributes;n.length;)t.removeAttributeNode(n[0]);Pe(t,a,l),t[$e]=e,t[at]=l}catch(u){xe(e,e.return,u)}}var ll=!1,Ve=!1,ts=!1,Zo=typeof WeakSet=="function"?WeakSet:Set,ke=null;function Rv(e,t){if(e=e.containerInfo,xs=li,e=tr(e),ki(e)){if("selectionStart"in e)var l={start:e.selectionStart,end:e.selectionEnd};else e:{l=(l=e.ownerDocument)&&l.defaultView||window;var a=l.getSelection&&l.getSelection();if(a&&a.rangeCount!==0){l=a.anchorNode;var n=a.anchorOffset,u=a.focusNode;a=a.focusOffset;try{l.nodeType,u.nodeType}catch{l=null;break e}var s=0,o=-1,v=-1,N=0,C=0,H=e,T=null;t:for(;;){for(var z;H!==l||n!==0&&H.nodeType!==3||(o=s+n),H!==u||a!==0&&H.nodeType!==3||(v=s+a),H.nodeType===3&&(s+=H.nodeValue.length),(z=H.firstChild)!==null;)T=H,H=z;for(;;){if(H===e)break t;if(T===l&&++N===n&&(o=s),T===u&&++C===a&&(v=s),(z=H.nextSibling)!==null)break;H=T,T=H.parentNode}H=z}l=o===-1||v===-1?null:{start:o,end:v}}else l=null}l=l||{start:0,end:0}}else l=null;for(js={focusedElem:e,selectionRange:l},li=!1,ke=t;ke!==null;)if(t=ke,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,ke=e;else for(;ke!==null;){switch(t=ke,u=t.alternate,e=t.flags,t.tag){case 0:if((e&4)!==0&&(e=t.updateQueue,e=e!==null?e.events:null,e!==null))for(l=0;l title"))),Pe(u,a,l),u[$e]=e,Ke(u),a=u;break e;case"link":var s=Wd("link","href",n).get(a+(l.href||""));if(s){for(var o=0;oTe&&(s=Te,Te=te,te=s);var x=Pf(o,te),p=Pf(o,Te);if(x&&p&&(z.rangeCount!==1||z.anchorNode!==x.node||z.anchorOffset!==x.offset||z.focusNode!==p.node||z.focusOffset!==p.offset)){var j=H.createRange();j.setStart(x.node,x.offset),z.removeAllRanges(),te>Te?(z.addRange(j),z.extend(p.node,p.offset)):(j.setEnd(p.node,p.offset),z.addRange(j))}}}}for(H=[],z=o;z=z.parentNode;)z.nodeType===1&&H.push({element:z,left:z.scrollLeft,top:z.scrollTop});for(typeof o.focus=="function"&&o.focus(),o=0;ol?32:l,A.T=null,l=ss,ss=null;var u=Tl,s=cl;if(Je=0,Ma=Tl=null,cl=0,(_e&6)!==0)throw Error(f(331));var o=_e;if(_e|=4,td(u.current),Io(u,u.current,s,l),_e=o,zn(0,!1),ot&&typeof ot.onPostCommitFiberRoot=="function")try{ot.onPostCommitFiberRoot(Ja,u)}catch{}return!0}finally{G.p=n,A.T=a,bd(e,t)}}function Sd(e,t,l){t=Et(l,t),t=Gc(e.stateNode,t,2),e=bl(e,t,2),e!==null&&(ka(e,2),Xt(e))}function xe(e,t,l){if(e.tag===3)Sd(e,e,l);else for(;t!==null;){if(t.tag===3){Sd(t,e,l);break}else if(t.tag===1){var a=t.stateNode;if(typeof t.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(El===null||!El.has(a))){e=Et(l,e),l=xo(2),a=bl(t,l,2),a!==null&&(jo(l,a,t,e),ka(a,2),Xt(a));break}}t=t.return}}function ds(e,t,l){var a=e.pingCache;if(a===null){a=e.pingCache=new Ov;var n=new Set;a.set(t,n)}else n=a.get(t),n===void 0&&(n=new Set,a.set(t,n));n.has(l)||(ns=!0,n.add(l),e=Hv.bind(null,e,t,l),t.then(e,e))}function Hv(e,t,l){var a=e.pingCache;a!==null&&a.delete(t),e.pingedLanes|=e.suspendedLanes&l,e.warmLanes&=~l,Re===e&&(oe&l)===l&&(we===4||we===3&&(oe&62914560)===oe&&300>rt()-Lu?(_e&2)===0&&Ua(e,0):us|=l,Da===oe&&(Da=0)),Xt(e)}function xd(e,t){t===0&&(t=vf()),e=Gl(e,t),e!==null&&(ka(e,t),Xt(e))}function qv(e){var t=e.memoizedState,l=0;t!==null&&(l=t.retryLane),xd(e,l)}function Bv(e,t){var l=0;switch(e.tag){case 31:case 13:var a=e.stateNode,n=e.memoizedState;n!==null&&(l=n.retryLane);break;case 19:a=e.stateNode;break;case 22:a=e.stateNode._retryCache;break;default:throw Error(f(314))}a!==null&&a.delete(t),xd(e,l)}function Lv(e,t){return Ei(e,t)}var Vu=null,qa=null,ms=!1,Ju=!1,hs=!1,Al=0;function Xt(e){e!==qa&&e.next===null&&(qa===null?Vu=qa=e:qa=qa.next=e),Ju=!0,ms||(ms=!0,Yv())}function zn(e,t){if(!hs&&Ju){hs=!0;do for(var l=!1,a=Vu;a!==null;){if(e!==0){var n=a.pendingLanes;if(n===0)var u=0;else{var s=a.suspendedLanes,o=a.pingedLanes;u=(1<<31-dt(42|e)+1)-1,u&=n&~(s&~o),u=u&201326741?u&201326741|1:u?u|2:0}u!==0&&(l=!0,Td(a,u))}else u=oe,u=Wn(a,a===Re?u:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(u&3)===0||Ka(a,u)||(l=!0,Td(a,u));a=a.next}while(l);hs=!1}}function wv(){jd()}function jd(){Ju=ms=!1;var e=0;Al!==0&&Wv()&&(e=Al);for(var t=rt(),l=null,a=Vu;a!==null;){var n=a.next,u=Nd(a,t);u===0?(a.next=null,l===null?Vu=n:l.next=n,n===null&&(qa=l)):(l=a,(e!==0||(u&3)!==0)&&(Ju=!0)),a=n}Je!==0&&Je!==5||zn(e),Al!==0&&(Al=0)}function Nd(e,t){for(var l=e.suspendedLanes,a=e.pingedLanes,n=e.expirationTimes,u=e.pendingLanes&-62914561;0o)break;var C=v.transferSize,H=v.initiatorType;C&&Ud(H)&&(v=v.responseEnd,s+=C*(v"u"?null:document;function Jd(e,t,l){var a=Ba;if(a&&typeof t=="string"&&t){var n=jt(t);n='link[rel="'+e+'"][href="'+n+'"]',typeof l=="string"&&(n+='[crossorigin="'+l+'"]'),Vd.has(n)||(Vd.add(n),e={rel:e,crossOrigin:l,href:t},a.querySelector(n)===null&&(t=a.createElement("link"),Pe(t,"link",e),Ke(t),a.head.appendChild(t)))}}function uy(e){sl.D(e),Jd("dns-prefetch",e,null)}function iy(e,t){sl.C(e,t),Jd("preconnect",e,t)}function cy(e,t,l){sl.L(e,t,l);var a=Ba;if(a&&e&&t){var n='link[rel="preload"][as="'+jt(t)+'"]';t==="image"&&l&&l.imageSrcSet?(n+='[imagesrcset="'+jt(l.imageSrcSet)+'"]',typeof l.imageSizes=="string"&&(n+='[imagesizes="'+jt(l.imageSizes)+'"]')):n+='[href="'+jt(e)+'"]';var u=n;switch(t){case"style":u=La(e);break;case"script":u=wa(e)}Ct.has(u)||(e=E({rel:"preload",href:t==="image"&&l&&l.imageSrcSet?void 0:e,as:t},l),Ct.set(u,e),a.querySelector(n)!==null||t==="style"&&a.querySelector(Mn(u))||t==="script"&&a.querySelector(Un(u))||(t=a.createElement("link"),Pe(t,"link",e),Ke(t),a.head.appendChild(t)))}}function sy(e,t){sl.m(e,t);var l=Ba;if(l&&e){var a=t&&typeof t.as=="string"?t.as:"script",n='link[rel="modulepreload"][as="'+jt(a)+'"][href="'+jt(e)+'"]',u=n;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":u=wa(e)}if(!Ct.has(u)&&(e=E({rel:"modulepreload",href:e},t),Ct.set(u,e),l.querySelector(n)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(l.querySelector(Un(u)))return}a=l.createElement("link"),Pe(a,"link",e),Ke(a),l.head.appendChild(a)}}}function fy(e,t,l){sl.S(e,t,l);var a=Ba;if(a&&e){var n=ia(a).hoistableStyles,u=La(e);t=t||"default";var s=n.get(u);if(!s){var o={loading:0,preload:null};if(s=a.querySelector(Mn(u)))o.loading=5;else{e=E({rel:"stylesheet",href:e,"data-precedence":t},l),(l=Ct.get(u))&&Os(e,l);var v=s=a.createElement("link");Ke(v),Pe(v,"link",e),v._p=new Promise(function(N,C){v.onload=N,v.onerror=C}),v.addEventListener("load",function(){o.loading|=1}),v.addEventListener("error",function(){o.loading|=2}),o.loading|=4,Fu(s,t,a)}s={type:"stylesheet",instance:s,count:1,state:o},n.set(u,s)}}}function ry(e,t){sl.X(e,t);var l=Ba;if(l&&e){var a=ia(l).hoistableScripts,n=wa(e),u=a.get(n);u||(u=l.querySelector(Un(n)),u||(e=E({src:e,async:!0},t),(t=Ct.get(n))&&Cs(e,t),u=l.createElement("script"),Ke(u),Pe(u,"link",e),l.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},a.set(n,u))}}function oy(e,t){sl.M(e,t);var l=Ba;if(l&&e){var a=ia(l).hoistableScripts,n=wa(e),u=a.get(n);u||(u=l.querySelector(Un(n)),u||(e=E({src:e,async:!0,type:"module"},t),(t=Ct.get(n))&&Cs(e,t),u=l.createElement("script"),Ke(u),Pe(u,"link",e),l.head.appendChild(u)),u={type:"script",instance:u,count:1,state:null},a.set(n,u))}}function Kd(e,t,l,a){var n=(n=k.current)?Wu(n):null;if(!n)throw Error(f(446));switch(e){case"meta":case"title":return null;case"style":return typeof l.precedence=="string"&&typeof l.href=="string"?(t=La(l.href),l=ia(n).hoistableStyles,a=l.get(t),a||(a={type:"style",instance:null,count:0,state:null},l.set(t,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(l.rel==="stylesheet"&&typeof l.href=="string"&&typeof l.precedence=="string"){e=La(l.href);var u=ia(n).hoistableStyles,s=u.get(e);if(s||(n=n.ownerDocument||n,s={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},u.set(e,s),(u=n.querySelector(Mn(e)))&&!u._p&&(s.instance=u,s.state.loading=5),Ct.has(e)||(l={rel:"preload",as:"style",href:l.href,crossOrigin:l.crossOrigin,integrity:l.integrity,media:l.media,hrefLang:l.hrefLang,referrerPolicy:l.referrerPolicy},Ct.set(e,l),u||dy(n,e,l,s.state))),t&&a===null)throw Error(f(528,""));return s}if(t&&a!==null)throw Error(f(529,""));return null;case"script":return t=l.async,l=l.src,typeof l=="string"&&t&&typeof t!="function"&&typeof t!="symbol"?(t=wa(l),l=ia(n).hoistableScripts,a=l.get(t),a||(a={type:"script",instance:null,count:0,state:null},l.set(t,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(f(444,e))}}function La(e){return'href="'+jt(e)+'"'}function Mn(e){return'link[rel="stylesheet"]['+e+"]"}function kd(e){return E({},e,{"data-precedence":e.precedence,precedence:null})}function dy(e,t,l,a){e.querySelector('link[rel="preload"][as="style"]['+t+"]")?a.loading=1:(t=e.createElement("link"),a.preload=t,t.addEventListener("load",function(){return a.loading|=1}),t.addEventListener("error",function(){return a.loading|=2}),Pe(t,"link",l),Ke(t),e.head.appendChild(t))}function wa(e){return'[src="'+jt(e)+'"]'}function Un(e){return"script[async]"+e}function $d(e,t,l){if(t.count++,t.instance===null)switch(t.type){case"style":var a=e.querySelector('style[data-href~="'+jt(l.href)+'"]');if(a)return t.instance=a,Ke(a),a;var n=E({},l,{"data-href":l.href,"data-precedence":l.precedence,href:null,precedence:null});return a=(e.ownerDocument||e).createElement("style"),Ke(a),Pe(a,"style",n),Fu(a,l.precedence,e),t.instance=a;case"stylesheet":n=La(l.href);var u=e.querySelector(Mn(n));if(u)return t.state.loading|=4,t.instance=u,Ke(u),u;a=kd(l),(n=Ct.get(n))&&Os(a,n),u=(e.ownerDocument||e).createElement("link"),Ke(u);var s=u;return s._p=new Promise(function(o,v){s.onload=o,s.onerror=v}),Pe(u,"link",a),t.state.loading|=4,Fu(u,l.precedence,e),t.instance=u;case"script":return u=wa(l.src),(n=e.querySelector(Un(u)))?(t.instance=n,Ke(n),n):(a=l,(n=Ct.get(u))&&(a=E({},l),Cs(a,n)),e=e.ownerDocument||e,n=e.createElement("script"),Ke(n),Pe(n,"link",a),e.head.appendChild(n),t.instance=n);case"void":return null;default:throw Error(f(443,t.type))}else t.type==="stylesheet"&&(t.state.loading&4)===0&&(a=t.instance,t.state.loading|=4,Fu(a,l.precedence,e));return t.instance}function Fu(e,t,l){for(var a=l.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),n=a.length?a[a.length-1]:null,u=n,s=0;s title"):null)}function my(e,t,l){if(l===1||t.itemProp!=null)return!1;switch(e){case"meta":case"title":return!0;case"style":if(typeof t.precedence!="string"||typeof t.href!="string"||t.href==="")break;return!0;case"link":if(typeof t.rel!="string"||typeof t.href!="string"||t.href===""||t.onLoad||t.onError)break;return t.rel==="stylesheet"?(e=t.disabled,typeof t.precedence=="string"&&e==null):!0;case"script":if(t.async&&typeof t.async!="function"&&typeof t.async!="symbol"&&!t.onLoad&&!t.onError&&t.src&&typeof t.src=="string")return!0}return!1}function Id(e){return!(e.type==="stylesheet"&&(e.state.loading&3)===0)}function hy(e,t,l,a){if(l.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(l.state.loading&4)===0){if(l.instance===null){var n=La(a.href),u=t.querySelector(Mn(n));if(u){t=u._p,t!==null&&typeof t=="object"&&typeof t.then=="function"&&(e.count++,e=Pu.bind(e),t.then(e,e)),l.state.loading|=4,l.instance=u,Ke(u);return}u=t.ownerDocument||t,a=kd(a),(n=Ct.get(n))&&Os(a,n),u=u.createElement("link"),Ke(u);var s=u;s._p=new Promise(function(o,v){s.onload=o,s.onerror=v}),Pe(u,"link",a),l.instance=u}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(l,t),(t=l.state.preload)&&(l.state.loading&3)===0&&(e.count++,l=Pu.bind(e),t.addEventListener("load",l),t.addEventListener("error",l))}}var Ds=0;function vy(e,t){return e.stylesheets&&e.count===0&&ti(e,e.stylesheets),0Ds?50:800)+t);return e.unsuspend=l,function(){e.unsuspend=null,clearTimeout(a),clearTimeout(n)}}:null}function Pu(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)ti(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var ei=null;function ti(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,ei=new Map,t.forEach(yy,e),ei=null,Pu.call(e))}function yy(e,t){if(!(t.state.loading&4)){var l=ei.get(e);if(l)var a=l.get(null);else{l=new Map,ei.set(e,l);for(var n=e.querySelectorAll("link[data-precedence],style[data-precedence]"),u=0;u"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(r){console.error(r)}}return i(),Gs.exports=Dy(),Gs.exports}var Uy=My();var xm="popstate";function jm(i){return typeof i=="object"&&i!=null&&"pathname"in i&&"search"in i&&"hash"in i&&"state"in i&&"key"in i}function Hy(i={}){function r(m,h){let{pathname:_="/",search:R="",hash:S=""}=la(m.location.hash.substring(1));return!_.startsWith("/")&&!_.startsWith(".")&&(_="/"+_),Ws("",{pathname:_,search:R,hash:S},h.state&&h.state.usr||null,h.state&&h.state.key||"default")}function d(m,h){let _=m.document.querySelector("base"),R="";if(_&&_.getAttribute("href")){let S=m.location.href,y=S.indexOf("#");R=y===-1?S:S.slice(0,y)}return R+"#"+(typeof h=="string"?h:Qn(h))}function f(m,h){Mt(m.pathname.charAt(0)==="/",`relative pathnames are not supported in hash history.push(${JSON.stringify(h)})`)}return By(r,d,f,i)}function He(i,r){if(i===!1||i===null||typeof i>"u")throw new Error(r)}function Mt(i,r){if(!i){typeof console<"u"&&console.warn(r);try{throw new Error(r)}catch{}}}function qy(){return Math.random().toString(36).substring(2,10)}function Nm(i,r){return{usr:i.state,key:i.key,idx:r,masked:i.unstable_mask?{pathname:i.pathname,search:i.search,hash:i.hash}:void 0}}function Ws(i,r,d=null,f,m){return{pathname:typeof i=="string"?i:i.pathname,search:"",hash:"",...typeof r=="string"?la(r):r,state:d,key:r&&r.key||f||qy(),unstable_mask:m}}function Qn({pathname:i="/",search:r="",hash:d=""}){return r&&r!=="?"&&(i+=r.charAt(0)==="?"?r:"?"+r),d&&d!=="#"&&(i+=d.charAt(0)==="#"?d:"#"+d),i}function la(i){let r={};if(i){let d=i.indexOf("#");d>=0&&(r.hash=i.substring(d),i=i.substring(0,d));let f=i.indexOf("?");f>=0&&(r.search=i.substring(f),i=i.substring(0,f)),i&&(r.pathname=i)}return r}function By(i,r,d,f={}){let{window:m=document.defaultView,v5Compat:h=!1}=f,_=m.history,R="POP",S=null,y=O();y==null&&(y=0,_.replaceState({..._.state,idx:y},""));function O(){return(_.state||{idx:null}).idx}function E(){R="POP";let B=O(),J=B==null?null:B-y;y=B,S&&S({action:R,location:V.location,delta:J})}function Y(B,J){R="PUSH";let I=jm(B)?B:Ws(V.location,B,J);d&&d(I,B),y=O()+1;let W=Nm(I,y),X=V.createHref(I.unstable_mask||I);try{_.pushState(W,"",X)}catch(ue){if(ue instanceof DOMException&&ue.name==="DataCloneError")throw ue;m.location.assign(X)}h&&S&&S({action:R,location:V.location,delta:1})}function L(B,J){R="REPLACE";let I=jm(B)?B:Ws(V.location,B,J);d&&d(I,B),y=O();let W=Nm(I,y),X=V.createHref(I.unstable_mask||I);_.replaceState(W,"",X),h&&S&&S({action:R,location:V.location,delta:0})}function Z(B){return Ly(B)}let V={get action(){return R},get location(){return i(m,_)},listen(B){if(S)throw new Error("A history only accepts one active listener");return m.addEventListener(xm,E),S=B,()=>{m.removeEventListener(xm,E),S=null}},createHref(B){return r(m,B)},createURL:Z,encodeLocation(B){let J=Z(B);return{pathname:J.pathname,search:J.search,hash:J.hash}},push:Y,replace:L,go(B){return _.go(B)}};return V}function Ly(i,r=!1){let d="http://localhost";typeof window<"u"&&(d=window.location.origin!=="null"?window.location.origin:window.location.href),He(d,"No window.location.(origin|href) available to create URL");let f=typeof i=="string"?i:Qn(i);return f=f.replace(/ $/,"%20"),!r&&f.startsWith("//")&&(f=d+f),new URL(f,d)}function Dm(i,r,d="/"){return wy(i,r,d,!1)}function wy(i,r,d,f){let m=typeof r=="string"?la(r):r,h=fl(m.pathname||"/",d);if(h==null)return null;let _=Mm(i);Yy(_);let R=null;for(let S=0;R==null&&S<_.length;++S){let y=Fy(h);R=$y(_[S],y,f)}return R}function Mm(i,r=[],d=[],f="",m=!1){let h=(_,R,S=m,y)=>{let O={relativePath:y===void 0?_.path||"":y,caseSensitive:_.caseSensitive===!0,childrenIndex:R,route:_};if(O.relativePath.startsWith("/")){if(!O.relativePath.startsWith(f)&&S)return;He(O.relativePath.startsWith(f),`Absolute route path "${O.relativePath}" nested under path "${f}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),O.relativePath=O.relativePath.slice(f.length)}let E=Bt([f,O.relativePath]),Y=d.concat(O);_.children&&_.children.length>0&&(He(_.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${E}".`),Mm(_.children,r,Y,E,S)),!(_.path==null&&!_.index)&&r.push({path:E,score:Ky(E,_.index),routesMeta:Y})};return i.forEach((_,R)=>{if(_.path===""||!_.path?.includes("?"))h(_,R);else for(let S of Um(_.path))h(_,R,!0,S)}),r}function Um(i){let r=i.split("/");if(r.length===0)return[];let[d,...f]=r,m=d.endsWith("?"),h=d.replace(/\?$/,"");if(f.length===0)return m?[h,""]:[h];let _=Um(f.join("/")),R=[];return R.push(..._.map(S=>S===""?h:[h,S].join("/"))),m&&R.push(..._),R.map(S=>i.startsWith("/")&&S===""?"/":S)}function Yy(i){i.sort((r,d)=>r.score!==d.score?d.score-r.score:ky(r.routesMeta.map(f=>f.childrenIndex),d.routesMeta.map(f=>f.childrenIndex)))}var Gy=/^:[\w-]+$/,Xy=3,Qy=2,Zy=1,Vy=10,Jy=-2,Em=i=>i==="*";function Ky(i,r){let d=i.split("/"),f=d.length;return d.some(Em)&&(f+=Jy),r&&(f+=Qy),d.filter(m=>!Em(m)).reduce((m,h)=>m+(Gy.test(h)?Xy:h===""?Zy:Vy),f)}function ky(i,r){return i.length===r.length&&i.slice(0,-1).every((f,m)=>f===r[m])?i[i.length-1]-r[r.length-1]:0}function $y(i,r,d=!1){let{routesMeta:f}=i,m={},h="/",_=[];for(let R=0;R{if(O==="*"){let Z=R[Y]||"";_=h.slice(0,h.length-Z.length).replace(/(.)\/+$/,"$1")}const L=R[Y];return E&&!L?y[O]=void 0:y[O]=(L||"").replace(/%2F/g,"/"),y},{}),pathname:h,pathnameBase:_,pattern:i}}function Wy(i,r=!1,d=!0){Mt(i==="*"||!i.endsWith("*")||i.endsWith("/*"),`Route path "${i}" will be treated as if it were "${i.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${i.replace(/\*$/,"/*")}".`);let f=[],m="^"+i.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(_,R,S,y,O)=>{if(f.push({paramName:R,isOptional:S!=null}),S){let E=O.charAt(y+_.length);return E&&E!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return i.endsWith("*")?(f.push({paramName:"*"}),m+=i==="*"||i==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):d?m+="\\/*$":i!==""&&i!=="/"&&(m+="(?:(?=\\/|$))"),[new RegExp(m,r?void 0:"i"),f]}function Fy(i){try{return i.split("/").map(r=>decodeURIComponent(r).replace(/\//g,"%2F")).join("/")}catch(r){return Mt(!1,`The URL path "${i}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${r}).`),i}}function fl(i,r){if(r==="/")return i;if(!i.toLowerCase().startsWith(r.toLowerCase()))return null;let d=r.endsWith("/")?r.length-1:r.length,f=i.charAt(d);return f&&f!=="/"?null:i.slice(d)||"/"}var Iy=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function Py(i,r="/"){let{pathname:d,search:f="",hash:m=""}=typeof i=="string"?la(i):i,h;return d?(d=Hm(d),d.startsWith("/")?h=Tm(d.substring(1),"/"):h=Tm(d,r)):h=r,{pathname:h,search:lp(f),hash:ap(m)}}function Tm(i,r){let d=pi(r).split("/");return i.split("/").forEach(m=>{m===".."?d.length>1&&d.pop():m!=="."&&d.push(m)}),d.length>1?d.join("/"):"/"}function Vs(i,r,d,f){return`Cannot include a '${i}' character in a manually specified \`to.${r}\` field [${JSON.stringify(f)}]. Please separate it out to the \`to.${d}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function ep(i){return i.filter((r,d)=>d===0||r.route.path&&r.route.path.length>0)}function tf(i){let r=ep(i);return r.map((d,f)=>f===r.length-1?d.pathname:d.pathnameBase)}function bi(i,r,d,f=!1){let m;typeof i=="string"?m=la(i):(m={...i},He(!m.pathname||!m.pathname.includes("?"),Vs("?","pathname","search",m)),He(!m.pathname||!m.pathname.includes("#"),Vs("#","pathname","hash",m)),He(!m.search||!m.search.includes("#"),Vs("#","search","hash",m)));let h=i===""||m.pathname==="",_=h?"/":m.pathname,R;if(_==null)R=d;else{let E=r.length-1;if(!f&&_.startsWith("..")){let Y=_.split("/");for(;Y[0]==="..";)Y.shift(),E-=1;m.pathname=Y.join("/")}R=E>=0?r[E]:"/"}let S=Py(m,R),y=_&&_!=="/"&&_.endsWith("/"),O=(h||_===".")&&d.endsWith("/");return!S.pathname.endsWith("/")&&(y||O)&&(S.pathname+="/"),S}var Hm=i=>i.replace(/\/\/+/g,"/"),Bt=i=>Hm(i.join("/")),pi=i=>i.replace(/\/+$/,""),tp=i=>pi(i).replace(/^\/*/,"/"),lp=i=>!i||i==="?"?"":i.startsWith("?")?i:"?"+i,ap=i=>!i||i==="#"?"":i.startsWith("#")?i:"#"+i,np=class{constructor(i,r,d,f=!1){this.status=i,this.statusText=r||"",this.internal=f,d instanceof Error?(this.data=d.toString(),this.error=d):this.data=d}};function up(i){return i!=null&&typeof i.status=="number"&&typeof i.statusText=="string"&&typeof i.internal=="boolean"&&"data"in i}function ip(i){let r=i.map(d=>d.route.path).filter(Boolean);return Bt(r)||"/"}var qm=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Bm(i,r){let d=i;if(typeof d!="string"||!Iy.test(d))return{absoluteURL:void 0,isExternal:!1,to:d};let f=d,m=!1;if(qm)try{let h=new URL(window.location.href),_=d.startsWith("//")?new URL(h.protocol+d):new URL(d),R=fl(_.pathname,r);_.origin===h.origin&&R!=null?d=R+_.search+_.hash:m=!0}catch{Mt(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:f,isExternal:m,to:d}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var Lm=["POST","PUT","PATCH","DELETE"];new Set(Lm);var cp=["GET",...Lm];new Set(cp);var Za=g.createContext(null);Za.displayName="DataRouter";var _i=g.createContext(null);_i.displayName="DataRouterState";var wm=g.createContext(!1);function sp(){return g.useContext(wm)}var Ym=g.createContext({isTransitioning:!1});Ym.displayName="ViewTransition";var fp=g.createContext(new Map);fp.displayName="Fetchers";var rp=g.createContext(null);rp.displayName="Await";var _t=g.createContext(null);_t.displayName="Navigation";var Zn=g.createContext(null);Zn.displayName="Location";var Lt=g.createContext({outlet:null,matches:[],isDataRoute:!1});Lt.displayName="Route";var lf=g.createContext(null);lf.displayName="RouteError";var Gm="REACT_ROUTER_ERROR",op="REDIRECT",dp="ROUTE_ERROR_RESPONSE";function mp(i){if(i.startsWith(`${Gm}:${op}:{`))try{let r=JSON.parse(i.slice(28));if(typeof r=="object"&&r&&typeof r.status=="number"&&typeof r.statusText=="string"&&typeof r.location=="string"&&typeof r.reloadDocument=="boolean"&&typeof r.replace=="boolean")return r}catch{}}function hp(i){if(i.startsWith(`${Gm}:${dp}:{`))try{let r=JSON.parse(i.slice(40));if(typeof r=="object"&&r&&typeof r.status=="number"&&typeof r.statusText=="string")return new np(r.status,r.statusText,r.data)}catch{}}function vp(i,{relative:r}={}){He(Va(),"useHref() may be used only in the context of a component.");let{basename:d,navigator:f}=g.useContext(_t),{hash:m,pathname:h,search:_}=Vn(i,{relative:r}),R=h;return d!=="/"&&(R=h==="/"?d:Bt([d,h])),f.createHref({pathname:R,search:_,hash:m})}function Va(){return g.useContext(Zn)!=null}function Qt(){return He(Va(),"useLocation() may be used only in the context of a component."),g.useContext(Zn).location}var Xm="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function Qm(i){g.useContext(_t).static||g.useLayoutEffect(i)}function Zm(){let{isDataRoute:i}=g.useContext(Lt);return i?Op():yp()}function yp(){He(Va(),"useNavigate() may be used only in the context of a component.");let i=g.useContext(Za),{basename:r,navigator:d}=g.useContext(_t),{matches:f}=g.useContext(Lt),{pathname:m}=Qt(),h=JSON.stringify(tf(f)),_=g.useRef(!1);return Qm(()=>{_.current=!0}),g.useCallback((S,y={})=>{if(Mt(_.current,Xm),!_.current)return;if(typeof S=="number"){d.go(S);return}let O=bi(S,JSON.parse(h),m,y.relative==="path");i==null&&r!=="/"&&(O.pathname=O.pathname==="/"?r:Bt([r,O.pathname])),(y.replace?d.replace:d.push)(O,y.state,y)},[r,d,h,m,i])}var pp=g.createContext(null);function gp(i){let r=g.useContext(Lt).outlet;return g.useMemo(()=>r&&g.createElement(pp.Provider,{value:i},r),[r,i])}function Vn(i,{relative:r}={}){let{matches:d}=g.useContext(Lt),{pathname:f}=Qt(),m=JSON.stringify(tf(d));return g.useMemo(()=>bi(i,JSON.parse(m),f,r==="path"),[i,m,f,r])}function bp(i,r){return Vm(i,r)}function Vm(i,r,d){He(Va(),"useRoutes() may be used only in the context of a component.");let{navigator:f}=g.useContext(_t),{matches:m}=g.useContext(Lt),h=m[m.length-1],_=h?h.params:{},R=h?h.pathname:"/",S=h?h.pathnameBase:"/",y=h&&h.route;{let B=y&&y.path||"";Km(R,!y||B.endsWith("*")||B.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${R}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. - -Please change the parent to .`)}let O=Qt(),E;if(r){let B=typeof r=="string"?la(r):r;He(S==="/"||B.pathname?.startsWith(S),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${S}" but pathname "${B.pathname}" was given in the \`location\` prop.`),E=B}else E=O;let Y=E.pathname||"/",L=Y;if(S!=="/"){let B=S.replace(/^\//,"").split("/");L="/"+Y.replace(/^\//,"").split("/").slice(B.length).join("/")}let Z=Dm(i,{pathname:L});Mt(y||Z!=null,`No routes matched location "${E.pathname}${E.search}${E.hash}" `),Mt(Z==null||Z[Z.length-1].route.element!==void 0||Z[Z.length-1].route.Component!==void 0||Z[Z.length-1].route.lazy!==void 0,`Matched leaf route at location "${E.pathname}${E.search}${E.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let V=Np(Z&&Z.map(B=>Object.assign({},B,{params:Object.assign({},_,B.params),pathname:Bt([S,f.encodeLocation?f.encodeLocation(B.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:B.pathname]),pathnameBase:B.pathnameBase==="/"?S:Bt([S,f.encodeLocation?f.encodeLocation(B.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:B.pathnameBase])})),m,d);return r&&V?g.createElement(Zn.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",unstable_mask:void 0,...E},navigationType:"POP"}},V):V}function _p(){let i=zp(),r=up(i)?`${i.status} ${i.statusText}`:i instanceof Error?i.message:JSON.stringify(i),d=i instanceof Error?i.stack:null,f="rgba(200,200,200, 0.5)",m={padding:"0.5rem",backgroundColor:f},h={padding:"2px 4px",backgroundColor:f},_=null;return console.error("Error handled by React Router default ErrorBoundary:",i),_=g.createElement(g.Fragment,null,g.createElement("p",null,"💿 Hey developer 👋"),g.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",g.createElement("code",{style:h},"ErrorBoundary")," or"," ",g.createElement("code",{style:h},"errorElement")," prop on your route.")),g.createElement(g.Fragment,null,g.createElement("h2",null,"Unexpected Application Error!"),g.createElement("h3",{style:{fontStyle:"italic"}},r),d?g.createElement("pre",{style:m},d):null,_)}var Sp=g.createElement(_p,null),Jm=class extends g.Component{constructor(i){super(i),this.state={location:i.location,revalidation:i.revalidation,error:i.error}}static getDerivedStateFromError(i){return{error:i}}static getDerivedStateFromProps(i,r){return r.location!==i.location||r.revalidation!=="idle"&&i.revalidation==="idle"?{error:i.error,location:i.location,revalidation:i.revalidation}:{error:i.error!==void 0?i.error:r.error,location:r.location,revalidation:i.revalidation||r.revalidation}}componentDidCatch(i,r){this.props.onError?this.props.onError(i,r):console.error("React Router caught the following error during render",i)}render(){let i=this.state.error;if(this.context&&typeof i=="object"&&i&&"digest"in i&&typeof i.digest=="string"){const d=hp(i.digest);d&&(i=d)}let r=i!==void 0?g.createElement(Lt.Provider,{value:this.props.routeContext},g.createElement(lf.Provider,{value:i,children:this.props.component})):this.props.children;return this.context?g.createElement(xp,{error:i},r):r}};Jm.contextType=wm;var Js=new WeakMap;function xp({children:i,error:r}){let{basename:d}=g.useContext(_t);if(typeof r=="object"&&r&&"digest"in r&&typeof r.digest=="string"){let f=mp(r.digest);if(f){let m=Js.get(r);if(m)throw m;let h=Bm(f.location,d);if(qm&&!Js.get(r))if(h.isExternal||f.reloadDocument)window.location.href=h.absoluteURL||h.to;else{const _=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(h.to,{replace:f.replace}));throw Js.set(r,_),_}return g.createElement("meta",{httpEquiv:"refresh",content:`0;url=${h.absoluteURL||h.to}`})}}return i}function jp({routeContext:i,match:r,children:d}){let f=g.useContext(Za);return f&&f.static&&f.staticContext&&(r.route.errorElement||r.route.ErrorBoundary)&&(f.staticContext._deepestRenderedBoundaryId=r.route.id),g.createElement(Lt.Provider,{value:i},d)}function Np(i,r=[],d){let f=d?.state;if(i==null){if(!f)return null;if(f.errors)i=f.matches;else if(r.length===0&&!f.initialized&&f.matches.length>0)i=f.matches;else return null}let m=i,h=f?.errors;if(h!=null){let O=m.findIndex(E=>E.route.id&&h?.[E.route.id]!==void 0);He(O>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(h).join(",")}`),m=m.slice(0,Math.min(m.length,O+1))}let _=!1,R=-1;if(d&&f){_=f.renderFallback;for(let O=0;O=0?m=m.slice(0,R+1):m=[m[0]];break}}}}let S=d?.onError,y=f&&S?(O,E)=>{S(O,{location:f.location,params:f.matches?.[0]?.params??{},unstable_pattern:ip(f.matches),errorInfo:E})}:void 0;return m.reduceRight((O,E,Y)=>{let L,Z=!1,V=null,B=null;f&&(L=h&&E.route.id?h[E.route.id]:void 0,V=E.route.errorElement||Sp,_&&(R<0&&Y===0?(Km("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),Z=!0,B=null):R===Y&&(Z=!0,B=E.route.hydrateFallbackElement||null)));let J=r.concat(m.slice(0,Y+1)),I=()=>{let W;return L?W=V:Z?W=B:E.route.Component?W=g.createElement(E.route.Component,null):E.route.element?W=E.route.element:W=O,g.createElement(jp,{match:E,routeContext:{outlet:O,matches:J,isDataRoute:f!=null},children:W})};return f&&(E.route.ErrorBoundary||E.route.errorElement||Y===0)?g.createElement(Jm,{location:f.location,revalidation:f.revalidation,component:V,error:L,children:I(),routeContext:{outlet:null,matches:J,isDataRoute:!0},onError:y}):I()},null)}function af(i){return`${i} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Ep(i){let r=g.useContext(Za);return He(r,af(i)),r}function Tp(i){let r=g.useContext(_i);return He(r,af(i)),r}function Rp(i){let r=g.useContext(Lt);return He(r,af(i)),r}function nf(i){let r=Rp(i),d=r.matches[r.matches.length-1];return He(d.route.id,`${i} can only be used on routes that contain a unique "id"`),d.route.id}function Ap(){return nf("useRouteId")}function zp(){let i=g.useContext(lf),r=Tp("useRouteError"),d=nf("useRouteError");return i!==void 0?i:r.errors?.[d]}function Op(){let{router:i}=Ep("useNavigate"),r=nf("useNavigate"),d=g.useRef(!1);return Qm(()=>{d.current=!0}),g.useCallback(async(m,h={})=>{Mt(d.current,Xm),d.current&&(typeof m=="number"?await i.navigate(m):await i.navigate(m,{fromRouteId:r,...h}))},[i,r])}var Rm={};function Km(i,r,d){!r&&!Rm[i]&&(Rm[i]=!0,Mt(!1,d))}g.memo(Cp);function Cp({routes:i,future:r,state:d,isStatic:f,onError:m}){return Vm(i,void 0,{state:d,isStatic:f,onError:m})}function Dp({to:i,replace:r,state:d,relative:f}){He(Va()," may be used only in the context of a component.");let{static:m}=g.useContext(_t);Mt(!m," must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.");let{matches:h}=g.useContext(Lt),{pathname:_}=Qt(),R=Zm(),S=bi(i,tf(h),_,f==="path"),y=JSON.stringify(S);return g.useEffect(()=>{R(JSON.parse(y),{replace:r,state:d,relative:f})},[R,y,f,r,d]),null}function Mp(i){return gp(i.context)}function ta(i){He(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function Up({basename:i="/",children:r=null,location:d,navigationType:f="POP",navigator:m,static:h=!1,unstable_useTransitions:_}){He(!Va(),"You cannot render a inside another . You should never have more than one in your app.");let R=i.replace(/^\/*/,"/"),S=g.useMemo(()=>({basename:R,navigator:m,static:h,unstable_useTransitions:_,future:{}}),[R,m,h,_]);typeof d=="string"&&(d=la(d));let{pathname:y="/",search:O="",hash:E="",state:Y=null,key:L="default",unstable_mask:Z}=d,V=g.useMemo(()=>{let B=fl(y,R);return B==null?null:{location:{pathname:B,search:O,hash:E,state:Y,key:L,unstable_mask:Z},navigationType:f}},[R,y,O,E,Y,L,f,Z]);return Mt(V!=null,` is not able to match the URL "${y}${O}${E}" because it does not start with the basename, so the won't render anything.`),V==null?null:g.createElement(_t.Provider,{value:S},g.createElement(Zn.Provider,{children:r,value:V}))}function Hp({children:i,location:r}){return bp(Fs(i),r)}function Fs(i,r=[]){let d=[];return g.Children.forEach(i,(f,m)=>{if(!g.isValidElement(f))return;let h=[...r,m];if(f.type===g.Fragment){d.push.apply(d,Fs(f.props.children,h));return}He(f.type===ta,`[${typeof f.type=="string"?f.type:f.type.name}] is not a component. All component children of must be a or `),He(!f.props.index||!f.props.children,"An index route cannot have child routes.");let _={id:f.props.id||h.join("-"),caseSensitive:f.props.caseSensitive,element:f.props.element,Component:f.props.Component,index:f.props.index,path:f.props.path,middleware:f.props.middleware,loader:f.props.loader,action:f.props.action,hydrateFallbackElement:f.props.hydrateFallbackElement,HydrateFallback:f.props.HydrateFallback,errorElement:f.props.errorElement,ErrorBoundary:f.props.ErrorBoundary,hasErrorBoundary:f.props.hasErrorBoundary===!0||f.props.ErrorBoundary!=null||f.props.errorElement!=null,shouldRevalidate:f.props.shouldRevalidate,handle:f.props.handle,lazy:f.props.lazy};f.props.children&&(_.children=Fs(f.props.children,h)),d.push(_)}),d}var mi="get",hi="application/x-www-form-urlencoded";function Si(i){return typeof HTMLElement<"u"&&i instanceof HTMLElement}function qp(i){return Si(i)&&i.tagName.toLowerCase()==="button"}function Bp(i){return Si(i)&&i.tagName.toLowerCase()==="form"}function Lp(i){return Si(i)&&i.tagName.toLowerCase()==="input"}function wp(i){return!!(i.metaKey||i.altKey||i.ctrlKey||i.shiftKey)}function Yp(i,r){return i.button===0&&(!r||r==="_self")&&!wp(i)}var fi=null;function Gp(){if(fi===null)try{new FormData(document.createElement("form"),0),fi=!1}catch{fi=!0}return fi}var Xp=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function Ks(i){return i!=null&&!Xp.has(i)?(Mt(!1,`"${i}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${hi}"`),null):i}function Qp(i,r){let d,f,m,h,_;if(Bp(i)){let R=i.getAttribute("action");f=R?fl(R,r):null,d=i.getAttribute("method")||mi,m=Ks(i.getAttribute("enctype"))||hi,h=new FormData(i)}else if(qp(i)||Lp(i)&&(i.type==="submit"||i.type==="image")){let R=i.form;if(R==null)throw new Error('Cannot submit a + {showBearerTokenHint ? ( + + Server uses Bearer for mutations — set{" "} + VITE_FLIGHTDECK_LOCAL_API_TOKEN to match the server. + + ) : null} diff --git a/web/src/pages/OverviewPage.tsx b/web/src/pages/OverviewPage.tsx index 196b5ca..7b38dc4 100644 --- a/web/src/pages/OverviewPage.tsx +++ b/web/src/pages/OverviewPage.tsx @@ -6,6 +6,8 @@ import { useTimelineRefresh } from "../context/TimelineRefreshContext"; import { Badge } from "../components/Badge"; import { JsonPanel } from "../components/JsonPanel"; +const OVERVIEW_POLL_MS = 30_000; + function shortId(id: string, keepStart = 10, keepEnd = 6) { if (id.length <= keepStart + keepEnd + 1) return id; return `${id.slice(0, keepStart)}…${id.slice(-keepEnd)}`; @@ -42,8 +44,11 @@ export function OverviewPage() { const [error, setError] = useState(null); const [loading, setLoading] = useState(true); - const refresh = useCallback(async () => { - setLoading(true); + const refresh = useCallback(async (opts?: { silent?: boolean }) => { + const silent = opts?.silent === true; + if (!silent) { + setLoading(true); + } setError(null); setMetricsError(null); try { @@ -61,14 +66,24 @@ export function OverviewPage() { setMetrics(null); } } finally { - setLoading(false); + if (!silent) { + setLoading(false); + } } }, []); useEffect(() => { - void refresh(); + void refresh({ silent: generation > 0 }); }, [generation, refresh]); + useEffect(() => { + const id = window.setInterval(() => { + if (document.visibilityState !== "visible") return; + void refresh({ silent: true }); + }, OVERVIEW_POLL_MS); + return () => window.clearInterval(id); + }, [refresh]); + const raw = data === null ? error ?? "" @@ -83,11 +98,12 @@ export function OverviewPage() {

Overview

-

Registered releases, promotion pointers, and recent ledger actions.

+

+ Registered releases, promotion pointers, and recent ledger actions. Refreshes automatically every{" "} + {OVERVIEW_POLL_MS / 1000}s while this tab is visible; also updates after promote or rollback from{" "} + Actions. +

-
{error && !loading ?

{error}

: null} diff --git a/web/src/pages/RunsPage.tsx b/web/src/pages/RunsPage.tsx index 56a1cbd..80cd0fd 100644 --- a/web/src/pages/RunsPage.tsx +++ b/web/src/pages/RunsPage.tsx @@ -27,6 +27,49 @@ function getTraceId(ev: Record): string { return typeof t === "string" ? t : ""; } +function getSessionId(ev: Record): string { + const req = getRequest(ev); + const v = req?.session_id; + return typeof v === "string" ? v : ""; +} + +function getSpanId(ev: Record): string { + const req = getRequest(ev); + const v = req?.span_id; + return typeof v === "string" ? v : ""; +} + +type RunsQueryErrorKind = "network" | "client" | "server" | "unknown"; + +function classifyRunsFetchError(e: unknown): { kind: RunsQueryErrorKind; title: string; detail: string } { + const detail = e instanceof Error ? e.message : String(e); + const lower = detail.toLowerCase(); + if ( + e instanceof TypeError || + lower.includes("failed to fetch") || + lower.includes("networkerror") || + lower.includes("load failed") || + lower.includes("network request failed") + ) { + return { + kind: "network", + title: "Could not reach the server", + detail: detail || "Check your connection, VPN, and that FlightDeck is running.", + }; + } + const httpMatch = detail.match(/HTTP\s+(\d{3})\b/i); + if (httpMatch) { + const code = Number.parseInt(httpMatch[1], 10); + if (code >= 500) { + return { kind: "server", title: `Server error (${code})`, detail }; + } + if (code >= 400) { + return { kind: "client", title: `Request rejected (${code})`, detail }; + } + } + return { kind: "unknown", title: "Run query failed", detail }; +} + function getLatencyMs(ev: Record): number | null { const m = getMetrics(ev); const n = m?.latency_ms; @@ -59,6 +102,8 @@ function buildTraceGroups(events: unknown[]): { key: string; rows: Record(null); + const drawerPanelRef = useRef(null); + const drawerReturnFocusRef = useRef(null); const [releases, setReleases] = useState([]); const [releaseId, setReleaseId] = useState(""); @@ -74,6 +119,11 @@ export function RunsPage() { const [result, setResult] = useState(null); const [rawErr, setRawErr] = useState(null); + const [runsQueryError, setRunsQueryError] = useState<{ + kind: RunsQueryErrorKind; + title: string; + detail: string; + } | null>(null); const [busy, setBusy] = useState(false); const [exportBusy, setExportBusy] = useState(false); const [detailEvent, setDetailEvent] = useState | null>(null); @@ -87,14 +137,25 @@ export function RunsPage() { }); }, []); + const closeDrawer = useCallback(() => { + setDetailEvent(null); + window.setTimeout(() => { + drawerReturnFocusRef.current?.focus(); + drawerReturnFocusRef.current = null; + }, 0); + }, []); + useEffect(() => { if (!detailEvent) return; const onKey = (e: KeyboardEvent) => { - if (e.key === "Escape") setDetailEvent(null); + if (e.key === "Escape") { + e.preventDefault(); + closeDrawer(); + } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); - }, [detailEvent]); + }, [detailEvent, closeDrawer]); useEffect(() => { if (!detailEvent) return; @@ -102,8 +163,44 @@ export function RunsPage() { return () => window.clearTimeout(t); }, [detailEvent]); + useEffect(() => { + if (!detailEvent) return; + const drawer = drawerPanelRef.current; + if (!drawer) return; + + const selector = + 'a[href]:not([tabindex="-1"]), button:not([disabled]):not([tabindex="-1"]), textarea:not([disabled]):not([tabindex="-1"]), input:not([disabled]):not([tabindex="-1"]), select:not([disabled]):not([tabindex="-1"]), [tabindex]:not([tabindex="-1"])'; + + const focusables = (): HTMLElement[] => + Array.from(drawer.querySelectorAll(selector)).filter( + (el) => !el.hasAttribute("disabled") && el.tabIndex !== -1, + ); + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key !== "Tab") return; + const nodes = focusables(); + if (nodes.length === 0) return; + const first = nodes[0]; + const last = nodes[nodes.length - 1]; + const active = document.activeElement as HTMLElement | null; + if (e.shiftKey) { + if (active === first || !drawer.contains(active)) { + e.preventDefault(); + last.focus(); + } + } else if (active === last) { + e.preventDefault(); + first.focus(); + } + }; + + document.addEventListener("keydown", onKeyDown, true); + return () => document.removeEventListener("keydown", onKeyDown, true); + }, [detailEvent]); + const runQuery = useCallback(async () => { setRawErr(null); + setRunsQueryError(null); setResult(null); const rid = releaseId.trim(); if (!rid) { @@ -135,8 +232,9 @@ export function RunsPage() { limit: lim, }); setResult(data); + setRunsQueryError(null); } catch (e) { - setRawErr(String(e)); + setRunsQueryError(classifyRunsFetchError(e)); } finally { setBusy(false); } @@ -204,8 +302,6 @@ export function RunsPage() { windowVal, ]); - const closeDrawer = useCallback(() => setDetailEvent(null), []); - const renderEventRow = useCallback( (rec: Record, idx: number, keyPrefix: string) => { const runId = typeof rec.run_id === "string" ? rec.run_id : ""; @@ -235,7 +331,10 @@ export function RunsPage() { type="button" className="fd-btn fd-btn--ghost fd-btn--sm" aria-haspopup="dialog" - onClick={() => setDetailEvent(rec)} + onClick={(e) => { + drawerReturnFocusRef.current = e.currentTarget; + setDetailEvent(rec); + }} > View @@ -352,6 +451,43 @@ export function RunsPage() { {rawErr ?

{rawErr}

: null} + {runsQueryError && !result ? ( +
+
+

Could not load runs

+
+
+

{runsQueryError.title}

+

+ {runsQueryError.kind === "network" ? ( + <> + This usually means the UI lost contact with the FlightDeck server (offline, wrong host, or CORS). + Confirm flightdeck serve is up and you are on the same + origin the UI expects. + + ) : runsQueryError.kind === "client" ? ( + <> + The server refused this query (validation, auth, or missing data). Adjust filters or release ID, or + check API credentials if your deployment requires them. + + ) : runsQueryError.kind === "server" ? ( + <>The server reported an internal error. Retry in a moment; if it persists, check server logs. + ) : ( + <>Something went wrong while loading run events. Details appear below. + )} +

+

+ {runsQueryError.detail} +

+
+ +
+
+
+ ) : null} + {result ? (
@@ -470,7 +606,7 @@ export function RunsPage() {
- ) : ( + ) : runsQueryError ? null : (

Choose a Release ID (datalist is filled from registered releases when the server is @@ -481,7 +617,7 @@ export function RunsPage() { )} {detailEvent ? ( -

+