diff --git a/.gitignore b/.gitignore index 21a3985..7727610 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,9 @@ scripts/integration_scenarios.py # Coverage / CI artefacts coverage.xml junit.xml + +# React UI build artifacts (Phase 1+) +web/node_modules +web/dist +web/playwright-report +web/test-results diff --git a/dist/app.py b/dist/app.py index 295fca2..90120e9 100644 --- a/dist/app.py +++ b/dist/app.py @@ -1429,12 +1429,17 @@ async def _poll(self, registry): from typing import AsyncIterator, Literal -from fastapi import FastAPI, HTTPException, Request, Response, WebSocket, WebSocketDisconnect +from fastapi import APIRouter, FastAPI, HTTPException, Request, Response, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse +from fastapi.responses import JSONResponse, RedirectResponse, StreamingResponse from starlette.exceptions import HTTPException as StarletteHTTPException + + + + + # ----- imports for runtime/api_dedup.py ----- """Dedup retraction HTTP routes. @@ -1461,6 +1466,87 @@ async def _poll(self, registry): from fastapi import FastAPI, HTTPException +# ----- imports for runtime/api_session_full.py ----- +"""Bootstrap endpoint for the React UI's single view-model. + +GET /api/v1/sessions/{id}/full returns everything the UI needs to render +the session in one round-trip — replaces the old pattern of multiple GETs. +The same shape is then patched in place by SSE delta events. + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.orchestrator``); unlike :mod:`runtime.api_dedup`, this module +is NOT suitable for lightweight test fixtures that construct a bare +``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" + + + +from fastapi import APIRouter, HTTPException, Request + + +# ----- imports for runtime/api_ui_hints.py ----- +"""UI hints endpoint — drives the React shell's brand and templates. + +GET /api/v1/config/ui-hints returns the runtime-configured ui block plus +the environments list. Read once at React-app boot and cached for the +session lifetime via `useUiHints()`. + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.cfg``); not suitable for lightweight test fixtures that +construct a bare ``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" + +from fastapi import APIRouter, Request + + +# ----- imports for runtime/api_apps_overlay.py ----- +"""App-overlay UI views discovery — Approach C extensibility. + +GET /api/v1/apps/{app}/ui-views returns the app-registered overlay views. +The framework UI's Selected-detail panel renders matching views as +"App-specific views →" links. v2.0 ships with one app per deployment; +multi-app per-app filtering is v2.1 scope (the path's app_name is +currently informational). + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.cfg``); not suitable for lightweight test fixtures that +construct a bare ``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" + + + +# ----- imports for runtime/api_recent_events.py ----- +"""Cross-session SSE — used by the React UI's Other Sessions monitor. + +Emits session-level events (not per-session detail events): session.created, +session.status_changed, session.agent_running. Lower frequency than the +per-session stream. + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.orchestrator``); not suitable for lightweight test fixtures +that construct a bare ``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" + +from fastapi.responses import StreamingResponse + +# ----- imports for runtime/api_static.py ----- +"""StaticFiles mount + SPA fallback for the React UI bundle. + +The React build output lives at $ASR_WEB_DIST (default: ../web/dist relative +to repo root). FastAPI serves /assets/* and /fonts/* directly; any unknown +path that isn't /api/v1/*, /health, or /docs falls back to index.html so +the React Router can pick up the URL. + +Registered only via :func:`runtime.api.build_app` (mounts onto the root +FastAPI app, not the api_v1 router). Must be invoked AFTER all API routes +are registered so the catch-all SPA fallback doesn't shadow them. +""" + +from fastapi import FastAPI, Request +from fastapi.responses import FileResponse, HTMLResponse, Response +from fastapi.staticfiles import StaticFiles + + # ====== module: runtime/errors.py ====== @@ -2182,10 +2268,26 @@ class UIDetailField(BaseModel): model_config = {"frozen": True, "extra": "forbid"} +class AppView(BaseModel): + """An app-overlay UI view registered by an app for the framework UI to link to. + + The framework UI lists matching app views in its Selected-detail panel + ("App-specific views →") so apps can ship bespoke deep-dive pages + without forking the framework. Approach C (per the v2.0 design spec). + """ + model_config = ConfigDict(extra="forbid") + + id: str + title: str + applies_to: str # "always" | "agent:" | "tool:" + url: str + + class UIConfig(BaseModel): """App-driven UI rendering knobs. Keeps the generic Streamlit shell in ``runtime/ui.py`` agnostic of any specific domain — colors, labels, - and tag prefixes come from YAML. + and tag prefixes come from YAML. Also drives the React UI (v2.0) shell + via ``GET /api/v1/config/ui-hints``. ``badges`` is a 2-level dict: ``{field_name: {value: UIBadge}}``. Example: ``{"status": {"open": {"label": "OPEN", "color": "red"}}}``. @@ -2196,10 +2298,26 @@ class UIConfig(BaseModel): ``tags`` is an opaque key->tag-string map the UI consults for cross-skill signals (e.g. ``prior_match_supported`` -> the literal tag a skill emits). + + React UI (v2.0) fields: ``brand_name``, ``brand_logo_url``, + ``approval_rationale_templates``, and ``hitl_question_templates`` + drive the React shell's topbar brand block, environment switcher, + and approval-rationale dropdown. Read at app boot via + ``useUiHints()`` and cached for the session lifetime. + + ``app_views`` is the Approach C extensibility surface — apps register + bespoke UI overlay views that the framework UI's Selected-detail + panel lists as "App-specific views →" links. Served via + ``GET /api/v1/apps/{app}/ui-views``. """ badges: dict[str, dict[str, UIBadge]] = Field(default_factory=dict) detail_fields: list[UIDetailField] = Field(default_factory=list) tags: dict[str, str] = Field(default_factory=dict) + brand_name: str = "" + brand_logo_url: str | None = None + approval_rationale_templates: list[str] = Field(default_factory=list) + hitl_question_templates: dict[str, str] = Field(default_factory=dict) + app_views: list[AppView] = Field(default_factory=list) model_config = {"frozen": True, "extra": "forbid"} @@ -4826,6 +4944,12 @@ def _field(name: str, default=None): "gate_fired", "status_changed", "lesson_extracted", + # Session-level lifecycle events — emitted on the cross-session SSE + # stream (/api/v1/sessions/recent/events) for the React UI's "Other + # Sessions" monitor panel. Lower frequency than per-step kinds. + "session.created", + "session.status_changed", + "session.agent_running", ] _VALID_EVENT_KINDS: frozenset[str] = frozenset(get_args(EventKind)) @@ -4916,6 +5040,32 @@ def iter_for( ts=row.ts, ) + def iter_recent(self, since: int = 0) -> Iterator[SessionEvent]: + """Iterate events across ALL sessions where seq > since, ordered + by global seq. + + Used by the cross-session SSE stream + (``/api/v1/sessions/recent/events``) to deliver session.* + lifecycle events to the React UI's Other Sessions monitor panel. + Capped at 500 rows per call so a long-disconnected client can't + blow memory replaying years of history. + """ + with Session(self.engine) as s: + stmt = ( + select(SessionEventRow) + .where(SessionEventRow.seq > since) + .order_by(SessionEventRow.seq.asc()) + .limit(500) + ) + for row in s.execute(stmt).scalars(): + yield SessionEvent( + seq=row.seq, + session_id=row.session_id, + kind=row.kind, + payload=row.payload, + ts=row.ts, + ) + # ====== module: runtime/storage/migrations.py ====== _FORWARD_COLUMNS: list[tuple[str, str]] = [ @@ -6200,6 +6350,20 @@ async def _scheduler() -> str: reporter_team=sub_team, ) session_id = inc.id + # Emit session.created on the cross-session SSE stream so + # the React UI's Other Sessions monitor lights up the new + # tile in real time. Telemetry must not break start. + event_log = getattr(orch, "event_log", None) + if event_log is not None: + try: + # ``session_id`` already lands on the row; the payload + # carries no extra fields for ``session.created``. + event_log.record(session_id, "session.created") + except Exception: # noqa: BLE001 — telemetry must not break start + _log.debug( + "event_log.record(session.created) failed", + exc_info=True, + ) # Stamp trigger provenance onto the row before the graph # runs so any crash mid-graph still leaves an audit trail. # ``inc.findings`` is a JSON dict on the row. @@ -13786,6 +13950,21 @@ def _emit_status_changed_event( _log.debug( "event_log.record(status_changed) failed", exc_info=True, ) + # Mirror onto the cross-session SSE stream for the React UI's + # Other Sessions monitor (see api_recent_events.py). + # ``session_id`` already lands on the row; the payload carries + # the new ``status`` only. + try: + event_log.record( + inc.id, + "session.status_changed", + status=to_status, + ) + except Exception: # noqa: BLE001 — telemetry must not break finalize + _log.debug( + "event_log.record(session.status_changed) failed", + exc_info=True, + ) # M5 hook point: when ``to_status`` is terminal per app config, # invoke the lesson extractor. M4 leaves it as a no-op; M5 swaps @@ -14732,6 +14911,19 @@ async def start_session(self, *, query: str, env = (state_overrides or {}).get("environment", "") inc = self.store.create(query=query, environment=env, reporter_id=sub_id, reporter_team=sub_team) + # Emit session.created on the cross-session SSE stream so the + # React UI's Other Sessions monitor lights up the new tile in + # real time. ``session_id`` already lands on the row; the + # payload carries no extra fields. Telemetry must not break + # the start path. + event_log = getattr(self, "event_log", None) + if event_log is not None: + try: + event_log.record(inc.id, "session.created") + except Exception: # noqa: BLE001 — telemetry must not break start + _log.debug( + "event_log.record(session.created) failed", exc_info=True, + ) if trigger is not None: inc.findings["trigger"] = { "name": trigger.name, @@ -15424,6 +15616,9 @@ async def _lifespan(app: FastAPI) -> AsyncIterator[None]: orch = svc.submit_and_wait(svc._ensure_orchestrator(), timeout=30.0) app.state.service = svc app.state.orchestrator = orch + # Surface the validated AppConfig so app.state.cfg-readers + # (e.g. /api/v1/config/ui-hints) don't have to re-load YAML. + app.state.cfg = cfg # Environments roster is app-specific (incident-management has # production/staging/dev/local; code-review doesn't expose one). # Read it from the YAML's top-level ``environments:`` block; @@ -15511,16 +15706,28 @@ def build_app(cfg: AppConfig) -> FastAPI: title="ASR — Agent Orchestrator", lifespan=_make_lifespan(cfg), ) - - # CORS: configure once with the AppConfig-supplied origins so the - # React dev server (Vite at :5173, CRA/Next at :3000 by default) can - # call every endpoint, SSE included. Production deployments lock - # the origin list down via YAML — same shape, narrower allow-list. + # All framework routes (except /health) live under /api/v1 so the + # React client can stably target a versioned surface; /health stays + # at root for monitor / load-balancer health-check conventions. + api_v1 = APIRouter(prefix="/api/v1") + + # CORS: env-driven so the React dev server (Vite at :5173) can call + # every endpoint, SSE included. Override via ``ASR_CORS_ORIGINS`` + # (comma-separated) — production deployments lock the origin list + # down by setting the env var to the narrower allow-list. + # ``allow_credentials=False`` matches the bearer-token auth pattern + # (no cookies); methods are explicit so OPTIONS preflights are + # handled the same way for every route. + _cors_origins_raw = os.environ.get( + "ASR_CORS_ORIGINS", + "http://localhost:5173,http://127.0.0.1:5173", # Vite dev defaults + ) + _cors_origins = [o.strip() for o in _cors_origins_raw.split(",") if o.strip()] fastapi_app.add_middleware( CORSMiddleware, - allow_origins=cfg.api.cors_origins, - allow_credentials=cfg.api.cors_allow_credentials, - allow_methods=["*"], + allow_origins=_cors_origins, + allow_credentials=False, + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], ) @@ -15555,27 +15762,27 @@ async def _http_exception_handler( async def health(): return {"status": "ok"} - @fastapi_app.get("/agents") + @api_v1.get("/agents") async def agents(): return fastapi_app.state.orchestrator.list_agents() - @fastapi_app.get("/tools") + @api_v1.get("/tools") async def tools(): return fastapi_app.state.orchestrator.list_tools() - @fastapi_app.get("/incidents") + @api_v1.get("/incidents") async def incidents(limit: int = 20): return fastapi_app.state.orchestrator.list_recent_incidents(limit=limit) - @fastapi_app.get("/incidents/{incident_id}") + @api_v1.get("/incidents/{incident_id}") async def incident(incident_id: str): return fastapi_app.state.orchestrator.get_incident(incident_id) - @fastapi_app.delete("/incidents/{incident_id}") + @api_v1.delete("/incidents/{incident_id}") async def delete_incident(incident_id: str): return fastapi_app.state.orchestrator.delete_incident(incident_id) - @fastapi_app.post("/investigate") + @api_v1.post("/investigate") async def investigate(req: InvestigateRequest, request: Request) -> InvestigateResponse: """Legacy alias for ``POST /sessions`` — kept for back-compat. @@ -15609,11 +15816,11 @@ async def investigate(req: InvestigateRequest, request: Request) -> InvestigateR raise return InvestigateResponse(incident_id=sid) - @fastapi_app.get("/environments") + @api_v1.get("/environments") async def environments(): return fastapi_app.state.environments - @fastapi_app.post("/investigate/stream") + @api_v1.post("/investigate/stream") async def investigate_stream(req: InvestigateRequest) -> StreamingResponse: orch = fastapi_app.state.orchestrator @@ -15626,7 +15833,7 @@ async def _events(): return StreamingResponse(_events(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.post("/incidents/{incident_id}/resume") + @api_v1.post("/incidents/{incident_id}/resume") async def resume_incident(incident_id: str, req: ResumeRequest) -> StreamingResponse: orch = fastapi_app.state.orchestrator decision: dict = {"action": req.decision} @@ -15658,7 +15865,7 @@ async def _events(): # Multi-session endpoints # ------------------------------------------------------------------ - @fastapi_app.post( + @api_v1.post( "/sessions", status_code=201, ) @@ -15689,7 +15896,7 @@ class is matched by name so this handler does not depend on a raise return SessionStartResponse(session_id=sid) - @fastapi_app.get("/sessions") + @api_v1.get("/sessions") async def list_sessions_endpoint(request: Request) -> list[SessionStatus]: """Snapshot of in-flight sessions (running / awaiting_input / error).""" svc = request.app.state.service @@ -15699,7 +15906,7 @@ async def list_sessions_endpoint(request: Request) -> list[SessionStatus]: # HITL approval endpoints (risk-rated tool gateway) # ------------------------------------------------------------------ - @fastapi_app.get("/sessions/{session_id}/approvals") + @api_v1.get("/sessions/{session_id}/approvals") async def list_pending_approvals( session_id: str, request: Request ) -> list[PendingApproval]: @@ -15741,7 +15948,7 @@ async def list_pending_approvals( )) return out - @fastapi_app.post( + @api_v1.post( "/sessions/{session_id}/approvals/{tool_call_id}", status_code=200, ) @@ -15827,7 +16034,7 @@ async def _resume() -> None: "rationale": body.rationale, } - @fastapi_app.delete("/sessions/{session_id}", status_code=204) + @api_v1.delete("/sessions/{session_id}", status_code=204) async def stop_session_endpoint( session_id: str, request: Request ) -> Response: @@ -15858,7 +16065,7 @@ async def stop_session_endpoint( # T2: generic /sessions/* endpoints (React-ready, non-legacy). # ================================================================== - @fastapi_app.get("/sessions/recent") + @api_v1.get("/sessions/recent") async def recent_sessions(request: Request, limit: int = 20) -> list[dict]: """List recent sessions of ANY status — closed + active. @@ -15868,7 +16075,7 @@ async def recent_sessions(request: Request, limit: int = 20) -> list[dict]: orch = request.app.state.orchestrator return orch.list_recent_sessions(limit=limit) - @fastapi_app.get("/sessions/{session_id}") + @api_v1.get("/sessions/{session_id}") async def get_session_detail(session_id: str, request: Request) -> dict: """Full session detail. Generic equivalent of the legacy domain-flavoured detail route. 404 when the id is unknown.""" @@ -15880,7 +16087,7 @@ async def get_session_detail(session_id: str, request: Request) -> dict: status_code=404, detail=_SESSION_NOT_FOUND_DETAIL, ) from e - @fastapi_app.post("/sessions/{session_id}/resume") + @api_v1.post("/sessions/{session_id}/resume") async def resume_session_sse( session_id: str, req: ResumeRequest, request: Request, ) -> StreamingResponse: @@ -15914,7 +16121,7 @@ async def _events(): return StreamingResponse(_events(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.post("/sessions/{session_id}/retry") + @api_v1.post("/sessions/{session_id}/retry") async def retry_session_sse( session_id: str, request: Request, ) -> StreamingResponse: @@ -15937,7 +16144,7 @@ async def _events(): return StreamingResponse(_events(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.get("/sessions/{session_id}/retry/preview") + @api_v1.get("/sessions/{session_id}/retry/preview") async def preview_retry( session_id: str, request: Request, ) -> RetryDecisionPreview: @@ -15956,7 +16163,7 @@ async def preview_retry( reason=str(decision.reason), ) - @fastapi_app.get("/sessions/{session_id}/lessons") + @api_v1.get("/sessions/{session_id}/lessons") async def list_session_lessons( session_id: str, request: Request, ) -> list[LessonResponse]: @@ -16000,7 +16207,7 @@ async def list_session_lessons( # T3: SSE event stream + T4: WebSocket fallback. # ================================================================== - @fastapi_app.get("/sessions/{session_id}/events") + @api_v1.get("/sessions/{session_id}/events") async def sse_events( session_id: str, request: Request, since: int = 0, ) -> StreamingResponse: @@ -16054,7 +16261,7 @@ async def _stream(): return StreamingResponse(_stream(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.websocket("/ws/sessions/{session_id}/events") + @api_v1.websocket("/ws/sessions/{session_id}/events") async def ws_events(websocket: WebSocket, session_id: str) -> None: """WebSocket fallback for the SSE event stream. Same payload shape (:class:`EventEnvelope`); clients that prefer WS over @@ -16101,6 +16308,81 @@ async def ws_events(websocket: WebSocket, session_id: str) -> None: except Exception: # noqa: BLE001 pass + # ================================================================== + # Bootstrap bundle: GET /api/v1/sessions/{id}/full + # Single round-trip the React UI calls on session open. Module + # lives next door so this file stays focused on routing wiring. + # ================================================================== + add_session_full_routes(api_v1) + + # ================================================================== + # UI hints: GET /api/v1/config/ui-hints + # Drives the React shell's brand block, environment switcher list, + # and approval-rationale dropdown. Read once at app boot. + # ================================================================== + add_ui_hints_routes(api_v1) + + # ================================================================== + # App-overlay UI views: GET /api/v1/apps/{app}/ui-views + # Approach C extensibility — apps register bespoke deep-dive pages + # (e.g. "Deploy diff") that the framework UI's Selected-detail + # panel lists as "App-specific views →" links. + # ================================================================== + add_apps_overlay_routes(api_v1) + + # ================================================================== + # Cross-session SSE: GET /api/v1/sessions/recent/events + # Drives the React UI's "Other Sessions" monitor — session.created / + # session.status_changed / session.agent_running events across ALL + # sessions, ordered by global seq. + # ================================================================== + add_recent_events_routes(api_v1) + + # Legacy /incidents/* and /investigate redirects to /api/v1/* equivalents. + # 308 preserves method + body so legacy POSTs (e.g. /incidents/{id}/resume) + # keep working transparently. Removed in v2.1. + @fastapi_app.api_route( + "/incidents", methods=["GET", "POST"], include_in_schema=False, + ) + async def _legacy_incidents_collection() -> RedirectResponse: + return RedirectResponse(url="/api/v1/sessions", status_code=308) + + @fastapi_app.api_route( + "/incidents/{path:path}", + methods=["GET", "POST", "DELETE", "PUT"], + include_in_schema=False, + ) + async def _legacy_incidents_detail(path: str) -> RedirectResponse: + return RedirectResponse(url=f"/api/v1/sessions/{path}", status_code=308) + + @fastapi_app.api_route( + "/investigate", methods=["POST"], include_in_schema=False, + ) + async def _legacy_investigate() -> RedirectResponse: + return RedirectResponse(url="/api/v1/investigate", status_code=308) + + @fastapi_app.api_route( + "/investigate/{path:path}", + methods=["POST"], + include_in_schema=False, + ) + async def _legacy_investigate_subpath(path: str) -> RedirectResponse: + return RedirectResponse( + url=f"/api/v1/investigate/{path}", status_code=308, + ) + + # Mount the versioned router. /health stays at root (registered + # directly on ``fastapi_app`` above); everything else lives under + # /api/v1. + fastapi_app.include_router(api_v1) + # ================================================================== + # React UI bundle: StaticFiles mount at / + SPA fallback. + # MUST be the last route-registration step in build_app — the + # catch-all ``GET /{full_path:path}`` would otherwise shadow every + # API route and legacy redirect. The fallback excludes /api/, /health, + # and /docs so unknown API paths still return structured JSON 404s. + # ================================================================== + mount_static_assets(fastapi_app) return fastapi_app @@ -16209,3 +16491,227 @@ async def un_duplicate( retracted_by=payload.retracted_by, note=payload.note, ) + +# ====== module: runtime/api_session_full.py ====== + +def add_session_full_routes(api_v1: APIRouter) -> None: + """Mount the /sessions/{id}/full handler on the api_v1 router. + + The function name is intentionally module-qualified (rather than the + bare ``add_routes`` we used pre-bundle-fix) so that ``scripts/build_single_file.py`` + can flatten this module alongside its sibling ``api_*`` side-cars without + the four ``add_routes`` defs colliding at module scope. Source-side callers + import the symbol directly: ``from runtime.api_session_full import add_session_full_routes``. + """ + + @api_v1.get("/sessions/{session_id}/full") + async def get_session_full( + session_id: str, request: Request, + ) -> dict[str, Any]: + orch = request.app.state.orchestrator + try: + inc = orch.store.load(session_id) + except (FileNotFoundError, ValueError, KeyError, LookupError) as e: + # ``ValueError`` covers the SessionStore id-format guard + # (``Invalid session id ...``); semantically a 404 at the + # API boundary — same convention as other /sessions/* GETs. + raise HTTPException( + status_code=404, detail="session not found", + ) from e + + # Replay the EventLog backlog. ``vm_seq`` is the high-water mark + # the UI uses to ?since=N when it later opens the SSE stream, so + # delta events stitch onto the same view-model without gap or + # overlap. + event_log = getattr(orch, "event_log", None) + events: list[dict[str, Any]] = [] + vm_seq = 0 + if event_log is not None: + for ev in event_log.iter_for(session_id, since=0): + events.append({ + "seq": ev.seq, + "kind": ev.kind, + "payload": ev.payload, + "ts": ev.ts, + }) + if ev.seq > vm_seq: + vm_seq = ev.seq + + # Agent definitions: skill metadata the UI needs to render the + # graph diagram + per-agent header chips. ``orch.skills`` is a + # ``dict[str, Skill]`` keyed by name. ``Skill.tools`` is itself + # a ``dict[str, list[str]]`` (server -> tool list) — expose the + # server keys as the ref strings; ``Skill.routes`` is a + # ``list[RouteRule]`` (when/next/gate) — flatten to the + # signal->next mapping the UI consumes. + agent_definitions: dict[str, dict[str, Any]] = {} + for name, skill in orch.skills.items(): + agent_definitions[name] = { + "name": skill.name, + "kind": skill.kind, + "model": skill.model or orch.cfg.llm.default, + "tools": list(skill.tools or {}), + "routes": {r.when: r.next for r in skill.routes}, + "system_prompt_excerpt": (skill.system_prompt or "")[:500], + } + + return { + "session": inc.model_dump(mode="json"), + "agents_run": [r.model_dump(mode="json") for r in inc.agents_run], + "tool_calls": [tc.model_dump(mode="json") for tc in inc.tool_calls], + "events": events, + "agent_definitions": agent_definitions, + "vm_seq": vm_seq, + } + +# ====== module: runtime/api_ui_hints.py ====== + +def add_ui_hints_routes(api_v1: APIRouter) -> None: + """Mount the /config/ui-hints handler on the api_v1 router. + + Module-qualified name (vs. bare ``add_routes``) so the bundler can + flatten this alongside its sibling ``api_*`` side-cars without the + four module-scope ``add_routes`` defs colliding. See + ``runtime.api_session_full.add_session_full_routes``. + """ + + @api_v1.get("/config/ui-hints") + async def get_ui_hints(request: Request) -> dict[str, Any]: + cfg = request.app.state.cfg + ui = cfg.ui + return { + "brand_name": ui.brand_name, + "brand_logo_url": ui.brand_logo_url, + "approval_rationale_templates": list(ui.approval_rationale_templates), + "hitl_question_templates": dict(ui.hitl_question_templates), + "environments": list(cfg.environments or []), + } + +# ====== module: runtime/api_apps_overlay.py ====== + +def add_apps_overlay_routes(api_v1: APIRouter) -> None: + """Mount the /apps/{app}/ui-views handler on the api_v1 router. + + Module-qualified name so the bundler can flatten alongside sibling + ``api_*`` side-cars without ``add_routes`` collisions. See + ``runtime.api_session_full.add_session_full_routes``. + """ + + @api_v1.get("/apps/{app_name}/ui-views") + async def list_app_views(app_name: str, request: Request) -> list[dict]: + # app_name is informational for now; v2.0 has one app per deploy. + cfg = request.app.state.cfg + return [v.model_dump() for v in cfg.ui.app_views] + +# ====== module: runtime/api_recent_events.py ====== + +_SSE_MEDIA_TYPE = "text/event-stream" +_SESSION_KINDS = frozenset({ + "session.created", + "session.status_changed", + "session.agent_running", +}) + + +def add_recent_events_routes(api_v1: APIRouter) -> None: + """Mount the /sessions/recent/events SSE handler on the api_v1 router. + + Module-qualified name so the bundler can flatten alongside sibling + ``api_*`` side-cars without ``add_routes`` collisions. See + ``runtime.api_session_full.add_session_full_routes``. + """ + + @api_v1.get("/sessions/recent/events") + async def stream_recent_events(request: Request, since: int = 0): + orch = request.app.state.orchestrator + event_log = getattr(orch, "event_log", None) + if event_log is None: + raise HTTPException( + status_code=503, detail="event_log not configured", + ) + + async def _stream() -> AsyncIterator[str]: + last_seq = since + # Backlog: emit session-level events past `since` + for ev in event_log.iter_recent(since=last_seq): + if ev.kind in _SESSION_KINDS: + payload = {"seq": ev.seq, "kind": ev.kind, + "session_id": ev.session_id, + "payload": ev.payload, "ts": ev.ts} + last_seq = ev.seq + yield f"data: {json.dumps(payload)}\n\n" + # Tail: poll for new rows; exit on client disconnect + while not await request.is_disconnected(): + await _asyncio.sleep(0.5) + for ev in event_log.iter_recent(since=last_seq): + if ev.kind in _SESSION_KINDS: + payload = {"seq": ev.seq, "kind": ev.kind, + "session_id": ev.session_id, + "payload": ev.payload, "ts": ev.ts} + last_seq = ev.seq + yield f"data: {json.dumps(payload)}\n\n" + + return StreamingResponse(_stream(), media_type=_SSE_MEDIA_TYPE) + +# ====== module: runtime/api_static.py ====== + +_BUILD_HINT = """ + +

