From 048fa377167334ddb34ae8158e1192aa7d36572e Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 19 Apr 2026 19:05:09 -0400 Subject: [PATCH] refactor: simplify pass on Run Files feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied fixes from three parallel reviews (reuse, quality, efficiency): Server - Delete dead `sandbox_git_env()` in run_files_security.rs — duplicated `sandbox_git.rs::sandbox_git_hardening_env` but had no callers. - Combine `resolve_head_sha` + `resolve_commit_time` into one `resolve_head_sha_and_time` using `git show -s --format=%H\ %cI HEAD` — saves ~100ms per request (one fewer sandbox round-trip). - Parallelize `list_changed_files_raw` + `list_binary_paths` with `tokio::join!` — both are mutually independent once `to_sha` is known, saves another ~100ms per request. - Skip phase-1 `cat-file --batch-check` for SHA lists below 10 entries. Phase-2 size-caps per-blob anyway; the pre-filter earned its cost only for large batches where a single malformed blob could poison the parse. Saves another ~100ms on small diffs. - Extract `transient_503(op, message)` helper — dedupes three identical `DiffError::Transient => ApiError::new(503, ...)` arms. - Strip plan-referencing comments ("§ Unit 5", "P1-X", "P2-Y regression") from production code and tests. The R4/R5 taxonomy labels are kept where they anchor semantic intent. Web - Dedupe `extractRequestId`: one canonical parser in `run-files.tsx` (consumed by the loader), one ErrorBoundary-only variant in `states.tsx::extractRequestIdFromUnknown`. Both share the same logic; separated only so each source can pick its own type discipline. - Extract `renderStatusError({status, requestId, onRetry})` shared between the loader's inline-error path and `RunFilesErrorBoundary`. One canonical source of R5 copy. - Gate the `useFreshness` 10s interval on `hasLabel` — previously it ticked every 10s even when `meta == null` and there was no label to refresh, re-rendering the whole route for nothing. Now the interval only runs while there's actually a timestamp label mounted. - Fix render-time ref mutation (`lastGoodDataRef.current = result.data` in the render body) — violates React render purity. Moved into the `useEffect` that watches `result?.data`. Also collapsed `previousDataLengthRef` and `lastToShaRef` into single reads off `lastGoodDataRef.current` — both were derivable from the cached last-good payload. - Type `DegradedBanner.reason` and `bannerCopyForReason` as `RunFilesMetaDegradedReasonEnum` instead of raw `string`. Tests: 4172 Rust + 94 web, clippy clean, fmt clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/fabro-web/app/routes/run-files.tsx | 113 +++------ .../app/routes/run-files/placeholders.tsx | 15 +- .../fabro-web/app/routes/run-files/states.tsx | 112 +++++---- lib/crates/fabro-server/src/run_files.rs | 222 +++++++++--------- .../fabro-server/src/run_files_security.rs | 41 +--- .../{entry-8hk343fy.js => entry-8nkj5gta.js} | 214 ++++++++--------- lib/crates/fabro-spa/assets/index.html | 2 +- 7 files changed, 341 insertions(+), 378 deletions(-) rename lib/crates/fabro-spa/assets/assets/{entry-8hk343fy.js => entry-8nkj5gta.js} (52%) diff --git a/apps/fabro-web/app/routes/run-files.tsx b/apps/fabro-web/app/routes/run-files.tsx index cbea8bd60..ebd11df8c 100644 --- a/apps/fabro-web/app/routes/run-files.tsx +++ b/apps/fabro-web/app/routes/run-files.tsx @@ -26,6 +26,7 @@ import { EmptyState, InlineErrorBanner, LoadingSkeleton, + renderStatusError, RunFilesErrorBoundary, Toast, } from "./run-files/states"; @@ -38,13 +39,12 @@ export const handle = { wide: true }; * Loader return type. Both initial loads and revalidations flow through the * same discriminated union so a revalidation failure does NOT unmount to * the route ErrorBoundary — it stays in-band as `{ data: null, error }` - * and the component keeps showing the last-good data with an inline banner. - * This is the plan's "prior content stays mounted; inline banner with - * Retry" behavior for mid-session refresh failures (§ Unit 11). + * and the component keeps showing the last-good data with an inline + * banner. * - * `error.requestId` is extracted from the 500-response body per R5 so the - * UI can surface it verbatim in the copy ("Request ID: xyz. Contact - * support.") — not just the bare status code. + * `error.requestId` is extracted from the 500-response body so the UI can + * surface it verbatim ("Request ID: xyz. Contact support.") rather than + * just the bare status code. */ export type RunFilesLoaderResult = { data: PaginatedRunFileList | null; @@ -185,11 +185,17 @@ function useFreshness( meta: PaginatedRunFileList["meta"] | null, lastFetchedAt: number | null, ): string | null { + // Only tick when there is actually a freshness label to keep fresh — no + // point re-rendering every 10s when `meta == null` and the toolbar + // would show nothing. + const hasLabel = + !!meta && (!!meta.to_sha_committed_at || lastFetchedAt !== null); const [, setTick] = useState(0); useEffect(() => { + if (!hasLabel) return undefined; const id = setInterval(() => setTick((t) => t + 1), 10_000); return () => clearInterval(id); - }, []); + }, [hasLabel]); if (!meta) return null; const now = Date.now(); @@ -289,39 +295,30 @@ export default function RunFiles({ loaderData }: any) { const runStatus = resolveRunStatus(matches); // Preserve the last successful payload so a failed revalidation can keep - // rendering the previous files while surfacing an inline banner. On the - // very first failure (no prior good data), we render the error state - // equivalent of the ErrorBoundary inline. + // rendering the previous files while surfacing an inline banner. const lastGoodDataRef = useRef(null); - if (result?.data) { - lastGoodDataRef.current = result.data; - } - const data: PaginatedRunFileList | null = - result?.data ?? lastGoodDataRef.current; - const lastFetchedAtRef = useRef(null); - const lastToShaRef = useRef(null); - const previousDataLengthRef = useRef(null); const [emptyToast, setEmptyToast] = useState(null); const [deepLinkToast, setDeepLinkToast] = useState(null); useEffect(() => { if (!result?.data) return; - lastFetchedAtRef.current = Date.now(); - const currentToSha = (result.data.meta?.to_sha ?? null) as string | null; - const prevLen = previousDataLengthRef.current; - if (prevLen !== null && prevLen > 0 && result.data.data.length === 0) { - // Revalidation-now-empty toast: the user was looking at files, the - // latest fetch shows none. + const prev = lastGoodDataRef.current; + if (prev && prev.data.length > 0 && result.data.data.length === 0) { setEmptyToast("No changes in this run."); const id = setTimeout(() => setEmptyToast(null), 3500); + lastGoodDataRef.current = result.data; + lastFetchedAtRef.current = Date.now(); return () => clearTimeout(id); } - previousDataLengthRef.current = result.data.data.length; - lastToShaRef.current = currentToSha; + lastGoodDataRef.current = result.data; + lastFetchedAtRef.current = Date.now(); return undefined; }, [result?.data]); + const data: PaginatedRunFileList | null = + result?.data ?? lastGoodDataRef.current; + useSseRevalidation(params.id); const isInitialLoading = navigation.state === "loading" && !loaderData; @@ -469,54 +466,15 @@ export default function RunFiles({ loaderData }: any) { return ; } - // Initial load failed and we have no prior data to fall back on — render - // the status-specific error state inline per plan § R5. The route does - // not unmount; the RunFilesErrorBoundary is reserved for render-time - // errors that aren't surfaced by the loader at all. + // Initial load failed with no prior data to fall back on. The route + // stays mounted; `RunFilesErrorBoundary` is reserved for render-time + // React errors (the loader doesn't throw). if (initialError) { - // R5(c): access denied. - if (initialError.status === 401 || initialError.status === 403) { - return ( -
- You don't have access to this run's files. -
- ); - } - // R5(a): 4xx transient (429) / 503 — retry affordance. - if (initialError.status === 429 || initialError.status === 503) { - return ( - revalidator.revalidate()} - /> - ); - } - // R5(d): 500 — surface request ID if we have it. - if (initialError.status >= 500) { - const suffix = initialError.requestId - ? ` Request ID: ${initialError.requestId}.` - : ""; - return ( -
- Something went wrong.{suffix} Please contact support if this - persists. -
- ); - } - // Any other 4xx that isn't 401/403/404/429 — treat like a retryable - // transient failure. The banner keeps the user in context. - return ( - revalidator.revalidate()} - /> - ); + return renderStatusError({ + status: initialError.status, + requestId: initialError.requestId, + onRetry: () => revalidator.revalidate(), + }); } if (!data) { @@ -534,11 +492,12 @@ export default function RunFiles({ loaderData }: any) { const { data: files, meta } = data; // Refresh is disabled when the server reports the same `to_sha` it - // reported on the previous fetch — no new checkpoint yet. + // reported on the previous successful fetch — no new checkpoint yet. + // `lastGoodDataRef.current` is updated in a useEffect, so during render + // it still holds the previous render's data (or null on first load). + const prevToSha = lastGoodDataRef.current?.meta?.to_sha ?? null; const refreshDisabled = - !!meta.to_sha && - lastToShaRef.current !== null && - lastToShaRef.current === meta.to_sha; + !!meta.to_sha && prevToSha !== null && prevToSha === meta.to_sha; const toolbar = ( {bannerCopyForReason(reason)} @@ -103,7 +110,9 @@ export function DegradedBanner({ reason }: { reason?: string }) { ); } -export function bannerCopyForReason(reason: string | undefined): string { +export function bannerCopyForReason( + reason: RunFilesMetaDegradedReasonEnum | undefined | string, +): string { switch (reason) { case "sandbox_gone": return "Showing final patch only. This run's sandbox has been cleaned up, so individual file contents are no longer available."; diff --git a/apps/fabro-web/app/routes/run-files/states.tsx b/apps/fabro-web/app/routes/run-files/states.tsx index f1da48aba..39faa950b 100644 --- a/apps/fabro-web/app/routes/run-files/states.tsx +++ b/apps/fabro-web/app/routes/run-files/states.tsx @@ -141,52 +141,85 @@ export function Toast({ children }: { children: React.ReactNode }) { } /** - * Route-level ErrorBoundary that handles the documented status codes from - * the plan § Unit 11 taxonomy. 500 responses with a `request_id` in the - * body surface it in the copy so users can cite it when contacting support. + * Shared helper for rendering the documented status-code taxonomy. Consumed + * by both the inline `initialError` branch in run-files.tsx and the + * `RunFilesErrorBoundary` below — keeps the copy in one place so updates + * don't drift between the two surfaces. */ -export function RunFilesErrorBoundary() { - const error = useRouteError(); - if (isRouteErrorResponse(error)) { - if (error.status === 401 || error.status === 403) { - return ( -
- You don't have access to this run's files. -
- ); - } - if (error.status === 503 || error.status === 429) { - return ( - window.location.reload()} - /> - ); - } - if (error.status === 500) { - const requestId = extractRequestId(error.data); - return ( -
- Something went wrong. - {requestId ? ` Request ID: ${requestId}.` : null} Please contact - support if this persists. -
- ); - } +export function renderStatusError(args: { + status: number; + requestId: string | null; + onRetry: () => void; +}): React.ReactElement { + const { status, requestId, onRetry } = args; + if (status === 401 || status === 403) { return ( -
- Something went wrong ({error.status}). +
+ You don't have access to this run's files. +
+ ); + } + if (status === 429 || status === 503) { + return ( + + ); + } + if (status >= 500) { + const suffix = requestId ? ` Request ID: ${requestId}.` : ""; + return ( +
+ Something went wrong.{suffix} Please contact support if this persists.
); } return ( -
+ + ); +} + +/** + * Route-level ErrorBoundary for render-time React errors. The Files loader + * no longer throws (it returns errors in-band via RunFilesLoaderResult), so + * this only fires for React render crashes. + */ +export function RunFilesErrorBoundary() { + const error = useRouteError(); + if (isRouteErrorResponse(error)) { + return renderStatusError({ + status: error.status, + requestId: extractRequestIdFromUnknown(error.data), + onRetry: () => window.location.reload(), + }); + } + return ( +
Something went wrong loading this run's files.
); } -function extractRequestId(body: unknown): string | null { +/** + * Request-ID parser used only by the ErrorBoundary path. The loader path + * already extracts request_id into `RunFilesLoaderResult.error.requestId` + * via `run-files.tsx::extractRequestId` — this is the body shape + * react-router hands us in `useRouteError().data` for non-Response errors. + */ +function extractRequestIdFromUnknown(body: unknown): string | null { if (!body || typeof body !== "object") return null; const b = body as Record; if (typeof b.request_id === "string") return b.request_id; @@ -194,13 +227,12 @@ function extractRequestId(body: unknown): string | null { if (Array.isArray(errors) && errors.length > 0) { const first = errors[0]; if (first && typeof first === "object") { - const detail = (first as Record).detail; - if (typeof detail === "string") { - const match = detail.match(/request[_ ]id[=:]?\s*([a-zA-Z0-9-_]+)/i); + const rec = first as Record; + if (typeof rec.request_id === "string") return rec.request_id; + if (typeof rec.detail === "string") { + const match = rec.detail.match(/request[_ ]id[=:]?\s*([a-zA-Z0-9-_]+)/i); if (match) return match[1]; } - const reqId = (first as Record).request_id; - if (typeof reqId === "string") return reqId; } } return null; diff --git a/lib/crates/fabro-server/src/run_files.rs b/lib/crates/fabro-server/src/run_files.rs index 9a38c374b..cada469ec 100644 --- a/lib/crates/fabro-server/src/run_files.rs +++ b/lib/crates/fabro-server/src/run_files.rs @@ -1,18 +1,15 @@ #![allow(clippy::result_large_err, unreachable_pub)] -//! GET /api/v1/runs/{id}/files — per-request coalescing and handler. +//! `GET /api/v1/runs/{id}/files` — handler, coalescing primitive, and +//! per-run materialization pipeline. //! -//! This module exposes the per-run request-coalescing primitive consumed by -//! the Files Changed endpoint. Concurrent HTTP callers for the same run share -//! one materialization; concurrent callers for different runs proceed in -//! parallel. See Unit 4 of the Run Files Changed plan for design rationale. -//! -//! The materialization is deliberately driven by [`tokio::spawn`] rather than -//! polling a `Shared` future: the spawned task makes progress regardless of -//! whether any caller is still waiting, so an abandoned request cannot leave -//! orphan git subprocesses in the sandbox. All panics are caught and surfaced -//! as a 500 `ApiError`, and the registry entry is removed on task completion -//! so a follow-up request triggers a fresh materialization. +//! Concurrent callers for the same run share one materialization; different +//! runs proceed in parallel. Materialization is driven by [`tokio::spawn`] +//! so it makes progress regardless of caller liveness — an abandoned +//! request cannot leave orphan git subprocesses in the sandbox. Panics are +//! caught and surfaced as 500 `ApiError` to every coalesced caller; the +//! registry entry is removed on task completion so a follow-up request +//! triggers a fresh materialization. use std::collections::{HashMap, HashSet}; use std::future::Future; @@ -54,6 +51,18 @@ pub(crate) const FILE_COUNT_CAP: usize = 200; /// Sandbox git timeout. Matches Unit 3 helpers (10 s). const SANDBOX_GIT_TIMEOUT_MS: u64 = 10_000; +/// Below this SHA count the phase-1 `cat-file --batch-check` pre-filter is +/// skipped — its ~100 ms round-trip dominates for small diffs, and phase-2 +/// already size-caps per blob. +const METADATA_PHASE_SHA_THRESHOLD: usize = 10; + +fn transient_503(op: &str, message: &str) -> ApiError { + ApiError::new( + StatusCode::SERVICE_UNAVAILABLE, + format!("Sandbox {op} failed: {message}"), + ) +} + /// Query parameters accepted by `GET /runs/{id}/files`. #[derive(Debug, Deserialize, Default)] pub struct ListRunFilesParams { @@ -151,22 +160,19 @@ where // ── HTTP handler ────────────────────────────────────────────────────────── -/// `GET /api/v1/runs/{id}/files` — real handler. +/// `GET /api/v1/runs/{id}/files` handler. /// -/// Flow: -/// 1. Parse & authenticate -/// 2. Reject non-default `from_sha`/`to_sha` per R15 (v1 only serves the full -/// run diff) -/// 3. Load run projection (404 on missing/unauthorized — IDOR-safe) -/// 4. Try to reconnect the sandbox; on success, run the sandbox git helpers and -/// build a structured response -/// 5. Fall through to empty envelope when no sandbox path is available. Unit 6 -/// replaces this branch with the `final_patch` degraded fallback. +/// 1. Parse + authenticate. Reject non-default `from_sha`/`to_sha` (v1 only +/// serves the full run diff). +/// 2. Load the run projection. 404 covers both missing run and missing access — +/// IDOR-safe. +/// 3. Try to reconnect the sandbox; on success, build a structured diff. +/// 4. On reconnect failure or garbage-collected base, fall through to a +/// degraded response built from `RunProjection.final_patch`. /// /// All logging emits a single `tracing::info!` with an allowlisted field -/// set: `run_id, file_count, bytes_total, duration_ms, truncated, -/// binary_count, sensitive_count, symlink_count, submodule_count`. No -/// paths, contents, or raw git stderr are logged. +/// set enforced by [`RunFilesMetrics::emit`] — no paths, contents, or raw +/// git stderr. pub async fn list_run_files( _auth: AuthenticatedService, State(state): State>, @@ -235,7 +241,7 @@ async fn materialize_sandbox_path(state: &Arc, run_id: &RunId) -> List let projection = load_projection(state, run_id).await?; let Some(base_sha) = projection.start.as_ref().and_then(|s| s.base_sha.clone()) else { - // Run hasn't started yet (no base_sha). UI maps this to R4(a). + // Run hasn't started yet — no base_sha, no diff to compute. return Ok(empty_envelope()); }; @@ -247,14 +253,20 @@ async fn materialize_sandbox_path(state: &Arc, run_id: &RunId) -> List )); }; - // Resolve HEAD to a concrete `to_sha` and capture its commit time for - // the "Checkpoint Xm ago" freshness indicator on the client. - let to_sha = resolve_head_sha(sandbox.as_ref()).await?; - let to_sha_committed_at = resolve_commit_time(sandbox.as_ref(), &to_sha).await; + // Resolve HEAD (sha + commit time) in one round-trip. + let (to_sha, to_sha_committed_at) = resolve_head_sha_and_time(sandbox.as_ref()).await?; - // Enumerate changes. Permanent errors (bad_sha, missing object) fall - // through to the patch-only fallback; transient errors surface as 503. - let raw_entries = match list_changed_files_raw(sandbox.as_ref(), &base_sha, &to_sha).await { + // Enumerate changes and classify binary vs text in parallel — both + // traversals are mutually independent once `to_sha` is known, and + // running them sequentially would add ~100 ms per request on Daytona. + let (raw_res, binary_res) = tokio::join!( + list_changed_files_raw(sandbox.as_ref(), &base_sha, &to_sha), + list_binary_paths(sandbox.as_ref(), &base_sha, &to_sha), + ); + + // Permanent errors (bad_sha, missing object) fall through to the + // patch-only fallback; transient errors surface as 503. + let raw_entries = match raw_res { Ok(v) => v, Err(DiffError::Permanent { .. }) => { return Ok(build_fallback_response( @@ -263,29 +275,23 @@ async fn materialize_sandbox_path(state: &Arc, run_id: &RunId) -> List )); } Err(DiffError::Transient { message }) => { - return Err(ApiError::new( - StatusCode::SERVICE_UNAVAILABLE, - format!("Sandbox git subprocess failed: {message}"), - )); + return Err(transient_503("git diff --raw", &message)); } }; - let binary_paths = match list_binary_paths(sandbox.as_ref(), &base_sha, &to_sha).await { + let binary_paths = match binary_res { Ok(v) => v, Err(DiffError::Permanent { .. }) => HashSet::new(), Err(DiffError::Transient { message }) => { - return Err(ApiError::new( - StatusCode::SERVICE_UNAVAILABLE, - format!("Sandbox git numstat failed: {message}"), - )); + return Err(transient_503("git diff --numstat", &message)); } }; let total_changed_before_cap = raw_entries.len(); // Classify every entry against the denylist + binary/symlink/submodule - // flags FIRST so no-blob-needed placeholders don't consume cap slots that - // belong to real file changes (plan § Unit 5: R31 acts before R27). + // flags FIRST so no-blob-needed placeholders don't consume cap slots + // that belong to real file changes. let classified = classify_entries(&raw_entries, &binary_paths, is_sensitive); // Then cap the combined list at 200 entries. @@ -559,8 +565,8 @@ async fn load_projection( /// Reconnect semantics tailored to the Files endpoint: /// - `Ok(Some(sandbox))`: reconnected, caller proceeds on the sandbox path. /// - `Ok(None)`: no sandbox record, reconnect failed, or the provider isn't -/// supported by this build — caller falls through to the fallback branch -/// (Unit 6) instead of returning 409. +/// supported by this build — caller falls through to the degraded fallback +/// instead of returning 409. /// - `Err(ApiError)`: unrecoverable error loading run state. async fn try_reconnect_run_sandbox( state: &Arc, @@ -576,36 +582,16 @@ async fn try_reconnect_run_sandbox( } } -/// Return the commit time of `sha` in strict ISO 8601 via -/// `git show -s --format=%cI`. `None` on any error (best-effort: the -/// handler still succeeds without the freshness timestamp). -async fn resolve_commit_time( +/// Resolve HEAD's SHA and its commit time in a single sandbox round-trip. +/// `git show -s --format=%H %cI HEAD` prints both on one line separated by +/// a space. The commit time is best-effort — if parsing fails the handler +/// still succeeds without the freshness timestamp. +async fn resolve_head_sha_and_time( sandbox: &dyn Sandbox, - sha: &str, -) -> Option> { - // SHAs come from `git rev-parse HEAD` so they're trusted hex; reject - // anything non-conforming as defense in depth before interpolation. - if !sha.chars().all(|c| c.is_ascii_hexdigit()) || sha.is_empty() { - return None; - } - let cmd = format!("git -c core.hooksPath=/dev/null show -s --format=%cI {sha}"); - let res = sandbox - .exec_command(&cmd, SANDBOX_GIT_TIMEOUT_MS, None, None, None) - .await - .ok()?; - if res.exit_code != 0 { - return None; - } - let iso = res.stdout.trim(); - chrono::DateTime::parse_from_rfc3339(iso) - .ok() - .map(|d| d.with_timezone(&chrono::Utc)) -} - -async fn resolve_head_sha(sandbox: &dyn Sandbox) -> std::result::Result { +) -> std::result::Result<(String, Option>), ApiError> { let res = sandbox .exec_command( - "git rev-parse HEAD", + "git -c core.hooksPath=/dev/null show -s --format=%H\\ %cI HEAD", SANDBOX_GIT_TIMEOUT_MS, None, None, @@ -619,14 +605,20 @@ async fn resolve_head_sha(sandbox: &dyn Sandbox) -> std::result::Result, @@ -939,12 +930,12 @@ fn collect_blob_shas(classified: &[ClassifiedEntry]) -> Vec { /// Phase 2 (contents): bulk `cat-file --batch` on the remaining SHAs. /// /// Failure modes: -/// - Phase 1 permanent error: fall through with an empty size map; phase 2 -/// runs against the full SHA list (current behavior before this split). +/// - Phase 1 permanent error: fall through with an empty size map; phase 2 runs +/// against the full SHA list (current behavior before this split). /// - Phase 1 transient error: 503 to the client. -/// - Phase 2 permanent error (malformed blob in stream): only the -/// phase-2 SHAs get `None`; phase-1-classified oversized SHAs keep their -/// `None` entries but with a semantically-accurate cause. +/// - Phase 2 permanent error (malformed blob in stream): only the phase-2 SHAs +/// get `None`; phase-1-classified oversized SHAs keep their `None` entries +/// but with a semantically-accurate cause. /// - Phase 2 transient error: 503 to the client. async fn fetch_blob_table( sandbox: &dyn Sandbox, @@ -956,26 +947,28 @@ async fn fetch_blob_table( let mut table: HashMap> = HashMap::with_capacity(shas.len()); - // Phase 1: --batch-check for sizes. - let oversized: HashSet = match stream_blob_metadata(sandbox, shas).await { - Ok(metas) => { - let mut set = HashSet::new(); - for meta in metas { - if let Some(size) = meta.size { - if size > PER_FILE_BYTES_CAP { - set.insert(meta.sha); - } - } + // Phase 1: --batch-check for sizes. Skipped for small SHA lists where + // the pre-filter's ~100 ms round-trip is pure overhead — `stream_blobs` + // already size-caps per blob and returns `None` for oversized ones. + // Phase 1 only earns its cost when a single malformed/huge blob could + // poison a large batch's parse. + let oversized: HashSet = if shas.len() >= METADATA_PHASE_SHA_THRESHOLD { + match stream_blob_metadata(sandbox, shas).await { + Ok(metas) => metas + .into_iter() + .filter_map(|m| { + m.size + .filter(|size| *size > PER_FILE_BYTES_CAP) + .map(|_| m.sha) + }) + .collect(), + Err(DiffError::Permanent { .. }) => HashSet::new(), + Err(DiffError::Transient { message }) => { + return Err(transient_503("git cat-file --batch-check", &message)); } - set - } - Err(DiffError::Permanent { .. }) => HashSet::new(), - Err(DiffError::Transient { message }) => { - return Err(ApiError::new( - StatusCode::SERVICE_UNAVAILABLE, - format!("Sandbox git cat-file --batch-check failed: {message}"), - )); } + } else { + HashSet::new() }; // Record oversized blobs in the table as `None` so the caller emits @@ -1009,10 +1002,7 @@ async fn fetch_blob_table( } } Err(DiffError::Transient { message }) => { - return Err(ApiError::new( - StatusCode::SERVICE_UNAVAILABLE, - format!("Sandbox git cat-file --batch failed: {message}"), - )); + return Err(transient_503("git cat-file --batch", &message)); } } @@ -1189,9 +1179,9 @@ mod tests { #[tokio::test] async fn first_caller_cancelling_does_not_block_other_callers() { - // P2-13 regression: tokio::spawn detaches materialization from any - // individual caller, so the first caller dropping its future must - // not prevent a subsequent caller from receiving the result. + // tokio::spawn detaches materialization from any individual + // caller; the first caller dropping its future must not prevent + // a subsequent caller from receiving the result. let inflight = new_registry(); let counter = Arc::new(AtomicUsize::new(0)); let run = run_id("run_ffffffffffffffffffffffffff"); @@ -1230,7 +1220,7 @@ mod tests { assert_eq!(counter.load(Ordering::SeqCst), 1); } - // ── Tracing allowlist assertion (P2-11) ────────────────────────────── + // ── Tracing allowlist assertion ────────────────────────────── use std::sync::{Mutex as StdMutex, OnceLock}; @@ -1282,8 +1272,8 @@ mod tests { #[test] fn run_files_metrics_emit_writes_only_allowlisted_fields() { - // Plan § Unit 5: the tracing field set is an allowlist — no paths, - // contents, or raw git stderr may leak. + // The tracing field set is an allowlist — no paths, contents, or + // raw git stderr may leak. let captured = install_tracing_capture(); captured.lock().unwrap().clear(); @@ -1391,9 +1381,9 @@ diff --git a/src/bar.rs b/src/bar.rs #[test] fn strip_denylisted_sections_catches_rename_with_sensitive_old_side() { - // Regression for P1-2: renaming away from a sensitive path must - // still strip the patch — the benign new path alone doesn't reveal - // the secret but the hunk body does. + // Renaming away from a sensitive path must still strip the patch + // — the benign new path alone doesn't reveal the secret but the + // hunk body does. let patch = "\ diff --git a/.env.production b/docs/NOTES.md rename from .env.production @@ -1414,9 +1404,9 @@ rename to docs/NOTES.md #[test] fn stitch_file_diff_returns_distinct_old_and_new_contents_for_modified() { - // Regression for P1-1: before this fix, the handler fetched only the - // new-side blob and duplicated it onto both sides, producing no-op - // diffs for all modified files. + // Modified files must expose distinct old/new contents; pulling + // only the new_blob and duplicating it would render as a no-op + // diff in `MultiFileDiff`. let entry = RawDiffEntry::Modified { path: "src/main.rs".to_string(), old_blob: "aaaa000000000000000000000000000000000000".to_string(), diff --git a/lib/crates/fabro-server/src/run_files_security.rs b/lib/crates/fabro-server/src/run_files_security.rs index b4e6fa948..61e7adeba 100644 --- a/lib/crates/fabro-server/src/run_files_security.rs +++ b/lib/crates/fabro-server/src/run_files_security.rs @@ -1,19 +1,13 @@ -#![allow(unreachable_pub, dead_code)] +#![allow(unreachable_pub)] -//! Security helpers shared by the Run Files Changed endpoint: a globset-based -//! sensitive-path denylist, a sandbox-git env-hardening helper, and a -//! structured metrics emitter that enforces the tracing allowlist. +//! Security helpers for the Run Files Changed endpoint: a globset-based +//! sensitive-path denylist and a structured metrics emitter that enforces +//! the tracing allowlist. //! -//! All matching is path-based and case-insensitive. The denylist is a -//! defense-in-depth control — it is not a content scanner and will not -//! catch arbitrary secrets hidden inside non-secret file extensions. -//! -//! `sandbox_git_env` and `RunFilesMetrics` are intentionally public APIs -//! even though they're currently consumed by a single caller — the module -//! is designed as a reusable surface for any future sensitive-data-adjacent -//! endpoint. +//! Matching is path-based and case-insensitive. The denylist is a +//! defense-in-depth control — not a content scanner, and it won't catch +//! arbitrary secrets hidden inside non-secret file extensions. -use std::collections::HashMap; use std::sync::OnceLock; use fabro_types::RunId; @@ -140,17 +134,6 @@ fn normalize_for_match(path: &str) -> String { out } -/// Environment additions applied to every sandbox-side git invocation under -/// the Run Files endpoint. Pairs with the hardened `-c` flags the sandbox -/// git helpers already use. -#[must_use] -pub fn sandbox_git_env() -> HashMap { - HashMap::from([ - ("GIT_TERMINAL_PROMPT".to_string(), "0".to_string()), - ("GIT_EXTERNAL_DIFF".to_string(), String::new()), - ]) -} - /// Metrics emitted at the tail of every Run Files response. The field set is /// deliberately the only shape of tracing output the endpoint produces — /// enforced by `emit`, which never interpolates or logs individual paths, @@ -262,14 +245,4 @@ mod tests { // A non-sensitive unicode path must still not match any glob. assert!(!is_sensitive("docs/Ähnlichkeit.md")); } - - #[test] - fn sandbox_git_env_sets_expected_pairs() { - let env = sandbox_git_env(); - assert_eq!( - env.get("GIT_TERMINAL_PROMPT").map(String::as_str), - Some("0") - ); - assert_eq!(env.get("GIT_EXTERNAL_DIFF").map(String::as_str), Some("")); - } } diff --git a/lib/crates/fabro-spa/assets/assets/entry-8hk343fy.js b/lib/crates/fabro-spa/assets/assets/entry-8nkj5gta.js similarity index 52% rename from lib/crates/fabro-spa/assets/assets/entry-8hk343fy.js rename to lib/crates/fabro-spa/assets/assets/entry-8nkj5gta.js index 3daeea9ba..0b2b60d63 100644 --- a/lib/crates/fabro-spa/assets/assets/entry-8hk343fy.js +++ b/lib/crates/fabro-spa/assets/assets/entry-8nkj5gta.js @@ -1,42 +1,42 @@ -import{X as u,Y as _8,Z as o5,_ as f}from"./chunk-q07bg6gn.js";var Q0=_8((Xl,MU)=>{(function(){function Z(I,Z0){Object.defineProperty(q.prototype,I,{get:function(){console.warn("%s(...) is deprecated in plain JavaScript React classes. %s",Z0[0],Z0[1])}})}function Y(I){if(I===null||typeof I!=="object")return null;return I=X1&&I[X1]||I["@@iterator"],typeof I==="function"?I:null}function J(I,Z0){I=(I=I.constructor)&&(I.displayName||I.name)||"ReactClass";var _0=I+"."+Z0;L0[_0]||(console.error("Can't call %s on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the %s component.",Z0,I),L0[_0]=!0)}function q(I,Z0,_0){this.props=I,this.context=Z0,this.refs=e5,this.updater=_0||C1}function z(){}function B(I,Z0,_0){this.props=I,this.context=Z0,this.refs=e5,this.updater=_0||C1}function W(){}function U(I){return""+I}function G(I){try{U(I);var Z0=!1}catch(S0){Z0=!0}if(Z0){Z0=console;var _0=Z0.error,v0=typeof Symbol==="function"&&Symbol.toStringTag&&I[Symbol.toStringTag]||I.constructor.name||"Object";return _0.call(Z0,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",v0),U(I)}}function w(I){if(I==null)return null;if(typeof I==="function")return I.$$typeof===m6?null:I.displayName||I.name||null;if(typeof I==="string")return I;switch(I){case G0:return"Fragment";case B0:return"Profiler";case k:return"StrictMode";case u0:return"Suspense";case W0:return"SuspenseList";case w1:return"Activity"}if(typeof I==="object")switch(typeof I.tag==="number"&&console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),I.$$typeof){case i:return"Portal";case w0:return I.displayName||"Context";case H0:return(I._context.displayName||"Context")+".Consumer";case b0:var Z0=I.render;return I=I.displayName,I||(I=Z0.displayName||Z0.name||"",I=I!==""?"ForwardRef("+I+")":"ForwardRef"),I;case x0:return Z0=I.displayName||null,Z0!==null?Z0:w(I.type)||"Memo";case d0:Z0=I._payload,I=I._init;try{return w(I(Z0))}catch(_0){}}return null}function H(I){if(I===G0)return"<>";if(typeof I==="object"&&I!==null&&I.$$typeof===d0)return"<...>";try{var Z0=w(I);return Z0?"<"+Z0+">":"<...>"}catch(_0){return"<...>"}}function O(){var I=K1.A;return I===null?null:I.getOwner()}function A(){return Error("react-stack-top-frame")}function _(I){if(U4.call(I,"key")){var Z0=Object.getOwnPropertyDescriptor(I,"key").get;if(Z0&&Z0.isReactWarning)return!1}return I.key!==void 0}function P(I,Z0){function _0(){N6||(N6=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",Z0))}_0.isReactWarning=!0,Object.defineProperty(I,"key",{get:_0,configurable:!0})}function L(){var I=w(this.type);return f4[I]||(f4[I]=!0,console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.")),I=this.props.ref,I!==void 0?I:null}function R(I,Z0,_0,v0,S0,e0){var k0=_0.ref;return I={$$typeof:e,type:I,key:Z0,props:_0,_owner:v0},(k0!==void 0?k0:null)!==null?Object.defineProperty(I,"ref",{enumerable:!1,get:L}):Object.defineProperty(I,"ref",{enumerable:!1,value:null}),I._store={},Object.defineProperty(I._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(I,"_debugInfo",{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(I,"_debugStack",{configurable:!1,enumerable:!1,writable:!0,value:S0}),Object.defineProperty(I,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:e0}),Object.freeze&&(Object.freeze(I.props),Object.freeze(I)),I}function T(I,Z0){return Z0=R(I.type,Z0,I.props,I._owner,I._debugStack,I._debugTask),I._store&&(Z0._store.validated=I._store.validated),Z0}function C(I){v(I)?I._store&&(I._store.validated=1):typeof I==="object"&&I!==null&&I.$$typeof===d0&&(I._payload.status==="fulfilled"?v(I._payload.value)&&I._payload.value._store&&(I._payload.value._store.validated=1):I._store&&(I._store.validated=1))}function v(I){return typeof I==="object"&&I!==null&&I.$$typeof===e}function b(I){var Z0={"=":"=0",":":"=2"};return"$"+I.replace(/[=:]/g,function(_0){return Z0[_0]})}function S(I,Z0){return typeof I==="object"&&I!==null&&I.key!=null?(G(I.key),b(""+I.key)):Z0.toString(36)}function j(I){switch(I.status){case"fulfilled":return I.value;case"rejected":throw I.reason;default:switch(typeof I.status==="string"?I.then(W,W):(I.status="pending",I.then(function(Z0){I.status==="pending"&&(I.status="fulfilled",I.value=Z0)},function(Z0){I.status==="pending"&&(I.status="rejected",I.reason=Z0)})),I.status){case"fulfilled":return I.value;case"rejected":throw I.reason}}throw I}function E(I,Z0,_0,v0,S0){var e0=typeof I;if(e0==="undefined"||e0==="boolean")I=null;var k0=!1;if(I===null)k0=!0;else switch(e0){case"bigint":case"string":case"number":k0=!0;break;case"object":switch(I.$$typeof){case e:case i:k0=!0;break;case d0:return k0=I._init,E(k0(I._payload),Z0,_0,v0,S0)}}if(k0){k0=I,S0=S0(k0);var Q1=v0===""?"."+S(k0,0):v0;return N1(S0)?(_0="",Q1!=null&&(_0=Q1.replace(e2,"$&/")+"/"),E(S0,Z0,_0,"",function(H5){return H5})):S0!=null&&(v(S0)&&(S0.key!=null&&(k0&&k0.key===S0.key||G(S0.key)),_0=T(S0,_0+(S0.key==null||k0&&k0.key===S0.key?"":(""+S0.key).replace(e2,"$&/")+"/")+Q1),v0!==""&&k0!=null&&v(k0)&&k0.key==null&&k0._store&&!k0._store.validated&&(_0._store.validated=2),S0=_0),Z0.push(S0)),1}if(k0=0,Q1=v0===""?".":v0+":",N1(I))for(var y0=0;y0{(function(){function Z(I,e){Object.defineProperty(q.prototype,I,{get:function(){console.warn("%s(...) is deprecated in plain JavaScript React classes. %s",e[0],e[1])}})}function Y(I){if(I===null||typeof I!=="object")return null;return I=z1&&I[z1]||I["@@iterator"],typeof I==="function"?I:null}function J(I,e){I=(I=I.constructor)&&(I.displayName||I.name)||"ReactClass";var _0=I+"."+e;L0[_0]||(console.error("Can't call %s on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the %s component.",e,I),L0[_0]=!0)}function q(I,e,_0){this.props=I,this.context=e,this.refs=e5,this.updater=_0||C1}function z(){}function B(I,e,_0){this.props=I,this.context=e,this.refs=e5,this.updater=_0||C1}function W(){}function U(I){return""+I}function G(I){try{U(I);var e=!1}catch(x0){e=!0}if(e){e=console;var _0=e.error,R0=typeof Symbol==="function"&&Symbol.toStringTag&&I[Symbol.toStringTag]||I.constructor.name||"Object";return _0.call(e,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",R0),U(I)}}function w(I){if(I==null)return null;if(typeof I==="function")return I.$$typeof===m6?null:I.displayName||I.name||null;if(typeof I==="string")return I;switch(I){case B0:return"Fragment";case U0:return"Profiler";case k:return"StrictMode";case S0:return"Suspense";case M0:return"SuspenseList";case O1:return"Activity"}if(typeof I==="object")switch(typeof I.tag==="number"&&console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),I.$$typeof){case i:return"Portal";case w0:return I.displayName||"Context";case N0:return(I._context.displayName||"Context")+".Consumer";case v0:var e=I.render;return I=I.displayName,I||(I=e.displayName||e.name||"",I=I!==""?"ForwardRef("+I+")":"ForwardRef"),I;case p0:return e=I.displayName||null,e!==null?e:w(I.type)||"Memo";case r0:e=I._payload,I=I._init;try{return w(I(e))}catch(_0){}}return null}function H(I){if(I===B0)return"<>";if(typeof I==="object"&&I!==null&&I.$$typeof===r0)return"<...>";try{var e=w(I);return e?"<"+e+">":"<...>"}catch(_0){return"<...>"}}function O(){var I=K1.A;return I===null?null:I.getOwner()}function A(){return Error("react-stack-top-frame")}function _(I){if(U4.call(I,"key")){var e=Object.getOwnPropertyDescriptor(I,"key").get;if(e&&e.isReactWarning)return!1}return I.key!==void 0}function P(I,e){function _0(){N6||(N6=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",e))}_0.isReactWarning=!0,Object.defineProperty(I,"key",{get:_0,configurable:!0})}function L(){var I=w(this.type);return f4[I]||(f4[I]=!0,console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.")),I=this.props.ref,I!==void 0?I:null}function R(I,e,_0,R0,x0,e0){var k0=_0.ref;return I={$$typeof:Z0,type:I,key:e,props:_0,_owner:R0},(k0!==void 0?k0:null)!==null?Object.defineProperty(I,"ref",{enumerable:!1,get:L}):Object.defineProperty(I,"ref",{enumerable:!1,value:null}),I._store={},Object.defineProperty(I._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(I,"_debugInfo",{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(I,"_debugStack",{configurable:!1,enumerable:!1,writable:!0,value:x0}),Object.defineProperty(I,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:e0}),Object.freeze&&(Object.freeze(I.props),Object.freeze(I)),I}function C(I,e){return e=R(I.type,e,I.props,I._owner,I._debugStack,I._debugTask),I._store&&(e._store.validated=I._store.validated),e}function T(I){v(I)?I._store&&(I._store.validated=1):typeof I==="object"&&I!==null&&I.$$typeof===r0&&(I._payload.status==="fulfilled"?v(I._payload.value)&&I._payload.value._store&&(I._payload.value._store.validated=1):I._store&&(I._store.validated=1))}function v(I){return typeof I==="object"&&I!==null&&I.$$typeof===Z0}function b(I){var e={"=":"=0",":":"=2"};return"$"+I.replace(/[=:]/g,function(_0){return e[_0]})}function S(I,e){return typeof I==="object"&&I!==null&&I.key!=null?(G(I.key),b(""+I.key)):e.toString(36)}function j(I){switch(I.status){case"fulfilled":return I.value;case"rejected":throw I.reason;default:switch(typeof I.status==="string"?I.then(W,W):(I.status="pending",I.then(function(e){I.status==="pending"&&(I.status="fulfilled",I.value=e)},function(e){I.status==="pending"&&(I.status="rejected",I.reason=e)})),I.status){case"fulfilled":return I.value;case"rejected":throw I.reason}}throw I}function E(I,e,_0,R0,x0){var e0=typeof I;if(e0==="undefined"||e0==="boolean")I=null;var k0=!1;if(I===null)k0=!0;else switch(e0){case"bigint":case"string":case"number":k0=!0;break;case"object":switch(I.$$typeof){case Z0:case i:k0=!0;break;case r0:return k0=I._init,E(k0(I._payload),e,_0,R0,x0)}}if(k0){k0=I,x0=x0(k0);var Q1=R0===""?"."+S(k0,0):R0;return w1(x0)?(_0="",Q1!=null&&(_0=Q1.replace(e2,"$&/")+"/"),E(x0,e,_0,"",function(N5){return N5})):x0!=null&&(v(x0)&&(x0.key!=null&&(k0&&k0.key===x0.key||G(x0.key)),_0=C(x0,_0+(x0.key==null||k0&&k0.key===x0.key?"":(""+x0.key).replace(e2,"$&/")+"/")+Q1),R0!==""&&k0!=null&&v(k0)&&k0.key==null&&k0._store&&!k0._store.validated&&(_0._store.validated=2),x0=_0),e.push(x0)),1}if(k0=0,Q1=R0===""?".":R0+":",w1(I))for(var y0=0;y0 import('./MyComponent')) -Did you accidentally put curly braces around the import?`,Z0),"default"in Z0||console.error(`lazy: Expected the result of a dynamic import() call. Instead received: %s +Did you accidentally put curly braces around the import?`,e),"default"in e||console.error(`lazy: Expected the result of a dynamic import() call. Instead received: %s Your code should look like: - const MyComponent = lazy(() => import('./MyComponent'))`,Z0),Z0.default;throw I._result}function x(){var I=K1.H;return I===null&&console.error(`Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: + const MyComponent = lazy(() => import('./MyComponent'))`,e),e.default;throw I._result}function x(){var I=K1.H;return I===null&&console.error(`Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3. You might have more than one copy of React in the same app -See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.`),I}function l(){K1.asyncTransitions--}function X0(I){if(G4===null)try{var Z0=("require"+Math.random()).slice(0,7);G4=(MU&&MU[Z0]).call(MU,"timers").setImmediate}catch(_0){G4=function(v0){$2===!1&&($2=!0,typeof MessageChannel>"u"&&console.error("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."));var S0=new MessageChannel;S0.port1.onmessage=v0,S0.port2.postMessage(void 0)}}return G4(I)}function K0(I){return 1 ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"))}),{then:function(y0,H5){S0=!0,k0.then(function(q5){if(n(Z0,_0),_0===0){try{J0(v0),X0(function(){return p(q5,y0,H5)})}catch(Z2){K1.thrownErrors.push(Z2)}if(0 ...)"))}),K1.actQueue=null),0K1.recentlyCreatedOwnerStacks++;return R(I,S0,v0,O(),y0?Error("react-stack-top-frame"):k2,y0?S1(H(I)):H6)},Xl.createRef=function(){var I={current:null};return Object.seal(I),I},Xl.forwardRef=function(I){I!=null&&I.$$typeof===x0?console.error("forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...))."):typeof I!=="function"?console.error("forwardRef requires a render function but was given %s.",I===null?"null":typeof I):I.length!==0&&I.length!==2&&console.error("forwardRef render functions accept exactly two parameters: props and ref. %s",I.length===1?"Did you forget to use the ref parameter?":"Any additional parameter will be undefined."),I!=null&&I.defaultProps!=null&&console.error("forwardRef render functions do not support defaultProps. Did you accidentally pass a React component?");var Z0={$$typeof:b0,render:I},_0;return Object.defineProperty(Z0,"displayName",{enumerable:!1,configurable:!0,get:function(){return _0},set:function(v0){_0=v0,I.name||I.displayName||(Object.defineProperty(I,"name",{value:v0}),I.displayName=v0)}}),Z0},Xl.isValidElement=v,Xl.lazy=function(I){I={_status:-1,_result:I};var Z0={$$typeof:d0,_payload:I,_init:s},_0={name:"lazy",start:-1,end:-1,value:null,owner:null,debugStack:Error("react-stack-top-frame"),debugTask:console.createTask?console.createTask("lazy()"):null};return I._ioInfo=_0,Z0._debugInfo=[{awaited:_0}],Z0},Xl.memo=function(I,Z0){I==null&&console.error("memo: The first argument must be a component. Instead received: %s",I===null?"null":typeof I),Z0={$$typeof:x0,type:I,compare:Z0===void 0?null:Z0};var _0;return Object.defineProperty(Z0,"displayName",{enumerable:!1,configurable:!0,get:function(){return _0},set:function(v0){_0=v0,I.name||I.displayName||(Object.defineProperty(I,"name",{value:v0}),I.displayName=v0)}}),Z0},Xl.startTransition=function(I){var Z0=K1.T,_0={};_0._updatedFibers=new Set,K1.T=_0;try{var v0=I(),S0=K1.S;S0!==null&&S0(_0,v0),typeof v0==="object"&&v0!==null&&typeof v0.then==="function"&&(K1.asyncTransitions++,v0.then(l,l),v0.then(W,T5))}catch(e0){T5(e0)}finally{Z0===null&&_0._updatedFibers&&(I=_0._updatedFibers.size,_0._updatedFibers.clear(),10{(function(){function Z(){if(b=!1,g){var p=ql.unstable_now();l=p;var J0=!0;try{Z:{C=!1,v&&(v=!1,j(s),s=-1),T=!0;var e=R;try{Y:{B(p);for(L=J(A);L!==null&&!(L.expirationTime>p&&U());){var i=L.callback;if(typeof i==="function"){L.callback=null,R=L.priorityLevel;var G0=i(L.expirationTime<=p);if(p=ql.unstable_now(),typeof G0==="function"){L.callback=G0,B(p),J0=!0;break Y}L===J(A)&&q(A),B(p)}else q(A);L=J(A)}if(L!==null)J0=!0;else{var k=J(_);k!==null&&G(W,k.startTime-p),J0=!1}}break Z}finally{L=null,R=e,T=!1}J0=void 0}}finally{J0?X0():g=!1}}}function Y(p,J0){var e=p.length;p.push(J0);Z:for(;0>>1,G0=p[i];if(0>>1;iz(H0,e))w0z(b0,H0)?(p[i]=b0,p[w0]=e,i=w0):(p[i]=H0,p[B0]=e,i=B0);else if(w0z(b0,e))p[i]=b0,p[w0]=e,i=w0;else break Z}}return J0}function z(p,J0){var e=p.sortIndex-J0.sortIndex;return e!==0?e:p.id-J0.id}function B(p){for(var J0=J(_);J0!==null;){if(J0.callback===null)q(_);else if(J0.startTime<=p)q(_),J0.sortIndex=J0.expirationTime,Y(A,J0);else break;J0=J(_)}}function W(p){if(v=!1,B(p),!C)if(J(A)!==null)C=!0,g||(g=!0,X0());else{var J0=J(_);J0!==null&&G(W,J0.startTime-p)}}function U(){return b?!0:ql.unstable_now()-lp||125i?(p.sortIndex=e,Y(_,p),J(A)===null&&p===J(_)&&(v?(j(s),s=-1):v=!0,G(W,e-i))):(p.sortIndex=G0,Y(A,p),C||T||(C=!0,g||(g=!0,X0()))),p},ql.unstable_shouldYield=U,ql.unstable_wrapCallback=function(p){var J0=R;return function(){var e=R;R=J0;try{return p.apply(this,arguments)}finally{R=e}}},typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop==="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error())})()});var NE=_8((zl)=>{var gA=u(Q0());(function(){function Z(){}function Y(H){return""+H}function J(H,O,A){var _=3"u"&&console.error("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."));var x0=new MessageChannel;x0.port1.onmessage=R0,x0.port2.postMessage(void 0)}}return G4(I)}function K0(I){return 1 ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"))}),{then:function(y0,N5){x0=!0,k0.then(function(q5){if(n(e,_0),_0===0){try{X0(R0),J0(function(){return m(q5,y0,N5)})}catch(Z2){K1.thrownErrors.push(Z2)}if(0 ...)"))}),K1.actQueue=null),0K1.recentlyCreatedOwnerStacks++;return R(I,x0,R0,O(),y0?Error("react-stack-top-frame"):k2,y0?S1(H(I)):H6)},ql.createRef=function(){var I={current:null};return Object.seal(I),I},ql.forwardRef=function(I){I!=null&&I.$$typeof===p0?console.error("forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...))."):typeof I!=="function"?console.error("forwardRef requires a render function but was given %s.",I===null?"null":typeof I):I.length!==0&&I.length!==2&&console.error("forwardRef render functions accept exactly two parameters: props and ref. %s",I.length===1?"Did you forget to use the ref parameter?":"Any additional parameter will be undefined."),I!=null&&I.defaultProps!=null&&console.error("forwardRef render functions do not support defaultProps. Did you accidentally pass a React component?");var e={$$typeof:v0,render:I},_0;return Object.defineProperty(e,"displayName",{enumerable:!1,configurable:!0,get:function(){return _0},set:function(R0){_0=R0,I.name||I.displayName||(Object.defineProperty(I,"name",{value:R0}),I.displayName=R0)}}),e},ql.isValidElement=v,ql.lazy=function(I){I={_status:-1,_result:I};var e={$$typeof:r0,_payload:I,_init:o},_0={name:"lazy",start:-1,end:-1,value:null,owner:null,debugStack:Error("react-stack-top-frame"),debugTask:console.createTask?console.createTask("lazy()"):null};return I._ioInfo=_0,e._debugInfo=[{awaited:_0}],e},ql.memo=function(I,e){I==null&&console.error("memo: The first argument must be a component. Instead received: %s",I===null?"null":typeof I),e={$$typeof:p0,type:I,compare:e===void 0?null:e};var _0;return Object.defineProperty(e,"displayName",{enumerable:!1,configurable:!0,get:function(){return _0},set:function(R0){_0=R0,I.name||I.displayName||(Object.defineProperty(I,"name",{value:R0}),I.displayName=R0)}}),e},ql.startTransition=function(I){var e=K1.T,_0={};_0._updatedFibers=new Set,K1.T=_0;try{var R0=I(),x0=K1.S;x0!==null&&x0(_0,R0),typeof R0==="object"&&R0!==null&&typeof R0.then==="function"&&(K1.asyncTransitions++,R0.then(l,l),R0.then(W,v5))}catch(e0){v5(e0)}finally{e===null&&_0._updatedFibers&&(I=_0._updatedFibers.size,_0._updatedFibers.clear(),10{(function(){function Z(){if(b=!1,g){var m=zl.unstable_now();l=m;var X0=!0;try{Z:{T=!1,v&&(v=!1,j(o),o=-1),C=!0;var Z0=R;try{Y:{B(m);for(L=J(A);L!==null&&!(L.expirationTime>m&&U());){var i=L.callback;if(typeof i==="function"){L.callback=null,R=L.priorityLevel;var B0=i(L.expirationTime<=m);if(m=zl.unstable_now(),typeof B0==="function"){L.callback=B0,B(m),X0=!0;break Y}L===J(A)&&q(A),B(m)}else q(A);L=J(A)}if(L!==null)X0=!0;else{var k=J(_);k!==null&&G(W,k.startTime-m),X0=!1}}break Z}finally{L=null,R=Z0,C=!1}X0=void 0}}finally{X0?J0():g=!1}}}function Y(m,X0){var Z0=m.length;m.push(X0);Z:for(;0>>1,B0=m[i];if(0>>1;iz(N0,Z0))w0z(v0,N0)?(m[i]=v0,m[w0]=Z0,i=w0):(m[i]=N0,m[U0]=Z0,i=U0);else if(w0z(v0,Z0))m[i]=v0,m[w0]=Z0,i=w0;else break Z}}return X0}function z(m,X0){var Z0=m.sortIndex-X0.sortIndex;return Z0!==0?Z0:m.id-X0.id}function B(m){for(var X0=J(_);X0!==null;){if(X0.callback===null)q(_);else if(X0.startTime<=m)q(_),X0.sortIndex=X0.expirationTime,Y(A,X0);else break;X0=J(_)}}function W(m){if(v=!1,B(m),!T)if(J(A)!==null)T=!0,g||(g=!0,J0());else{var X0=J(_);X0!==null&&G(W,X0.startTime-m)}}function U(){return b?!0:zl.unstable_now()-lm||125i?(m.sortIndex=Z0,Y(_,m),J(A)===null&&m===J(_)&&(v?(j(o),o=-1):v=!0,G(W,Z0-i))):(m.sortIndex=B0,Y(A,m),T||C||(T=!0,g||(g=!0,J0()))),m},zl.unstable_shouldYield=U,zl.unstable_wrapCallback=function(m){var X0=R;return function(){var Z0=R;R=X0;try{return m.apply(this,arguments)}finally{R=Z0}}},typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop==="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error())})()});var HE=_8((Kl)=>{var gA=u(Q0());(function(){function Z(){}function Y(H){return""+H}function J(H,O,A){var _=3` tag.%s',A),typeof H==="string"&&typeof O==="object"&&O!==null&&typeof O.as==="string"){A=O.as;var _=q(A,O.crossOrigin);U.d.L(H,A,{crossOrigin:_,integrity:typeof O.integrity==="string"?O.integrity:void 0,nonce:typeof O.nonce==="string"?O.nonce:void 0,type:typeof O.type==="string"?O.type:void 0,fetchPriority:typeof O.fetchPriority==="string"?O.fetchPriority:void 0,referrerPolicy:typeof O.referrerPolicy==="string"?O.referrerPolicy:void 0,imageSrcSet:typeof O.imageSrcSet==="string"?O.imageSrcSet:void 0,imageSizes:typeof O.imageSizes==="string"?O.imageSizes:void 0,media:typeof O.media==="string"?O.media:void 0})}},zl.preloadModule=function(H,O){var A="";typeof H==="string"&&H||(A+=" The `href` argument encountered was "+z(H)+"."),O!==void 0&&typeof O!=="object"?A+=" The `options` argument encountered was "+z(O)+".":O&&("as"in O)&&typeof O.as!=="string"&&(A+=" The `as` option encountered was "+z(O.as)+"."),A&&console.error('ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `` tag.%s',A),typeof H==="string"&&(O?(A=q(O.as,O.crossOrigin),U.d.m(H,{as:typeof O.as==="string"&&O.as!=="script"?O.as:void 0,crossOrigin:A,integrity:typeof O.integrity==="string"?O.integrity:void 0})):U.d.m(H))},zl.requestFormReset=function(H){U.d.r(H)},zl.unstable_batchedUpdates=function(H,O){return H(O)},zl.useFormState=function(H,O,A){return W().useFormState(H,O,A)},zl.useFormStatus=function(){return W().useHostTransitionStatus()},zl.version="19.2.4",typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop==="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error())})()});var A3=_8((t90,HE)=>{var Kl=u(NE());HE.exports=Kl});var OE=_8((Bl)=>{var r1=u(wE()),PJ=u(Q0()),uA=u(A3());(function(){function Z(Q,X){for(Q=Q.memoizedState;Q!==null&&0=X.length)return $;var M=X[K],N=R2(Q)?Q.slice():E1({},Q);return N[M]=Y(Q[M],X,K+1,$),N}function J(Q,X,K){if(X.length!==K.length)console.warn("copyWithRename() expects paths of the same length");else{for(var $=0;$Q8?console.error("Unexpected pop."):(X!==HO[Q8]&&console.error("Unexpected Fiber popped."),Q.current=NO[Q8],NO[Q8]=null,HO[Q8]=null,Q8--)}function K0(Q,X,K){Q8++,NO[Q8]=Q.current,HO[Q8]=K,Q.current=X}function n(Q){return Q===null&&console.error("Expected host context to exist. This error is likely caused by a bug in React. Please file an issue."),Q}function p(Q,X){K0(w9,X,Q),K0(Wz,Q,Q),K0(M9,null,Q);var K=X.nodeType;switch(K){case 9:case 11:K=K===9?"#document":"#fragment",X=(X=X.documentElement)?(X=X.namespaceURI)?tT(X):H8:H8;break;default:if(K=X.tagName,X=X.namespaceURI)X=tT(X),X=nT(X,K);else switch(K){case"svg":X=_J;break;case"math":X=zU;break;default:X=H8}}K=K.toLowerCase(),K=lL(null,K),K={context:X,ancestorInfo:K},X0(M9,Q),K0(M9,K,Q)}function J0(Q){X0(M9,Q),X0(Wz,Q),X0(w9,Q)}function e(){return n(M9.current)}function i(Q){Q.memoizedState!==null&&K0(W$,Q,Q);var X=n(M9.current),K=Q.type,$=nT(X.context,K);K=lL(X.ancestorInfo,K),$={context:$,ancestorInfo:K},X!==$&&(K0(Wz,Q,Q),K0(M9,$,Q))}function G0(Q){Wz.current===Q&&(X0(M9,Q),X0(Wz,Q)),W$.current===Q&&(X0(W$,Q),ZK._currentValue=UY)}function k(){}function B0(){if($z===0){DC=console.log,bC=console.info,EC=console.warn,yC=console.error,IC=console.group,jC=console.groupCollapsed,SC=console.groupEnd;var Q={configurable:!0,enumerable:!0,value:k,writable:!0};Object.defineProperties(console,{info:Q,log:Q,warn:Q,error:Q,group:Q,groupCollapsed:Q,groupEnd:Q})}$z++}function H0(){if($z--,$z===0){var Q={configurable:!0,enumerable:!0,writable:!0};Object.defineProperties(console,{log:E1({},Q,{value:DC}),info:E1({},Q,{value:bC}),warn:E1({},Q,{value:EC}),error:E1({},Q,{value:yC}),group:E1({},Q,{value:IC}),groupCollapsed:E1({},Q,{value:jC}),groupEnd:E1({},Q,{value:SC})})}0>$z&&console.error("disabledDepth fell below zero. This is a bug in React. Please file an issue.")}function w0(Q){var X=Error.prepareStackTrace;if(Error.prepareStackTrace=void 0,Q=Q.stack,Error.prepareStackTrace=X,Q.startsWith(`Error: react-stack-top-frame +See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.`),H}typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart==="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());var U={d:{f:Z,r:function(){throw Error("Invalid form element. requestFormReset must be passed a form that was rendered by React.")},D:Z,C:Z,L:Z,m:Z,X:Z,S:Z,M:Z},p:0,findDOMNode:null},G=Symbol.for("react.portal"),w=gA.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;typeof Map==="function"&&Map.prototype!=null&&typeof Map.prototype.forEach==="function"&&typeof Set==="function"&&Set.prototype!=null&&typeof Set.prototype.clear==="function"&&typeof Set.prototype.forEach==="function"||console.error("React depends on Map and Set built-in types. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"),Kl.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=U,Kl.createPortal=function(H,O){var A=2` tag.%s',A),typeof H==="string"&&typeof O==="object"&&O!==null&&typeof O.as==="string"){A=O.as;var _=q(A,O.crossOrigin);U.d.L(H,A,{crossOrigin:_,integrity:typeof O.integrity==="string"?O.integrity:void 0,nonce:typeof O.nonce==="string"?O.nonce:void 0,type:typeof O.type==="string"?O.type:void 0,fetchPriority:typeof O.fetchPriority==="string"?O.fetchPriority:void 0,referrerPolicy:typeof O.referrerPolicy==="string"?O.referrerPolicy:void 0,imageSrcSet:typeof O.imageSrcSet==="string"?O.imageSrcSet:void 0,imageSizes:typeof O.imageSizes==="string"?O.imageSizes:void 0,media:typeof O.media==="string"?O.media:void 0})}},Kl.preloadModule=function(H,O){var A="";typeof H==="string"&&H||(A+=" The `href` argument encountered was "+z(H)+"."),O!==void 0&&typeof O!=="object"?A+=" The `options` argument encountered was "+z(O)+".":O&&("as"in O)&&typeof O.as!=="string"&&(A+=" The `as` option encountered was "+z(O.as)+"."),A&&console.error('ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `` tag.%s',A),typeof H==="string"&&(O?(A=q(O.as,O.crossOrigin),U.d.m(H,{as:typeof O.as==="string"&&O.as!=="script"?O.as:void 0,crossOrigin:A,integrity:typeof O.integrity==="string"?O.integrity:void 0})):U.d.m(H))},Kl.requestFormReset=function(H){U.d.r(H)},Kl.unstable_batchedUpdates=function(H,O){return H(O)},Kl.useFormState=function(H,O,A){return W().useFormState(H,O,A)},Kl.useFormStatus=function(){return W().useHostTransitionStatus()},Kl.version="19.2.4",typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop==="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error())})()});var A3=_8((n90,OE)=>{var Bl=u(HE());OE.exports=Bl});var AE=_8((Wl)=>{var r1=u(NE()),PJ=u(Q0()),uA=u(A3());(function(){function Z(Q,X){for(Q=Q.memoizedState;Q!==null&&0=X.length)return $;var M=X[K],N=R2(Q)?Q.slice():E1({},Q);return N[M]=Y(Q[M],X,K+1,$),N}function J(Q,X,K){if(X.length!==K.length)console.warn("copyWithRename() expects paths of the same length");else{for(var $=0;$Q8?console.error("Unexpected pop."):(X!==HO[Q8]&&console.error("Unexpected Fiber popped."),Q.current=NO[Q8],NO[Q8]=null,HO[Q8]=null,Q8--)}function K0(Q,X,K){Q8++,NO[Q8]=Q.current,HO[Q8]=K,Q.current=X}function n(Q){return Q===null&&console.error("Expected host context to exist. This error is likely caused by a bug in React. Please file an issue."),Q}function m(Q,X){K0(w9,X,Q),K0(Bz,Q,Q),K0(M9,null,Q);var K=X.nodeType;switch(K){case 9:case 11:K=K===9?"#document":"#fragment",X=(X=X.documentElement)?(X=X.namespaceURI)?nT(X):H8:H8;break;default:if(K=X.tagName,X=X.namespaceURI)X=nT(X),X=eT(X,K);else switch(K){case"svg":X=_J;break;case"math":X=zU;break;default:X=H8}}K=K.toLowerCase(),K=rL(null,K),K={context:X,ancestorInfo:K},J0(M9,Q),K0(M9,K,Q)}function X0(Q){J0(M9,Q),J0(Bz,Q),J0(w9,Q)}function Z0(){return n(M9.current)}function i(Q){Q.memoizedState!==null&&K0(W$,Q,Q);var X=n(M9.current),K=Q.type,$=eT(X.context,K);K=rL(X.ancestorInfo,K),$={context:$,ancestorInfo:K},X!==$&&(K0(Bz,Q,Q),K0(M9,$,Q))}function B0(Q){Bz.current===Q&&(J0(M9,Q),J0(Bz,Q)),W$.current===Q&&(J0(W$,Q),ez._currentValue=UY)}function k(){}function U0(){if(Wz===0){bC=console.log,EC=console.info,yC=console.warn,IC=console.error,jC=console.group,SC=console.groupCollapsed,xC=console.groupEnd;var Q={configurable:!0,enumerable:!0,value:k,writable:!0};Object.defineProperties(console,{info:Q,log:Q,warn:Q,error:Q,group:Q,groupCollapsed:Q,groupEnd:Q})}Wz++}function N0(){if(Wz--,Wz===0){var Q={configurable:!0,enumerable:!0,writable:!0};Object.defineProperties(console,{log:E1({},Q,{value:bC}),info:E1({},Q,{value:EC}),warn:E1({},Q,{value:yC}),error:E1({},Q,{value:IC}),group:E1({},Q,{value:jC}),groupCollapsed:E1({},Q,{value:SC}),groupEnd:E1({},Q,{value:xC})})}0>Wz&&console.error("disabledDepth fell below zero. This is a bug in React. Please file an issue.")}function w0(Q){var X=Error.prepareStackTrace;if(Error.prepareStackTrace=void 0,Q=Q.stack,Error.prepareStackTrace=X,Q.startsWith(`Error: react-stack-top-frame `)&&(Q=Q.slice(29)),X=Q.indexOf(` `),X!==-1&&(Q=Q.slice(X+1)),X=Q.indexOf("react_stack_bottom_frame"),X!==-1&&(X=Q.lastIndexOf(` -`,X)),X!==-1)Q=Q.slice(0,X);else return"";return Q}function b0(Q){if(OO===void 0)try{throw Error()}catch(K){var X=K.stack.trim().match(/\n( *(at )?)/);OO=X&&X[1]||"",xC=-1)":-1F||y[N]!==a[F]){var o=` -`+y[N].replace(" at new "," at ");return Q.displayName&&o.includes("")&&(o=o.replace("",Q.displayName)),typeof Q==="function"&&_O.set(Q,o),o}while(1<=N&&0<=F);break}}}finally{AO=!1,M0.H=$,H0(),Error.prepareStackTrace=K}return y=(y=Q?Q.displayName||Q.name:"")?b0(y):"",typeof Q==="function"&&_O.set(Q,y),y}function W0(Q,X){switch(Q.tag){case 26:case 27:case 5:return b0(Q.type);case 16:return b0("Lazy");case 13:return Q.child!==X&&X!==null?b0("Suspense Fallback"):b0("Suspense");case 19:return b0("SuspenseList");case 0:case 15:return u0(Q.type,!1);case 11:return u0(Q.type.render,!1);case 1:return u0(Q.type,!0);case 31:return b0("Activity");default:return""}}function x0(Q){try{var X="",K=null;do{X+=W0(Q,K);var $=Q._debugInfo;if($)for(var M=$.length-1;0<=M;M--){var N=$[M];if(typeof N.name==="string"){var F=X;Z:{var{name:V,env:D,debugLocation:y}=N;if(y!=null){var a=w0(y),o=a.lastIndexOf(` -`),c=o===-1?a:a.slice(o+1);if(c.indexOf(V)!==-1){var q0=` -`+c;break Z}}q0=b0(V+(D?" ["+D+"]":""))}X=F+q0}}K=Q,Q=Q.return}while(Q);return X}catch(R0){return` -Error generating stack: `+R0.message+` -`+R0.stack}}function d0(Q){return(Q=Q?Q.displayName||Q.name:"")?b0(Q):""}function w1(){if(P4===null)return null;var Q=P4._debugOwner;return Q!=null?s(Q):null}function X1(){if(P4===null)return"";var Q=P4;try{var X="";switch(Q.tag===6&&(Q=Q.return),Q.tag){case 26:case 27:case 5:X+=b0(Q.type);break;case 13:X+=b0("Suspense");break;case 19:X+=b0("SuspenseList");break;case 31:X+=b0("Activity");break;case 30:case 0:case 15:case 1:Q._debugOwner||X!==""||(X+=d0(Q.type));break;case 11:Q._debugOwner||X!==""||(X+=d0(Q.type.render))}for(;Q;)if(typeof Q.tag==="number"){var K=Q;Q=K._debugOwner;var $=K._debugStack;if(Q&&$){var M=w0($);M!==""&&(X+=` +`);for(F=N=0;NF||y[N]!==a[F]){var s=` +`+y[N].replace(" at new "," at ");return Q.displayName&&s.includes("")&&(s=s.replace("",Q.displayName)),typeof Q==="function"&&_O.set(Q,s),s}while(1<=N&&0<=F);break}}}finally{AO=!1,G0.H=$,N0(),Error.prepareStackTrace=K}return y=(y=Q?Q.displayName||Q.name:"")?v0(y):"",typeof Q==="function"&&_O.set(Q,y),y}function M0(Q,X){switch(Q.tag){case 26:case 27:case 5:return v0(Q.type);case 16:return v0("Lazy");case 13:return Q.child!==X&&X!==null?v0("Suspense Fallback"):v0("Suspense");case 19:return v0("SuspenseList");case 0:case 15:return S0(Q.type,!1);case 11:return S0(Q.type.render,!1);case 1:return S0(Q.type,!0);case 31:return v0("Activity");default:return""}}function p0(Q){try{var X="",K=null;do{X+=M0(Q,K);var $=Q._debugInfo;if($)for(var M=$.length-1;0<=M;M--){var N=$[M];if(typeof N.name==="string"){var F=X;Z:{var{name:V,env:D,debugLocation:y}=N;if(y!=null){var a=w0(y),s=a.lastIndexOf(` +`),c=s===-1?a:a.slice(s+1);if(c.indexOf(V)!==-1){var q0=` +`+c;break Z}}q0=v0(V+(D?" ["+D+"]":""))}X=F+q0}}K=Q,Q=Q.return}while(Q);return X}catch(T0){return` +Error generating stack: `+T0.message+` +`+T0.stack}}function r0(Q){return(Q=Q?Q.displayName||Q.name:"")?v0(Q):""}function O1(){if(P4===null)return null;var Q=P4._debugOwner;return Q!=null?o(Q):null}function z1(){if(P4===null)return"";var Q=P4;try{var X="";switch(Q.tag===6&&(Q=Q.return),Q.tag){case 26:case 27:case 5:X+=v0(Q.type);break;case 13:X+=v0("Suspense");break;case 19:X+=v0("SuspenseList");break;case 31:X+=v0("Activity");break;case 30:case 0:case 15:case 1:Q._debugOwner||X!==""||(X+=r0(Q.type));break;case 11:Q._debugOwner||X!==""||(X+=r0(Q.type.render))}for(;Q;)if(typeof Q.tag==="number"){var K=Q;Q=K._debugOwner;var $=K._debugStack;if(Q&&$){var M=w0($);M!==""&&(X+=` `+M)}}else if(Q.debugStack!=null){var N=Q.debugStack;(Q=Q.owner)&&N&&(X+=` `+w0(N))}else break;var F=X}catch(V){F=` Error generating stack: `+V.message+` -`+V.stack}return F}function L0(Q,X,K,$,M,N,F){var V=P4;C1(Q);try{return Q!==null&&Q._debugTask?Q._debugTask.run(X.bind(null,K,$,M,N,F)):X(K,$,M,N,F)}finally{C1(V)}throw Error("runWithFiberInDEV should never be called in production. This is a bug in React.")}function C1(Q){M0.getCurrentStack=Q===null?null:X1,B3=!1,P4=Q}function Y5(Q){return typeof Symbol==="function"&&Symbol.toStringTag&&Q[Symbol.toStringTag]||Q.constructor.name||"Object"}function e5(Q){try{return N5(Q),!1}catch(X){return!0}}function N5(Q){return""+Q}function N1(Q,X){if(e5(Q))return console.error("The provided `%s` attribute is an unsupported type %s. This value must be coerced to a string before using it here.",X,Y5(Q)),N5(Q)}function m6(Q,X){if(e5(Q))return console.error("The provided `%s` CSS property is an unsupported type %s. This value must be coerced to a string before using it here.",X,Y5(Q)),N5(Q)}function K1(Q){if(e5(Q))return console.error("Form field values (value, checked, defaultValue, or defaultChecked props) must be strings, not %s. This value must be coerced to a string before using it here.",Y5(Q)),N5(Q)}function U4(Q){if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u")return!1;var X=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(X.isDisabled)return!0;if(!X.supportsFiber)return console.error("The installed version of React DevTools is too old and will not work with the current version of React. Please update React DevTools. https://react.dev/link/react-devtools"),!0;try{mQ=X.inject(Q),O7=X}catch(K){console.error("React instrumentation encountered an error: %o.",K)}return X.checkDCE?!0:!1}function S1(Q){if(typeof Pc==="function"&&Vc(Q),O7&&typeof O7.setStrictMode==="function")try{O7.setStrictMode(mQ,Q)}catch(X){W3||(W3=!0,console.error("React instrumentation encountered an error: %o",X))}}function N6(Q){return Q>>>=0,Q===0?32:31-(Lc(Q)/vc|0)|0}function n2(Q){var X=Q&42;if(X!==0)return X;switch(Q&-Q){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 Q&261888;case 262144:case 524288:case 1048576:case 2097152:return Q&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return Q&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return console.error("Should have found matching lanes. This is a bug in React."),Q}}function f4(Q,X,K){var $=Q.pendingLanes;if($===0)return 0;var M=0,N=Q.suspendedLanes,F=Q.pingedLanes;Q=Q.warmLanes;var V=$&134217727;return V!==0?($=V&~N,$!==0?M=n2($):(F&=V,F!==0?M=n2(F):K||(K=V&~Q,K!==0&&(M=n2(K))))):(V=$&~N,V!==0?M=n2(V):F!==0?M=n2(F):K||(K=$&~Q,K!==0&&(M=n2(K)))),M===0?0:X!==0&&X!==M&&(X&N)===0&&(N=M&-M,K=X&-X,N>=K||N===32&&(K&4194048)!==0)?X:M}function k2(Q,X){return(Q.pendingLanes&~(Q.suspendedLanes&~Q.pingedLanes)&X)===0}function H6(Q,X){switch(Q){case 1:case 2:case 4:case 8:case 64:return X+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 X+5000;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return console.error("Should have found matching lanes. This is a bug in React."),-1}}function O6(){var Q=G$;return G$<<=1,(G$&62914560)===0&&(G$=4194304),Q}function e2(Q){for(var X=[],K=0;31>K;K++)X.push(Q);return X}function T5(Q,X){Q.pendingLanes|=X,X!==268435456&&(Q.suspendedLanes=0,Q.pingedLanes=0,Q.warmLanes=0)}function $2(Q,X,K,$,M,N){var F=Q.pendingLanes;Q.pendingLanes=K,Q.suspendedLanes=0,Q.pingedLanes=0,Q.warmLanes=0,Q.expiredLanes&=K,Q.entangledLanes&=K,Q.errorRecoveryDisabledLanes&=K,Q.shellSuspendCounter=0;var{entanglements:V,expirationTimes:D,hiddenUpdates:y}=Q;for(K=F&~K;0"u")return null;try{return Q.activeElement||Q.body}catch(X){return Q.body}}function j0(Q){return Q.replace(bc,function(X){return"\\"+X.charCodeAt(0).toString(16)+" "})}function l0(Q,X){X.checked===void 0||X.defaultChecked===void 0||mC||(console.error("%s contains an input of type %s with both checked and defaultChecked props. Input elements must be either controlled or uncontrolled (specify either the checked prop, or the defaultChecked prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://react.dev/link/controlled-components",w1()||"A component",X.type),mC=!0),X.value===void 0||X.defaultValue===void 0||hC||(console.error("%s contains an input of type %s with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://react.dev/link/controlled-components",w1()||"A component",X.type),hC=!0)}function a0(Q,X,K,$,M,N,F,V){if(Q.name="",F!=null&&typeof F!=="function"&&typeof F!=="symbol"&&typeof F!=="boolean"?(N1(F,"type"),Q.type=F):Q.removeAttribute("type"),X!=null)if(F==="number"){if(X===0&&Q.value===""||Q.value!=X)Q.value=""+t(X)}else Q.value!==""+t(X)&&(Q.value=""+t(X));else F!=="submit"&&F!=="reset"||Q.removeAttribute("value");X!=null?r0(Q,F,t(X)):K!=null?r0(Q,F,t(K)):$!=null&&Q.removeAttribute("value"),M==null&&N!=null&&(Q.defaultChecked=!!N),M!=null&&(Q.checked=M&&typeof M!=="function"&&typeof M!=="symbol"),V!=null&&typeof V!=="function"&&typeof V!=="symbol"&&typeof V!=="boolean"?(N1(V,"name"),Q.name=""+t(V)):Q.removeAttribute("name")}function Z1(Q,X,K,$,M,N,F,V){if(N!=null&&typeof N!=="function"&&typeof N!=="symbol"&&typeof N!=="boolean"&&(N1(N,"type"),Q.type=N),X!=null||K!=null){if(!(N!=="submit"&&N!=="reset"||X!==void 0&&X!==null)){P0(Q);return}K=K!=null?""+t(K):"",X=X!=null?""+t(X):K,V||X===Q.value||(Q.value=X),Q.defaultValue=X}$=$!=null?$:M,$=typeof $!=="function"&&typeof $!=="symbol"&&!!$,Q.checked=V?Q.checked:!!$,Q.defaultChecked=!!$,F!=null&&typeof F!=="function"&&typeof F!=="symbol"&&typeof F!=="boolean"&&(N1(F,"name"),Q.name=F),P0(Q)}function r0(Q,X,K){X==="number"&&I0(Q.ownerDocument)===Q||Q.defaultValue===""+K||(Q.defaultValue=""+K)}function p1(Q,X){X.value==null&&(typeof X.children==="object"&&X.children!==null?PJ.Children.forEach(X.children,function(K){K==null||typeof K==="string"||typeof K==="number"||typeof K==="bigint"||cC||(cC=!0,console.error("Cannot infer the option value of complex children. Pass a `value` prop or use a plain string as children to