React UI not built yet

+

Run cd web && npm ci && npm run build +to populate web/dist/.

+

Then refresh.

+ +""" + +_NOT_FOUND_JSON = ( + '{"error":{"code":"not_found","message":"unknown api path","details":{}}}' +) + + +def mount_static_assets(app: FastAPI) -> None: + """Mount static assets + SPA fallback. API routes must be registered first. + + Module-qualified name (vs. the bare ``mount`` we had pre-bundle-fix) so + the bundler can flatten this alongside its sibling ``api_*`` side-cars + without stepping on FastAPI's ``app.mount`` or any future bundled module + that happens to define a ``mount`` symbol. See + ``runtime.api_session_full.add_session_full_routes``. + """ + web_dist_path = os.environ.get("ASR_WEB_DIST") + if web_dist_path: + web_dist = Path(web_dist_path) + else: + # Default: web/dist relative to repo root. + web_dist = ( + Path(__file__).resolve().parent.parent.parent / "web" / "dist" + ) + + if not (web_dist / "index.html").exists(): + # Stub fallback when the bundle isn't built — useful in dev. + @app.get("/", include_in_schema=False) + async def _missing_root() -> HTMLResponse: + return HTMLResponse(content=_BUILD_HINT, status_code=503) + return + + # Serve assets at /assets/* (Vite output) + assets_dir = web_dist / "assets" + if assets_dir.exists(): + app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") + # Serve fonts at /fonts/* (vendored) + fonts_dir = web_dist / "fonts" + if fonts_dir.exists(): + app.mount("/fonts", StaticFiles(directory=fonts_dir), name="fonts") + + # SPA fallback: anything not matched by API or other static mounts → index.html + @app.get("/{full_path:path}", include_in_schema=False) + async def _spa_fallback(full_path: str, request: Request) -> Response: + # Reserve API/health/docs paths + if (full_path.startswith("api/") or full_path == "health" + or full_path.startswith("docs") or full_path == "openapi.json"): + return Response( + content=_NOT_FOUND_JSON, + status_code=404, + media_type="application/json", + ) + return FileResponse(web_dist / "index.html") diff --git a/dist/apps/code-review.py b/dist/apps/code-review.py index 2629fee..af76ea4 100644 --- a/dist/apps/code-review.py +++ b/dist/apps/code-review.py @@ -1429,12 +1429,17 @@ async def _poll(self, registry): from typing import AsyncIterator, Literal -from fastapi import FastAPI, HTTPException, Request, Response, WebSocket, WebSocketDisconnect +from fastapi import APIRouter, FastAPI, HTTPException, Request, Response, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse +from fastapi.responses import JSONResponse, RedirectResponse, StreamingResponse from starlette.exceptions import HTTPException as StarletteHTTPException + + + + + # ----- imports for runtime/api_dedup.py ----- """Dedup retraction HTTP routes. @@ -1461,6 +1466,87 @@ async def _poll(self, registry): from fastapi import FastAPI, HTTPException +# ----- imports for runtime/api_session_full.py ----- +"""Bootstrap endpoint for the React UI's single view-model. + +GET /api/v1/sessions/{id}/full returns everything the UI needs to render +the session in one round-trip — replaces the old pattern of multiple GETs. +The same shape is then patched in place by SSE delta events. + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.orchestrator``); unlike :mod:`runtime.api_dedup`, this module +is NOT suitable for lightweight test fixtures that construct a bare +``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" + + + +from fastapi import APIRouter, HTTPException, Request + + +# ----- imports for runtime/api_ui_hints.py ----- +"""UI hints endpoint — drives the React shell's brand and templates. + +GET /api/v1/config/ui-hints returns the runtime-configured ui block plus +the environments list. Read once at React-app boot and cached for the +session lifetime via `useUiHints()`. + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.cfg``); not suitable for lightweight test fixtures that +construct a bare ``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" + +from fastapi import APIRouter, Request + + +# ----- imports for runtime/api_apps_overlay.py ----- +"""App-overlay UI views discovery — Approach C extensibility. + +GET /api/v1/apps/{app}/ui-views returns the app-registered overlay views. +The framework UI's Selected-detail panel renders matching views as +"App-specific views →" links. v2.0 ships with one app per deployment; +multi-app per-app filtering is v2.1 scope (the path's app_name is +currently informational). + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.cfg``); not suitable for lightweight test fixtures that +construct a bare ``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" + + + +# ----- imports for runtime/api_recent_events.py ----- +"""Cross-session SSE — used by the React UI's Other Sessions monitor. + +Emits session-level events (not per-session detail events): session.created, +session.status_changed, session.agent_running. Lower frequency than the +per-session stream. + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.orchestrator``); not suitable for lightweight test fixtures +that construct a bare ``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" + +from fastapi.responses import StreamingResponse + +# ----- imports for runtime/api_static.py ----- +"""StaticFiles mount + SPA fallback for the React UI bundle. + +The React build output lives at $ASR_WEB_DIST (default: ../web/dist relative +to repo root). FastAPI serves /assets/* and /fonts/* directly; any unknown +path that isn't /api/v1/*, /health, or /docs falls back to index.html so +the React Router can pick up the URL. + +Registered only via :func:`runtime.api.build_app` (mounts onto the root +FastAPI app, not the api_v1 router). Must be invoked AFTER all API routes +are registered so the catch-all SPA fallback doesn't shadow them. +""" + +from fastapi import FastAPI, Request +from fastapi.responses import FileResponse, HTMLResponse, Response +from fastapi.staticfiles import StaticFiles + + # ----- imports for examples/code_review/state.py ----- """Code-review state-overrides schema (DECOUPLE-05 / D-08-01). @@ -2235,10 +2321,26 @@ class UIDetailField(BaseModel): model_config = {"frozen": True, "extra": "forbid"} +class AppView(BaseModel): + """An app-overlay UI view registered by an app for the framework UI to link to. + + The framework UI lists matching app views in its Selected-detail panel + ("App-specific views →") so apps can ship bespoke deep-dive pages + without forking the framework. Approach C (per the v2.0 design spec). + """ + model_config = ConfigDict(extra="forbid") + + id: str + title: str + applies_to: str # "always" | "agent:" | "tool:" + url: str + + class UIConfig(BaseModel): """App-driven UI rendering knobs. Keeps the generic Streamlit shell in ``runtime/ui.py`` agnostic of any specific domain — colors, labels, - and tag prefixes come from YAML. + and tag prefixes come from YAML. Also drives the React UI (v2.0) shell + via ``GET /api/v1/config/ui-hints``. ``badges`` is a 2-level dict: ``{field_name: {value: UIBadge}}``. Example: ``{"status": {"open": {"label": "OPEN", "color": "red"}}}``. @@ -2249,10 +2351,26 @@ class UIConfig(BaseModel): ``tags`` is an opaque key->tag-string map the UI consults for cross-skill signals (e.g. ``prior_match_supported`` -> the literal tag a skill emits). + + React UI (v2.0) fields: ``brand_name``, ``brand_logo_url``, + ``approval_rationale_templates``, and ``hitl_question_templates`` + drive the React shell's topbar brand block, environment switcher, + and approval-rationale dropdown. Read at app boot via + ``useUiHints()`` and cached for the session lifetime. + + ``app_views`` is the Approach C extensibility surface — apps register + bespoke UI overlay views that the framework UI's Selected-detail + panel lists as "App-specific views →" links. Served via + ``GET /api/v1/apps/{app}/ui-views``. """ badges: dict[str, dict[str, UIBadge]] = Field(default_factory=dict) detail_fields: list[UIDetailField] = Field(default_factory=list) tags: dict[str, str] = Field(default_factory=dict) + brand_name: str = "" + brand_logo_url: str | None = None + approval_rationale_templates: list[str] = Field(default_factory=list) + hitl_question_templates: dict[str, str] = Field(default_factory=dict) + app_views: list[AppView] = Field(default_factory=list) model_config = {"frozen": True, "extra": "forbid"} @@ -4879,6 +4997,12 @@ def _field(name: str, default=None): "gate_fired", "status_changed", "lesson_extracted", + # Session-level lifecycle events — emitted on the cross-session SSE + # stream (/api/v1/sessions/recent/events) for the React UI's "Other + # Sessions" monitor panel. Lower frequency than per-step kinds. + "session.created", + "session.status_changed", + "session.agent_running", ] _VALID_EVENT_KINDS: frozenset[str] = frozenset(get_args(EventKind)) @@ -4969,6 +5093,32 @@ def iter_for( ts=row.ts, ) + def iter_recent(self, since: int = 0) -> Iterator[SessionEvent]: + """Iterate events across ALL sessions where seq > since, ordered + by global seq. + + Used by the cross-session SSE stream + (``/api/v1/sessions/recent/events``) to deliver session.* + lifecycle events to the React UI's Other Sessions monitor panel. + Capped at 500 rows per call so a long-disconnected client can't + blow memory replaying years of history. + """ + with Session(self.engine) as s: + stmt = ( + select(SessionEventRow) + .where(SessionEventRow.seq > since) + .order_by(SessionEventRow.seq.asc()) + .limit(500) + ) + for row in s.execute(stmt).scalars(): + yield SessionEvent( + seq=row.seq, + session_id=row.session_id, + kind=row.kind, + payload=row.payload, + ts=row.ts, + ) + # ====== module: runtime/storage/migrations.py ====== _FORWARD_COLUMNS: list[tuple[str, str]] = [ @@ -6253,6 +6403,20 @@ async def _scheduler() -> str: reporter_team=sub_team, ) session_id = inc.id + # Emit session.created on the cross-session SSE stream so + # the React UI's Other Sessions monitor lights up the new + # tile in real time. Telemetry must not break start. + event_log = getattr(orch, "event_log", None) + if event_log is not None: + try: + # ``session_id`` already lands on the row; the payload + # carries no extra fields for ``session.created``. + event_log.record(session_id, "session.created") + except Exception: # noqa: BLE001 — telemetry must not break start + _log.debug( + "event_log.record(session.created) failed", + exc_info=True, + ) # Stamp trigger provenance onto the row before the graph # runs so any crash mid-graph still leaves an audit trail. # ``inc.findings`` is a JSON dict on the row. @@ -13839,6 +14003,21 @@ def _emit_status_changed_event( _log.debug( "event_log.record(status_changed) failed", exc_info=True, ) + # Mirror onto the cross-session SSE stream for the React UI's + # Other Sessions monitor (see api_recent_events.py). + # ``session_id`` already lands on the row; the payload carries + # the new ``status`` only. + try: + event_log.record( + inc.id, + "session.status_changed", + status=to_status, + ) + except Exception: # noqa: BLE001 — telemetry must not break finalize + _log.debug( + "event_log.record(session.status_changed) failed", + exc_info=True, + ) # M5 hook point: when ``to_status`` is terminal per app config, # invoke the lesson extractor. M4 leaves it as a no-op; M5 swaps @@ -14785,6 +14964,19 @@ async def start_session(self, *, query: str, env = (state_overrides or {}).get("environment", "") inc = self.store.create(query=query, environment=env, reporter_id=sub_id, reporter_team=sub_team) + # Emit session.created on the cross-session SSE stream so the + # React UI's Other Sessions monitor lights up the new tile in + # real time. ``session_id`` already lands on the row; the + # payload carries no extra fields. Telemetry must not break + # the start path. + event_log = getattr(self, "event_log", None) + if event_log is not None: + try: + event_log.record(inc.id, "session.created") + except Exception: # noqa: BLE001 — telemetry must not break start + _log.debug( + "event_log.record(session.created) failed", exc_info=True, + ) if trigger is not None: inc.findings["trigger"] = { "name": trigger.name, @@ -15477,6 +15669,9 @@ async def _lifespan(app: FastAPI) -> AsyncIterator[None]: orch = svc.submit_and_wait(svc._ensure_orchestrator(), timeout=30.0) app.state.service = svc app.state.orchestrator = orch + # Surface the validated AppConfig so app.state.cfg-readers + # (e.g. /api/v1/config/ui-hints) don't have to re-load YAML. + app.state.cfg = cfg # Environments roster is app-specific (incident-management has # production/staging/dev/local; code-review doesn't expose one). # Read it from the YAML's top-level ``environments:`` block; @@ -15564,16 +15759,28 @@ def build_app(cfg: AppConfig) -> FastAPI: title="ASR — Agent Orchestrator", lifespan=_make_lifespan(cfg), ) - - # CORS: configure once with the AppConfig-supplied origins so the - # React dev server (Vite at :5173, CRA/Next at :3000 by default) can - # call every endpoint, SSE included. Production deployments lock - # the origin list down via YAML — same shape, narrower allow-list. + # All framework routes (except /health) live under /api/v1 so the + # React client can stably target a versioned surface; /health stays + # at root for monitor / load-balancer health-check conventions. + api_v1 = APIRouter(prefix="/api/v1") + + # CORS: env-driven so the React dev server (Vite at :5173) can call + # every endpoint, SSE included. Override via ``ASR_CORS_ORIGINS`` + # (comma-separated) — production deployments lock the origin list + # down by setting the env var to the narrower allow-list. + # ``allow_credentials=False`` matches the bearer-token auth pattern + # (no cookies); methods are explicit so OPTIONS preflights are + # handled the same way for every route. + _cors_origins_raw = os.environ.get( + "ASR_CORS_ORIGINS", + "http://localhost:5173,http://127.0.0.1:5173", # Vite dev defaults + ) + _cors_origins = [o.strip() for o in _cors_origins_raw.split(",") if o.strip()] fastapi_app.add_middleware( CORSMiddleware, - allow_origins=cfg.api.cors_origins, - allow_credentials=cfg.api.cors_allow_credentials, - allow_methods=["*"], + allow_origins=_cors_origins, + allow_credentials=False, + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], ) @@ -15608,27 +15815,27 @@ async def _http_exception_handler( async def health(): return {"status": "ok"} - @fastapi_app.get("/agents") + @api_v1.get("/agents") async def agents(): return fastapi_app.state.orchestrator.list_agents() - @fastapi_app.get("/tools") + @api_v1.get("/tools") async def tools(): return fastapi_app.state.orchestrator.list_tools() - @fastapi_app.get("/incidents") + @api_v1.get("/incidents") async def incidents(limit: int = 20): return fastapi_app.state.orchestrator.list_recent_incidents(limit=limit) - @fastapi_app.get("/incidents/{incident_id}") + @api_v1.get("/incidents/{incident_id}") async def incident(incident_id: str): return fastapi_app.state.orchestrator.get_incident(incident_id) - @fastapi_app.delete("/incidents/{incident_id}") + @api_v1.delete("/incidents/{incident_id}") async def delete_incident(incident_id: str): return fastapi_app.state.orchestrator.delete_incident(incident_id) - @fastapi_app.post("/investigate") + @api_v1.post("/investigate") async def investigate(req: InvestigateRequest, request: Request) -> InvestigateResponse: """Legacy alias for ``POST /sessions`` — kept for back-compat. @@ -15662,11 +15869,11 @@ async def investigate(req: InvestigateRequest, request: Request) -> InvestigateR raise return InvestigateResponse(incident_id=sid) - @fastapi_app.get("/environments") + @api_v1.get("/environments") async def environments(): return fastapi_app.state.environments - @fastapi_app.post("/investigate/stream") + @api_v1.post("/investigate/stream") async def investigate_stream(req: InvestigateRequest) -> StreamingResponse: orch = fastapi_app.state.orchestrator @@ -15679,7 +15886,7 @@ async def _events(): return StreamingResponse(_events(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.post("/incidents/{incident_id}/resume") + @api_v1.post("/incidents/{incident_id}/resume") async def resume_incident(incident_id: str, req: ResumeRequest) -> StreamingResponse: orch = fastapi_app.state.orchestrator decision: dict = {"action": req.decision} @@ -15711,7 +15918,7 @@ async def _events(): # Multi-session endpoints # ------------------------------------------------------------------ - @fastapi_app.post( + @api_v1.post( "/sessions", status_code=201, ) @@ -15742,7 +15949,7 @@ class is matched by name so this handler does not depend on a raise return SessionStartResponse(session_id=sid) - @fastapi_app.get("/sessions") + @api_v1.get("/sessions") async def list_sessions_endpoint(request: Request) -> list[SessionStatus]: """Snapshot of in-flight sessions (running / awaiting_input / error).""" svc = request.app.state.service @@ -15752,7 +15959,7 @@ async def list_sessions_endpoint(request: Request) -> list[SessionStatus]: # HITL approval endpoints (risk-rated tool gateway) # ------------------------------------------------------------------ - @fastapi_app.get("/sessions/{session_id}/approvals") + @api_v1.get("/sessions/{session_id}/approvals") async def list_pending_approvals( session_id: str, request: Request ) -> list[PendingApproval]: @@ -15794,7 +16001,7 @@ async def list_pending_approvals( )) return out - @fastapi_app.post( + @api_v1.post( "/sessions/{session_id}/approvals/{tool_call_id}", status_code=200, ) @@ -15880,7 +16087,7 @@ async def _resume() -> None: "rationale": body.rationale, } - @fastapi_app.delete("/sessions/{session_id}", status_code=204) + @api_v1.delete("/sessions/{session_id}", status_code=204) async def stop_session_endpoint( session_id: str, request: Request ) -> Response: @@ -15911,7 +16118,7 @@ async def stop_session_endpoint( # T2: generic /sessions/* endpoints (React-ready, non-legacy). # ================================================================== - @fastapi_app.get("/sessions/recent") + @api_v1.get("/sessions/recent") async def recent_sessions(request: Request, limit: int = 20) -> list[dict]: """List recent sessions of ANY status — closed + active. @@ -15921,7 +16128,7 @@ async def recent_sessions(request: Request, limit: int = 20) -> list[dict]: orch = request.app.state.orchestrator return orch.list_recent_sessions(limit=limit) - @fastapi_app.get("/sessions/{session_id}") + @api_v1.get("/sessions/{session_id}") async def get_session_detail(session_id: str, request: Request) -> dict: """Full session detail. Generic equivalent of the legacy domain-flavoured detail route. 404 when the id is unknown.""" @@ -15933,7 +16140,7 @@ async def get_session_detail(session_id: str, request: Request) -> dict: status_code=404, detail=_SESSION_NOT_FOUND_DETAIL, ) from e - @fastapi_app.post("/sessions/{session_id}/resume") + @api_v1.post("/sessions/{session_id}/resume") async def resume_session_sse( session_id: str, req: ResumeRequest, request: Request, ) -> StreamingResponse: @@ -15967,7 +16174,7 @@ async def _events(): return StreamingResponse(_events(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.post("/sessions/{session_id}/retry") + @api_v1.post("/sessions/{session_id}/retry") async def retry_session_sse( session_id: str, request: Request, ) -> StreamingResponse: @@ -15990,7 +16197,7 @@ async def _events(): return StreamingResponse(_events(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.get("/sessions/{session_id}/retry/preview") + @api_v1.get("/sessions/{session_id}/retry/preview") async def preview_retry( session_id: str, request: Request, ) -> RetryDecisionPreview: @@ -16009,7 +16216,7 @@ async def preview_retry( reason=str(decision.reason), ) - @fastapi_app.get("/sessions/{session_id}/lessons") + @api_v1.get("/sessions/{session_id}/lessons") async def list_session_lessons( session_id: str, request: Request, ) -> list[LessonResponse]: @@ -16053,7 +16260,7 @@ async def list_session_lessons( # T3: SSE event stream + T4: WebSocket fallback. # ================================================================== - @fastapi_app.get("/sessions/{session_id}/events") + @api_v1.get("/sessions/{session_id}/events") async def sse_events( session_id: str, request: Request, since: int = 0, ) -> StreamingResponse: @@ -16107,7 +16314,7 @@ async def _stream(): return StreamingResponse(_stream(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.websocket("/ws/sessions/{session_id}/events") + @api_v1.websocket("/ws/sessions/{session_id}/events") async def ws_events(websocket: WebSocket, session_id: str) -> None: """WebSocket fallback for the SSE event stream. Same payload shape (:class:`EventEnvelope`); clients that prefer WS over @@ -16154,6 +16361,81 @@ async def ws_events(websocket: WebSocket, session_id: str) -> None: except Exception: # noqa: BLE001 pass + # ================================================================== + # Bootstrap bundle: GET /api/v1/sessions/{id}/full + # Single round-trip the React UI calls on session open. Module + # lives next door so this file stays focused on routing wiring. + # ================================================================== + add_session_full_routes(api_v1) + + # ================================================================== + # UI hints: GET /api/v1/config/ui-hints + # Drives the React shell's brand block, environment switcher list, + # and approval-rationale dropdown. Read once at app boot. + # ================================================================== + add_ui_hints_routes(api_v1) + + # ================================================================== + # App-overlay UI views: GET /api/v1/apps/{app}/ui-views + # Approach C extensibility — apps register bespoke deep-dive pages + # (e.g. "Deploy diff") that the framework UI's Selected-detail + # panel lists as "App-specific views →" links. + # ================================================================== + add_apps_overlay_routes(api_v1) + + # ================================================================== + # Cross-session SSE: GET /api/v1/sessions/recent/events + # Drives the React UI's "Other Sessions" monitor — session.created / + # session.status_changed / session.agent_running events across ALL + # sessions, ordered by global seq. + # ================================================================== + add_recent_events_routes(api_v1) + + # Legacy /incidents/* and /investigate redirects to /api/v1/* equivalents. + # 308 preserves method + body so legacy POSTs (e.g. /incidents/{id}/resume) + # keep working transparently. Removed in v2.1. + @fastapi_app.api_route( + "/incidents", methods=["GET", "POST"], include_in_schema=False, + ) + async def _legacy_incidents_collection() -> RedirectResponse: + return RedirectResponse(url="/api/v1/sessions", status_code=308) + + @fastapi_app.api_route( + "/incidents/{path:path}", + methods=["GET", "POST", "DELETE", "PUT"], + include_in_schema=False, + ) + async def _legacy_incidents_detail(path: str) -> RedirectResponse: + return RedirectResponse(url=f"/api/v1/sessions/{path}", status_code=308) + + @fastapi_app.api_route( + "/investigate", methods=["POST"], include_in_schema=False, + ) + async def _legacy_investigate() -> RedirectResponse: + return RedirectResponse(url="/api/v1/investigate", status_code=308) + + @fastapi_app.api_route( + "/investigate/{path:path}", + methods=["POST"], + include_in_schema=False, + ) + async def _legacy_investigate_subpath(path: str) -> RedirectResponse: + return RedirectResponse( + url=f"/api/v1/investigate/{path}", status_code=308, + ) + + # Mount the versioned router. /health stays at root (registered + # directly on ``fastapi_app`` above); everything else lives under + # /api/v1. + fastapi_app.include_router(api_v1) + # ================================================================== + # React UI bundle: StaticFiles mount at / + SPA fallback. + # MUST be the last route-registration step in build_app — the + # catch-all ``GET /{full_path:path}`` would otherwise shadow every + # API route and legacy redirect. The fallback excludes /api/, /health, + # and /docs so unknown API paths still return structured JSON 404s. + # ================================================================== + mount_static_assets(fastapi_app) return fastapi_app @@ -16263,6 +16545,230 @@ async def un_duplicate( note=payload.note, ) +# ====== module: runtime/api_session_full.py ====== + +def add_session_full_routes(api_v1: APIRouter) -> None: + """Mount the /sessions/{id}/full handler on the api_v1 router. + + The function name is intentionally module-qualified (rather than the + bare ``add_routes`` we used pre-bundle-fix) so that ``scripts/build_single_file.py`` + can flatten this module alongside its sibling ``api_*`` side-cars without + the four ``add_routes`` defs colliding at module scope. Source-side callers + import the symbol directly: ``from runtime.api_session_full import add_session_full_routes``. + """ + + @api_v1.get("/sessions/{session_id}/full") + async def get_session_full( + session_id: str, request: Request, + ) -> dict[str, Any]: + orch = request.app.state.orchestrator + try: + inc = orch.store.load(session_id) + except (FileNotFoundError, ValueError, KeyError, LookupError) as e: + # ``ValueError`` covers the SessionStore id-format guard + # (``Invalid session id ...``); semantically a 404 at the + # API boundary — same convention as other /sessions/* GETs. + raise HTTPException( + status_code=404, detail="session not found", + ) from e + + # Replay the EventLog backlog. ``vm_seq`` is the high-water mark + # the UI uses to ?since=N when it later opens the SSE stream, so + # delta events stitch onto the same view-model without gap or + # overlap. + event_log = getattr(orch, "event_log", None) + events: list[dict[str, Any]] = [] + vm_seq = 0 + if event_log is not None: + for ev in event_log.iter_for(session_id, since=0): + events.append({ + "seq": ev.seq, + "kind": ev.kind, + "payload": ev.payload, + "ts": ev.ts, + }) + if ev.seq > vm_seq: + vm_seq = ev.seq + + # Agent definitions: skill metadata the UI needs to render the + # graph diagram + per-agent header chips. ``orch.skills`` is a + # ``dict[str, Skill]`` keyed by name. ``Skill.tools`` is itself + # a ``dict[str, list[str]]`` (server -> tool list) — expose the + # server keys as the ref strings; ``Skill.routes`` is a + # ``list[RouteRule]`` (when/next/gate) — flatten to the + # signal->next mapping the UI consumes. + agent_definitions: dict[str, dict[str, Any]] = {} + for name, skill in orch.skills.items(): + agent_definitions[name] = { + "name": skill.name, + "kind": skill.kind, + "model": skill.model or orch.cfg.llm.default, + "tools": list(skill.tools or {}), + "routes": {r.when: r.next for r in skill.routes}, + "system_prompt_excerpt": (skill.system_prompt or "")[:500], + } + + return { + "session": inc.model_dump(mode="json"), + "agents_run": [r.model_dump(mode="json") for r in inc.agents_run], + "tool_calls": [tc.model_dump(mode="json") for tc in inc.tool_calls], + "events": events, + "agent_definitions": agent_definitions, + "vm_seq": vm_seq, + } + +# ====== module: runtime/api_ui_hints.py ====== + +def add_ui_hints_routes(api_v1: APIRouter) -> None: + """Mount the /config/ui-hints handler on the api_v1 router. + + Module-qualified name (vs. bare ``add_routes``) so the bundler can + flatten this alongside its sibling ``api_*`` side-cars without the + four module-scope ``add_routes`` defs colliding. See + ``runtime.api_session_full.add_session_full_routes``. + """ + + @api_v1.get("/config/ui-hints") + async def get_ui_hints(request: Request) -> dict[str, Any]: + cfg = request.app.state.cfg + ui = cfg.ui + return { + "brand_name": ui.brand_name, + "brand_logo_url": ui.brand_logo_url, + "approval_rationale_templates": list(ui.approval_rationale_templates), + "hitl_question_templates": dict(ui.hitl_question_templates), + "environments": list(cfg.environments or []), + } + +# ====== module: runtime/api_apps_overlay.py ====== + +def add_apps_overlay_routes(api_v1: APIRouter) -> None: + """Mount the /apps/{app}/ui-views handler on the api_v1 router. + + Module-qualified name so the bundler can flatten alongside sibling + ``api_*`` side-cars without ``add_routes`` collisions. See + ``runtime.api_session_full.add_session_full_routes``. + """ + + @api_v1.get("/apps/{app_name}/ui-views") + async def list_app_views(app_name: str, request: Request) -> list[dict]: + # app_name is informational for now; v2.0 has one app per deploy. + cfg = request.app.state.cfg + return [v.model_dump() for v in cfg.ui.app_views] + +# ====== module: runtime/api_recent_events.py ====== + +_SSE_MEDIA_TYPE = "text/event-stream" +_SESSION_KINDS = frozenset({ + "session.created", + "session.status_changed", + "session.agent_running", +}) + + +def add_recent_events_routes(api_v1: APIRouter) -> None: + """Mount the /sessions/recent/events SSE handler on the api_v1 router. + + Module-qualified name so the bundler can flatten alongside sibling + ``api_*`` side-cars without ``add_routes`` collisions. See + ``runtime.api_session_full.add_session_full_routes``. + """ + + @api_v1.get("/sessions/recent/events") + async def stream_recent_events(request: Request, since: int = 0): + orch = request.app.state.orchestrator + event_log = getattr(orch, "event_log", None) + if event_log is None: + raise HTTPException( + status_code=503, detail="event_log not configured", + ) + + async def _stream() -> AsyncIterator[str]: + last_seq = since + # Backlog: emit session-level events past `since` + for ev in event_log.iter_recent(since=last_seq): + if ev.kind in _SESSION_KINDS: + payload = {"seq": ev.seq, "kind": ev.kind, + "session_id": ev.session_id, + "payload": ev.payload, "ts": ev.ts} + last_seq = ev.seq + yield f"data: {json.dumps(payload)}\n\n" + # Tail: poll for new rows; exit on client disconnect + while not await request.is_disconnected(): + await _asyncio.sleep(0.5) + for ev in event_log.iter_recent(since=last_seq): + if ev.kind in _SESSION_KINDS: + payload = {"seq": ev.seq, "kind": ev.kind, + "session_id": ev.session_id, + "payload": ev.payload, "ts": ev.ts} + last_seq = ev.seq + yield f"data: {json.dumps(payload)}\n\n" + + return StreamingResponse(_stream(), media_type=_SSE_MEDIA_TYPE) + +# ====== module: runtime/api_static.py ====== + +_BUILD_HINT = """ + +

React UI not built yet

+

Run cd web && npm ci && npm run build +to populate web/dist/.

+

Then refresh.

+ +""" + +_NOT_FOUND_JSON = ( + '{"error":{"code":"not_found","message":"unknown api path","details":{}}}' +) + + +def mount_static_assets(app: FastAPI) -> None: + """Mount static assets + SPA fallback. API routes must be registered first. + + Module-qualified name (vs. the bare ``mount`` we had pre-bundle-fix) so + the bundler can flatten this alongside its sibling ``api_*`` side-cars + without stepping on FastAPI's ``app.mount`` or any future bundled module + that happens to define a ``mount`` symbol. See + ``runtime.api_session_full.add_session_full_routes``. + """ + web_dist_path = os.environ.get("ASR_WEB_DIST") + if web_dist_path: + web_dist = Path(web_dist_path) + else: + # Default: web/dist relative to repo root. + web_dist = ( + Path(__file__).resolve().parent.parent.parent / "web" / "dist" + ) + + if not (web_dist / "index.html").exists(): + # Stub fallback when the bundle isn't built — useful in dev. + @app.get("/", include_in_schema=False) + async def _missing_root() -> HTMLResponse: + return HTMLResponse(content=_BUILD_HINT, status_code=503) + return + + # Serve assets at /assets/* (Vite output) + assets_dir = web_dist / "assets" + if assets_dir.exists(): + app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") + # Serve fonts at /fonts/* (vendored) + fonts_dir = web_dist / "fonts" + if fonts_dir.exists(): + app.mount("/fonts", StaticFiles(directory=fonts_dir), name="fonts") + + # SPA fallback: anything not matched by API or other static mounts → index.html + @app.get("/{full_path:path}", include_in_schema=False) + async def _spa_fallback(full_path: str, request: Request) -> Response: + # Reserve API/health/docs paths + if (full_path.startswith("api/") or full_path == "health" + or full_path.startswith("docs") or full_path == "openapi.json"): + return Response( + content=_NOT_FOUND_JSON, + status_code=404, + media_type="application/json", + ) + return FileResponse(web_dist / "index.html") + # ====== module: examples/code_review/state.py ====== class CodeReviewStateOverrides(BaseModel): diff --git a/dist/apps/incident-management.py b/dist/apps/incident-management.py index b3725dc..1011c32 100644 --- a/dist/apps/incident-management.py +++ b/dist/apps/incident-management.py @@ -1429,12 +1429,17 @@ async def _poll(self, registry): from typing import AsyncIterator, Literal -from fastapi import FastAPI, HTTPException, Request, Response, WebSocket, WebSocketDisconnect +from fastapi import APIRouter, FastAPI, HTTPException, Request, Response, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse +from fastapi.responses import JSONResponse, RedirectResponse, StreamingResponse from starlette.exceptions import HTTPException as StarletteHTTPException + + + + + # ----- imports for runtime/api_dedup.py ----- """Dedup retraction HTTP routes. @@ -1461,6 +1466,87 @@ async def _poll(self, registry): from fastapi import FastAPI, HTTPException +# ----- imports for runtime/api_session_full.py ----- +"""Bootstrap endpoint for the React UI's single view-model. + +GET /api/v1/sessions/{id}/full returns everything the UI needs to render +the session in one round-trip — replaces the old pattern of multiple GETs. +The same shape is then patched in place by SSE delta events. + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.orchestrator``); unlike :mod:`runtime.api_dedup`, this module +is NOT suitable for lightweight test fixtures that construct a bare +``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" + + + +from fastapi import APIRouter, HTTPException, Request + + +# ----- imports for runtime/api_ui_hints.py ----- +"""UI hints endpoint — drives the React shell's brand and templates. + +GET /api/v1/config/ui-hints returns the runtime-configured ui block plus +the environments list. Read once at React-app boot and cached for the +session lifetime via `useUiHints()`. + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.cfg``); not suitable for lightweight test fixtures that +construct a bare ``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" + +from fastapi import APIRouter, Request + + +# ----- imports for runtime/api_apps_overlay.py ----- +"""App-overlay UI views discovery — Approach C extensibility. + +GET /api/v1/apps/{app}/ui-views returns the app-registered overlay views. +The framework UI's Selected-detail panel renders matching views as +"App-specific views →" links. v2.0 ships with one app per deployment; +multi-app per-app filtering is v2.1 scope (the path's app_name is +currently informational). + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.cfg``); not suitable for lightweight test fixtures that +construct a bare ``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" + + + +# ----- imports for runtime/api_recent_events.py ----- +"""Cross-session SSE — used by the React UI's Other Sessions monitor. + +Emits session-level events (not per-session detail events): session.created, +session.status_changed, session.agent_running. Lower frequency than the +per-session stream. + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.orchestrator``); not suitable for lightweight test fixtures +that construct a bare ``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" + +from fastapi.responses import StreamingResponse + +# ----- imports for runtime/api_static.py ----- +"""StaticFiles mount + SPA fallback for the React UI bundle. + +The React build output lives at $ASR_WEB_DIST (default: ../web/dist relative +to repo root). FastAPI serves /assets/* and /fonts/* directly; any unknown +path that isn't /api/v1/*, /health, or /docs falls back to index.html so +the React Router can pick up the URL. + +Registered only via :func:`runtime.api.build_app` (mounts onto the root +FastAPI app, not the api_v1 router). Must be invoked AFTER all API routes +are registered so the catch-all SPA fallback doesn't shadow them. +""" + +from fastapi import FastAPI, Request +from fastapi.responses import FileResponse, HTMLResponse, Response +from fastapi.staticfiles import StaticFiles + + # ----- imports for examples/incident_management/state.py ----- """Incident-management state-overrides schema (DECOUPLE-05 / D-08-01). @@ -2247,10 +2333,26 @@ class UIDetailField(BaseModel): model_config = {"frozen": True, "extra": "forbid"} +class AppView(BaseModel): + """An app-overlay UI view registered by an app for the framework UI to link to. + + The framework UI lists matching app views in its Selected-detail panel + ("App-specific views →") so apps can ship bespoke deep-dive pages + without forking the framework. Approach C (per the v2.0 design spec). + """ + model_config = ConfigDict(extra="forbid") + + id: str + title: str + applies_to: str # "always" | "agent:" | "tool:" + url: str + + class UIConfig(BaseModel): """App-driven UI rendering knobs. Keeps the generic Streamlit shell in ``runtime/ui.py`` agnostic of any specific domain — colors, labels, - and tag prefixes come from YAML. + and tag prefixes come from YAML. Also drives the React UI (v2.0) shell + via ``GET /api/v1/config/ui-hints``. ``badges`` is a 2-level dict: ``{field_name: {value: UIBadge}}``. Example: ``{"status": {"open": {"label": "OPEN", "color": "red"}}}``. @@ -2261,10 +2363,26 @@ class UIConfig(BaseModel): ``tags`` is an opaque key->tag-string map the UI consults for cross-skill signals (e.g. ``prior_match_supported`` -> the literal tag a skill emits). + + React UI (v2.0) fields: ``brand_name``, ``brand_logo_url``, + ``approval_rationale_templates``, and ``hitl_question_templates`` + drive the React shell's topbar brand block, environment switcher, + and approval-rationale dropdown. Read at app boot via + ``useUiHints()`` and cached for the session lifetime. + + ``app_views`` is the Approach C extensibility surface — apps register + bespoke UI overlay views that the framework UI's Selected-detail + panel lists as "App-specific views →" links. Served via + ``GET /api/v1/apps/{app}/ui-views``. """ badges: dict[str, dict[str, UIBadge]] = Field(default_factory=dict) detail_fields: list[UIDetailField] = Field(default_factory=list) tags: dict[str, str] = Field(default_factory=dict) + brand_name: str = "" + brand_logo_url: str | None = None + approval_rationale_templates: list[str] = Field(default_factory=list) + hitl_question_templates: dict[str, str] = Field(default_factory=dict) + app_views: list[AppView] = Field(default_factory=list) model_config = {"frozen": True, "extra": "forbid"} @@ -4891,6 +5009,12 @@ def _field(name: str, default=None): "gate_fired", "status_changed", "lesson_extracted", + # Session-level lifecycle events — emitted on the cross-session SSE + # stream (/api/v1/sessions/recent/events) for the React UI's "Other + # Sessions" monitor panel. Lower frequency than per-step kinds. + "session.created", + "session.status_changed", + "session.agent_running", ] _VALID_EVENT_KINDS: frozenset[str] = frozenset(get_args(EventKind)) @@ -4981,6 +5105,32 @@ def iter_for( ts=row.ts, ) + def iter_recent(self, since: int = 0) -> Iterator[SessionEvent]: + """Iterate events across ALL sessions where seq > since, ordered + by global seq. + + Used by the cross-session SSE stream + (``/api/v1/sessions/recent/events``) to deliver session.* + lifecycle events to the React UI's Other Sessions monitor panel. + Capped at 500 rows per call so a long-disconnected client can't + blow memory replaying years of history. + """ + with Session(self.engine) as s: + stmt = ( + select(SessionEventRow) + .where(SessionEventRow.seq > since) + .order_by(SessionEventRow.seq.asc()) + .limit(500) + ) + for row in s.execute(stmt).scalars(): + yield SessionEvent( + seq=row.seq, + session_id=row.session_id, + kind=row.kind, + payload=row.payload, + ts=row.ts, + ) + # ====== module: runtime/storage/migrations.py ====== _FORWARD_COLUMNS: list[tuple[str, str]] = [ @@ -6265,6 +6415,20 @@ async def _scheduler() -> str: reporter_team=sub_team, ) session_id = inc.id + # Emit session.created on the cross-session SSE stream so + # the React UI's Other Sessions monitor lights up the new + # tile in real time. Telemetry must not break start. + event_log = getattr(orch, "event_log", None) + if event_log is not None: + try: + # ``session_id`` already lands on the row; the payload + # carries no extra fields for ``session.created``. + event_log.record(session_id, "session.created") + except Exception: # noqa: BLE001 — telemetry must not break start + _log.debug( + "event_log.record(session.created) failed", + exc_info=True, + ) # Stamp trigger provenance onto the row before the graph # runs so any crash mid-graph still leaves an audit trail. # ``inc.findings`` is a JSON dict on the row. @@ -13851,6 +14015,21 @@ def _emit_status_changed_event( _log.debug( "event_log.record(status_changed) failed", exc_info=True, ) + # Mirror onto the cross-session SSE stream for the React UI's + # Other Sessions monitor (see api_recent_events.py). + # ``session_id`` already lands on the row; the payload carries + # the new ``status`` only. + try: + event_log.record( + inc.id, + "session.status_changed", + status=to_status, + ) + except Exception: # noqa: BLE001 — telemetry must not break finalize + _log.debug( + "event_log.record(session.status_changed) failed", + exc_info=True, + ) # M5 hook point: when ``to_status`` is terminal per app config, # invoke the lesson extractor. M4 leaves it as a no-op; M5 swaps @@ -14797,6 +14976,19 @@ async def start_session(self, *, query: str, env = (state_overrides or {}).get("environment", "") inc = self.store.create(query=query, environment=env, reporter_id=sub_id, reporter_team=sub_team) + # Emit session.created on the cross-session SSE stream so the + # React UI's Other Sessions monitor lights up the new tile in + # real time. ``session_id`` already lands on the row; the + # payload carries no extra fields. Telemetry must not break + # the start path. + event_log = getattr(self, "event_log", None) + if event_log is not None: + try: + event_log.record(inc.id, "session.created") + except Exception: # noqa: BLE001 — telemetry must not break start + _log.debug( + "event_log.record(session.created) failed", exc_info=True, + ) if trigger is not None: inc.findings["trigger"] = { "name": trigger.name, @@ -15489,6 +15681,9 @@ async def _lifespan(app: FastAPI) -> AsyncIterator[None]: orch = svc.submit_and_wait(svc._ensure_orchestrator(), timeout=30.0) app.state.service = svc app.state.orchestrator = orch + # Surface the validated AppConfig so app.state.cfg-readers + # (e.g. /api/v1/config/ui-hints) don't have to re-load YAML. + app.state.cfg = cfg # Environments roster is app-specific (incident-management has # production/staging/dev/local; code-review doesn't expose one). # Read it from the YAML's top-level ``environments:`` block; @@ -15576,16 +15771,28 @@ def build_app(cfg: AppConfig) -> FastAPI: title="ASR — Agent Orchestrator", lifespan=_make_lifespan(cfg), ) - - # CORS: configure once with the AppConfig-supplied origins so the - # React dev server (Vite at :5173, CRA/Next at :3000 by default) can - # call every endpoint, SSE included. Production deployments lock - # the origin list down via YAML — same shape, narrower allow-list. + # All framework routes (except /health) live under /api/v1 so the + # React client can stably target a versioned surface; /health stays + # at root for monitor / load-balancer health-check conventions. + api_v1 = APIRouter(prefix="/api/v1") + + # CORS: env-driven so the React dev server (Vite at :5173) can call + # every endpoint, SSE included. Override via ``ASR_CORS_ORIGINS`` + # (comma-separated) — production deployments lock the origin list + # down by setting the env var to the narrower allow-list. + # ``allow_credentials=False`` matches the bearer-token auth pattern + # (no cookies); methods are explicit so OPTIONS preflights are + # handled the same way for every route. + _cors_origins_raw = os.environ.get( + "ASR_CORS_ORIGINS", + "http://localhost:5173,http://127.0.0.1:5173", # Vite dev defaults + ) + _cors_origins = [o.strip() for o in _cors_origins_raw.split(",") if o.strip()] fastapi_app.add_middleware( CORSMiddleware, - allow_origins=cfg.api.cors_origins, - allow_credentials=cfg.api.cors_allow_credentials, - allow_methods=["*"], + allow_origins=_cors_origins, + allow_credentials=False, + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], ) @@ -15620,27 +15827,27 @@ async def _http_exception_handler( async def health(): return {"status": "ok"} - @fastapi_app.get("/agents") + @api_v1.get("/agents") async def agents(): return fastapi_app.state.orchestrator.list_agents() - @fastapi_app.get("/tools") + @api_v1.get("/tools") async def tools(): return fastapi_app.state.orchestrator.list_tools() - @fastapi_app.get("/incidents") + @api_v1.get("/incidents") async def incidents(limit: int = 20): return fastapi_app.state.orchestrator.list_recent_incidents(limit=limit) - @fastapi_app.get("/incidents/{incident_id}") + @api_v1.get("/incidents/{incident_id}") async def incident(incident_id: str): return fastapi_app.state.orchestrator.get_incident(incident_id) - @fastapi_app.delete("/incidents/{incident_id}") + @api_v1.delete("/incidents/{incident_id}") async def delete_incident(incident_id: str): return fastapi_app.state.orchestrator.delete_incident(incident_id) - @fastapi_app.post("/investigate") + @api_v1.post("/investigate") async def investigate(req: InvestigateRequest, request: Request) -> InvestigateResponse: """Legacy alias for ``POST /sessions`` — kept for back-compat. @@ -15674,11 +15881,11 @@ async def investigate(req: InvestigateRequest, request: Request) -> InvestigateR raise return InvestigateResponse(incident_id=sid) - @fastapi_app.get("/environments") + @api_v1.get("/environments") async def environments(): return fastapi_app.state.environments - @fastapi_app.post("/investigate/stream") + @api_v1.post("/investigate/stream") async def investigate_stream(req: InvestigateRequest) -> StreamingResponse: orch = fastapi_app.state.orchestrator @@ -15691,7 +15898,7 @@ async def _events(): return StreamingResponse(_events(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.post("/incidents/{incident_id}/resume") + @api_v1.post("/incidents/{incident_id}/resume") async def resume_incident(incident_id: str, req: ResumeRequest) -> StreamingResponse: orch = fastapi_app.state.orchestrator decision: dict = {"action": req.decision} @@ -15723,7 +15930,7 @@ async def _events(): # Multi-session endpoints # ------------------------------------------------------------------ - @fastapi_app.post( + @api_v1.post( "/sessions", status_code=201, ) @@ -15754,7 +15961,7 @@ class is matched by name so this handler does not depend on a raise return SessionStartResponse(session_id=sid) - @fastapi_app.get("/sessions") + @api_v1.get("/sessions") async def list_sessions_endpoint(request: Request) -> list[SessionStatus]: """Snapshot of in-flight sessions (running / awaiting_input / error).""" svc = request.app.state.service @@ -15764,7 +15971,7 @@ async def list_sessions_endpoint(request: Request) -> list[SessionStatus]: # HITL approval endpoints (risk-rated tool gateway) # ------------------------------------------------------------------ - @fastapi_app.get("/sessions/{session_id}/approvals") + @api_v1.get("/sessions/{session_id}/approvals") async def list_pending_approvals( session_id: str, request: Request ) -> list[PendingApproval]: @@ -15806,7 +16013,7 @@ async def list_pending_approvals( )) return out - @fastapi_app.post( + @api_v1.post( "/sessions/{session_id}/approvals/{tool_call_id}", status_code=200, ) @@ -15892,7 +16099,7 @@ async def _resume() -> None: "rationale": body.rationale, } - @fastapi_app.delete("/sessions/{session_id}", status_code=204) + @api_v1.delete("/sessions/{session_id}", status_code=204) async def stop_session_endpoint( session_id: str, request: Request ) -> Response: @@ -15923,7 +16130,7 @@ async def stop_session_endpoint( # T2: generic /sessions/* endpoints (React-ready, non-legacy). # ================================================================== - @fastapi_app.get("/sessions/recent") + @api_v1.get("/sessions/recent") async def recent_sessions(request: Request, limit: int = 20) -> list[dict]: """List recent sessions of ANY status — closed + active. @@ -15933,7 +16140,7 @@ async def recent_sessions(request: Request, limit: int = 20) -> list[dict]: orch = request.app.state.orchestrator return orch.list_recent_sessions(limit=limit) - @fastapi_app.get("/sessions/{session_id}") + @api_v1.get("/sessions/{session_id}") async def get_session_detail(session_id: str, request: Request) -> dict: """Full session detail. Generic equivalent of the legacy domain-flavoured detail route. 404 when the id is unknown.""" @@ -15945,7 +16152,7 @@ async def get_session_detail(session_id: str, request: Request) -> dict: status_code=404, detail=_SESSION_NOT_FOUND_DETAIL, ) from e - @fastapi_app.post("/sessions/{session_id}/resume") + @api_v1.post("/sessions/{session_id}/resume") async def resume_session_sse( session_id: str, req: ResumeRequest, request: Request, ) -> StreamingResponse: @@ -15979,7 +16186,7 @@ async def _events(): return StreamingResponse(_events(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.post("/sessions/{session_id}/retry") + @api_v1.post("/sessions/{session_id}/retry") async def retry_session_sse( session_id: str, request: Request, ) -> StreamingResponse: @@ -16002,7 +16209,7 @@ async def _events(): return StreamingResponse(_events(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.get("/sessions/{session_id}/retry/preview") + @api_v1.get("/sessions/{session_id}/retry/preview") async def preview_retry( session_id: str, request: Request, ) -> RetryDecisionPreview: @@ -16021,7 +16228,7 @@ async def preview_retry( reason=str(decision.reason), ) - @fastapi_app.get("/sessions/{session_id}/lessons") + @api_v1.get("/sessions/{session_id}/lessons") async def list_session_lessons( session_id: str, request: Request, ) -> list[LessonResponse]: @@ -16065,7 +16272,7 @@ async def list_session_lessons( # T3: SSE event stream + T4: WebSocket fallback. # ================================================================== - @fastapi_app.get("/sessions/{session_id}/events") + @api_v1.get("/sessions/{session_id}/events") async def sse_events( session_id: str, request: Request, since: int = 0, ) -> StreamingResponse: @@ -16119,7 +16326,7 @@ async def _stream(): return StreamingResponse(_stream(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.websocket("/ws/sessions/{session_id}/events") + @api_v1.websocket("/ws/sessions/{session_id}/events") async def ws_events(websocket: WebSocket, session_id: str) -> None: """WebSocket fallback for the SSE event stream. Same payload shape (:class:`EventEnvelope`); clients that prefer WS over @@ -16166,6 +16373,81 @@ async def ws_events(websocket: WebSocket, session_id: str) -> None: except Exception: # noqa: BLE001 pass + # ================================================================== + # Bootstrap bundle: GET /api/v1/sessions/{id}/full + # Single round-trip the React UI calls on session open. Module + # lives next door so this file stays focused on routing wiring. + # ================================================================== + add_session_full_routes(api_v1) + + # ================================================================== + # UI hints: GET /api/v1/config/ui-hints + # Drives the React shell's brand block, environment switcher list, + # and approval-rationale dropdown. Read once at app boot. + # ================================================================== + add_ui_hints_routes(api_v1) + + # ================================================================== + # App-overlay UI views: GET /api/v1/apps/{app}/ui-views + # Approach C extensibility — apps register bespoke deep-dive pages + # (e.g. "Deploy diff") that the framework UI's Selected-detail + # panel lists as "App-specific views →" links. + # ================================================================== + add_apps_overlay_routes(api_v1) + + # ================================================================== + # Cross-session SSE: GET /api/v1/sessions/recent/events + # Drives the React UI's "Other Sessions" monitor — session.created / + # session.status_changed / session.agent_running events across ALL + # sessions, ordered by global seq. + # ================================================================== + add_recent_events_routes(api_v1) + + # Legacy /incidents/* and /investigate redirects to /api/v1/* equivalents. + # 308 preserves method + body so legacy POSTs (e.g. /incidents/{id}/resume) + # keep working transparently. Removed in v2.1. + @fastapi_app.api_route( + "/incidents", methods=["GET", "POST"], include_in_schema=False, + ) + async def _legacy_incidents_collection() -> RedirectResponse: + return RedirectResponse(url="/api/v1/sessions", status_code=308) + + @fastapi_app.api_route( + "/incidents/{path:path}", + methods=["GET", "POST", "DELETE", "PUT"], + include_in_schema=False, + ) + async def _legacy_incidents_detail(path: str) -> RedirectResponse: + return RedirectResponse(url=f"/api/v1/sessions/{path}", status_code=308) + + @fastapi_app.api_route( + "/investigate", methods=["POST"], include_in_schema=False, + ) + async def _legacy_investigate() -> RedirectResponse: + return RedirectResponse(url="/api/v1/investigate", status_code=308) + + @fastapi_app.api_route( + "/investigate/{path:path}", + methods=["POST"], + include_in_schema=False, + ) + async def _legacy_investigate_subpath(path: str) -> RedirectResponse: + return RedirectResponse( + url=f"/api/v1/investigate/{path}", status_code=308, + ) + + # Mount the versioned router. /health stays at root (registered + # directly on ``fastapi_app`` above); everything else lives under + # /api/v1. + fastapi_app.include_router(api_v1) + # ================================================================== + # React UI bundle: StaticFiles mount at / + SPA fallback. + # MUST be the last route-registration step in build_app — the + # catch-all ``GET /{full_path:path}`` would otherwise shadow every + # API route and legacy redirect. The fallback excludes /api/, /health, + # and /docs so unknown API paths still return structured JSON 404s. + # ================================================================== + mount_static_assets(fastapi_app) return fastapi_app @@ -16275,6 +16557,230 @@ async def un_duplicate( note=payload.note, ) +# ====== module: runtime/api_session_full.py ====== + +def add_session_full_routes(api_v1: APIRouter) -> None: + """Mount the /sessions/{id}/full handler on the api_v1 router. + + The function name is intentionally module-qualified (rather than the + bare ``add_routes`` we used pre-bundle-fix) so that ``scripts/build_single_file.py`` + can flatten this module alongside its sibling ``api_*`` side-cars without + the four ``add_routes`` defs colliding at module scope. Source-side callers + import the symbol directly: ``from runtime.api_session_full import add_session_full_routes``. + """ + + @api_v1.get("/sessions/{session_id}/full") + async def get_session_full( + session_id: str, request: Request, + ) -> dict[str, Any]: + orch = request.app.state.orchestrator + try: + inc = orch.store.load(session_id) + except (FileNotFoundError, ValueError, KeyError, LookupError) as e: + # ``ValueError`` covers the SessionStore id-format guard + # (``Invalid session id ...``); semantically a 404 at the + # API boundary — same convention as other /sessions/* GETs. + raise HTTPException( + status_code=404, detail="session not found", + ) from e + + # Replay the EventLog backlog. ``vm_seq`` is the high-water mark + # the UI uses to ?since=N when it later opens the SSE stream, so + # delta events stitch onto the same view-model without gap or + # overlap. + event_log = getattr(orch, "event_log", None) + events: list[dict[str, Any]] = [] + vm_seq = 0 + if event_log is not None: + for ev in event_log.iter_for(session_id, since=0): + events.append({ + "seq": ev.seq, + "kind": ev.kind, + "payload": ev.payload, + "ts": ev.ts, + }) + if ev.seq > vm_seq: + vm_seq = ev.seq + + # Agent definitions: skill metadata the UI needs to render the + # graph diagram + per-agent header chips. ``orch.skills`` is a + # ``dict[str, Skill]`` keyed by name. ``Skill.tools`` is itself + # a ``dict[str, list[str]]`` (server -> tool list) — expose the + # server keys as the ref strings; ``Skill.routes`` is a + # ``list[RouteRule]`` (when/next/gate) — flatten to the + # signal->next mapping the UI consumes. + agent_definitions: dict[str, dict[str, Any]] = {} + for name, skill in orch.skills.items(): + agent_definitions[name] = { + "name": skill.name, + "kind": skill.kind, + "model": skill.model or orch.cfg.llm.default, + "tools": list(skill.tools or {}), + "routes": {r.when: r.next for r in skill.routes}, + "system_prompt_excerpt": (skill.system_prompt or "")[:500], + } + + return { + "session": inc.model_dump(mode="json"), + "agents_run": [r.model_dump(mode="json") for r in inc.agents_run], + "tool_calls": [tc.model_dump(mode="json") for tc in inc.tool_calls], + "events": events, + "agent_definitions": agent_definitions, + "vm_seq": vm_seq, + } + +# ====== module: runtime/api_ui_hints.py ====== + +def add_ui_hints_routes(api_v1: APIRouter) -> None: + """Mount the /config/ui-hints handler on the api_v1 router. + + Module-qualified name (vs. bare ``add_routes``) so the bundler can + flatten this alongside its sibling ``api_*`` side-cars without the + four module-scope ``add_routes`` defs colliding. See + ``runtime.api_session_full.add_session_full_routes``. + """ + + @api_v1.get("/config/ui-hints") + async def get_ui_hints(request: Request) -> dict[str, Any]: + cfg = request.app.state.cfg + ui = cfg.ui + return { + "brand_name": ui.brand_name, + "brand_logo_url": ui.brand_logo_url, + "approval_rationale_templates": list(ui.approval_rationale_templates), + "hitl_question_templates": dict(ui.hitl_question_templates), + "environments": list(cfg.environments or []), + } + +# ====== module: runtime/api_apps_overlay.py ====== + +def add_apps_overlay_routes(api_v1: APIRouter) -> None: + """Mount the /apps/{app}/ui-views handler on the api_v1 router. + + Module-qualified name so the bundler can flatten alongside sibling + ``api_*`` side-cars without ``add_routes`` collisions. See + ``runtime.api_session_full.add_session_full_routes``. + """ + + @api_v1.get("/apps/{app_name}/ui-views") + async def list_app_views(app_name: str, request: Request) -> list[dict]: + # app_name is informational for now; v2.0 has one app per deploy. + cfg = request.app.state.cfg + return [v.model_dump() for v in cfg.ui.app_views] + +# ====== module: runtime/api_recent_events.py ====== + +_SSE_MEDIA_TYPE = "text/event-stream" +_SESSION_KINDS = frozenset({ + "session.created", + "session.status_changed", + "session.agent_running", +}) + + +def add_recent_events_routes(api_v1: APIRouter) -> None: + """Mount the /sessions/recent/events SSE handler on the api_v1 router. + + Module-qualified name so the bundler can flatten alongside sibling + ``api_*`` side-cars without ``add_routes`` collisions. See + ``runtime.api_session_full.add_session_full_routes``. + """ + + @api_v1.get("/sessions/recent/events") + async def stream_recent_events(request: Request, since: int = 0): + orch = request.app.state.orchestrator + event_log = getattr(orch, "event_log", None) + if event_log is None: + raise HTTPException( + status_code=503, detail="event_log not configured", + ) + + async def _stream() -> AsyncIterator[str]: + last_seq = since + # Backlog: emit session-level events past `since` + for ev in event_log.iter_recent(since=last_seq): + if ev.kind in _SESSION_KINDS: + payload = {"seq": ev.seq, "kind": ev.kind, + "session_id": ev.session_id, + "payload": ev.payload, "ts": ev.ts} + last_seq = ev.seq + yield f"data: {json.dumps(payload)}\n\n" + # Tail: poll for new rows; exit on client disconnect + while not await request.is_disconnected(): + await _asyncio.sleep(0.5) + for ev in event_log.iter_recent(since=last_seq): + if ev.kind in _SESSION_KINDS: + payload = {"seq": ev.seq, "kind": ev.kind, + "session_id": ev.session_id, + "payload": ev.payload, "ts": ev.ts} + last_seq = ev.seq + yield f"data: {json.dumps(payload)}\n\n" + + return StreamingResponse(_stream(), media_type=_SSE_MEDIA_TYPE) + +# ====== module: runtime/api_static.py ====== + +_BUILD_HINT = """ + +

React UI not built yet

+

Run cd web && npm ci && npm run build +to populate web/dist/.

+

Then refresh.

+ +""" + +_NOT_FOUND_JSON = ( + '{"error":{"code":"not_found","message":"unknown api path","details":{}}}' +) + + +def mount_static_assets(app: FastAPI) -> None: + """Mount static assets + SPA fallback. API routes must be registered first. + + Module-qualified name (vs. the bare ``mount`` we had pre-bundle-fix) so + the bundler can flatten this alongside its sibling ``api_*`` side-cars + without stepping on FastAPI's ``app.mount`` or any future bundled module + that happens to define a ``mount`` symbol. See + ``runtime.api_session_full.add_session_full_routes``. + """ + web_dist_path = os.environ.get("ASR_WEB_DIST") + if web_dist_path: + web_dist = Path(web_dist_path) + else: + # Default: web/dist relative to repo root. + web_dist = ( + Path(__file__).resolve().parent.parent.parent / "web" / "dist" + ) + + if not (web_dist / "index.html").exists(): + # Stub fallback when the bundle isn't built — useful in dev. + @app.get("/", include_in_schema=False) + async def _missing_root() -> HTMLResponse: + return HTMLResponse(content=_BUILD_HINT, status_code=503) + return + + # Serve assets at /assets/* (Vite output) + assets_dir = web_dist / "assets" + if assets_dir.exists(): + app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") + # Serve fonts at /fonts/* (vendored) + fonts_dir = web_dist / "fonts" + if fonts_dir.exists(): + app.mount("/fonts", StaticFiles(directory=fonts_dir), name="fonts") + + # SPA fallback: anything not matched by API or other static mounts → index.html + @app.get("/{full_path:path}", include_in_schema=False) + async def _spa_fallback(full_path: str, request: Request) -> Response: + # Reserve API/health/docs paths + if (full_path.startswith("api/") or full_path == "health" + or full_path.startswith("docs") or full_path == "openapi.json"): + return Response( + content=_NOT_FOUND_JSON, + status_code=404, + media_type="application/json", + ) + return FileResponse(web_dist / "index.html") + # ====== module: examples/incident_management/state.py ====== class IncidentStateOverrides(BaseModel): diff --git a/scripts/build_single_file.py b/scripts/build_single_file.py index 1d60b8f..d741bfc 100644 --- a/scripts/build_single_file.py +++ b/scripts/build_single_file.py @@ -187,6 +187,29 @@ # api.py. Bundled after api.py so register_dedup_routes can be # invoked against the FastAPI app at the bottom of the bundle. (RUNTIME_ROOT, "api_dedup.py"), + # Bootstrap bundle endpoint — single round-trip the React UI hits + # on session open. Side-car module mounted on api_v1 inside + # ``api.build_app``; bundled after api.py for the same reason as + # api_dedup.py. + (RUNTIME_ROOT, "api_session_full.py"), + # UI hints endpoint — read once at React boot for the topbar brand + # block, env switcher list, and approval-rationale dropdown. Same + # side-car pattern as api_session_full.py. + (RUNTIME_ROOT, "api_ui_hints.py"), + # App-overlay UI views endpoint — Approach C extensibility surface + # the framework UI's Selected-detail panel queries to render + # "App-specific views →" links. Same side-car pattern. + (RUNTIME_ROOT, "api_apps_overlay.py"), + # Cross-session SSE endpoint — drives the React UI's "Other + # Sessions" monitor with session-level lifecycle events + # (session.created / session.status_changed / session.agent_running) + # across ALL sessions, ordered by global seq. Same side-car pattern. + (RUNTIME_ROOT, "api_recent_events.py"), + # React SPA static-file mount + SPA fallback. Mounts onto the + # FastAPI root app (not the api_v1 router) via ``api_static.mount``, + # which build_app invokes as the LAST route-registration step. + # Bundled after api.py for the same reason as the side-cars above. + (RUNTIME_ROOT, "api_static.py"), ] # Example app modules — flattened *after* the runtime modules in the diff --git a/src/runtime/api.py b/src/runtime/api.py index f47ed77..d5ecae4 100644 --- a/src/runtime/api.py +++ b/src/runtime/api.py @@ -28,12 +28,17 @@ from pathlib import Path from typing import AsyncIterator, Literal -from fastapi import FastAPI, HTTPException, Request, Response, WebSocket, WebSocketDisconnect +from fastapi import APIRouter, FastAPI, HTTPException, Request, Response, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse +from fastapi.responses import JSONResponse, RedirectResponse, StreamingResponse from pydantic import BaseModel, Field from starlette.exceptions import HTTPException as StarletteHTTPException +from runtime.api_apps_overlay import add_apps_overlay_routes +from runtime.api_recent_events import add_recent_events_routes +from runtime.api_session_full import add_session_full_routes +from runtime.api_static import mount_static_assets +from runtime.api_ui_hints import add_ui_hints_routes from runtime.config import AppConfig, load_config _log = logging.getLogger("runtime.api") @@ -258,6 +263,9 @@ async def _lifespan(app: FastAPI) -> AsyncIterator[None]: orch = svc.submit_and_wait(svc._ensure_orchestrator(), timeout=30.0) app.state.service = svc app.state.orchestrator = orch + # Surface the validated AppConfig so app.state.cfg-readers + # (e.g. /api/v1/config/ui-hints) don't have to re-load YAML. + app.state.cfg = cfg # Environments roster is app-specific (incident-management has # production/staging/dev/local; code-review doesn't expose one). # Read it from the YAML's top-level ``environments:`` block; @@ -345,16 +353,28 @@ def build_app(cfg: AppConfig) -> FastAPI: title="ASR — Agent Orchestrator", lifespan=_make_lifespan(cfg), ) - - # CORS: configure once with the AppConfig-supplied origins so the - # React dev server (Vite at :5173, CRA/Next at :3000 by default) can - # call every endpoint, SSE included. Production deployments lock - # the origin list down via YAML — same shape, narrower allow-list. + # All framework routes (except /health) live under /api/v1 so the + # React client can stably target a versioned surface; /health stays + # at root for monitor / load-balancer health-check conventions. + api_v1 = APIRouter(prefix="/api/v1") + + # CORS: env-driven so the React dev server (Vite at :5173) can call + # every endpoint, SSE included. Override via ``ASR_CORS_ORIGINS`` + # (comma-separated) — production deployments lock the origin list + # down by setting the env var to the narrower allow-list. + # ``allow_credentials=False`` matches the bearer-token auth pattern + # (no cookies); methods are explicit so OPTIONS preflights are + # handled the same way for every route. + _cors_origins_raw = os.environ.get( + "ASR_CORS_ORIGINS", + "http://localhost:5173,http://127.0.0.1:5173", # Vite dev defaults + ) + _cors_origins = [o.strip() for o in _cors_origins_raw.split(",") if o.strip()] fastapi_app.add_middleware( CORSMiddleware, - allow_origins=cfg.api.cors_origins, - allow_credentials=cfg.api.cors_allow_credentials, - allow_methods=["*"], + allow_origins=_cors_origins, + allow_credentials=False, + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], ) @@ -389,27 +409,27 @@ async def _http_exception_handler( async def health(): return {"status": "ok"} - @fastapi_app.get("/agents") + @api_v1.get("/agents") async def agents(): return fastapi_app.state.orchestrator.list_agents() - @fastapi_app.get("/tools") + @api_v1.get("/tools") async def tools(): return fastapi_app.state.orchestrator.list_tools() - @fastapi_app.get("/incidents") + @api_v1.get("/incidents") async def incidents(limit: int = 20): return fastapi_app.state.orchestrator.list_recent_incidents(limit=limit) - @fastapi_app.get("/incidents/{incident_id}") + @api_v1.get("/incidents/{incident_id}") async def incident(incident_id: str): return fastapi_app.state.orchestrator.get_incident(incident_id) - @fastapi_app.delete("/incidents/{incident_id}") + @api_v1.delete("/incidents/{incident_id}") async def delete_incident(incident_id: str): return fastapi_app.state.orchestrator.delete_incident(incident_id) - @fastapi_app.post("/investigate") + @api_v1.post("/investigate") async def investigate(req: InvestigateRequest, request: Request) -> InvestigateResponse: """Legacy alias for ``POST /sessions`` — kept for back-compat. @@ -443,11 +463,11 @@ async def investigate(req: InvestigateRequest, request: Request) -> InvestigateR raise return InvestigateResponse(incident_id=sid) - @fastapi_app.get("/environments") + @api_v1.get("/environments") async def environments(): return fastapi_app.state.environments - @fastapi_app.post("/investigate/stream") + @api_v1.post("/investigate/stream") async def investigate_stream(req: InvestigateRequest) -> StreamingResponse: orch = fastapi_app.state.orchestrator @@ -460,7 +480,7 @@ async def _events(): return StreamingResponse(_events(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.post("/incidents/{incident_id}/resume") + @api_v1.post("/incidents/{incident_id}/resume") async def resume_incident(incident_id: str, req: ResumeRequest) -> StreamingResponse: orch = fastapi_app.state.orchestrator decision: dict = {"action": req.decision} @@ -492,7 +512,7 @@ async def _events(): # Multi-session endpoints # ------------------------------------------------------------------ - @fastapi_app.post( + @api_v1.post( "/sessions", status_code=201, ) @@ -523,7 +543,7 @@ class is matched by name so this handler does not depend on a raise return SessionStartResponse(session_id=sid) - @fastapi_app.get("/sessions") + @api_v1.get("/sessions") async def list_sessions_endpoint(request: Request) -> list[SessionStatus]: """Snapshot of in-flight sessions (running / awaiting_input / error).""" svc = request.app.state.service @@ -533,7 +553,7 @@ async def list_sessions_endpoint(request: Request) -> list[SessionStatus]: # HITL approval endpoints (risk-rated tool gateway) # ------------------------------------------------------------------ - @fastapi_app.get("/sessions/{session_id}/approvals") + @api_v1.get("/sessions/{session_id}/approvals") async def list_pending_approvals( session_id: str, request: Request ) -> list[PendingApproval]: @@ -575,7 +595,7 @@ async def list_pending_approvals( )) return out - @fastapi_app.post( + @api_v1.post( "/sessions/{session_id}/approvals/{tool_call_id}", status_code=200, ) @@ -661,7 +681,7 @@ async def _resume() -> None: "rationale": body.rationale, } - @fastapi_app.delete("/sessions/{session_id}", status_code=204) + @api_v1.delete("/sessions/{session_id}", status_code=204) async def stop_session_endpoint( session_id: str, request: Request ) -> Response: @@ -692,7 +712,7 @@ async def stop_session_endpoint( # T2: generic /sessions/* endpoints (React-ready, non-legacy). # ================================================================== - @fastapi_app.get("/sessions/recent") + @api_v1.get("/sessions/recent") async def recent_sessions(request: Request, limit: int = 20) -> list[dict]: """List recent sessions of ANY status — closed + active. @@ -702,7 +722,7 @@ async def recent_sessions(request: Request, limit: int = 20) -> list[dict]: orch = request.app.state.orchestrator return orch.list_recent_sessions(limit=limit) - @fastapi_app.get("/sessions/{session_id}") + @api_v1.get("/sessions/{session_id}") async def get_session_detail(session_id: str, request: Request) -> dict: """Full session detail. Generic equivalent of the legacy domain-flavoured detail route. 404 when the id is unknown.""" @@ -714,7 +734,7 @@ async def get_session_detail(session_id: str, request: Request) -> dict: status_code=404, detail=_SESSION_NOT_FOUND_DETAIL, ) from e - @fastapi_app.post("/sessions/{session_id}/resume") + @api_v1.post("/sessions/{session_id}/resume") async def resume_session_sse( session_id: str, req: ResumeRequest, request: Request, ) -> StreamingResponse: @@ -748,7 +768,7 @@ async def _events(): return StreamingResponse(_events(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.post("/sessions/{session_id}/retry") + @api_v1.post("/sessions/{session_id}/retry") async def retry_session_sse( session_id: str, request: Request, ) -> StreamingResponse: @@ -771,7 +791,7 @@ async def _events(): return StreamingResponse(_events(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.get("/sessions/{session_id}/retry/preview") + @api_v1.get("/sessions/{session_id}/retry/preview") async def preview_retry( session_id: str, request: Request, ) -> RetryDecisionPreview: @@ -790,7 +810,7 @@ async def preview_retry( reason=str(decision.reason), ) - @fastapi_app.get("/sessions/{session_id}/lessons") + @api_v1.get("/sessions/{session_id}/lessons") async def list_session_lessons( session_id: str, request: Request, ) -> list[LessonResponse]: @@ -835,7 +855,7 @@ async def list_session_lessons( # T3: SSE event stream + T4: WebSocket fallback. # ================================================================== - @fastapi_app.get("/sessions/{session_id}/events") + @api_v1.get("/sessions/{session_id}/events") async def sse_events( session_id: str, request: Request, since: int = 0, ) -> StreamingResponse: @@ -889,7 +909,7 @@ async def _stream(): return StreamingResponse(_stream(), media_type=_SSE_MEDIA_TYPE) - @fastapi_app.websocket("/ws/sessions/{session_id}/events") + @api_v1.websocket("/ws/sessions/{session_id}/events") async def ws_events(websocket: WebSocket, session_id: str) -> None: """WebSocket fallback for the SSE event stream. Same payload shape (:class:`EventEnvelope`); clients that prefer WS over @@ -936,6 +956,81 @@ async def ws_events(websocket: WebSocket, session_id: str) -> None: except Exception: # noqa: BLE001 pass + # ================================================================== + # Bootstrap bundle: GET /api/v1/sessions/{id}/full + # Single round-trip the React UI calls on session open. Module + # lives next door so this file stays focused on routing wiring. + # ================================================================== + add_session_full_routes(api_v1) + + # ================================================================== + # UI hints: GET /api/v1/config/ui-hints + # Drives the React shell's brand block, environment switcher list, + # and approval-rationale dropdown. Read once at app boot. + # ================================================================== + add_ui_hints_routes(api_v1) + + # ================================================================== + # App-overlay UI views: GET /api/v1/apps/{app}/ui-views + # Approach C extensibility — apps register bespoke deep-dive pages + # (e.g. "Deploy diff") that the framework UI's Selected-detail + # panel lists as "App-specific views →" links. + # ================================================================== + add_apps_overlay_routes(api_v1) + + # ================================================================== + # Cross-session SSE: GET /api/v1/sessions/recent/events + # Drives the React UI's "Other Sessions" monitor — session.created / + # session.status_changed / session.agent_running events across ALL + # sessions, ordered by global seq. + # ================================================================== + add_recent_events_routes(api_v1) + + # Legacy /incidents/* and /investigate redirects to /api/v1/* equivalents. + # 308 preserves method + body so legacy POSTs (e.g. /incidents/{id}/resume) + # keep working transparently. Removed in v2.1. + @fastapi_app.api_route( + "/incidents", methods=["GET", "POST"], include_in_schema=False, + ) + async def _legacy_incidents_collection() -> RedirectResponse: + return RedirectResponse(url="/api/v1/sessions", status_code=308) + + @fastapi_app.api_route( + "/incidents/{path:path}", + methods=["GET", "POST", "DELETE", "PUT"], + include_in_schema=False, + ) + async def _legacy_incidents_detail(path: str) -> RedirectResponse: + return RedirectResponse(url=f"/api/v1/sessions/{path}", status_code=308) + + @fastapi_app.api_route( + "/investigate", methods=["POST"], include_in_schema=False, + ) + async def _legacy_investigate() -> RedirectResponse: + return RedirectResponse(url="/api/v1/investigate", status_code=308) + + @fastapi_app.api_route( + "/investigate/{path:path}", + methods=["POST"], + include_in_schema=False, + ) + async def _legacy_investigate_subpath(path: str) -> RedirectResponse: + return RedirectResponse( + url=f"/api/v1/investigate/{path}", status_code=308, + ) + + # Mount the versioned router. /health stays at root (registered + # directly on ``fastapi_app`` above); everything else lives under + # /api/v1. + fastapi_app.include_router(api_v1) + # ================================================================== + # React UI bundle: StaticFiles mount at / + SPA fallback. + # MUST be the last route-registration step in build_app — the + # catch-all ``GET /{full_path:path}`` would otherwise shadow every + # API route and legacy redirect. The fallback excludes /api/, /health, + # and /docs so unknown API paths still return structured JSON 404s. + # ================================================================== + mount_static_assets(fastapi_app) return fastapi_app diff --git a/src/runtime/api_apps_overlay.py b/src/runtime/api_apps_overlay.py new file mode 100644 index 0000000..8fddddf --- /dev/null +++ b/src/runtime/api_apps_overlay.py @@ -0,0 +1,29 @@ +"""App-overlay UI views discovery — Approach C extensibility. + +GET /api/v1/apps/{app}/ui-views returns the app-registered overlay views. +The framework UI's Selected-detail panel renders matching views as +"App-specific views →" links. v2.0 ships with one app per deployment; +multi-app per-app filtering is v2.1 scope (the path's app_name is +currently informational). + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.cfg``); not suitable for lightweight test fixtures that +construct a bare ``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" +from __future__ import annotations +from fastapi import APIRouter, Request + + +def add_apps_overlay_routes(api_v1: APIRouter) -> None: + """Mount the /apps/{app}/ui-views handler on the api_v1 router. + + Module-qualified name so the bundler can flatten alongside sibling + ``api_*`` side-cars without ``add_routes`` collisions. See + ``runtime.api_session_full.add_session_full_routes``. + """ + + @api_v1.get("/apps/{app_name}/ui-views") + async def list_app_views(app_name: str, request: Request) -> list[dict]: + # app_name is informational for now; v2.0 has one app per deploy. + cfg = request.app.state.cfg + return [v.model_dump() for v in cfg.ui.app_views] diff --git a/src/runtime/api_recent_events.py b/src/runtime/api_recent_events.py new file mode 100644 index 0000000..7432c28 --- /dev/null +++ b/src/runtime/api_recent_events.py @@ -0,0 +1,64 @@ +"""Cross-session SSE — used by the React UI's Other Sessions monitor. + +Emits session-level events (not per-session detail events): session.created, +session.status_changed, session.agent_running. Lower frequency than the +per-session stream. + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.orchestrator``); not suitable for lightweight test fixtures +that construct a bare ``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" +from __future__ import annotations +import asyncio as _asyncio +import json +from typing import AsyncIterator +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import StreamingResponse + +_SSE_MEDIA_TYPE = "text/event-stream" +_SESSION_KINDS = frozenset({ + "session.created", + "session.status_changed", + "session.agent_running", +}) + + +def add_recent_events_routes(api_v1: APIRouter) -> None: + """Mount the /sessions/recent/events SSE handler on the api_v1 router. + + Module-qualified name so the bundler can flatten alongside sibling + ``api_*`` side-cars without ``add_routes`` collisions. See + ``runtime.api_session_full.add_session_full_routes``. + """ + + @api_v1.get("/sessions/recent/events") + async def stream_recent_events(request: Request, since: int = 0): + orch = request.app.state.orchestrator + event_log = getattr(orch, "event_log", None) + if event_log is None: + raise HTTPException( + status_code=503, detail="event_log not configured", + ) + + async def _stream() -> AsyncIterator[str]: + last_seq = since + # Backlog: emit session-level events past `since` + for ev in event_log.iter_recent(since=last_seq): + if ev.kind in _SESSION_KINDS: + payload = {"seq": ev.seq, "kind": ev.kind, + "session_id": ev.session_id, + "payload": ev.payload, "ts": ev.ts} + last_seq = ev.seq + yield f"data: {json.dumps(payload)}\n\n" + # Tail: poll for new rows; exit on client disconnect + while not await request.is_disconnected(): + await _asyncio.sleep(0.5) + for ev in event_log.iter_recent(since=last_seq): + if ev.kind in _SESSION_KINDS: + payload = {"seq": ev.seq, "kind": ev.kind, + "session_id": ev.session_id, + "payload": ev.payload, "ts": ev.ts} + last_seq = ev.seq + yield f"data: {json.dumps(payload)}\n\n" + + return StreamingResponse(_stream(), media_type=_SSE_MEDIA_TYPE) diff --git a/src/runtime/api_session_full.py b/src/runtime/api_session_full.py new file mode 100644 index 0000000..466fb3c --- /dev/null +++ b/src/runtime/api_session_full.py @@ -0,0 +1,87 @@ +"""Bootstrap endpoint for the React UI's single view-model. + +GET /api/v1/sessions/{id}/full returns everything the UI needs to render +the session in one round-trip — replaces the old pattern of multiple GETs. +The same shape is then patched in place by SSE delta events. + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.orchestrator``); unlike :mod:`runtime.api_dedup`, this module +is NOT suitable for lightweight test fixtures that construct a bare +``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, HTTPException, Request + + +def add_session_full_routes(api_v1: APIRouter) -> None: + """Mount the /sessions/{id}/full handler on the api_v1 router. + + The function name is intentionally module-qualified (rather than the + bare ``add_routes`` we used pre-bundle-fix) so that ``scripts/build_single_file.py`` + can flatten this module alongside its sibling ``api_*`` side-cars without + the four ``add_routes`` defs colliding at module scope. Source-side callers + import the symbol directly: ``from runtime.api_session_full import add_session_full_routes``. + """ + + @api_v1.get("/sessions/{session_id}/full") + async def get_session_full( + session_id: str, request: Request, + ) -> dict[str, Any]: + orch = request.app.state.orchestrator + try: + inc = orch.store.load(session_id) + except (FileNotFoundError, ValueError, KeyError, LookupError) as e: + # ``ValueError`` covers the SessionStore id-format guard + # (``Invalid session id ...``); semantically a 404 at the + # API boundary — same convention as other /sessions/* GETs. + raise HTTPException( + status_code=404, detail="session not found", + ) from e + + # Replay the EventLog backlog. ``vm_seq`` is the high-water mark + # the UI uses to ?since=N when it later opens the SSE stream, so + # delta events stitch onto the same view-model without gap or + # overlap. + event_log = getattr(orch, "event_log", None) + events: list[dict[str, Any]] = [] + vm_seq = 0 + if event_log is not None: + for ev in event_log.iter_for(session_id, since=0): + events.append({ + "seq": ev.seq, + "kind": ev.kind, + "payload": ev.payload, + "ts": ev.ts, + }) + if ev.seq > vm_seq: + vm_seq = ev.seq + + # Agent definitions: skill metadata the UI needs to render the + # graph diagram + per-agent header chips. ``orch.skills`` is a + # ``dict[str, Skill]`` keyed by name. ``Skill.tools`` is itself + # a ``dict[str, list[str]]`` (server -> tool list) — expose the + # server keys as the ref strings; ``Skill.routes`` is a + # ``list[RouteRule]`` (when/next/gate) — flatten to the + # signal->next mapping the UI consumes. + agent_definitions: dict[str, dict[str, Any]] = {} + for name, skill in orch.skills.items(): + agent_definitions[name] = { + "name": skill.name, + "kind": skill.kind, + "model": skill.model or orch.cfg.llm.default, + "tools": list(skill.tools or {}), + "routes": {r.when: r.next for r in skill.routes}, + "system_prompt_excerpt": (skill.system_prompt or "")[:500], + } + + return { + "session": inc.model_dump(mode="json"), + "agents_run": [r.model_dump(mode="json") for r in inc.agents_run], + "tool_calls": [tc.model_dump(mode="json") for tc in inc.tool_calls], + "events": events, + "agent_definitions": agent_definitions, + "vm_seq": vm_seq, + } diff --git a/src/runtime/api_static.py b/src/runtime/api_static.py new file mode 100644 index 0000000..b25ed59 --- /dev/null +++ b/src/runtime/api_static.py @@ -0,0 +1,79 @@ +"""StaticFiles mount + SPA fallback for the React UI bundle. + +The React build output lives at $ASR_WEB_DIST (default: ../web/dist relative +to repo root). FastAPI serves /assets/* and /fonts/* directly; any unknown +path that isn't /api/v1/*, /health, or /docs falls back to index.html so +the React Router can pick up the URL. + +Registered only via :func:`runtime.api.build_app` (mounts onto the root +FastAPI app, not the api_v1 router). Must be invoked AFTER all API routes +are registered so the catch-all SPA fallback doesn't shadow them. +""" +from __future__ import annotations +import os +from pathlib import Path +from fastapi import FastAPI, Request +from fastapi.responses import FileResponse, HTMLResponse, Response +from fastapi.staticfiles import StaticFiles + + +_BUILD_HINT = """ + +

React UI not built yet

+

Run cd web && npm ci && npm run build +to populate web/dist/.

+

Then refresh.

+ +""" + +_NOT_FOUND_JSON = ( + '{"error":{"code":"not_found","message":"unknown api path","details":{}}}' +) + + +def mount_static_assets(app: FastAPI) -> None: + """Mount static assets + SPA fallback. API routes must be registered first. + + Module-qualified name (vs. the bare ``mount`` we had pre-bundle-fix) so + the bundler can flatten this alongside its sibling ``api_*`` side-cars + without stepping on FastAPI's ``app.mount`` or any future bundled module + that happens to define a ``mount`` symbol. See + ``runtime.api_session_full.add_session_full_routes``. + """ + web_dist_path = os.environ.get("ASR_WEB_DIST") + if web_dist_path: + web_dist = Path(web_dist_path) + else: + # Default: web/dist relative to repo root. + web_dist = ( + Path(__file__).resolve().parent.parent.parent / "web" / "dist" + ) + + if not (web_dist / "index.html").exists(): + # Stub fallback when the bundle isn't built — useful in dev. + @app.get("/", include_in_schema=False) + async def _missing_root() -> HTMLResponse: + return HTMLResponse(content=_BUILD_HINT, status_code=503) + return + + # Serve assets at /assets/* (Vite output) + assets_dir = web_dist / "assets" + if assets_dir.exists(): + app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") + # Serve fonts at /fonts/* (vendored) + fonts_dir = web_dist / "fonts" + if fonts_dir.exists(): + app.mount("/fonts", StaticFiles(directory=fonts_dir), name="fonts") + + # SPA fallback: anything not matched by API or other static mounts → index.html + @app.get("/{full_path:path}", include_in_schema=False) + async def _spa_fallback(full_path: str, request: Request) -> Response: + # Reserve API/health/docs paths + if (full_path.startswith("api/") or full_path == "health" + or full_path.startswith("docs") or full_path == "openapi.json"): + return Response( + content=_NOT_FOUND_JSON, + status_code=404, + media_type="application/json", + ) + return FileResponse(web_dist / "index.html") diff --git a/src/runtime/api_ui_hints.py b/src/runtime/api_ui_hints.py new file mode 100644 index 0000000..4b57ab5 --- /dev/null +++ b/src/runtime/api_ui_hints.py @@ -0,0 +1,35 @@ +"""UI hints endpoint — drives the React shell's brand and templates. + +GET /api/v1/config/ui-hints returns the runtime-configured ui block plus +the environments list. Read once at React-app boot and cached for the +session lifetime via `useUiHints()`. + +Registered only via :func:`runtime.api.build_app` (requires +``app.state.cfg``); not suitable for lightweight test fixtures that +construct a bare ``FastAPI()`` app — use ``build_app(cfg)`` for tests. +""" +from __future__ import annotations +from typing import Any +from fastapi import APIRouter, Request + + +def add_ui_hints_routes(api_v1: APIRouter) -> None: + """Mount the /config/ui-hints handler on the api_v1 router. + + Module-qualified name (vs. bare ``add_routes``) so the bundler can + flatten this alongside its sibling ``api_*`` side-cars without the + four module-scope ``add_routes`` defs colliding. See + ``runtime.api_session_full.add_session_full_routes``. + """ + + @api_v1.get("/config/ui-hints") + async def get_ui_hints(request: Request) -> dict[str, Any]: + cfg = request.app.state.cfg + ui = cfg.ui + return { + "brand_name": ui.brand_name, + "brand_logo_url": ui.brand_logo_url, + "approval_rationale_templates": list(ui.approval_rationale_templates), + "hitl_question_templates": dict(ui.hitl_question_templates), + "environments": list(cfg.environments or []), + } diff --git a/src/runtime/config.py b/src/runtime/config.py index e355448..dcebcc5 100644 --- a/src/runtime/config.py +++ b/src/runtime/config.py @@ -626,10 +626,26 @@ class UIDetailField(BaseModel): model_config = {"frozen": True, "extra": "forbid"} +class AppView(BaseModel): + """An app-overlay UI view registered by an app for the framework UI to link to. + + The framework UI lists matching app views in its Selected-detail panel + ("App-specific views →") so apps can ship bespoke deep-dive pages + without forking the framework. Approach C (per the v2.0 design spec). + """ + model_config = ConfigDict(extra="forbid") + + id: str + title: str + applies_to: str # "always" | "agent:" | "tool:" + url: str + + class UIConfig(BaseModel): """App-driven UI rendering knobs. Keeps the generic Streamlit shell in ``runtime/ui.py`` agnostic of any specific domain — colors, labels, - and tag prefixes come from YAML. + and tag prefixes come from YAML. Also drives the React UI (v2.0) shell + via ``GET /api/v1/config/ui-hints``. ``badges`` is a 2-level dict: ``{field_name: {value: UIBadge}}``. Example: ``{"status": {"open": {"label": "OPEN", "color": "red"}}}``. @@ -640,10 +656,26 @@ class UIConfig(BaseModel): ``tags`` is an opaque key->tag-string map the UI consults for cross-skill signals (e.g. ``prior_match_supported`` -> the literal tag a skill emits). + + React UI (v2.0) fields: ``brand_name``, ``brand_logo_url``, + ``approval_rationale_templates``, and ``hitl_question_templates`` + drive the React shell's topbar brand block, environment switcher, + and approval-rationale dropdown. Read at app boot via + ``useUiHints()`` and cached for the session lifetime. + + ``app_views`` is the Approach C extensibility surface — apps register + bespoke UI overlay views that the framework UI's Selected-detail + panel lists as "App-specific views →" links. Served via + ``GET /api/v1/apps/{app}/ui-views``. """ badges: dict[str, dict[str, UIBadge]] = Field(default_factory=dict) detail_fields: list[UIDetailField] = Field(default_factory=list) tags: dict[str, str] = Field(default_factory=dict) + brand_name: str = "" + brand_logo_url: str | None = None + approval_rationale_templates: list[str] = Field(default_factory=list) + hitl_question_templates: dict[str, str] = Field(default_factory=dict) + app_views: list[AppView] = Field(default_factory=list) model_config = {"frozen": True, "extra": "forbid"} diff --git a/src/runtime/orchestrator.py b/src/runtime/orchestrator.py index abff038..1fac9bd 100644 --- a/src/runtime/orchestrator.py +++ b/src/runtime/orchestrator.py @@ -276,6 +276,21 @@ def _emit_status_changed_event( _log.debug( "event_log.record(status_changed) failed", exc_info=True, ) + # Mirror onto the cross-session SSE stream for the React UI's + # Other Sessions monitor (see api_recent_events.py). + # ``session_id`` already lands on the row; the payload carries + # the new ``status`` only. + try: + event_log.record( + inc.id, + "session.status_changed", + status=to_status, + ) + except Exception: # noqa: BLE001 — telemetry must not break finalize + _log.debug( + "event_log.record(session.status_changed) failed", + exc_info=True, + ) # M5 hook point: when ``to_status`` is terminal per app config, # invoke the lesson extractor. M4 leaves it as a no-op; M5 swaps @@ -1227,6 +1242,19 @@ async def start_session(self, *, query: str, env = (state_overrides or {}).get("environment", "") inc = self.store.create(query=query, environment=env, reporter_id=sub_id, reporter_team=sub_team) + # Emit session.created on the cross-session SSE stream so the + # React UI's Other Sessions monitor lights up the new tile in + # real time. ``session_id`` already lands on the row; the + # payload carries no extra fields. Telemetry must not break + # the start path. + event_log = getattr(self, "event_log", None) + if event_log is not None: + try: + event_log.record(inc.id, "session.created") + except Exception: # noqa: BLE001 — telemetry must not break start + _log.debug( + "event_log.record(session.created) failed", exc_info=True, + ) if trigger is not None: inc.findings["trigger"] = { "name": trigger.name, diff --git a/src/runtime/service.py b/src/runtime/service.py index c75b5e6..f52ef6b 100644 --- a/src/runtime/service.py +++ b/src/runtime/service.py @@ -496,6 +496,20 @@ async def _scheduler() -> str: reporter_team=sub_team, ) session_id = inc.id + # Emit session.created on the cross-session SSE stream so + # the React UI's Other Sessions monitor lights up the new + # tile in real time. Telemetry must not break start. + event_log = getattr(orch, "event_log", None) + if event_log is not None: + try: + # ``session_id`` already lands on the row; the payload + # carries no extra fields for ``session.created``. + event_log.record(session_id, "session.created") + except Exception: # noqa: BLE001 — telemetry must not break start + _log.debug( + "event_log.record(session.created) failed", + exc_info=True, + ) # Stamp trigger provenance onto the row before the graph # runs so any crash mid-graph still leaves an audit trail. # ``inc.findings`` is a JSON dict on the row. diff --git a/src/runtime/storage/event_log.py b/src/runtime/storage/event_log.py index 746f96a..f9ce541 100644 --- a/src/runtime/storage/event_log.py +++ b/src/runtime/storage/event_log.py @@ -30,6 +30,12 @@ "gate_fired", "status_changed", "lesson_extracted", + # Session-level lifecycle events — emitted on the cross-session SSE + # stream (/api/v1/sessions/recent/events) for the React UI's "Other + # Sessions" monitor panel. Lower frequency than per-step kinds. + "session.created", + "session.status_changed", + "session.agent_running", ] _VALID_EVENT_KINDS: frozenset[str] = frozenset(get_args(EventKind)) @@ -119,3 +125,29 @@ def iter_for( payload=row.payload, ts=row.ts, ) + + def iter_recent(self, since: int = 0) -> Iterator[SessionEvent]: + """Iterate events across ALL sessions where seq > since, ordered + by global seq. + + Used by the cross-session SSE stream + (``/api/v1/sessions/recent/events``) to deliver session.* + lifecycle events to the React UI's Other Sessions monitor panel. + Capped at 500 rows per call so a long-disconnected client can't + blow memory replaying years of history. + """ + with Session(self.engine) as s: + stmt = ( + select(SessionEventRow) + .where(SessionEventRow.seq > since) + .order_by(SessionEventRow.seq.asc()) + .limit(500) + ) + for row in s.execute(stmt).scalars(): + yield SessionEvent( + seq=row.seq, + session_id=row.session_id, + kind=row.kind, + payload=row.payload, + ts=row.ts, + ) diff --git a/tests/test_api.py b/tests/test_api.py index 7ea401b..46dce05 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -80,7 +80,7 @@ async def test_health_returns_200(cfg): async def test_agents_endpoint_returns_4(cfg): app = build_app(cfg) async with _client_with_lifespan(app) as client: - res = await client.get("/agents") + res = await client.get("/api/v1/agents") assert res.status_code == 200 names = {a["name"] for a in res.json()} assert names == {"intake", "triage", "deep_investigator", "resolution"} @@ -91,7 +91,7 @@ async def test_investigate_endpoint_creates_incident(cfg): app = build_app(cfg) async with _client_with_lifespan(app) as client: res = await client.post( - "/investigate", + "/api/v1/investigate", json={"query": "api latency", "environment": "production"}, ) assert res.status_code == 200 @@ -107,7 +107,7 @@ async def test_investigate_endpoint_creates_incident(cfg): async def test_environments_endpoint_returns_list(cfg): app = build_app(cfg) async with _client_with_lifespan(app) as client: - res = await client.get("/environments") + res = await client.get("/api/v1/environments") assert res.status_code == 200 envs = res.json() assert isinstance(envs, list) @@ -120,7 +120,7 @@ async def test_investigate_stream_emits_events(cfg): app = build_app(cfg) async with _client_with_lifespan(app) as client: async with client.stream( - "POST", "/investigate/stream", + "POST", "/api/v1/investigate/stream", json={"query": "api latency", "environment": "production"}, ) as res: assert res.status_code == 200 @@ -138,19 +138,19 @@ async def test_delete_endpoint_soft_deletes_incident(cfg): app = build_app(cfg) async with _client_with_lifespan(app) as client: created = await client.post( - "/investigate", + "/api/v1/investigate", json={"query": "to be deleted", "environment": "production"}, ) inc_id = created.json()["incident_id"] - del_res = await client.delete(f"/incidents/{inc_id}") + del_res = await client.delete(f"/api/v1/incidents/{inc_id}") assert del_res.status_code == 200 body = del_res.json() assert body["status"] == "deleted" assert body["deleted_at"] is not None - listing = await client.get("/incidents") + listing = await client.get("/api/v1/incidents") assert all(i["id"] != inc_id for i in listing.json()), \ "deleted incident must be hidden from /incidents" - single = await client.get(f"/incidents/{inc_id}") + single = await client.get(f"/api/v1/incidents/{inc_id}") assert single.status_code == 200 assert single.json()["status"] == "deleted" @@ -162,7 +162,7 @@ async def test_resume_endpoint_streams_error_for_unknown_incident(cfg): app = build_app(cfg) async with _client_with_lifespan(app) as client: async with client.stream( - "POST", "/incidents/INC-does-not-exist/resume", + "POST", "/api/v1/sessions/INC-does-not-exist/resume", json={"decision": "stop"}, ) as res: assert res.status_code == 200 @@ -189,7 +189,7 @@ async def test_post_sessions_returns_201_with_session_id(cfg): app = build_app(cfg) async with _client_with_lifespan(app) as client: res = await client.post( - "/sessions", + "/api/v1/sessions", json={ "query": "api latency", "environment": "production", @@ -211,7 +211,7 @@ async def test_post_sessions_omitting_submitter_uses_defaults(cfg): app = build_app(cfg) async with _client_with_lifespan(app) as client: res = await client.post( - "/sessions", + "/api/v1/sessions", json={"query": "no submitter", "environment": "dev"}, ) assert res.status_code == 201 @@ -224,7 +224,7 @@ async def test_get_sessions_returns_list(cfg): the ``SessionStatus`` schema for any active rows.""" app = build_app(cfg) async with _client_with_lifespan(app) as client: - res = await client.get("/sessions") + res = await client.get("/api/v1/sessions") assert res.status_code == 200 body = res.json() assert isinstance(body, list) @@ -241,7 +241,7 @@ async def test_delete_session_endpoint_returns_204_or_501(cfg): app = build_app(cfg) async with _client_with_lifespan(app) as client: start = await client.post( - "/sessions", + "/api/v1/sessions", json={ "query": "to be stopped", "environment": "production", @@ -250,7 +250,7 @@ async def test_delete_session_endpoint_returns_204_or_501(cfg): ) assert start.status_code == 201 sid = start.json()["session_id"] - res = await client.delete(f"/sessions/{sid}") + res = await client.delete(f"/api/v1/sessions/{sid}") # 204 = stopped cleanly; 501 = stop_session not wired up; # 404 = session already evicted. assert res.status_code in (204, 404, 501) @@ -264,7 +264,7 @@ async def test_legacy_investigate_route_still_works(cfg): app = build_app(cfg) async with _client_with_lifespan(app) as client: res = await client.post( - "/investigate", + "/api/v1/investigate", json={"query": "back-compat", "environment": "production"}, ) assert res.status_code in (200, 201) @@ -286,7 +286,7 @@ async def test_legacy_investigate_route_emits_no_deprecation_warnings(cfg): with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always", DeprecationWarning) res = await client.post( - "/investigate", + "/api/v1/investigate", json={ "query": "no-warn", "environment": "production", @@ -317,8 +317,8 @@ async def test_app_state_exposes_service_and_orchestrator(cfg): app = build_app(cfg) async with _client_with_lifespan(app) as client: # Both legacy and new routes work in the same lifespan window. - legacy = await client.get("/agents") - new = await client.get("/sessions") + legacy = await client.get("/api/v1/agents") + new = await client.get("/api/v1/sessions") assert legacy.status_code == 200 assert new.status_code == 200 assert hasattr(app.state, "service") diff --git a/tests/test_api_apps_overlay.py b/tests/test_api_apps_overlay.py new file mode 100644 index 0000000..ef310aa --- /dev/null +++ b/tests/test_api_apps_overlay.py @@ -0,0 +1,56 @@ +"""GET /api/v1/apps/{app}/ui-views returns app-specific UI overlay links. + +Apps register their bespoke pages via cfg.ui.app_views. The framework UI +lists these in the Selected-detail panel ("App-specific views →"). +""" +from fastapi.testclient import TestClient +from runtime.api import build_app +from runtime.config import UIConfig, AppView +from tests.test_api_v1_url_move import _cfg + + +def test_app_views_returns_empty_list_when_unconfigured(tmp_path): + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + r = client.get("/api/v1/apps/incident_management/ui-views") + assert r.status_code == 200 + assert r.json() == [] + + +def test_app_views_returns_configured_views(tmp_path): + cfg = _cfg(tmp_path) + cfg.ui = UIConfig( + app_views=[ + AppView(id="deploy-diff", title="Deploy diff", + applies_to="agent:investigate", + url="/apps/incident_management/ui/deploy-diff"), + AppView(id="topology", title="Service topology", + applies_to="always", + url="/apps/incident_management/ui/topology"), + ], + ) + app = build_app(cfg) + with TestClient(app) as client: + r = client.get("/api/v1/apps/incident_management/ui-views") + body = r.json() + assert len(body) == 2 + assert body[0]["id"] == "deploy-diff" + assert body[0]["applies_to"] == "agent:investigate" + assert body[1]["id"] == "topology" + + +def test_app_views_arbitrary_app_name_returns_same_list(tmp_path): + """v2.0: one app per deployment, app_name is informational only. + Multi-app routing is v2.1 scope.""" + cfg = _cfg(tmp_path) + cfg.ui = UIConfig( + app_views=[ + AppView(id="x", title="X", applies_to="always", url="/apps/x/ui/x"), + ], + ) + app = build_app(cfg) + with TestClient(app) as client: + r1 = client.get("/api/v1/apps/incident_management/ui-views") + r2 = client.get("/api/v1/apps/code_review/ui-views") + # Both return the same list — no per-app filtering yet. + assert r1.json() == r2.json() diff --git a/tests/test_api_react_surface.py b/tests/test_api_react_surface.py index 38f4f00..d367b3d 100644 --- a/tests/test_api_react_surface.py +++ b/tests/test_api_react_surface.py @@ -128,7 +128,7 @@ async def test_get_sessions_recent_returns_list(cfg): async with _client_with_lifespan(app) as client: orch = app.state.orchestrator _seed_resolved_session(orch, query="latency spike") - res = await client.get("/sessions/recent?limit=5") + res = await client.get("/api/v1/sessions/recent?limit=5") assert res.status_code == 200 body = res.json() assert isinstance(body, list) @@ -141,7 +141,7 @@ async def test_get_session_detail_404_for_unknown_id(cfg): on a 404.""" app = build_app(cfg) async with _client_with_lifespan(app) as client: - res = await client.get("/sessions/SES-DOES-NOT-EXIST") + res = await client.get("/api/v1/sessions/SES-DOES-NOT-EXIST") assert res.status_code == 404 body = res.json() assert "error" in body @@ -157,7 +157,7 @@ async def test_get_session_detail_returns_row(cfg): async with _client_with_lifespan(app) as client: orch = app.state.orchestrator sid = _seed_resolved_session(orch, query="api latency") - res = await client.get(f"/sessions/{sid}") + res = await client.get(f"/api/v1/sessions/{sid}") assert res.status_code == 200 body = res.json() assert body["id"] == sid @@ -171,7 +171,7 @@ async def test_get_retry_preview_404_for_unknown(cfg): branch, which the endpoint maps to 404.""" app = build_app(cfg) async with _client_with_lifespan(app) as client: - res = await client.get("/sessions/UNKNOWN/retry/preview") + res = await client.get("/api/v1/sessions/UNKNOWN/retry/preview") assert res.status_code == 404 body = res.json() assert body["error"]["code"] == "not_found" @@ -191,7 +191,7 @@ async def test_get_retry_preview_happy_path_returns_decision(cfg): inc.status = "error" inc.extra_fields["retry_count"] = 0 orch.store.save(inc) - res = await client.get(f"/sessions/{inc.id}/retry/preview") + res = await client.get(f"/api/v1/sessions/{inc.id}/retry/preview") assert res.status_code == 200 body = res.json() assert isinstance(body["retry"], bool) @@ -214,7 +214,7 @@ async def test_get_session_lessons_returns_extracted_rows(cfg): sid = _seed_resolved_session(orch, query="payments-svc") # Drive the finalize hook so the M5 lesson row lands. orch._finalize_session_status(sid) - res = await client.get(f"/sessions/{sid}/lessons") + res = await client.get(f"/api/v1/sessions/{sid}/lessons") assert res.status_code == 200 body = res.json() assert isinstance(body, list) @@ -230,7 +230,7 @@ async def test_get_session_lessons_empty_when_no_corpus(cfg): """Sessions that never resolved produce no lessons.""" app = build_app(cfg) async with _client_with_lifespan(app) as client: - res = await client.get("/sessions/SES-EMPTY/lessons") + res = await client.get("/api/v1/sessions/SES-EMPTY/lessons") # No matching row -> empty list, not 404. assert res.status_code == 200 assert res.json() == [] @@ -265,7 +265,7 @@ async def test_sse_events_replays_backlog(cfg): # Find the SSE route + invoke its handler directly. sse_route = next( r for r in app.router.routes - if getattr(r, "path", "") == "/sessions/{session_id}/events" + if getattr(r, "path", "") == "/api/v1/sessions/{session_id}/events" ) # The handler is the async function under .endpoint. # Fake a Request with the orchestrator wired + a disconnect-False @@ -278,7 +278,7 @@ async def _disconnected() -> bool: scope = { "type": "http", "method": "GET", - "path": "/sessions/SES-SSE/events", + "path": "/api/v1/sessions/SES-SSE/events", "query_string": b"since=0", "headers": [], "app": app, @@ -360,7 +360,7 @@ def test_post_resume_sse_returns_event_stream(cfg): orch = app.state.orchestrator sid = _seed_resolved_session(orch, query="resume-target") with client.stream( - "POST", f"/sessions/{sid}/resume", + "POST", f"/api/v1/sessions/{sid}/resume", json={"decision": "resume_with_input", "user_input": "go"}, ) as resp: assert resp.status_code == 200 @@ -400,7 +400,7 @@ def test_post_retry_sse_returns_event_stream(cfg): orch.store.save(inc) with client.stream( - "POST", f"/sessions/{inc.id}/retry", + "POST", f"/api/v1/sessions/{inc.id}/retry", ) as resp: assert resp.status_code == 200 assert resp.headers["content-type"].startswith("text/event-stream") @@ -434,7 +434,7 @@ def test_websocket_event_stream_replays_backlog(cfg): orch.event_log.record("SES-WS", "agent_finished", agent="triage") with client.websocket_connect( - "/ws/sessions/SES-WS/events?since=0", + "/api/v1/ws/sessions/SES-WS/events?since=0", ) as ws: frames = [ws.receive_json() for _ in range(3)] @@ -455,7 +455,7 @@ async def test_cors_allows_react_dev_origins(cfg): app = build_app(cfg) async with _client_with_lifespan(app) as client: res = await client.options( - "/sessions", + "/api/v1/sessions", headers={ "Origin": "http://localhost:5173", "Access-Control-Request-Method": "POST", @@ -474,7 +474,7 @@ async def test_404_renders_structured_error_envelope(cfg): """HTTPException 404 -> {"error":{"code":"not_found", ...}}.""" app = build_app(cfg) async with _client_with_lifespan(app) as client: - res = await client.get("/sessions/NOPE") + res = await client.get("/api/v1/sessions/NOPE") assert res.status_code == 404 body = res.json() assert set(body.keys()) == {"error"} @@ -515,15 +515,15 @@ async def test_react_surface_e2e_terminal_session(cfg): orch._finalize_session_status(sid) # 1. GET /sessions/recent — session A is in the list. - recent = client.get("/sessions/recent").json() + recent = client.get("/api/v1/sessions/recent").json() assert any(r["id"] == sid for r in recent) # 2. GET /sessions/{sid} — terminal status. - detail = client.get(f"/sessions/{sid}").json() + detail = client.get(f"/api/v1/sessions/{sid}").json() assert detail["status"] == "resolved" # 3. GET /sessions/{sid}/lessons — at least one lesson row. - lessons = client.get(f"/sessions/{sid}/lessons").json() + lessons = client.get(f"/api/v1/sessions/{sid}/lessons").json() assert len(lessons) >= 1 assert lessons[0]["outcome_status"] == "resolved" @@ -533,7 +533,7 @@ async def test_react_surface_e2e_terminal_session(cfg): # clean test-time disconnect. frames: list[dict] = [] with client.websocket_connect( - f"/ws/sessions/{sid}/events?since=0", + f"/api/v1/ws/sessions/{sid}/events?since=0", ) as ws: # Pull all backlog frames quickly. We seeded the session # so the finalize emitted at least status_changed + @@ -576,7 +576,7 @@ async def _boom(*_a, **_k): orch.resume_investigation = _boom # type: ignore[method-assign] with client.stream( - "POST", "/sessions/SES-RESUME-FAIL/resume", + "POST", "/api/v1/sessions/SES-RESUME-FAIL/resume", json={"decision": "resume_with_input", "user_input": "go"}, ) as resp: assert resp.status_code == 200 @@ -607,7 +607,7 @@ async def _boom(*_a, **_k): orch.retry_session = _boom # type: ignore[method-assign] with client.stream( - "POST", "/sessions/SES-RETRY-FAIL/retry", + "POST", "/api/v1/sessions/SES-RETRY-FAIL/retry", ) as resp: assert resp.status_code == 200 frames: list[dict] = [] @@ -631,7 +631,7 @@ def test_get_session_lessons_503_when_lesson_store_absent(cfg): with TestClient(app) as client: orch = app.state.orchestrator orch.lesson_store = None - res = client.get("/sessions/ANY/lessons") + res = client.get("/api/v1/sessions/ANY/lessons") assert res.status_code == 200 assert res.json() == [] @@ -647,7 +647,7 @@ def test_websocket_close_when_event_log_absent(cfg): orch.event_log = None with pytest.raises(WebSocketDisconnect) as excinfo: with client.websocket_connect( - "/ws/sessions/ANY/events?since=0", + "/api/v1/ws/sessions/ANY/events?since=0", ) as ws: ws.receive_json() assert excinfo.value.code == 1011 @@ -661,7 +661,7 @@ def test_websocket_handles_invalid_since_param(cfg): orch = app.state.orchestrator orch.event_log.record("SES-WS-BAD", "agent_started", agent="a") with client.websocket_connect( - "/ws/sessions/SES-WS-BAD/events?since=not-a-number", + "/api/v1/ws/sessions/SES-WS-BAD/events?since=not-a-number", ) as ws: f = ws.receive_json() assert f["kind"] == "agent_started" diff --git a/tests/test_api_recent_events.py b/tests/test_api_recent_events.py new file mode 100644 index 0000000..e06bcb2 --- /dev/null +++ b/tests/test_api_recent_events.py @@ -0,0 +1,55 @@ +"""Cross-session SSE — emits session-level events across all sessions.""" +import asyncio +import json +from fastapi.testclient import TestClient +from runtime.api import build_app +from tests.test_api_v1_url_move import _cfg + + +def test_recent_events_replays_session_creates(tmp_path): + """Two POST /sessions calls each emit a session.created event; + the recent SSE stream replays them on connect.""" + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + # Create two sessions + for q in ["alpha", "beta"]: + r = client.post("/api/v1/sessions", json={ + "query": q, "environment": "dev", + "submitter": {"id": "u1", "team": "p"}, + }) + assert r.status_code == 201 + + # Direct-call the SSE handler with a forced-disconnect to drain + # the backlog (avoids long-poll deadlock in TestClient). + from starlette.requests import Request as StarletteRequest + + async def _disc(): + return True # exit tail loop after backlog drain + + sse_route = next( + r for r in app.router.routes + if getattr(r, "path", "") == "/api/v1/sessions/recent/events" + ) + scope = { + "type": "http", "method": "GET", + "path": "/api/v1/sessions/recent/events", + "query_string": b"since=0", "headers": [], "app": app, + } + request = StarletteRequest(scope) + request.is_disconnected = _disc # type: ignore[method-assign] + response = asyncio.run( + sse_route.endpoint(request=request, since=0) # type: ignore[attr-defined] + ) + + async def _drain(): + frames = [] + async for chunk in response.body_iterator: + text = chunk.decode() if isinstance(chunk, bytes) else chunk + for line in text.splitlines(): + if line.startswith("data: "): + frames.append(json.loads(line[len("data: "):])) + return frames + + frames = asyncio.run(_drain()) + kinds = [f["kind"] for f in frames] + assert kinds.count("session.created") == 2 diff --git a/tests/test_api_session_full.py b/tests/test_api_session_full.py new file mode 100644 index 0000000..dd62bd7 --- /dev/null +++ b/tests/test_api_session_full.py @@ -0,0 +1,83 @@ +"""GET /api/v1/sessions/{id}/full returns the bootstrap bundle. + +Single round-trip on session open: replaces multiple GETs. +Returns: session, agents_run, tool_calls, events, agent_definitions, vm_seq. +""" +from fastapi.testclient import TestClient +from runtime.api import build_app +from tests.test_api_v1_url_move import _cfg + + +def test_full_returns_404_for_unknown_session(tmp_path): + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + r = client.get("/api/v1/sessions/SES-20990101-999/full") + assert r.status_code == 404 + body = r.json() + assert body["error"]["code"] == "not_found" + + +def test_full_returns_complete_bundle_for_existing_session(tmp_path): + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + # Create a session first + r = client.post("/api/v1/sessions", json={ + "query": "test bootstrap", "environment": "dev", + "submitter": {"id": "u1", "team": "platform"}, + }) + assert r.status_code == 201 + sid = r.json()["session_id"] + + # Bootstrap fetch + r = client.get(f"/api/v1/sessions/{sid}/full") + assert r.status_code == 200 + body = r.json() + # All five keys present + assert set(body.keys()) >= { + "session", "agents_run", "tool_calls", "events", "agent_definitions", "vm_seq" + } + assert body["session"]["id"] == sid + assert isinstance(body["agents_run"], list) + assert isinstance(body["tool_calls"], list) + assert isinstance(body["events"], list) + assert isinstance(body["agent_definitions"], dict) + assert isinstance(body["vm_seq"], int) + + +def test_full_agent_definitions_includes_skill_metadata(tmp_path): + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + r = client.post("/api/v1/sessions", json={ + "query": "x", "environment": "dev", + "submitter": {"id": "u1", "team": "p"}, + }) + sid = r.json()["session_id"] + + r = client.get(f"/api/v1/sessions/{sid}/full") + defs = r.json()["agent_definitions"] + # Each agent has at least: name, kind, model, tools, routes + for name, d in defs.items(): + assert "name" in d + assert "kind" in d + assert "model" in d + assert "tools" in d # list of tool ref strings + assert "routes" in d # dict of signal -> next agent + + +def test_full_vm_seq_matches_event_log_length(tmp_path): + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + r = client.post("/api/v1/sessions", json={ + "query": "x", "environment": "dev", + "submitter": {"id": "u1", "team": "p"}, + }) + sid = r.json()["session_id"] + + r = client.get(f"/api/v1/sessions/{sid}/full") + body = r.json() + # vm_seq is the max event seq the bundle includes; if no events, both 0 + assert body["vm_seq"] >= 0 + if body["events"]: + assert body["vm_seq"] == max(ev["seq"] for ev in body["events"]) + else: + assert body["vm_seq"] == 0 diff --git a/tests/test_api_static_serve.py b/tests/test_api_static_serve.py new file mode 100644 index 0000000..98815f8 --- /dev/null +++ b/tests/test_api_static_serve.py @@ -0,0 +1,56 @@ +"""StaticFiles mount at / serves the React SPA with a fallback to index.html.""" +from fastapi.testclient import TestClient +from runtime.api import build_app +from tests.test_api_v1_url_move import _cfg + + +def test_root_serves_index_html(tmp_path, monkeypatch): + # Stage a fake web/dist with an index.html + web_dist = tmp_path / "web_dist" + web_dist.mkdir() + (web_dist / "index.html").write_text("SPA OK") + monkeypatch.setenv("ASR_WEB_DIST", str(web_dist)) + + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + r = client.get("/") + assert r.status_code == 200 + assert "SPA OK" in r.text + + +def test_unknown_path_falls_back_to_index_html(tmp_path, monkeypatch): + """SPA routes (/sessions/abc, /settings) fall back to index.html.""" + web_dist = tmp_path / "web_dist" + web_dist.mkdir() + (web_dist / "index.html").write_text("SPA OK") + monkeypatch.setenv("ASR_WEB_DIST", str(web_dist)) + + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + r = client.get("/sessions/SES-20260515-001") # SPA route, not API + assert r.status_code == 200 + assert "SPA OK" in r.text + + +def test_api_v1_paths_are_NOT_caught_by_spa_fallback(tmp_path, monkeypatch): + """Unknown /api/v1/* paths return 404 JSON, not the SPA HTML.""" + web_dist = tmp_path / "web_dist" + web_dist.mkdir() + (web_dist / "index.html").write_text("SPA OK") + monkeypatch.setenv("ASR_WEB_DIST", str(web_dist)) + + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + r = client.get("/api/v1/no-such-endpoint") + assert r.status_code == 404 + assert "SPA OK" not in r.text + + +def test_when_web_dist_missing_serves_helpful_message(tmp_path, monkeypatch): + """If web/dist not built yet, root returns a 503 with build instructions.""" + monkeypatch.setenv("ASR_WEB_DIST", "/nonexistent") + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + r = client.get("/") + assert r.status_code == 503 + assert "npm run build" in r.text diff --git a/tests/test_api_ui_hints.py b/tests/test_api_ui_hints.py new file mode 100644 index 0000000..c3f14e1 --- /dev/null +++ b/tests/test_api_ui_hints.py @@ -0,0 +1,44 @@ +"""GET /api/v1/config/ui-hints returns app-configured UI hints.""" +from fastapi.testclient import TestClient +from runtime.api import build_app +from runtime.config import UIConfig +from tests.test_api_v1_url_move import _cfg + + +def test_ui_hints_returns_defaults_when_unconfigured(tmp_path): + cfg = _cfg(tmp_path) + app = build_app(cfg) + with TestClient(app) as client: + r = client.get("/api/v1/config/ui-hints") + assert r.status_code == 200 + body = r.json() + # Defaults: empty brand, no logo, no rationale templates + assert body["brand_name"] == "" + assert body["brand_logo_url"] is None + assert body["approval_rationale_templates"] == [] + + +def test_ui_hints_returns_configured_values(tmp_path): + cfg = _cfg(tmp_path) + cfg.ui = UIConfig( + brand_name="Acme Agents", + brand_logo_url="/static/acme.svg", + approval_rationale_templates=["Standard runbook", "Off-hours"], + ) + app = build_app(cfg) + with TestClient(app) as client: + r = client.get("/api/v1/config/ui-hints") + body = r.json() + assert body["brand_name"] == "Acme Agents" + assert body["brand_logo_url"] == "/static/acme.svg" + assert body["approval_rationale_templates"] == ["Standard runbook", "Off-hours"] + + +def test_ui_hints_includes_environments_list(tmp_path): + cfg = _cfg(tmp_path) + cfg.environments = ["dev", "staging", "production"] + app = build_app(cfg) + with TestClient(app) as client: + r = client.get("/api/v1/config/ui-hints") + body = r.json() + assert body["environments"] == ["dev", "staging", "production"] diff --git a/tests/test_api_v1_legacy_redirects.py b/tests/test_api_v1_legacy_redirects.py new file mode 100644 index 0000000..2e28516 --- /dev/null +++ b/tests/test_api_v1_legacy_redirects.py @@ -0,0 +1,36 @@ +"""Legacy /incidents/* endpoints get HTTP 308 redirects to /api/v1/sessions/*.""" +from fastapi.testclient import TestClient +from runtime.api import build_app +from tests.test_api_v1_url_move import _cfg # reuse fixture builder + + +def test_legacy_incidents_list_redirects(tmp_path): + app = build_app(_cfg(tmp_path)) + with TestClient(app, follow_redirects=False) as client: + r = client.get("/incidents") + assert r.status_code == 308 + assert r.headers["location"] == "/api/v1/sessions" + + +def test_legacy_incident_detail_redirects(tmp_path): + app = build_app(_cfg(tmp_path)) + with TestClient(app, follow_redirects=False) as client: + r = client.get("/incidents/SES-20260515-001") + assert r.status_code == 308 + assert r.headers["location"] == "/api/v1/sessions/SES-20260515-001" + + +def test_legacy_resume_redirects(tmp_path): + app = build_app(_cfg(tmp_path)) + with TestClient(app, follow_redirects=False) as client: + r = client.post("/incidents/SES-20260515-001/resume", json={"decision": "resume_with_input"}) + assert r.status_code == 308 + assert r.headers["location"] == "/api/v1/sessions/SES-20260515-001/resume" + + +def test_legacy_investigate_redirects(tmp_path): + app = build_app(_cfg(tmp_path)) + with TestClient(app, follow_redirects=False) as client: + r = client.post("/investigate", json={"query": "x", "environment": "dev"}) + assert r.status_code == 308 + assert r.headers["location"] == "/api/v1/investigate" diff --git a/tests/test_api_v1_url_move.py b/tests/test_api_v1_url_move.py new file mode 100644 index 0000000..60c913b --- /dev/null +++ b/tests/test_api_v1_url_move.py @@ -0,0 +1,67 @@ +"""Verify all API endpoints have moved under /api/v1/*.""" +from fastapi.testclient import TestClient +from runtime.api import build_app +from runtime.config import ( + AppConfig, LLMConfig, MCPConfig, MCPServerConfig, + Paths, RuntimeConfig, +) + + +def _cfg(tmp_path): + return AppConfig( + llm=LLMConfig.stub(), + mcp=MCPConfig(servers=[ + MCPServerConfig(name="local_inc", transport="in_process", + module="examples.incident_management.mcp_server", + category="incident_management"), + MCPServerConfig(name="local_obs", transport="in_process", + module="examples.incident_management.mcp_servers.observability", + category="observability"), + MCPServerConfig(name="local_rem", transport="in_process", + module="examples.incident_management.mcp_servers.remediation", + category="remediation"), + MCPServerConfig(name="local_user", transport="in_process", + module="examples.incident_management.mcp_servers.user_context", + category="user_context"), + ]), + paths=Paths(skills_dir="examples/incident_management/skills", + incidents_dir=str(tmp_path)), + runtime=RuntimeConfig(state_class=None), + ) + + +def test_health_remains_at_root(tmp_path): + """Health stays at /health (monitor convention).""" + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + r = client.get("/health") + assert r.status_code == 200 + + +def test_sessions_endpoints_under_api_v1(tmp_path): + """GET /api/v1/sessions returns 200 (was at /sessions).""" + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + r = client.get("/api/v1/sessions") + assert r.status_code == 200 + + +def test_agents_under_api_v1(tmp_path): + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + r = client.get("/api/v1/agents") + assert r.status_code == 200 + + +def test_tools_under_api_v1(tmp_path): + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + r = client.get("/api/v1/tools") + assert r.status_code == 200 + + +def test_environments_under_api_v1(tmp_path): + app = build_app(_cfg(tmp_path)) + with TestClient(app) as client: + r = client.get("/api/v1/environments") + assert r.status_code == 200 diff --git a/tests/test_approval_api.py b/tests/test_approval_api.py index db6086e..e3ccf58 100644 --- a/tests/test_approval_api.py +++ b/tests/test_approval_api.py @@ -101,7 +101,7 @@ async def test_list_pending_approvals_returns_only_pending_rows(cfg): async with _client_with_lifespan(app) as client: # Create a session via the public API so the lifespan and store # are wired identically to a real run. - start = await client.post("/sessions", json={ + start = await client.post("/api/v1/sessions", json={ "query": "test", "environment": "dev", "reporter_id": "u", "reporter_team": "t", }) @@ -128,7 +128,7 @@ async def test_list_pending_approvals_returns_only_pending_rows(cfg): ] orch.store.save(inc) - res = await client.get(f"/sessions/{sid}/approvals") + res = await client.get(f"/api/v1/sessions/{sid}/approvals") assert res.status_code == 200 body = res.json() @@ -147,13 +147,13 @@ async def test_list_pending_approvals_empty_for_clean_session(cfg): """A session with no pending approvals must return ``[]``, not 404.""" app = build_app(cfg) async with _client_with_lifespan(app) as client: - start = await client.post("/sessions", json={ + start = await client.post("/api/v1/sessions", json={ "query": "test", "environment": "dev", "reporter_id": "u", "reporter_team": "t", }) sid = start.json()["session_id"] - res = await client.get(f"/sessions/{sid}/approvals") + res = await client.get(f"/api/v1/sessions/{sid}/approvals") assert res.status_code == 200 assert res.json() == [] @@ -164,7 +164,7 @@ async def test_list_pending_approvals_404_for_unknown_session(cfg): """Unknown session id must return 404, not silently empty.""" app = build_app(cfg) async with _client_with_lifespan(app) as client: - res = await client.get("/sessions/INC-DOES-NOT-EXIST/approvals") + res = await client.get("/api/v1/sessions/INC-DOES-NOT-EXIST/approvals") assert res.status_code == 404 @@ -179,7 +179,7 @@ async def test_post_approval_404_for_unknown_session(cfg): app = build_app(cfg) async with _client_with_lifespan(app) as client: res = await client.post( - "/sessions/INC-DOES-NOT-EXIST/approvals/0", + "/api/v1/sessions/INC-DOES-NOT-EXIST/approvals/0", json={"decision": "approve", "approver": "alice", "rationale": "ok"}, ) assert res.status_code == 404 @@ -191,13 +191,13 @@ async def test_post_approval_400_on_invalid_decision(cfg): ``{approve, reject}`` must yield 422 (FastAPI validation error).""" app = build_app(cfg) async with _client_with_lifespan(app) as client: - start = await client.post("/sessions", json={ + start = await client.post("/api/v1/sessions", json={ "query": "test", "environment": "dev", "reporter_id": "u", "reporter_team": "t", }) sid = start.json()["session_id"] res = await client.post( - f"/sessions/{sid}/approvals/0", + f"/api/v1/sessions/{sid}/approvals/0", json={"decision": "MAYBE", "approver": "alice"}, ) # FastAPI surfaces validation failures as 422; either 400 or 422 @@ -217,7 +217,7 @@ async def test_post_approval_happy_path_returns_decision_summary(cfg, monkeypatc """ app = build_app(cfg) async with _client_with_lifespan(app) as client: - start = await client.post("/sessions", json={ + start = await client.post("/api/v1/sessions", json={ "query": "test", "environment": "dev", "reporter_id": "u", "reporter_team": "t", }) @@ -241,7 +241,7 @@ async def _fake_ainvoke(arg, config=None): ) res = await client.post( - f"/sessions/{sid}/approvals/0", + f"/api/v1/sessions/{sid}/approvals/0", json={ "decision": "approve", "approver": "alice", @@ -292,7 +292,7 @@ async def test_submit_approval_real_loop_no_deadlock(cfg): """ app = build_app(cfg) async with _client_with_lifespan(app) as client: - start = await client.post("/sessions", json={ + start = await client.post("/api/v1/sessions", json={ "query": "test", "environment": "dev", "reporter_id": "u", "reporter_team": "t", }) @@ -318,7 +318,7 @@ async def _fake_ainvoke(arg, config=None): # would hang forever (no progress on the loop). res = await asyncio.wait_for( client.post( - f"/sessions/{sid}/approvals/0", + f"/api/v1/sessions/{sid}/approvals/0", json={ "decision": "approve", "approver": "bob", diff --git a/tests/test_event_log.py b/tests/test_event_log.py index b5bd2ad..2de04ff 100644 --- a/tests/test_event_log.py +++ b/tests/test_event_log.py @@ -87,4 +87,9 @@ def test_event_kind_literal_lists_full_vocabulary(): "gate_fired", "status_changed", "lesson_extracted", + # v2.0 cross-session SSE kinds — drive the React UI's "Other + # Sessions" monitor (api_recent_events.py). + "session.created", + "session.status_changed", + "session.agent_running", }) diff --git a/tests/test_session_lock.py b/tests/test_session_lock.py index 908b9de..b01b4e6 100644 --- a/tests/test_session_lock.py +++ b/tests/test_session_lock.py @@ -996,7 +996,7 @@ async def _client_with_lifespan(app): # Seed a session with a pending_approval ToolCall so the # endpoint's pre-flight ``orch.store.load`` succeeds and # the request reaches ``svc.submit_async``. - start = await client.post("/sessions", json={ + start = await client.post("/api/v1/sessions", json={ "query": "test", "environment": "dev", "reporter_id": "u", "reporter_team": "t", }) @@ -1038,7 +1038,7 @@ async def _busy_submit_async(coro): # surfaces as a test failure. res = await asyncio.wait_for( client.post( - f"/sessions/{sid}/approvals/0", + f"/api/v1/sessions/{sid}/approvals/0", json={ "decision": "approve", "approver": "alice", diff --git a/tests/test_sse_tail_loop.py b/tests/test_sse_tail_loop.py index 8ebd446..6f8bdc2 100644 --- a/tests/test_sse_tail_loop.py +++ b/tests/test_sse_tail_loop.py @@ -61,7 +61,7 @@ def cfg(tmp_path): def _build_request(app, sid: str, *, is_disconnected) -> StarletteRequest: scope = { "type": "http", "method": "GET", - "path": f"/sessions/{sid}/events", + "path": f"/api/v1/sessions/{sid}/events", "query_string": b"since=0", "headers": [], "app": app, @@ -74,7 +74,7 @@ def _build_request(app, sid: str, *, is_disconnected) -> StarletteRequest: def _sse_route(app): return next( r for r in app.router.routes - if getattr(r, "path", "") == "/sessions/{session_id}/events" + if getattr(r, "path", "") == "/api/v1/sessions/{session_id}/events" ) diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..066b770 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,14 @@ +node_modules +dist +playwright-report +test-results +*.log +.vite +.env.local + +# TypeScript project-references build artifacts (from `tsc -b`) +*.tsbuildinfo +vite.config.d.ts +vite.config.js +vitest.config.d.ts +vitest.config.js diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..15f1ade --- /dev/null +++ b/web/index.html @@ -0,0 +1,12 @@ + + + + + + ASR Operator Console + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..70d0cd8 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,6478 @@ +{ + "name": "asr-web", + "version": "2.0.0-rc1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "asr-web", + "version": "2.0.0-rc1", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.0", + "@radix-ui/react-dropdown-menu": "^2.1.0", + "@radix-ui/react-popover": "^1.1.0", + "@radix-ui/react-scroll-area": "^1.2.0", + "@radix-ui/react-tabs": "^1.1.0", + "@tanstack/react-query": "^5.62.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@axe-core/playwright": "^4.10.0", + "@playwright/test": "^1.49.0", + "@tailwindcss/vite": "^4.0.0", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.0", + "@types/node": "^25.8.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitejs/plugin-react": "^4.3.0", + "@vitest/ui": "^3.0.0", + "eslint": "^9.0.0", + "jsdom": "^25.0.0", + "prettier": "^3.4.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.7.0", + "vite": "^7.1.0", + "vitest": "^3.0.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@axe-core/playwright": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.3.tgz", + "integrity": "sha512-h/kfksv4F0cVIDlKpT4700OehdRgpvuVskuQ2nb7/JmtWUXpe9ftHAPtwyXGvVSsa6SJ64A9ER7Zrzc/sIvC4w==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.11.4" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz", + "integrity": "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.10.tgz", + "integrity": "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", + "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.3", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", + "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", + "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz", + "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.1", + "tinyglobby": "^0.2.14", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "3.2.4" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axe-core": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.356", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.356.tgz", + "integrity": "sha512-9NgFd7m5t5MCJ5rUSjJITUXAH9mEGlrlofnMf4YEr+pz6JlP7cWmTAH+JFmbPnaSW8koVTkuW7pacORWAnA5Yw==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..c66cb83 --- /dev/null +++ b/web/package.json @@ -0,0 +1,46 @@ +{ + "name": "asr-web", + "version": "2.0.0-rc1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint .", + "typecheck": "tsc -b", + "test:unit": "vitest run", + "test:e2e": "playwright test" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.0", + "@radix-ui/react-dropdown-menu": "^2.1.0", + "@radix-ui/react-popover": "^1.1.0", + "@radix-ui/react-scroll-area": "^1.2.0", + "@radix-ui/react-tabs": "^1.1.0", + "@tanstack/react-query": "^5.62.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@axe-core/playwright": "^4.10.0", + "@playwright/test": "^1.49.0", + "@tailwindcss/vite": "^4.0.0", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.0", + "@types/node": "^25.8.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitejs/plugin-react": "^4.3.0", + "@vitest/ui": "^3.0.0", + "eslint": "^9.0.0", + "jsdom": "^25.0.0", + "prettier": "^3.4.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.7.0", + "vite": "^7.1.0", + "vitest": "^3.0.0" + } +} diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..c53cffa --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,33 @@ +export default function App() { + return ( +
+

+ ASR Operator Console +

+

+ v2.0.0-rc1 · scaffold + design tokens · components land in tasks 16-20 +

+
+ Token preview: warm cream #FBFAF6 page, + accent navy #2A4365, + deep ink #15110A. +
+
+ ); +} diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..53662f4 --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,10 @@ +import './styles/global.css'; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/web/src/styles/global.css b/web/src/styles/global.css new file mode 100644 index 0000000..2b58831 --- /dev/null +++ b/web/src/styles/global.css @@ -0,0 +1,64 @@ +/* web/src/styles/global.css */ +@import "./tokens.css"; +@import "tailwindcss"; + +/* Font face declarations — vendored Geist + Geist Mono. + Files land via Task 13 (vendor fonts). For now these declarations + reference paths that don't yet exist; the browser falls back to the + next-available family in --ff-sans/--ff-mono. */ +@font-face { + font-family: "Geist"; + font-weight: 400; + font-style: normal; + font-display: swap; + src: url("/fonts/Geist-Regular.woff2") format("woff2"); +} +@font-face { + font-family: "Geist"; + font-weight: 500; + font-style: normal; + font-display: swap; + src: url("/fonts/Geist-Medium.woff2") format("woff2"); +} +@font-face { + font-family: "Geist"; + font-weight: 600; + font-style: normal; + font-display: swap; + src: url("/fonts/Geist-Bold.woff2") format("woff2"); +} +@font-face { + font-family: "Geist Mono"; + font-weight: 400; + font-style: normal; + font-display: swap; + src: url("/fonts/GeistMono-Regular.woff2") format("woff2"); +} +@font-face { + font-family: "Geist Mono"; + font-weight: 500; + font-style: normal; + font-display: swap; + src: url("/fonts/GeistMono-Medium.woff2") format("woff2"); +} + +* { box-sizing: border-box; } +html, body, #root { height: 100%; } +body { + margin: 0; + background: var(--bg-page); + color: var(--ink-1); + font-family: var(--ff-sans); + font-size: var(--t-body); + line-height: 1.55; + font-feature-settings: "tnum" 1, "ss01" 1; +} + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 60ms !important; + } +} diff --git a/web/src/styles/tokens.css b/web/src/styles/tokens.css new file mode 100644 index 0000000..64d66cf --- /dev/null +++ b/web/src/styles/tokens.css @@ -0,0 +1,75 @@ +/* web/src/styles/tokens.css + Single source of truth for design tokens. + Components NEVER use raw hex/px values — read from these vars. + Mirrored in web/src/tokens/*.ts for TS-side access (Task 14). */ +:root { + /* surfaces — warm off-white, not pure white */ + --bg-page: #FBFAF6; + --bg-elev: #FFFFFF; + --bg-subtle: #F4F2EC; + --bg-deep: #ECE7DB; + --bg-tint: #FAF6EA; + + /* ink — deep warm */ + --ink-1: #15110A; + --ink-2: #4A4540; + --ink-3: #918A80; + --ink-4: #C8C2B6; + + /* hairlines */ + --hair: #E6E1D4; + --hair-strong: #D4CDB8; + + /* accent — Deep Slate Navy */ + --acc: #2A4365; + --acc-dim: #1F3147; + --acc-soft: rgba(42, 67, 101, 0.08); + --acc-mid: rgba(42, 67, 101, 0.18); + + /* semantic */ + --warn: #B4814A; + --warn-bg: rgba(180, 129, 74, 0.08); + --danger: #B85A4F; + --danger-bg: rgba(184, 90, 79, 0.08); + --good: #5C8862; + --good-bg: rgba(92, 136, 98, 0.08); + --info: #4F6F8E; + + /* spacing — strict 4px grid */ + --s-1: 4px; + --s-2: 8px; + --s-3: 12px; + --s-4: 16px; + --s-5: 24px; + --s-6: 32px; + + /* type families */ + --ff-sans: "Geist", "Inter", -apple-system, system-ui, sans-serif; + --ff-mono: "Geist Mono", "JetBrains Mono", ui-monospace, monospace; + + /* type sizes (px) */ + --t-micro: 10px; + --t-meta: 11px; + --t-body: 13px; + --t-lead: 14px; + --t-h3: 15px; + --t-h2: 18px; + --t-h1: 24px; + --t-display: 30px; + + /* component sizes */ + --h-topbar: 48px; + --h-strip: 92px; + --h-statusbar: 24px; + --h-control: 28px; + --h-control-sm: 22px; + --avatar: 22px; + + /* elevation */ + --elev-1: 0 1px 2px rgba(21, 17, 10, 0.04), 0 0 0 1px var(--hair); + --elev-2: 0 2px 4px rgba(21, 17, 10, 0.05), 0 8px 16px rgba(21, 17, 10, 0.04), 0 0 0 1px var(--hair); + --elev-3: 0 4px 12px rgba(21, 17, 10, 0.07), 0 16px 32px rgba(21, 17, 10, 0.06), 0 0 0 1px var(--hair-strong); + + /* radius — sharp by project convention */ + --r: 0; +} diff --git a/web/tailwind.config.ts b/web/tailwind.config.ts new file mode 100644 index 0000000..9febf0d --- /dev/null +++ b/web/tailwind.config.ts @@ -0,0 +1,47 @@ +import type { Config } from 'tailwindcss'; + +export default { + content: ['./index.html', './src/**/*.{ts,tsx}'], + theme: { + extend: { + colors: { + 'bg-page': 'var(--bg-page)', + 'bg-elev': 'var(--bg-elev)', + 'bg-subtle': 'var(--bg-subtle)', + 'bg-deep': 'var(--bg-deep)', + 'bg-tint': 'var(--bg-tint)', + 'ink-1': 'var(--ink-1)', + 'ink-2': 'var(--ink-2)', + 'ink-3': 'var(--ink-3)', + 'ink-4': 'var(--ink-4)', + hair: 'var(--hair)', + 'hair-strong': 'var(--hair-strong)', + acc: 'var(--acc)', + 'acc-dim': 'var(--acc-dim)', + warn: 'var(--warn)', + danger: 'var(--danger)', + good: 'var(--good)', + info: 'var(--info)', + }, + fontFamily: { + sans: ['Geist', 'Inter', 'system-ui', 'sans-serif'], + mono: ['"Geist Mono"', '"JetBrains Mono"', 'ui-monospace', 'monospace'], + }, + fontSize: { + micro: '10px', + meta: '11px', + body: '13px', + lead: '14px', + h3: '15px', + h2: '18px', + h1: '24px', + display: '30px', + }, + spacing: { + 1: '4px', 2: '8px', 3: '12px', 4: '16px', 5: '24px', 6: '32px', + }, + borderRadius: { none: '0', DEFAULT: '0' }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..76965ed --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "exactOptionalPropertyTypes": true, + "skipLibCheck": true, + "isolatedModules": true, + "noEmit": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src", "tests"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 0000000..cf22020 --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "types": ["node"] + }, + "include": ["vite.config.ts", "vitest.config.ts"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..59b08f2 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + server: { + port: 5173, + strictPort: true, + proxy: { + '/api': 'http://localhost:8000', + }, + }, + build: { + outDir: 'dist', + emptyOutDir: true, + sourcemap: true, + target: 'es2022', + }, + define: { + __BUILD_VERSION__: JSON.stringify( + (process.env.npm_package_version ?? 'dev') + '-' + new Date().toISOString().slice(0, 10), + ), + }, +});