diff --git a/apps/web/routers/paper_trading.py b/apps/web/routers/paper_trading.py new file mode 100644 index 0000000..30302d3 --- /dev/null +++ b/apps/web/routers/paper_trading.py @@ -0,0 +1,761 @@ +"""Paper trading API endpoints — full session management.""" +from __future__ import annotations + +import os +import re +import subprocess +import sys +import threading +import uuid +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from fastapi import APIRouter, HTTPException, Query +from pydantic import BaseModel + +from apps.web.dependencies import get_project_root + +router = APIRouter(prefix="/paper", tags=["paper_trading"]) + +_ANSI_RE = re.compile(r'\x1b\[[0-9;]*[mGKHABCDFrsu]|\x1b[()][AB012]') + +# In-memory task registry +_tasks: dict[str, dict[str, Any]] = {} +_tasks_lock = threading.Lock() + +# ── Auto daemon state ───────────────────────────────────────────────────────── + +def _auto_pid_file() -> Path: + return get_project_root() / ".paper_auto.pid" + +def _auto_log_file() -> Path: + return get_project_root() / ".paper_auto.log" + +def _find_auto_process() -> int | None: + """Scan process list for a running auto daemon (regardless of how it was started). + + Matches any of: + fithia2 paper auto (installed CLI entrypoint) + python -m apps.paper_trader.cli auto + python -m apps.paper_trader.auto + """ + try: + import psutil + for proc in psutil.process_iter(["pid", "cmdline"]): + try: + cmdline: list[str] = proc.info.get("cmdline") or [] + if not cmdline: + continue + # Must have "auto" as one of the arguments + if "auto" not in cmdline: + continue + cmdline_str = " ".join(cmdline) + # fithia2 paper auto + if "fithia2" in cmdline_str and "paper" in cmdline: + return proc.info["pid"] + # python -m apps.paper_trader.cli auto + if "paper_trader" in cmdline_str: + return proc.info["pid"] + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + except ImportError: + pass + return None + + +def _get_process_stdout_path(pid: int) -> str | None: + """Return the file path that the process's stdout (fd=1) is writing to. + + Returns None if stdout is a TTY, socket, or unreadable. + """ + try: + import psutil + p = psutil.Process(pid) + for f in p.open_files(): + if f.fd == 1: + return f.path + except Exception: + pass + return None + + +def _get_process_tty(pid: int) -> str | None: + """Return the controlling TTY device path of the process, if any.""" + try: + import psutil + return psutil.Process(pid).terminal() + except Exception: + return None + + +def _auto_status() -> dict[str, Any]: + """Return current auto daemon status. + + Checks PID file first; if not found (e.g. process started from terminal), + falls back to psutil process scan. Keeps PID file in sync. + Also reports where the log output is going. + """ + pid_file = _auto_pid_file() + + # 1. Check PID file + if pid_file.exists(): + try: + pid = int(pid_file.read_text().strip()) + os.kill(pid, 0) + log_path = _get_process_stdout_path(pid) + return { + "running": True, "pid": pid, "source": "pid_file", + "log_path": log_path, + "log_tty": None if log_path else _get_process_tty(pid), + } + except (ProcessLookupError, ValueError, OSError): + pid_file.unlink(missing_ok=True) + + # 2. Fallback: scan process list + pid = _find_auto_process() + if pid is not None: + try: + pid_file.write_text(str(pid)) + except Exception: + pass + log_path = _get_process_stdout_path(pid) + return { + "running": True, "pid": pid, "source": "process_scan", + "log_path": log_path, + "log_tty": None if log_path else _get_process_tty(pid), + } + + return {"running": False, "pid": None, "source": None, "log_path": None, "log_tty": None} + +def _auto_schedule_info() -> list[dict[str, Any]]: + """Return schedule with countdown info (server-side computation).""" + import datetime as dt + from zoneinfo import ZoneInfo + TZ_ET = ZoneInfo("America/New_York") + SCHEDULE = [ + {"name": "pipeline_pre", "et_hour": 7, "et_min": 0, "label": "Pre-market pipeline"}, + {"name": "run_open", "et_hour": 9, "et_min": 35, "label": "run-open (장 시작 직후)"}, + {"name": "run_close", "et_hour": 15, "et_min": 45, "label": "run-close (장 마감 직전)"}, + {"name": "pipeline_post", "et_hour": 16, "et_min": 30, "label": "Post-close pipeline"}, + ] + now_et = dt.datetime.now(tz=TZ_ET) + today = now_et.date() + result = [] + for ev in SCHEDULE: + et_dt = dt.datetime(today.year, today.month, today.day, + ev["et_hour"], ev["et_min"], tzinfo=TZ_ET) + wait_secs = (et_dt - now_et).total_seconds() + result.append({ + "name": ev["name"], + "label": ev["label"], + "et_time": f"{ev['et_hour']:02d}:{ev['et_min']:02d} ET", + "et_iso": et_dt.isoformat(), + "past": wait_secs <= 0, + "wait_secs": max(0.0, wait_secs), + }) + return result + + +def _get_db_path() -> str: + db = os.environ.get("PAPER_TRADER_DB", "paper_trading.db") + p = Path(db) + if not p.is_absolute(): + p = get_project_root() / db + return str(p) + + +def _get_state_manager(): + from apps.paper_trader.state import StateManager + return StateManager(_get_db_path()) + + +def _get_broker(): + from apps.paper_trader.alpaca_broker import AlpacaBroker + return AlpacaBroker.from_env() + + +def _session_summary(session, state) -> dict[str, Any]: + """Build a JSON-serializable summary of a session.""" + snapshots = state.list_snapshots(session.session_id) + session_st = state.get_session_state(session.session_id) + latest = snapshots[-1] if snapshots else None + initial = session.initial_equity + current = float(latest["equity"]) if latest else initial + peak = state.get_peak_equity(session.session_id, initial) + drawdown_pct = max(0.0, (peak - current) / peak * 100) if peak > 0 else 0.0 + total_pnl = current - initial + total_pnl_pct = total_pnl / initial * 100 if initial else 0.0 + trades = state.list_trades(session.session_id) + return { + "session_id": session.session_id, + "session_name": session.session_name, + "config_path": session.config_path, + "initial_equity": initial, + "created_at": session.created_at, + "status": session.status, + "current_equity": current, + "peak_equity": peak, + "total_pnl": total_pnl, + "total_pnl_pct": total_pnl_pct, + "drawdown_pct": drawdown_pct, + "trade_count": len(trades), + "latest_date": latest["date"] if latest else None, + "kill_switch": bool(session_st.kill_switch_triggered), + "cooldown_remaining": session_st.cooldown_remaining, + "consecutive_losses": session_st.consecutive_losses, + "daily_new_risk_used": session_st.daily_new_risk_used, + "last_processed_date": session_st.last_processed_date, + } + + +# ── Sessions ────────────────────────────────────────────────────────────────── + +@router.get("/sessions") +def list_sessions() -> dict[str, Any]: + state = _get_state_manager() + sessions = state.list_sessions() + return {"sessions": [_session_summary(s, state) for s in sessions]} + + +class CreateSessionRequest(BaseModel): + name: str + config: str # experiment name, numeric ID, or config path + capital: float = 10000.0 + + +@router.post("/sessions") +def create_session(req: CreateSessionRequest) -> dict[str, Any]: + from apps.paper_trader.cli import _resolve_config_path + + config_path = _resolve_config_path(req.config) + abs_path = Path(config_path) + if not abs_path.is_absolute(): + abs_path = get_project_root() / config_path + if not abs_path.exists(): + raise HTTPException(status_code=400, detail=f"Config not found: {config_path}") + config_path = str(config_path) # keep relative for storage + + state = _get_state_manager() + if state.get_session(req.name) is not None: + raise HTTPException(status_code=409, detail=f"Session '{req.name}' already exists") + + session_id = state.create_session( + session_name=req.name, + config_path=config_path, + initial_equity=req.capital, + ) + return {"session_id": session_id, "name": req.name, "capital": req.capital, "config": config_path} + + +@router.get("/sessions/{session_id}") +def get_session(session_id: str) -> dict[str, Any]: + state = _get_state_manager() + session = state.get_session(session_id) + if session is None: + raise HTTPException(status_code=404, detail=f"Session not found: {session_id}") + return _session_summary(session, state) + + +@router.post("/sessions/{session_id}/pause") +def pause_session(session_id: str) -> dict[str, Any]: + state = _get_state_manager() + session = state.get_session(session_id) + if session is None: + raise HTTPException(status_code=404, detail="Session not found") + if session.status != "active": + raise HTTPException(status_code=400, detail=f"Session is already {session.status}") + state.set_session_status(session.session_id, "paused") + return {"session_id": session_id, "status": "paused"} + + +@router.post("/sessions/{session_id}/resume") +def resume_session(session_id: str) -> dict[str, Any]: + state = _get_state_manager() + session = state.get_session(session_id) + if session is None: + raise HTTPException(status_code=404, detail="Session not found") + if session.status != "paused": + raise HTTPException(status_code=400, detail=f"Session is {session.status}, not paused") + state.set_session_status(session.session_id, "active") + return {"session_id": session_id, "status": "active"} + + +@router.delete("/sessions/{session_id}") +def close_session(session_id: str) -> dict[str, Any]: + """Liquidate all Alpaca positions and delete all session data.""" + state = _get_state_manager() + session = state.get_session(session_id) + if session is None: + raise HTTPException(status_code=404, detail="Session not found") + + orders_closed = 0 + broker_error: str | None = None + try: + broker = _get_broker() + orders = broker.close_all_positions() + orders_closed = len(orders) + except Exception as exc: + broker_error = str(exc) + + for ss in state.get_open_strategy_states(session.session_id): + state.close_strategy_state(session.session_id, ss.symbol) + + state.delete_session(session.session_id) + return { + "deleted": True, + "session_id": session_id, + "positions_closed": orders_closed, + "broker_error": broker_error, + } + + +# ── Data endpoints ───────────────────────────────────────────────────────────── + +@router.get("/sessions/{session_id}/positions") +def get_positions(session_id: str) -> dict[str, Any]: + state = _get_state_manager() + session = state.get_session(session_id) + if session is None: + raise HTTPException(status_code=404, detail="Session not found") + + strategy_states = { + ss.symbol: ss + for ss in state.get_open_strategy_states(session.session_id) + } + + try: + broker = _get_broker() + positions = broker.list_positions() + result = [] + for p in sorted(positions, key=lambda x: x.symbol): + ss = strategy_states.get(p.symbol) + qty = float(p.qty) + entry = float(p.avg_entry_price) if p.avg_entry_price else 0.0 + pnl = float(p.unrealized_pl) + pnl_pct = pnl / (entry * qty) * 100 if entry and qty else 0.0 + result.append({ + "symbol": p.symbol, + "qty": qty, + "avg_entry_price": entry, + "current_price": float(p.current_price) if p.current_price else None, + "unrealized_pl": pnl, + "unrealized_pl_pct": pnl_pct, + "days_held": ss.days_held if ss else None, + "stop_price": ss.current_stop if ss else None, + "target_price": ss.target_price if ss else None, + "entry_date": ss.entry_date if ss else None, + "engine_id": ss.engine_id if ss else None, + "trade_direction": ss.trade_direction if ss else None, + }) + # Also include strategy states not in broker positions (ghost) + broker_symbols = {p.symbol for p in positions} + for sym, ss in strategy_states.items(): + if sym not in broker_symbols: + result.append({ + "symbol": sym, + "qty": None, + "avg_entry_price": None, + "current_price": None, + "unrealized_pl": None, + "unrealized_pl_pct": None, + "days_held": ss.days_held, + "stop_price": ss.current_stop, + "target_price": ss.target_price, + "entry_date": ss.entry_date, + "engine_id": ss.engine_id, + "trade_direction": ss.trade_direction, + "_ghost": True, + }) + return {"positions": result, "broker_available": True} + except Exception as exc: + # Return local strategy states only + result = [] + for sym, ss in sorted(strategy_states.items()): + result.append({ + "symbol": sym, + "qty": None, + "avg_entry_price": None, + "current_price": None, + "unrealized_pl": None, + "unrealized_pl_pct": None, + "days_held": ss.days_held, + "stop_price": ss.current_stop, + "target_price": ss.target_price, + "entry_date": ss.entry_date, + "engine_id": ss.engine_id, + "trade_direction": ss.trade_direction, + }) + return {"positions": result, "broker_available": False, "broker_error": str(exc)} + + +@router.get("/sessions/{session_id}/trades") +def get_trades( + session_id: str, + last: int | None = Query(None, description="Show last N trades"), +) -> dict[str, Any]: + state = _get_state_manager() + session = state.get_session(session_id) + if session is None: + raise HTTPException(status_code=404, detail="Session not found") + trades = state.list_trades(session.session_id, limit=last) + return {"trades": trades, "total": len(trades)} + + +@router.get("/sessions/{session_id}/equity") +def get_equity(session_id: str) -> dict[str, Any]: + state = _get_state_manager() + session = state.get_session(session_id) + if session is None: + raise HTTPException(status_code=404, detail="Session not found") + snapshots = state.list_snapshots(session.session_id) + return {"snapshots": snapshots, "initial_equity": session.initial_equity} + + +# ── Run tasks (subprocess-based) ────────────────────────────────────────────── + +class RunRequest(BaseModel): + date: str | None = None + force: bool = False + + +def _launch_task( + cmd: list[str], + session_name: str, + operation: str, +) -> dict[str, Any]: + task_id = str(uuid.uuid4())[:8] + now = datetime.now(timezone.utc).isoformat() + task: dict[str, Any] = { + "task_id": task_id, + "session_name": session_name, + "operation": operation, + "status": "running", + "created_at": now, + "started_at": now, + "finished_at": None, + "log": "", + "error": None, + } + with _tasks_lock: + _tasks[task_id] = task + + def _run() -> None: + try: + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=str(get_project_root()), + ) + raw, _ = proc.communicate() + log_text = _ANSI_RE.sub("", raw.decode("utf-8", errors="replace")) + with _tasks_lock: + t = _tasks.get(task_id) + if t: + t["log"] = log_text + t["finished_at"] = datetime.now(timezone.utc).isoformat() + t["status"] = "completed" if proc.returncode == 0 else "failed" + if proc.returncode != 0: + t["error"] = f"Exit code {proc.returncode}" + except Exception as exc: + with _tasks_lock: + t = _tasks.get(task_id) + if t: + t["status"] = "failed" + t["error"] = str(exc) + t["finished_at"] = datetime.now(timezone.utc).isoformat() + + threading.Thread(target=_run, daemon=True).start() + return dict(task) + + +def _paper_cmd(subcommand: str, session_name: str, date: str | None, force: bool) -> list[str]: + cmd = [ + sys.executable, "-m", "apps.paper_trader.cli", + subcommand, "--session", session_name, + "--db", _get_db_path(), + ] + if date: + cmd += ["--date", date] + if force: + cmd.append("--force") + return cmd + + +@router.post("/sessions/{session_id}/run") +def run_daily(session_id: str, req: RunRequest = RunRequest()) -> dict[str, Any]: + state = _get_state_manager() + session = state.get_session(session_id) + if session is None: + raise HTTPException(status_code=404, detail="Session not found") + cmd = _paper_cmd("run", session.session_name, req.date, req.force) + return _launch_task(cmd, session.session_name, "run") + + +@router.post("/sessions/{session_id}/run-close") +def run_close(session_id: str, req: RunRequest = RunRequest()) -> dict[str, Any]: + """장 마감 직전: same-day 이벤트 → MOC 매수.""" + state = _get_state_manager() + session = state.get_session(session_id) + if session is None: + raise HTTPException(status_code=404, detail="Session not found") + cmd = _paper_cmd("run-close", session.session_name, req.date, req.force) + return _launch_task(cmd, session.session_name, "run-close") + + +@router.post("/sessions/{session_id}/run-open") +def run_open(session_id: str, req: RunRequest = RunRequest()) -> dict[str, Any]: + """장 시작 직후: 전날 exit + after-close 이벤트 → 시장가 매수.""" + state = _get_state_manager() + session = state.get_session(session_id) + if session is None: + raise HTTPException(status_code=404, detail="Session not found") + cmd = _paper_cmd("run-open", session.session_name, req.date, req.force) + return _launch_task(cmd, session.session_name, "run-open") + + +@router.post("/run-all") +def run_all_sessions() -> dict[str, Any]: + """Run daily processing for all active sessions.""" + state = _get_state_manager() + active = [s for s in state.list_sessions() if s.status == "active"] + if not active: + return {"tasks": [], "message": "No active sessions found"} + cmd = [ + sys.executable, "-m", "apps.paper_trader.cli", + "run-all", "--db", _get_db_path(), + ] + task = _launch_task(cmd, "all", "run-all") + return {"tasks": [task]} + + +# ── Task management ──────────────────────────────────────────────────────────── + +@router.get("/tasks") +def list_tasks(session_name: str | None = Query(None)) -> dict[str, Any]: + with _tasks_lock: + tasks = list(_tasks.values()) + if session_name: + tasks = [t for t in tasks if t.get("session_name") == session_name] + tasks.sort(key=lambda t: t.get("created_at", ""), reverse=True) + # Don't send full log in list view + slim = [{k: v for k, v in t.items() if k != "log"} for t in tasks] + return {"tasks": slim} + + +@router.get("/tasks/{task_id}") +def get_task(task_id: str) -> dict[str, Any]: + with _tasks_lock: + task = _tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="Task not found") + return dict(task) + + +@router.get("/tasks/{task_id}/log") +def get_task_log(task_id: str) -> dict[str, Any]: + with _tasks_lock: + task = _tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="Task not found") + return {"log": task.get("log", ""), "status": task["status"]} + + +# ── Utility ──────────────────────────────────────────────────────────────────── + +@router.get("/configs") +def list_configs( + status: str | None = Query(None, description="Filter by status (active/retired)"), + pattern: str | None = Query(None, description="Filter by name pattern"), + limit: int = Query(200, le=1000), +) -> dict[str, Any]: + """List available experiment configs for session creation.""" + configs_dir = get_project_root() / "configs" / "experiments" + if not configs_dir.exists(): + return {"configs": [], "total": 0} + + index_path = configs_dir / ".index.json" + configs = [] + if index_path.exists(): + import json + try: + index = json.loads(index_path.read_text()) + for name, meta in sorted(index.get("experiments", {}).items(), key=lambda x: x[0]): + cfg_path = configs_dir / f"{name}.json" + if not cfg_path.exists(): + continue + s = meta.get("status", "active") + if status and s != status: + continue + if pattern and pattern.lower() not in name.lower(): + continue + configs.append({ + "name": name, + "path": f"configs/experiments/{name}.json", + "id": meta.get("id"), + "status": s, + "version_family": meta.get("version_family"), + "generation": meta.get("generation"), + "parent": meta.get("parent"), + }) + except Exception: + pass + + if not configs: + for f in sorted(configs_dir.glob("*.json")): + if f.name.startswith("."): + continue + if pattern and pattern.lower() not in f.stem.lower(): + continue + configs.append({ + "name": f.stem, + "path": f"configs/experiments/{f.name}", + "id": None, + "status": "active", + "version_family": None, + "generation": None, + "parent": None, + }) + + total = len(configs) + return {"configs": configs[:limit], "total": total} + + +# ── Auto daemon endpoints ────────────────────────────────────────────────────── + +class AutoStartRequest(BaseModel): + sessions: list[str] = [] # empty = all active + dry_run: bool = False + + +@router.get("/auto") +def get_auto_status() -> dict[str, Any]: + """Get auto daemon status + today's schedule.""" + status = _auto_status() + log_tail: list[str] = [] + log_file = _auto_log_file() + if log_file.exists(): + try: + lines = log_file.read_text(errors="replace").splitlines() + log_tail = lines[-80:] # last 80 lines for status panel + except Exception: + pass + return { + **status, + "schedule": _auto_schedule_info(), + "log_tail": log_tail, + "log_lines": len(log_tail), + } + + +@router.post("/auto/start") +def start_auto(req: AutoStartRequest) -> dict[str, Any]: + """Start the auto daemon. Returns 409 if already running.""" + status = _auto_status() + if status["running"]: + raise HTTPException( + status_code=409, + detail=f"Auto daemon already running (PID {status['pid']}). Stop it first.", + ) + + project_root = get_project_root() + log_file = _auto_log_file() + pid_file = _auto_pid_file() + + cmd = [sys.executable, "-m", "apps.paper_trader.cli", "auto", + "--db", _get_db_path()] + for s in req.sessions: + cmd += ["--session", s] + if req.dry_run: + cmd.append("--dry-run") + + # Open log file for subprocess output + log_fp = open(log_file, "w", buffering=1) + proc = subprocess.Popen( + cmd, + stdout=log_fp, + stderr=subprocess.STDOUT, + cwd=str(project_root), + start_new_session=True, # detach from web server process group + ) + pid_file.write_text(str(proc.pid)) + + # Monitor in background: clean up PID file when process exits + def _watch(): + proc.wait() + log_fp.close() + pid_file.unlink(missing_ok=True) + + threading.Thread(target=_watch, daemon=True).start() + + return {"started": True, "pid": proc.pid, "dry_run": req.dry_run, "sessions": req.sessions} + + +@router.post("/auto/stop") +def stop_auto() -> dict[str, Any]: + """Send SIGTERM to the auto daemon.""" + import signal as _signal + status = _auto_status() + if not status["running"]: + raise HTTPException(status_code=409, detail="Auto daemon is not running.") + pid = status["pid"] + try: + os.kill(pid, _signal.SIGTERM) + except ProcessLookupError: + _auto_pid_file().unlink(missing_ok=True) + raise HTTPException(status_code=404, detail="Process not found (already exited?)") + return {"stopped": True, "pid": pid} + + +@router.get("/auto/log") +def get_auto_log(lines: int = Query(200, le=2000)) -> dict[str, Any]: + """Get the last N lines of the auto daemon log. + + Priority: + 1. Process stdout file (wherever the running process is writing) + 2. Fallback: known web-GUI log file + """ + # Find where the running process is logging + status = _auto_status() + log_source: Path | None = None + log_source_label = "none" + + if status["running"] and status.get("log_path"): + candidate = Path(status["log_path"]) + if candidate.exists(): + log_source = candidate + log_source_label = str(candidate) + + # Fallback: web-GUI log file + if log_source is None: + fallback = _auto_log_file() + if fallback.exists(): + log_source = fallback + log_source_label = str(fallback) + + if log_source is None: + tty = status.get("log_tty") if status["running"] else None + if tty: + return { + "log": f"[Auto daemon is running but outputting to terminal: {tty}]\n" + f"Log cannot be captured from an active TTY.\n\n" + f"To view logs in the web GUI, stop the daemon and restart it here,\n" + f"or run with output redirected:\n\n" + f" fithia2 paper auto 2>&1 | tee ~/.paper_auto.log", + "lines": 0, + "source": f"tty:{tty}", + "tty": True, + } + return {"log": "", "lines": 0, "source": "none"} + + try: + all_lines = log_source.read_text(errors="replace").splitlines() + tail = all_lines[-lines:] + return { + "log": "\n".join(tail), + "lines": len(all_lines), + "source": log_source_label, + } + except Exception as exc: + raise HTTPException(status_code=500, detail=str(exc)) diff --git a/apps/web/static/assets/index-CFG6oWxa.js b/apps/web/static/assets/index-CFG6oWxa.js new file mode 100644 index 0000000..e185cb7 --- /dev/null +++ b/apps/web/static/assets/index-CFG6oWxa.js @@ -0,0 +1,76 @@ +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),s=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},c=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},l=(n,r,a)=>(a=n==null?{}:e(i(n)),c(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n)),u=(e=>typeof require<`u`?require:typeof Proxy<`u`?new Proxy(e,{get:(e,t)=>(typeof require<`u`?require:e)[t]}):e)(function(e){if(typeof require<`u`)return require.apply(this,arguments);throw Error('Calling `require` for "'+e+"\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.")});(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var d=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},T=Object.prototype.hasOwnProperty;function E(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function D(e,t){return E(e.type,t,e.props)}function O(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function k(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var A=/\/+/g;function j(e,t){return typeof e==`object`&&e&&e.key!=null?k(``+e.key):t.toString(36)}function M(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function N(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,N(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+j(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(A,`$&/`)+`/`),N(o,r,i,``,function(e){return e})):o!=null&&(O(o)&&(o=D(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(A,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=d()})),p=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,S||(S=!0,O());else{var t=n(l);t!==null&&j(x,t.startTime-e)}}var S=!1,C=-1,w=5,T=-1;function E(){return g?!0:!(e.unstable_now()-Tt&&E());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&j(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?O():S=!1}}}var O;if(typeof y==`function`)O=function(){y(D)};else if(typeof MessageChannel<`u`){var k=new MessageChannel,A=k.port2;k.port1.onmessage=D,O=function(){A.postMessage(null)}}else O=function(){_(D,0)};function j(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,j(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,O()))),r},e.unstable_shouldYield=E,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),m=o(((e,t)=>{t.exports=p()})),h=o((e=>{var t=f();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=h()})),_=o((e=>{var t=m(),n=f(),r=g();function i(e){var t=`https://react.dev/errors/`+e;if(1L||(e.current=ne[L],ne[L]=null,L--)}function z(e,t){L++,ne[L]=e.current,e.current=t}var ie=re(null),ae=re(null),oe=re(null),se=re(null);function ce(e,t){switch(z(oe,t),z(ae,e),z(ie,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Gd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Gd(t),e=Kd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}R(ie),z(ie,e)}function le(){R(ie),R(ae),R(oe)}function ue(e){e.memoizedState!==null&&z(se,e);var t=ie.current,n=Kd(t,e.type);t!==n&&(z(ae,e),z(ie,n))}function de(e){ae.current===e&&(R(ie),R(ae)),se.current===e&&(R(se),tp._currentValue=te)}var fe,pe;function me(e){if(fe===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);fe=t&&t[1]||``,pe=-1)`:-1i||c[r]!==l[i]){var u=` +`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{he=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?me(n):``}function _e(e,t){switch(e.tag){case 26:case 27:case 5:return me(e.type);case 16:return me(`Lazy`);case 13:return e.child!==t&&t!==null?me(`Suspense Fallback`):me(`Suspense`);case 19:return me(`SuspenseList`);case 0:case 15:return ge(e.type,!1);case 11:return ge(e.type.render,!1);case 1:return ge(e.type,!0);case 31:return me(`Activity`);default:return``}}function ve(e){try{var t=``,n=null;do t+=_e(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var ye=Object.prototype.hasOwnProperty,be=t.unstable_scheduleCallback,xe=t.unstable_cancelCallback,Se=t.unstable_shouldYield,Ce=t.unstable_requestPaint,we=t.unstable_now,Te=t.unstable_getCurrentPriorityLevel,Ee=t.unstable_ImmediatePriority,De=t.unstable_UserBlockingPriority,Oe=t.unstable_NormalPriority,ke=t.unstable_LowPriority,Ae=t.unstable_IdlePriority,je=t.log,Me=t.unstable_setDisableYieldValue,Ne=null,Pe=null;function Fe(e){if(typeof je==`function`&&Me(e),Pe&&typeof Pe.setStrictMode==`function`)try{Pe.setStrictMode(Ne,e)}catch{}}var Ie=Math.clz32?Math.clz32:ze,Le=Math.log,Re=Math.LN2;function ze(e){return e>>>=0,e===0?32:31-(Le(e)/Re|0)|0}var Be=256,Ve=262144,He=4194304;function Ue(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function We(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=Ue(n))):i=Ue(o):i=Ue(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Ue(n))):i=Ue(o)):i=Ue(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Ge(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Ke(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function qe(){var e=He;return He<<=1,!(He&62914560)&&(He=4194304),e}function Je(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Ye(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Xe(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),ln=!1;if(cn)try{var un={};Object.defineProperty(un,`passive`,{get:function(){ln=!0}}),window.addEventListener(`test`,un,un),window.removeEventListener(`test`,un,un)}catch{ln=!1}var dn=null,fn=null,pn=null;function mn(){if(pn)return pn;var e,t=fn,n=t.length,r,i=`value`in dn?dn.value:dn.textContent,a=i.length;for(e=0;e=Kn),Yn=` `,Xn=!1;function Zn(e,t){switch(e){case`keyup`:return Wn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Qn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var $n=!1;function er(e,t){switch(e){case`compositionend`:return Qn(t);case`keypress`:return t.which===32?(Xn=!0,Yn):null;case`textInput`:return e=t.data,e===Yn&&Xn?null:e;default:return null}}function tr(e,t){if($n)return e===`compositionend`||!Gn&&Zn(e,t)?(e=mn(),pn=fn=dn=null,$n=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=Cr(n)}}function Tr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Tr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Er(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Ft(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Ft(e.document)}return t}function Dr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Or=cn&&`documentMode`in document&&11>=document.documentMode,kr=null,Ar=null,jr=null,Mr=!1;function Nr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Mr||kr==null||kr!==Ft(r)||(r=kr,`selectionStart`in r&&Dr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),jr&&Sr(jr,r)||(jr=r,r=kd(Ar,`onSelect`),0>=o,i-=o,Ci=1<<32-Ie(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),H&&Ti(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),H&&Ti(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return H&&Ti(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),H&&Ti(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===O&&wa(l)===r.type){n(e,r.sibling),c=a(r,o.props),ja(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=di(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=ui(o.type,o.key,o.props,null,e.mode,c),ja(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=mi(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=wa(o),b(e,r,o,c)}if(ee(o))return h(e,r,o,c);if(M(o)){if(l=M(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,Aa(o),c);if(o.$$typeof===C)return b(e,r,$i(e,o),c);W(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=fi(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{ka=0;var i=b(e,t,n,r);return Oa=null,i}catch(t){if(t===va||t===ba)throw t;var a=oi(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ma=G(!0),Na=G(!1),Pa=!1;function Fa(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Ia(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function La(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Ra(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Rl&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=ri(e),ni(e,null,n),t}return $r(e,r,t,n),ri(e)}function za(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Qe(e,n)}}function Ba(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Va=!1;function Ha(){if(Va){var e=la;if(e!==null)throw e}}function Ua(e,t,n,r){Va=!1;var i=e.updateQueue;Pa=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,m=f!==s.lane;if(m?(J&f)===f:(r&f)===f){f!==0&&f===U&&(Va=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var h=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(h=g.payload,typeof h==`function`){d=h.call(_,d,f);break a}d=h;break a;case 3:h.flags=h.flags&-65537|128;case 0:if(h=g.payload,f=typeof h==`function`?h.call(_,d,f):h,f==null)break a;d=p({},d,f);break a;case 2:Pa=!0}}f=s.callback,f!==null&&(e.flags|=64,m&&(e.flags|=8192),m=i.callbacks,m===null?i.callbacks=[f]:m.push(f))}else m={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=m,c=d):u=u.next=m,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;m=s,s=m.next,m.next=null,i.lastBaseUpdate=m,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),ql|=o,e.lanes=o,e.memoizedState=d}}function Wa(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Ga(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=F.T,s={};F.T=s,js(e,!1,t,n);try{var c=i(),l=F.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?As(e,t,fa(c,r),hu(e)):As(e,t,r,hu(e))}catch(n){As(e,t,{then:function(){},status:`rejected`,reason:n},hu())}finally{I.p=a,o!==null&&s.types!==null&&(o.types=s.types),F.T=o}}function bs(){}function xs(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=Ss(e).queue;ys(e,a,t,te,n===null?bs:function(){return Cs(e),n(r)})}function Ss(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:te,baseState:te,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Mo,lastRenderedState:te},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Mo,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Cs(e){var t=Ss(e);t.next===null&&(t=e.alternate.memoizedState),As(e,t.next.queue,{},hu())}function ws(){return Qi(tp)}function Ts(){return Do().memoizedState}function Es(){return Do().memoizedState}function Ds(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=hu();e=La(n);var r=Ra(t,e,n);r!==null&&(_u(r,t,n),za(r,t,n)),t={cache:aa()},e.payload=t;return}t=t.return}}function Os(e,t,n){var r=hu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Ms(e)?Ns(t,n):(n=ei(e,t,n,r),n!==null&&(_u(n,e,r),Ps(n,t,r)))}function ks(e,t,n){As(e,t,n,hu())}function As(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ms(e))Ns(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,xr(s,o))return $r(e,t,i,0),zl===null&&Qr(),!1}catch{}if(n=ei(e,t,i,r),n!==null)return _u(n,e,r),Ps(n,t,r),!0}return!1}function js(e,t,n,r){if(r={lane:2,revertLane:md(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Ms(e)){if(t)throw Error(i(479))}else t=ei(e,n,r,2),t!==null&&_u(t,e,2)}function Ms(e){var t=e.alternate;return e===K||t!==null&&t===K}function Ns(e,t){uo=lo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ps(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Qe(e,n)}}var Fs={readContext:Qi,use:Ao,useCallback:_o,useContext:_o,useEffect:_o,useImperativeHandle:_o,useLayoutEffect:_o,useInsertionEffect:_o,useMemo:_o,useReducer:_o,useRef:_o,useState:_o,useDebugValue:_o,useDeferredValue:_o,useTransition:_o,useSyncExternalStore:_o,useId:_o,useHostTransitionStatus:_o,useFormState:_o,useActionState:_o,useOptimistic:_o,useMemoCache:_o,useCacheRefresh:_o};Fs.useEffectEvent=_o;var Is={readContext:Qi,use:Ao,useCallback:function(e,t){return Eo().memoizedState=[e,t===void 0?null:t],e},useContext:Qi,useEffect:os,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),is(4194308,4,fs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return is(4194308,4,e,t)},useInsertionEffect:function(e,t){is(4,2,e,t)},useMemo:function(e,t){var n=Eo();t=t===void 0?null:t;var r=e();if(fo){Fe(!0);try{e()}finally{Fe(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Eo();if(n!==void 0){var i=n(t);if(fo){Fe(!0);try{n(t)}finally{Fe(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Os.bind(null,K,e),[r.memoizedState,e]},useRef:function(e){var t=Eo();return e={current:e},t.memoizedState=e},useState:function(e){e=Ho(e);var t=e.queue,n=ks.bind(null,K,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:ms,useDeferredValue:function(e,t){return _s(Eo(),e,t)},useTransition:function(){var e=Ho(!1);return e=ys.bind(null,K,e.queue,!0,!1),Eo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=K,a=Eo();if(H){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),zl===null)throw Error(i(349));J&127||Lo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,os(zo.bind(null,r,o,e),[e]),r.flags|=2048,ns(9,{destroy:void 0},Ro.bind(null,r,o,n,t),null),n},useId:function(){var e=Eo(),t=zl.identifierPrefix;if(H){var n=wi,r=Ci;n=(r&~(1<<32-Ie(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=po++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[at]=t,o[ot]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Rd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Ac(t)}}return Fc(t),jc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Ac(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=oe.current,Ri(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Ai,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[at]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Fd(e.nodeValue,n)),e||Fi(t,!0)}else e=Wd(e).createTextNode(r),e[at]=t,t.stateNode=e}return Fc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Ri(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[at]=t}else zi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Fc(t),e=!1}else n=Bi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(ro(t),t):(ro(t),null);if(t.flags&128)throw Error(i(558))}return Fc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Ri(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[at]=t}else zi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Fc(t),a=!1}else a=Bi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(ro(t),t):(ro(t),null)}return ro(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Nc(t,t.updateQueue),Fc(t),null);case 4:return le(),e===null&&Td(t.stateNode.containerInfo),Fc(t),null;case 10:return Ki(t.type),Fc(t),null;case 19:if(R(io),r=t.memoizedState,r===null)return Fc(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Pc(r,!1);else{if(Kl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=ao(e),o!==null){for(t.flags|=128,Pc(r,!1),e=o.updateQueue,t.updateQueue=e,Nc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)li(n,e),n=n.sibling;return z(io,io.current&1|2),H&&Ti(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&we()>ru&&(t.flags|=128,a=!0,Pc(r,!1),t.lanes=4194304)}else{if(!a)if(e=ao(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Nc(t,e),Pc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!H)return Fc(t),null}else 2*we()-r.renderingStartTime>ru&&n!==536870912&&(t.flags|=128,a=!0,Pc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(Fc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=we(),e.sibling=null,n=io.current,z(io,a?n&1|2:n&1),H&&Ti(t,r.treeForkCount),e);case 22:case 23:return ro(t),Xa(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Fc(t),t.subtreeFlags&6&&(t.flags|=8192)):Fc(t),n=t.updateQueue,n!==null&&Nc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&R(ma),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Ki(ia),Fc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Lc(e,t){switch(Oi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Ki(ia),le(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return de(t),null;case 31:if(t.memoizedState!==null){if(ro(t),t.alternate===null)throw Error(i(340));zi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(ro(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));zi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return R(io),null;case 4:return le(),null;case 10:return Ki(t.type),null;case 22:case 23:return ro(t),Xa(),e!==null&&R(ma),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Ki(ia),null;case 25:return null;default:return null}}function Rc(e,t){switch(Oi(t),t.tag){case 3:Ki(ia),le();break;case 26:case 27:case 5:de(t);break;case 4:le();break;case 31:t.memoizedState!==null&&ro(t);break;case 13:ro(t);break;case 19:R(io);break;case 10:Ki(t.type);break;case 22:case 23:ro(t),Xa(),e!==null&&R(ma);break;case 24:Ki(ia)}}function zc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){qu(t,t.return,e)}}function Bc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){qu(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){qu(t,t.return,e)}}function Vc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Ga(t,n)}catch(t){qu(e,e.return,t)}}}function Hc(e,t,n){n.props=Us(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){qu(e,t,n)}}function Uc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){qu(e,t,n)}}function Wc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){qu(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){qu(e,t,n)}else n.current=null}function Gc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){qu(e,e.return,t)}}function Kc(e,t,n){try{var r=e.stateNode;zd(r,e.type,n,t),r[ot]=t}catch(t){qu(e,e.return,t)}}function qc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&tf(e.type)||e.tag===4}function Jc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||qc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&tf(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Yc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Qt));else if(r!==4&&(r===27&&tf(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Yc(e,t,n),e=e.sibling;e!==null;)Yc(e,t,n),e=e.sibling}function Xc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&tf(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(Xc(e,t,n),e=e.sibling;e!==null;)Xc(e,t,n),e=e.sibling}function Zc(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Rd(t,r,n),t[at]=e,t[ot]=n}catch(t){qu(e,e.return,t)}}var Qc=!1,$c=!1,el=!1,tl=typeof WeakSet==`function`?WeakSet:Set,nl=null;function rl(e,t){if(e=e.containerInfo,Hd=up,e=Er(e),Dr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(Ud={focusedElem:e,selectionRange:n},up=!1,nl=t;nl!==null;)if(t=nl,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,nl=e;else for(;nl!==null;){switch(t=nl,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Rd(o,r,n),o[at]=e,vt(o),r=o;break a;case`link`:var s=Gf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=wr(s,h),v=wr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,F.T=null,n=du,du=null;var o=su,s=lu;if(ou=0,cu=su=null,lu=0,Rl&6)throw Error(i(331));var c=Rl;if(Rl|=4,Nl(o.current),Tl(o,o.current,s,n),Rl=c,sd(0,!1),Pe&&typeof Pe.onPostCommitFiberRoot==`function`)try{Pe.onPostCommitFiberRoot(Ne,o)}catch{}return!0}finally{I.p=a,F.T=r,Uu(e,t)}}function Ku(e,t,n){t=gi(n,t),t=Ys(e.stateNode,t,2),e=Ra(e,t,2),e!==null&&(Ye(e,2),od(e))}function qu(e,t,n){if(e.tag===3)Ku(e,e,n);else for(;t!==null;){if(t.tag===3){Ku(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(au===null||!au.has(r))){e=gi(n,e),n=Xs(2),r=Ra(t,n,2),r!==null&&(Zs(n,r,t,e),Ye(r,2),od(r));break}}t=t.return}}function Ju(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Ll;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Wl=!0,i.add(n),e=Yu.bind(null,e,t,n),t.then(e,e))}function Yu(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,zl===e&&(J&n)===n&&(Kl===4||Kl===3&&(J&62914560)===J&&300>we()-tu?!(Rl&2)&&wu(e,0):Yl|=n,Zl===J&&(Zl=0)),od(e)}function Xu(e,t){t===0&&(t=qe()),e=ti(e,t),e!==null&&(Ye(e,t),od(e))}function Zu(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Xu(e,n)}function Qu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),Xu(e,n)}function $u(e,t){return be(e,t)}var ed=null,td=null,nd=!1,rd=!1,id=!1,ad=0;function od(e){e!==td&&e.next===null&&(td===null?ed=td=e:td=td.next=e),rd=!0,nd||(nd=!0,pd())}function sd(e,t){if(!id&&rd){id=!0;do for(var n=!1,r=ed;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Ie(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,fd(r,a))}else a=J,a=We(r,r===zl?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Ge(r,a)||(n=!0,fd(r,a));r=r.next}while(n);id=!1}}function cd(){ld()}function ld(){rd=nd=!1;var e=0;ad!==0&&Yd()&&(e=ad);for(var t=we(),n=null,r=ed;r!==null;){var i=r.next,a=ud(r,t);a===0?(r.next=null,n===null?ed=i:n.next=i,i===null&&(td=n)):(n=r,(e!==0||a&3)&&(rd=!0)),r=i}ou!==0&&ou!==5||sd(e,!1),ad!==0&&(ad=0)}function ud(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Bd(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function Tf(e,t,n){var r=wf;if(r&&typeof t==`string`&&t){var i=Lt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),yf.has(i)||(yf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Rd(t,`link`,e),vt(t),r.head.appendChild(t)))}}function Ef(e){xf.D(e),Tf(`dns-prefetch`,e,null)}function Df(e,t){xf.C(e,t),Tf(`preconnect`,e,t)}function Of(e,t,n){xf.L(e,t,n);var r=wf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Lt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Lt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Lt(n.imageSizes)+`"]`)):i+=`[href="`+Lt(e)+`"]`;var a=i;switch(t){case`style`:a=Pf(e);break;case`script`:a=Rf(e)}vf.has(a)||(e=p({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),vf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Ff(a))||t===`script`&&r.querySelector(zf(a))||(t=r.createElement(`link`),Rd(t,`link`,e),vt(t),r.head.appendChild(t)))}}function kf(e,t){xf.m(e,t);var n=wf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Lt(r)+`"][href="`+Lt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Rf(e)}if(!vf.has(a)&&(e=p({rel:`modulepreload`,href:e},t),vf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(zf(a)))return}r=n.createElement(`link`),Rd(r,`link`,e),vt(r),n.head.appendChild(r)}}}function Af(e,t,n){xf.S(e,t,n);var r=wf;if(r&&e){var i=_t(r).hoistableStyles,a=Pf(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Ff(a)))s.loading=5;else{e=p({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=vf.get(a))&&Hf(e,n);var c=o=r.createElement(`link`);vt(c),Rd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Vf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function jf(e,t){xf.X(e,t);var n=wf;if(n&&e){var r=_t(n).hoistableScripts,i=Rf(e),a=r.get(i);a||(a=n.querySelector(zf(i)),a||(e=p({src:e,async:!0},t),(t=vf.get(i))&&Uf(e,t),a=n.createElement(`script`),vt(a),Rd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Mf(e,t){xf.M(e,t);var n=wf;if(n&&e){var r=_t(n).hoistableScripts,i=Rf(e),a=r.get(i);a||(a=n.querySelector(zf(i)),a||(e=p({src:e,async:!0,type:`module`},t),(t=vf.get(i))&&Uf(e,t),a=n.createElement(`script`),vt(a),Rd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Nf(e,t,n,r){var a=(a=oe.current)?bf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Pf(n.href),n=_t(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Pf(n.href);var o=_t(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(Ff(e)))&&!o._p&&(s.instance=o,s.state.loading=5),vf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},vf.set(e,n),o||Lf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Rf(n),n=_t(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Pf(e){return`href="`+Lt(e)+`"`}function Ff(e){return`link[rel="stylesheet"][`+e+`]`}function If(e){return p({},e,{"data-precedence":e.precedence,precedence:null})}function Lf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Rd(t,`link`,n),vt(t),e.head.appendChild(t))}function Rf(e){return`[src="`+Lt(e)+`"]`}function zf(e){return`script[async]`+e}function Bf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Lt(n.href)+`"]`);if(r)return t.instance=r,vt(r),r;var a=p({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),vt(r),Rd(r,`style`,a),Vf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Pf(n.href);var o=e.querySelector(Ff(a));if(o)return t.state.loading|=4,t.instance=o,vt(o),o;r=If(n),(a=vf.get(a))&&Hf(r,a),o=(e.ownerDocument||e).createElement(`link`),vt(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Rd(o,`link`,r),t.state.loading|=4,Vf(o,n.precedence,e),t.instance=o;case`script`:return o=Rf(n.src),(a=e.querySelector(zf(o)))?(t.instance=a,vt(a),a):(r=n,(a=vf.get(o))&&(r=p({},n),Uf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),vt(a),Rd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Vf(r,n.precedence,e));return t.instance}function Vf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function qf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Jf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Yf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Pf(r.href),a=t.querySelector(Ff(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=X.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,vt(a);return}a=t.ownerDocument||t,r=If(r),(i=vf.get(i))&&Hf(r,i),a=a.createElement(`link`),vt(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Rd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=X.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Xf=0;function Zf(e,t){return e.stylesheets&&e.count===0&&$f(e,e.stylesheets),0Xf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function X(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)$f(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Qf=null;function $f(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Qf=new Map,t.forEach(ep,e),Qf=null,X.call(e))}function ep(e,t){if(!(t.state.loading&4)){var n=Qf.get(e);if(n)var r=n.get(null);else{n=new Map,Qf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=_()})),y=l(f(),1),b=v(),x=`modulepreload`,S=function(e){return`/`+e},C={},w=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=S(t,n),t in C)return;C[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:x,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},T=`popstate`;function E(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function D(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return M(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:N(t)}return ee(t,n,null,e)}function O(e,t){if(e===!1||e==null)throw Error(t)}function k(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function A(){return Math.random().toString(36).substring(2,10)}function j(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.unstable_mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function M(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?P(t):t,state:n,key:t&&t.key||r||A(),unstable_mask:i}}function N({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function P(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function ee(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=E(e)?e:M(h.location,e,t);n&&n(r,e),l=u()+1;let d=j(r,l),f=h.createHref(r.unstable_mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=E(e)?e:M(h.location,e,t);n&&n(r,e),l=u();let i=j(r,l),d=h.createHref(r.unstable_mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return F(e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(T,d),c=e,()=>{i.removeEventListener(T,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function F(e,t=!1){let n=`http://localhost`;typeof window<`u`&&(n=window.location.origin===`null`?window.location.href:window.location.origin),O(n,`No window.location.(origin|href) available to create URL`);let r=typeof e==`string`?e:N(e);return r=r.replace(/ $/,`%20`),!t&&r.startsWith(`//`)&&(r=n+r),new URL(r,n)}function I(e,t,n=`/`){return te(e,t,n,!1)}function te(e,t,n,r){let i=ge((typeof t==`string`?P(t):t).pathname||`/`,n);if(i==null)return null;let a=L(e);R(a);let o=null;for(let e=0;o==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;O(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=we([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(O(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),L(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:ue(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of re(e.path))a(e,t,!0,n)}),t}function re(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=re(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function R(e){e.sort((e,t)=>e.score===t.score?de(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var z=/^:[\w-]+$/,ie=3,ae=2,oe=1,se=10,ce=-2,le=e=>e===`*`;function ue(e,t){let n=e.split(`/`),r=n.length;return n.some(le)&&(r+=ce),t&&(r+=ae),n.filter(e=>!le(e)).reduce((e,t)=>e+(z.test(t)?ie:t===``?oe:se),r)}function de(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function fe(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return n&&!i?e[t]=void 0:e[t]=(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function me(e,t=!1,n=!0){k(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function he(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return k(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function ge(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}var _e=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function ve(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?P(e):e,a;return n?(n=n.replace(/\/\/+/g,`/`),a=n.startsWith(`/`)?ye(n.substring(1),`/`):ye(n,t)):a=t,{pathname:a,search:Ee(r),hash:De(i)}}function ye(e,t){let n=t.replace(/\/+$/,``).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function be(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function xe(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function Se(e){let t=xe(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function Ce(e,t,n,r=!1){let i;typeof e==`string`?i=P(e):(i={...e},O(!i.pathname||!i.pathname.includes(`?`),be(`?`,`pathname`,`search`,i)),O(!i.pathname||!i.pathname.includes(`#`),be(`#`,`pathname`,`hash`,i)),O(!i.search||!i.search.includes(`#`),be(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=ve(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var we=e=>e.join(`/`).replace(/\/\/+/g,`/`),Te=e=>e.replace(/\/+$/,``).replace(/^\/*/,`/`),Ee=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,De=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,Oe=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function ke(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Ae(e){return e.map(e=>e.route.path).filter(Boolean).join(`/`).replace(/\/\/*/g,`/`)||`/`}var je=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Me(e,t){let n=e;if(typeof n!=`string`||!_e.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(je)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=ge(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{k(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var Ne=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Ne);var Pe=[`GET`,...Ne];new Set(Pe);var Fe=y.createContext(null);Fe.displayName=`DataRouter`;var Ie=y.createContext(null);Ie.displayName=`DataRouterState`;var Le=y.createContext(!1),Re=y.createContext({isTransitioning:!1});Re.displayName=`ViewTransition`;var ze=y.createContext(new Map);ze.displayName=`Fetchers`;var Be=y.createContext(null);Be.displayName=`Await`;var Ve=y.createContext(null);Ve.displayName=`Navigation`;var He=y.createContext(null);He.displayName=`Location`;var Ue=y.createContext({outlet:null,matches:[],isDataRoute:!1});Ue.displayName=`Route`;var We=y.createContext(null);We.displayName=`RouteError`;var Ge=`REACT_ROUTER_ERROR`,Ke=`REDIRECT`,qe=`ROUTE_ERROR_RESPONSE`;function Je(e){if(e.startsWith(`${Ge}:${Ke}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function Ye(e){if(e.startsWith(`${Ge}:${qe}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new Oe(t.status,t.statusText,t.data)}catch{}}function Xe(e,{relative:t}={}){O(Ze(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=y.useContext(Ve),{hash:i,pathname:a,search:o}=it(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:we([n,a])),r.createHref({pathname:s,search:o,hash:i})}function Ze(){return y.useContext(He)!=null}function Qe(){return O(Ze(),`useLocation() may be used only in the context of a component.`),y.useContext(He).location}var $e=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function et(e){y.useContext(Ve).static||y.useLayoutEffect(e)}function tt(){let{isDataRoute:e}=y.useContext(Ue);return e?Ct():nt()}function nt(){O(Ze(),`useNavigate() may be used only in the context of a component.`);let e=y.useContext(Fe),{basename:t,navigator:n}=y.useContext(Ve),{matches:r}=y.useContext(Ue),{pathname:i}=Qe(),a=JSON.stringify(Se(r)),o=y.useRef(!1);return et(()=>{o.current=!0}),y.useCallback((r,s={})=>{if(k(o.current,$e),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=Ce(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:we([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}y.createContext(null);function rt(){let{matches:e}=y.useContext(Ue),t=e[e.length-1];return t?t.params:{}}function it(e,{relative:t}={}){let{matches:n}=y.useContext(Ue),{pathname:r}=Qe(),i=JSON.stringify(Se(n));return y.useMemo(()=>Ce(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function at(e,t){return ot(e,t)}function ot(e,t,n){O(Ze(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=y.useContext(Ve),{matches:i}=y.useContext(Ue),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;Tt(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let u=Qe(),d;if(t){let e=typeof t==`string`?P(t):t;O(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=I(e,{pathname:p});k(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),k(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let h=pt(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:we([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:we([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?y.createElement(He.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,unstable_mask:void 0,...d},navigationType:`POP`}},h):h}function st(){let e=St(),t=ke(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=y.createElement(y.Fragment,null,y.createElement(`p`,null,`💿 Hey developer 👋`),y.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,y.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,y.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),y.createElement(y.Fragment,null,y.createElement(`h2`,null,`Unexpected Application Error!`),y.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?y.createElement(`pre`,{style:i},n):null,o)}var ct=y.createElement(st,null),lt=class extends y.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=Ye(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:y.createElement(Ue.Provider,{value:this.props.routeContext},y.createElement(We.Provider,{value:e,children:this.props.component}));return this.context?y.createElement(dt,{error:e},t):t}};lt.contextType=Le;var ut=new WeakMap;function dt({children:e,error:t}){let{basename:n}=y.useContext(Ve);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=Je(t.digest);if(e){let r=ut.get(t);if(r)throw r;let i=Me(e.location,n);if(je&&!ut.get(t))if(i.isExternal||e.reloadDocument)window.location.href=i.absoluteURL||i.to;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw ut.set(t,n),n}return y.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function ft({routeContext:e,match:t,children:n}){let r=y.useContext(Fe);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),y.createElement(Ue.Provider,{value:e},n)}function pt(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);O(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},unstable_pattern:Ae(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||ct,o&&(s<0&&c===0?(Tt(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?y.createElement(n.route.Component,null):n.route.element?n.route.element:e,y.createElement(ft,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?y.createElement(lt,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function mt(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function ht(e){let t=y.useContext(Fe);return O(t,mt(e)),t}function gt(e){let t=y.useContext(Ie);return O(t,mt(e)),t}function _t(e){let t=y.useContext(Ue);return O(t,mt(e)),t}function vt(e){let t=_t(e),n=t.matches[t.matches.length-1];return O(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function yt(){return vt(`useRouteId`)}function bt(){return gt(`useNavigation`).navigation}function xt(){let{matches:e,loaderData:t}=gt(`useMatches`);return y.useMemo(()=>e.map(e=>ne(e,t)),[e,t])}function St(){let e=y.useContext(We),t=gt(`useRouteError`),n=vt(`useRouteError`);return e===void 0?t.errors?.[n]:e}function Ct(){let{router:e}=ht(`useNavigate`),t=vt(`useNavigate`),n=y.useRef(!1);return et(()=>{n.current=!0}),y.useCallback(async(r,i={})=>{k(n.current,$e),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var wt={};function Tt(e,t,n){!t&&!wt[e]&&(wt[e]=!0,k(!1,n))}y.useOptimistic,y.memo(Et);function Et({routes:e,future:t,state:n,isStatic:r,onError:i}){return ot(e,void 0,{state:n,isStatic:r,onError:i,future:t})}function Dt(e){O(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Ot({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,unstable_useTransitions:o}){O(!Ze(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=y.useMemo(()=>({basename:s,navigator:i,static:a,unstable_useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=P(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,unstable_mask:m}=n,h=y.useMemo(()=>{let e=ge(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,unstable_mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return k(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:y.createElement(Ve.Provider,{value:c},y.createElement(He.Provider,{children:t,value:h}))}function kt({children:e,location:t}){return at(At(e),t)}y.Component;function At(e,t=[]){let n=[];return y.Children.forEach(e,(e,r)=>{if(!y.isValidElement(e))return;let i=[...t,r];if(e.type===y.Fragment){n.push.apply(n,At(e.props.children,i));return}O(e.type===Dt,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),O(!e.props.index||!e.props.children,`An index route cannot have child routes.`);let a={id:e.props.id||i.join(`-`),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,middleware:e.props.middleware,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:e.props.hasErrorBoundary===!0||e.props.ErrorBoundary!=null||e.props.errorElement!=null,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(a.children=At(e.props.children,i)),n.push(a)}),n}var jt=`get`,Mt=`application/x-www-form-urlencoded`;function Nt(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Pt(e){return Nt(e)&&e.tagName.toLowerCase()===`button`}function Ft(e){return Nt(e)&&e.tagName.toLowerCase()===`form`}function It(e){return Nt(e)&&e.tagName.toLowerCase()===`input`}function Lt(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Rt(e,t){return e.button===0&&(!t||t===`_self`)&&!Lt(e)}function zt(e=``){return new URLSearchParams(typeof e==`string`||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce((t,n)=>{let r=e[n];return t.concat(Array.isArray(r)?r.map(e=>[n,e]):[[n,r]])},[]))}function Bt(e,t){let n=zt(e);return t&&t.forEach((e,r)=>{n.has(r)||t.getAll(r).forEach(e=>{n.append(r,e)})}),n}var Vt=null;function Ht(){if(Vt===null)try{new FormData(document.createElement(`form`),0),Vt=!1}catch{Vt=!0}return Vt}var Ut=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Wt(e){return e!=null&&!Ut.has(e)?(k(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Mt}"`),null):e}function Gt(e,t){let n,r,i,a,o;if(Ft(e)){let o=e.getAttribute(`action`);r=o?ge(o,t):null,n=e.getAttribute(`method`)||jt,i=Wt(e.getAttribute(`enctype`))||Mt,a=new FormData(e)}else if(Pt(e)||It(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a + + + {/* Run form — collapsible */} + {showForm && ( +
+
+ Run Paper Backtest +
+ + {/* Strategy selector */} +
+
+ Strategies +
+
0 ? 8 : 0 }}> + {selectedExps.map(n => ( + + {n} + + + ))} +
+
+ { setExpInput(e.target.value); setShowExpDrop(true); }} + onFocus={() => setShowExpDrop(true)} + style={{ ...inputStyle, maxWidth: 360 }} + /> + {showExpDrop && expSuggestions.length > 0 && ( +
+ {expSuggestions.map(e => ( + + ))} +
+ )} +
+
+ + {/* Date mode toggle + inputs */} +
+ {/* Mode toggle */} +
+
+ Period +
+
+ {(['year', 'range'] as const).map(m => ( + + ))} +
+
+ + {/* Capital */} +
+
+ Capital ($) +
+ setCapital(e.target.value)} style={inputStyle} min="100" step="1000" /> +
+ + {/* Date input(s) */} + {dateMode === 'year' ? ( +
+
+ Year (YYYY) +
+ setYear(e.target.value)} style={inputStyle} /> +
+ ) : ( + <> +
+
+ Start (YYYY-MM-DD) +
+ setStartDate(e.target.value)} style={inputStyle} /> +
+
+
+ End (optional) +
+ setEndDate(e.target.value)} style={inputStyle} /> +
+ + )} + + {/* Submit */} + +
+ +
+ +
+ + {submitMutation.error && ( +
+ {(submitMutation.error as Error).message} +
+ )} +
+ )} + + {isLoading && } + {error && } + + {!isLoading && tasks.length === 0 && ( +
+
+ NO TASKS +
+

+ 전략 페이지에서 백테스트를 실행하세요 +

+ +
+ )} + + {tasks.length > 0 && ( + <> + {/* Filter bar */} +
+
+ setTaskSearch(e.target.value)} + style={{ ...inputStyle, paddingRight: taskSearch ? 28 : 10 }} + /> + {taskSearch && ( + + )} +
+
+ + +
+ {(taskSearch || taskStatusFilter) && ( + + {filteredTasks.length} / {tasks.length} + + )} +
+ +
+
+ + + + {['Status', 'Experiment', 'Capital', 'Period', 'Duration', 'Created', 'Actions'].map((h, i) => ( + + ))} + + + + {filteredTasks.map((task, i) => { + const isActive = task.status === 'running' || task.status === 'queued'; + const isDone = !isActive; + return ( + navigate(`/backtest/tasks/${task.task_id}`)} + style={{ + borderBottom: i < filteredTasks.length - 1 ? '1px solid var(--border)' : 'none', + cursor: 'pointer', transition: 'background 0.1s', + }} + onMouseEnter={e => (e.currentTarget.style.background = 'var(--cyan-dim)')} + onMouseLeave={e => (e.currentTarget.style.background = 'transparent')} + > + + + + + + + + + ); + })} + {filteredTasks.length === 0 && ( + + + + )} + +
+ {h} +
+ + +
+ {task.experiment_name} +
+ {task.run_id && ( +
+ {task.run_id} +
+ )} +
+ ${task.capital?.toLocaleString() ?? '—'} + + {task.start_date + ? task.end_date ? `${task.start_date} → ${task.end_date}` : task.start_date + : '—'} + + {elapsed(task.started_at, task.finished_at)} + + {fmtDate(task.created_at)} + e.stopPropagation()}> +
+ + {isActive && ( + + )} + {task.status === 'completed' && task.run_id && ( + + )} + {isDone && ( + + )} +
+
+ No tasks match the filter +
+
+
+ + )} + + ); +} + +// ── Backtest Task Detail Page ──────────────────────────────────────────────── + +export function BacktestTaskDetailPage() { + const { taskId } = useParams<{ taskId: string }>(); + const navigate = useNavigate(); + const qc = useQueryClient(); + + const { data: task, isLoading } = useQuery({ + queryKey: ['backtest-task', taskId], + queryFn: () => backtestApi.task(taskId!), + enabled: !!taskId, + staleTime: 0, + refetchInterval: (query) => { + const status = query.state.data?.status; + return status === 'running' || status === 'queued' ? 2000 : false; + }, + }); + + const { data: logData } = useQuery({ + queryKey: ['backtest-log', taskId], + queryFn: () => backtestApi.taskLog(taskId!), + enabled: !!taskId, + staleTime: 0, + refetchInterval: (query) => { + const d = query.state.data as { log?: string } | undefined; + // Keep polling while the task is active — derive status from sibling query cache + const status = task?.status; + return (status === 'running' || status === 'queued') ? 2000 : (d?.log ? false : 1000); + }, + }); + + const cancelMutation = useMutation({ + mutationFn: () => backtestApi.cancel(taskId!), + onSuccess: () => qc.invalidateQueries({ queryKey: ['backtest-task', taskId] }), + }); + + // Don't flash stale data from a different task while the new one loads + if (isLoading || !task || task.task_id !== taskId) return
; + + const isActive = task.status === 'running' || task.status === 'queued'; + + return ( +
+ + + {/* Breadcrumb */} +
+ + + {task.task_id} +
+ + {/* Header */} +
+
+
+ +

+ {task.experiment_name} +

+
+
+ capital: ${task.capital?.toLocaleString() ?? '—'} + {task.start_date && ( + period: {task.start_date}{task.end_date ? ` → ${task.end_date}` : ''} + )} + duration: {elapsed(task.started_at, task.finished_at)} + created: {fmtDate(task.created_at)} + {task.run_id && run: {task.run_id}} +
+
+
+ + {isActive && ( + + )} + {task.status === 'completed' && task.run_id && ( + + )} +
+
+ + {/* Error banner */} + {task.status === 'failed' && task.error && ( +
+ {task.error} +
+ )} + + {/* Terminal log */} +
+ {/* Terminal titlebar */} +
+
+
+
+
+
+ + {task.task_id}.log + + {isActive && ( + + ● live + + )} + {logData?.total_lines != null && ( + + {logData.total_lines} lines + + )} +
+
+ {logData?.log + ? + : (waiting for output...) + } +
+
+
+ ); +} + +// ── Backtest Results Page ──────────────────────────────────────────────────── + +type ResultTab = 'summary' | 'equity' | 'trades' | 'engines'; + +function MetricCard({ label, value, color = 'var(--text1)', sub }: { label: string; value: string; color?: string; sub?: string }) { + return ( +
+
+ {label} +
+
+ {value} +
+ {sub &&
{sub}
} +
+ ); +} + +function fmt(v: unknown, decimals = 1): string { + if (v == null) return '—'; + const n = Number(v); + if (isNaN(n)) return String(v); + return n.toFixed(decimals); +} + +function fmtPct(v: unknown): string { + if (v == null) return '—'; + const n = Number(v); + if (isNaN(n)) return '—'; + return `${n >= 0 ? '+' : ''}${n.toFixed(1)}%`; +} + +export function BacktestResultsPage() { + const { runId } = useParams<{ runId: string }>(); + const navigate = useNavigate(); + const [tab, setTab] = useState('summary'); + const [tradePage, setTradePage] = useState(0); + const TRADE_PAGE_SIZE = 50; + + const { data: meta, isLoading: metaLoading } = useQuery({ + queryKey: ['run-meta', runId], + queryFn: () => runsApi.metadata(runId!), + enabled: !!runId, + }); + + const { data: metrics, isLoading: metricsLoading } = useQuery({ + queryKey: ['run-metrics', runId], + queryFn: () => runsApi.metrics(runId!), + enabled: !!runId, + }); + + const { data: equityCurveData } = useQuery({ + queryKey: ['run-equity', runId], + queryFn: () => runsApi.equityCurve(runId!), + enabled: tab === 'equity' && !!runId, + }); + + const { data: tradesData } = useQuery({ + queryKey: ['run-trades', runId, tradePage], + queryFn: () => runsApi.tradeBlotter(runId!, { limit: TRADE_PAGE_SIZE, offset: tradePage * TRADE_PAGE_SIZE }), + enabled: tab === 'trades' && !!runId, + }); + + const { data: enginesData } = useQuery({ + queryKey: ['run-engines', runId], + queryFn: () => runsApi.perEngineMetrics(runId!), + enabled: tab === 'engines' && !!runId, + }); + + if (metaLoading || metricsLoading) return
; + + const m = metrics as Record | null; + const expName = (meta as Record)?.experiment_name as string | undefined; + const splitName = (meta as Record)?.split_name as string | undefined; + + const TABS: { id: ResultTab; label: string }[] = [ + { id: 'summary', label: 'Summary' }, + { id: 'equity', label: 'Equity Curve' }, + { id: 'trades', label: 'Trade Blotter' }, + { id: 'engines', label: 'Per-Engine' }, + ]; + + const equityPoints = (equityCurveData?.data ?? []) as Record[]; + const chartData = equityPoints.map(row => ({ + date: String(row.date ?? row.Date ?? ''), + equity: Number(row.equity ?? row.portfolio_value ?? 0), + drawdown: Number(row.drawdown_pct ?? row.drawdown ?? 0), + })); + + const trades = (tradesData?.trades ?? []) as Record[]; + const totalTrades = (tradesData?.total as number) ?? 0; + const totalPages = Math.ceil(totalTrades / TRADE_PAGE_SIZE); + + return ( +
+ {/* Breadcrumb */} +
+ + + {expName && ( + <> + + + + )} + {runId} +
+ + {/* Header */} +
+
+ {splitName} split +
+

+ {runId} +

+
+ + {/* Metric cards */} + {m && ( +
+ = 0 ? 'var(--green)' : 'var(--red)'} /> + = 0 ? 'var(--green)' : 'var(--red)'} /> + + + + + + +
+ )} + + {/* Tabs */} +
+ {TABS.map(t => ( + + ))} +
+ + {/* Summary tab */} + {tab === 'summary' && m && ( +
+
+
Trade Stats
+ {[ + ['Total trades', m.trade_count], + ['Win rate', fmtPct(m.win_rate)], + ['Avg win', fmtPct(m.avg_win_pct)], + ['Avg loss', fmtPct(m.avg_loss_pct)], + ['Profit factor', fmt(m.profit_factor, 2)], + ['Expectancy R', fmt(m.expectancy_r, 3)], + ['Avg hold days', fmt(m.avg_holding_days)], + ['Stop exit rate', fmtPct(m.stop_exit_rate)], + ['Target exit rate', fmtPct(m.target_exit_rate)], + ].map(([k, v]) => ( +
+ {String(k)} + {String(v)} +
+ ))} +
+
+
Portfolio Stats
+ {[ + ['Total return', fmtPct(m.total_return_pct)], + ['Ann. return', fmtPct(m.annualized_return_pct)], + ['Max drawdown', fmtPct(m.max_drawdown_pct)], + ['Sharpe ratio', fmt(m.sharpe_ratio, 2)], + ['Sortino ratio', fmt(m.sortino_ratio, 2)], + ['Calmar ratio', fmt(m.calmar_ratio, 2)], + ['Avg exposure', fmtPct(m.avg_gross_exposure_pct)], + ['Days in market', fmtPct(m.days_in_market_pct)], + ['vs QQQ', fmtPct(m.excess_vs_qqq_pct)], + ].map(([k, v]) => ( +
+ {String(k)} + {String(v)} +
+ ))} +
+
+ )} + + {/* Equity curve tab */} + {tab === 'equity' && ( +
+ {chartData.length === 0 + ?
No equity curve data
+ : ( + <> +
Equity Curve
+ + + + + `$${(v / 1000).toFixed(0)}k`} /> + [`$${Number(v).toLocaleString()}`, 'Equity']} + /> + + + + +
Drawdown %
+ + + + + `${v.toFixed(0)}%`} /> + [`${Number(v).toFixed(2)}%`, 'Drawdown']} + /> + + + + + ) + } +
+ )} + + {/* Trade blotter tab */} + {tab === 'trades' && ( +
+ {trades.length === 0 + ?
No trades
+ : ( + <> +
+
+ + + + {['Symbol', 'Side', 'Entry Date', 'Exit Date', 'Entry $', 'Exit $', 'Return %', 'PnL', 'Exit Reason'].map(h => ( + + ))} + + + + {trades.map((t, i) => { + const ret = Number(t.return_pct ?? t.pct_return ?? 0); + return ( + + + + + + + + + + + + ); + })} + +
+ {h} +
{String(t.symbol ?? '—')}{String(t.side ?? t.direction ?? '—')}{String(t.entry_date ?? t.open_date ?? '—')}{String(t.exit_date ?? t.close_date ?? '—')}{t.entry_price != null ? `$${Number(t.entry_price).toFixed(2)}` : '—'}{t.exit_price != null ? `$${Number(t.exit_price).toFixed(2)}` : '—'}= 0 ? 'var(--green)' : 'var(--red)' }}> + {ret >= 0 ? '+' : ''}{ret.toFixed(2)}% + = 0 ? 'var(--green)' : 'var(--red)' }}> + {t.net_pnl != null ? `$${Number(t.net_pnl).toFixed(0)}` : t.pnl != null ? `$${Number(t.pnl).toFixed(0)}` : '—'} + {String(t.exit_reason ?? '—')}
+
+
+ {totalPages > 1 && ( +
+ + {tradePage + 1} / {totalPages} + +
+ )} + + ) + } +
+ )} + + {/* Per-engine tab */} + {tab === 'engines' && ( +
+ {!enginesData || Object.keys(enginesData).length === 0 + ?
No per-engine data
+ : ( +
+ {Object.entries(enginesData).map(([engineId, eData]) => ( +
+
+ {engineId} +
+
+                      {JSON.stringify(eData, null, 2)}
+                    
+
+ ))} +
+ ) + } +
+ )} +
+ ); +} diff --git a/apps/web_frontend/src/pages/PaperTrading.tsx b/apps/web_frontend/src/pages/PaperTrading.tsx new file mode 100644 index 0000000..2571ed1 --- /dev/null +++ b/apps/web_frontend/src/pages/PaperTrading.tsx @@ -0,0 +1,1659 @@ +import { useState, useEffect, useRef } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, +} from 'recharts'; +import { + LineChart as LineChartIcon, Play, Pause, RotateCcw, Plus, Trash2, X, CheckCircle, + XCircle, Clock, Loader, AlertTriangle, Activity, + ChevronDown, RefreshCw, Terminal, Shield, Cpu, Square, +} from 'lucide-react'; +import { + paperApi, + type PaperSession, + type PaperTask, +} from '../api/client'; +import { Loading, ErrorState } from '../components/common/Loading'; +import { ansiToHtml } from '../lib/utils'; + +// ── Shared styles ──────────────────────────────────────────────────────────── + +const card: React.CSSProperties = { + background: 'var(--bg1)', + border: '1px solid var(--border)', + borderRadius: 12, + overflow: 'hidden', +}; + +const inputStyle: React.CSSProperties = { + width: '100%', + padding: '8px 11px', + fontSize: 14, + fontFamily: 'var(--font-mono)', + background: 'var(--bg2)', + border: '1px solid var(--border-md)', + borderRadius: 7, + color: 'var(--text1)', + outline: 'none', +}; + +const btn = (variant: 'primary' | 'danger' | 'ghost' | 'outline' = 'outline'): React.CSSProperties => ({ + display: 'inline-flex', + alignItems: 'center', + gap: 6, + padding: '7px 14px', + fontSize: 13, + fontWeight: 500, + borderRadius: 7, + border: '1px solid', + cursor: 'pointer', + transition: 'all 0.12s', + whiteSpace: 'nowrap' as const, + background: variant === 'primary' ? 'var(--cyan)' : variant === 'danger' ? 'var(--red)' : 'transparent', + color: variant === 'primary' ? '#fff' : variant === 'danger' ? '#fff' : variant === 'ghost' ? 'var(--text3)' : 'var(--text2)', + borderColor: variant === 'primary' ? 'var(--cyan)' : variant === 'danger' ? 'var(--red)' : 'var(--border-md)', +}); + +// ── Helpers ────────────────────────────────────────────────────────────────── + +function fmtMoney(v: number | null | undefined, decimals = 2): string { + if (v == null) return '—'; + const sign = v >= 0 ? '+' : ''; + return `${sign}$${Math.abs(v).toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}`; +} + +function fmtPct(v: number | null | undefined, decimals = 2): string { + if (v == null) return '—'; + const sign = v >= 0 ? '+' : ''; + return `${sign}${v.toFixed(decimals)}%`; +} + +function fmtPrice(v: number | null | undefined): string { + if (v == null) return '—'; + return `$${v.toFixed(2)}`; +} + +function pnlColor(v: number | null | undefined): string { + if (v == null) return 'var(--text3)'; + return v >= 0 ? 'var(--green)' : 'var(--red)'; +} + +function StatusBadge({ status }: { status: string }) { + const map: Record = { + active: { color: 'var(--green)', bg: 'var(--green-dim)' }, + paused: { color: 'var(--gold)', bg: 'var(--gold-dim)' }, + closed: { color: 'var(--text3)', bg: 'var(--bg2)' }, + }; + const { color, bg } = map[status] ?? map.closed; + return ( + + + {status.toUpperCase()} + + ); +} + +function TaskStatusIcon({ status }: { status: PaperTask['status'] }) { + if (status === 'running') return ; + if (status === 'completed') return ; + if (status === 'failed') return ; + return ; +} + +function KillSwitchBadge({ on }: { on: boolean }) { + return ( + + {on ? 'KILL SW ON' : 'OK'} + + ); +} + +// ── Stat Card ──────────────────────────────────────────────────────────────── + +function StatCard({ label, value, sub, valueColor }: { + label: string; value: string; sub?: string; valueColor?: string +}) { + return ( +
+
+ {label} +
+
+ {value} +
+ {sub &&
{sub}
} +
+ ); +} + +// ── Create Session Modal ────────────────────────────────────────────────────── + +function CreateSessionModal({ onClose, onCreated }: { + onClose: () => void; + onCreated: (session: { session_id: string; name: string }) => void; +}) { + const [name, setName] = useState(''); + const [configSearch, setConfigSearch] = useState(''); + const [selectedConfig, setSelectedConfig] = useState(''); + const [capital, setCapital] = useState('10000'); + const [configOpen, setConfigOpen] = useState(false); + const dropRef = useRef(null); + + const { data: cfgData, isLoading: cfgLoading } = useQuery({ + queryKey: ['paper-configs', configSearch], + queryFn: () => paperApi.configs({ pattern: configSearch || undefined, limit: 100 }), + staleTime: 60_000, + }); + + const qc = useQueryClient(); + const { mutate, isPending, error } = useMutation({ + mutationFn: () => paperApi.createSession(name.trim(), selectedConfig, parseFloat(capital) || 10000), + onSuccess: (data) => { + qc.invalidateQueries({ queryKey: ['paper-sessions'] }); + onCreated({ session_id: data.session_id, name: data.name }); + }, + }); + + // Close dropdown on outside click + useEffect(() => { + function handle(e: MouseEvent) { + if (dropRef.current && !dropRef.current.contains(e.target as Node)) { + setConfigOpen(false); + } + } + document.addEventListener('mousedown', handle); + return () => document.removeEventListener('mousedown', handle); + }, []); + + const filtered = cfgData?.configs ?? []; + const canSubmit = name.trim() && selectedConfig && parseFloat(capital) > 0; + + return ( +
+
e.stopPropagation()}> +
+

New Paper Trading Session

+ +
+ +
+ {/* Session name */} +
+ + setName(e.target.value)} + /> +
+ + {/* Strategy config */} +
+ +
+
setConfigOpen(!configOpen)} + style={{ + ...inputStyle, + cursor: 'pointer', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + userSelect: 'none', + }} + > + + {selectedConfig || 'Select or type config name…'} + + +
+ {configOpen && ( +
+
+ setConfigSearch(e.target.value)} + autoFocus + onClick={e => e.stopPropagation()} + /> +
+
+ {cfgLoading &&
Loading…
} + {/* Also allow manual entry */} + {configSearch && !filtered.find(c => c.name === configSearch) && ( +
{ setSelectedConfig(configSearch); setConfigOpen(false); }} + style={{ + padding: '9px 14px', cursor: 'pointer', fontSize: 13, + fontFamily: 'var(--font-mono)', color: 'var(--cyan)', + borderBottom: '1px solid var(--border)', + }} + > + Use "{configSearch}" directly +
+ )} + {filtered.map(c => ( +
{ setSelectedConfig(c.name); setConfigOpen(false); }} + style={{ + padding: '8px 14px', cursor: 'pointer', + fontSize: 13, fontFamily: 'var(--font-mono)', + color: c.name === selectedConfig ? 'var(--cyan)' : 'var(--text1)', + background: c.name === selectedConfig ? 'var(--cyan-dim)' : 'transparent', + display: 'flex', justifyContent: 'space-between', alignItems: 'center', + }} + onMouseEnter={e => (e.currentTarget.style.background = 'var(--bg2)')} + onMouseLeave={e => (e.currentTarget.style.background = c.name === selectedConfig ? 'var(--cyan-dim)' : 'transparent')} + > + {c.name} + {c.id != null && #{c.id}} +
+ ))} +
+
+ )} +
+
+ + {/* Capital */} +
+ + setCapital(e.target.value)} + /> +
+ + {error && ( +
+ {(error as Error).message} +
+ )} + +
+ + +
+
+
+
+ ); +} + +// ── Close Confirm Modal ─────────────────────────────────────────────────────── + +function CloseConfirmModal({ session, onClose, onConfirm }: { + session: PaperSession; + onClose: () => void; + onConfirm: () => void; +}) { + return ( +
+
e.stopPropagation()}> +
+ +

Close Session?

+
+

+ This will liquidate all Alpaca positions and permanently delete session + "{session.session_name}" and all its trade history. + This cannot be undone. +

+
+ + +
+
+
+ ); +} + +// ── Session List Item ───────────────────────────────────────────────────────── + +function SessionItem({ session, selected, onClick }: { + session: PaperSession; + selected: boolean; + onClick: () => void; +}) { + const pnlPct = session.total_pnl_pct; + return ( +
{ if (!selected) e.currentTarget.style.background = 'var(--bg2)'; }} + onMouseLeave={e => { if (!selected) e.currentTarget.style.background = 'transparent'; }} + > +
+ {session.session_name} + +
+
+ {session.config_path.replace('configs/experiments/', '')} +
+
+ + ${session.current_equity.toLocaleString('en-US', { maximumFractionDigits: 0 })} + + + {fmtPct(pnlPct)} + + {session.kill_switch && } + {session.latest_date && ( + {session.latest_date} + )} +
+
+ ); +} + +// ── Overview Tab ───────────────────────────────────────────────────────────── + +function OverviewTab({ session }: { session: PaperSession }) { + const { data, isLoading } = useQuery({ + queryKey: ['paper-equity', session.session_id], + queryFn: () => paperApi.equity(session.session_id), + refetchInterval: 60_000, + }); + + const snapshots = data?.snapshots ?? []; + const chartData = snapshots.map(s => ({ + date: s.date, + equity: s.equity, + pnl: s.total_pnl ?? 0, + drawdown: s.drawdown_pct ?? 0, + })); + + return ( +
+ {/* Stats grid */} +
+ + + 10 ? 'var(--red)' : session.drawdown_pct > 5 ? 'var(--gold)' : 'var(--green)'} + /> + +
+ + {/* Risk state */} +
+ + = 3 ? 'var(--red)' : 'var(--text1)'} + /> + 0 ? `${session.cooldown_remaining}d` : 'None'} + valueColor={session.cooldown_remaining > 0 ? 'var(--gold)' : 'var(--text1)'} + /> + +
+ + {/* Equity chart */} +
+
+ Equity Curve + + {snapshots.length} days + +
+ {isLoading && } + {!isLoading && chartData.length === 0 && ( +
+ No equity history yet. Run the session to start tracking. +
+ )} + {chartData.length > 0 && ( + + + + v.slice(5)} + interval="preserveStartEnd" + /> + `$${(v / 1000).toFixed(1)}k`} + width={58} + /> + [`$${Number(v).toLocaleString('en-US', { maximumFractionDigits: 2 })}`, 'Equity']} + labelStyle={{ color: 'var(--text2)' }} + /> + + + + + )} +
+ + {/* Daily snapshots table */} + {snapshots.length > 0 && ( +
+
+ Daily Snapshots (last 20) +
+
+ + + + {['Date', 'Equity', 'Cash', 'Mkt Val', 'Daily P&L', 'Total P&L', 'Drawdown', 'Positions'].map((h, hi) => ( + + ))} + + + + {[...snapshots].reverse().slice(0, 20).map((s, i) => ( + + + + + + + + + + + ))} + +
+ {h} +
{s.date}${s.equity.toLocaleString('en-US', { maximumFractionDigits: 0 })}${s.cash.toLocaleString('en-US', { maximumFractionDigits: 0 })}${s.market_value.toLocaleString('en-US', { maximumFractionDigits: 0 })}{fmtMoney(s.daily_pnl)}{fmtMoney(s.total_pnl)} 5 ? 'var(--red)' : 'var(--text2)' }}>{s.drawdown_pct?.toFixed(2) ?? '—'}%{s.open_position_count ?? '—'}
+
+
+ )} +
+ ); +} + +// ── Positions Tab ───────────────────────────────────────────────────────────── + +function PositionsTab({ session }: { session: PaperSession }) { + const { data, isLoading, error, refetch, isFetching } = useQuery({ + queryKey: ['paper-positions', session.session_id], + queryFn: () => paperApi.positions(session.session_id), + staleTime: 30_000, + }); + + const positions = data?.positions ?? []; + const brokerAvailable = data?.broker_available; + + return ( +
+
+ + {positions.length} open position{positions.length !== 1 ? 's' : ''} + + {brokerAvailable === false && ( + + Alpaca unavailable — showing local state only + + )} + +
+ + {isLoading && } + {error && } + + {!isLoading && positions.length === 0 && ( +
+ No open positions +
+ )} + + {positions.length > 0 && ( +
+
+ + + + {['Symbol', 'Qty', 'Entry', 'Current', 'P&L $', 'P&L %', 'Days', 'Stop', 'Target', 'Direction'].map(h => ( + + ))} + + + + {positions.map((p, i) => ( + + + + + + + + + + + + + ))} + +
+ {h} +
+ {p.symbol} + {p._ghost && GHOST} + {p.qty ?? '—'}{fmtPrice(p.avg_entry_price)}{fmtPrice(p.current_price)}{fmtMoney(p.unrealized_pl)}{fmtPct(p.unrealized_pl_pct)}{p.days_held ?? '—'}{fmtPrice(p.stop_price)}{fmtPrice(p.target_price)}{p.trade_direction ?? '—'}
+
+
+ )} +
+ ); +} + +// ── Trades Tab ──────────────────────────────────────────────────────────────── + +function TradesTab({ session }: { session: PaperSession }) { + const [lastN, setLastN] = useState(''); + + const { data, isLoading, error } = useQuery({ + queryKey: ['paper-trades', session.session_id, lastN], + queryFn: () => paperApi.trades(session.session_id, lastN ? parseInt(lastN) : undefined), + }); + + const trades = data?.trades ?? []; + const wins = trades.filter(t => t.net_pnl > 0).length; + const totalPnl = trades.reduce((s, t) => s + (t.net_pnl ?? 0), 0); + + return ( +
+
+ + {data?.total ?? 0} total trades + {trades.length > 0 && ` · Win Rate: ${((wins / trades.length) * 100).toFixed(0)}%`} + {trades.length > 0 && ` · P&L: `} + {trades.length > 0 && {fmtMoney(totalPnl, 0)}} + +
+ Show last: + {[10, 20, 50, 100].map(n => ( + + ))} + +
+
+ + {isLoading && } + {error && } + + {!isLoading && trades.length === 0 && ( +
+ No trades recorded yet +
+ )} + + {trades.length > 0 && ( +
+
+ + + + {['Symbol', 'Entry Date', 'Exit Date', 'Entry $', 'Exit $', 'Shares', 'Net P&L', 'R', 'Days', 'Reason'].map(h => ( + + ))} + + + + {trades.map((t, i) => ( + + + + + + + + + + + + + ))} + +
+ {h} +
{t.symbol}{t.entry_date ?? '—'}{t.exit_date}{fmtPrice(t.entry_price)}{fmtPrice(t.exit_price)}{t.shares}{fmtMoney(t.net_pnl)} + {t.r_multiple >= 0 ? '+' : ''}{t.r_multiple.toFixed(2)}R + {t.holding_days}d{t.exit_reason}
+
+
+ )} +
+ ); +} + +// ── Run Tab ─────────────────────────────────────────────────────────────────── + +function RunTab({ session, onTaskLaunched }: { + session: PaperSession; + onTaskLaunched: (task: PaperTask) => void; +}) { + const [date, setDate] = useState(''); + const [force, setForce] = useState(false); + const [runningOp, setRunningOp] = useState(null); + + const qc = useQueryClient(); + + const execOp = async (op: 'run' | 'run-close' | 'run-open' | 'run-all') => { + setRunningOp(op); + try { + let task: PaperTask; + const d = date || null; + if (op === 'run') task = await paperApi.run(session.session_id, d, force); + else if (op === 'run-close') task = await paperApi.runClose(session.session_id, d, force); + else if (op === 'run-open') task = await paperApi.runOpen(session.session_id, d, force); + else { + const result = await paperApi.runAll(); + task = result.tasks[0]; + } + onTaskLaunched(task); + qc.invalidateQueries({ queryKey: ['paper-tasks'] }); + } catch (err) { + alert((err as Error).message); + } finally { + setRunningOp(null); + } + }; + + const isActive = session.status === 'active'; + + return ( +
+ {!isActive && ( +
+ + Session is {session.status}. Resume to run operations. +
+ )} + {session.kill_switch && ( +
+ + Kill switch is active — all trading halted. +
+ )} + + {/* Options */} +
+
Options
+
+
+ + setDate(e.target.value)} + placeholder="Leave empty for today" + /> +
+ +
+
+ + {/* Run operations */} +
+ {/* run */} +
+
run
+
+ 전체 일일 처리: exit 판단 + 신규 이벤트 검색 + 진입 +
+ +
+ + {/* run-close */} +
+
run-close
+
+ 장 마감 직전: same-day 이벤트 → MOC 매수 +
+ +
+ + {/* run-open */} +
+
run-open
+
+ 장 시작 직후: 전날 exit + after-close 이벤트 → 시장가 매수 +
+ +
+ + {/* run-all */} +
+
run-all
+
+ 전체 활성 세션에 대해 일일 처리 실행 +
+ +
+
+
+ ); +} + +// ── Tasks Tab ───────────────────────────────────────────────────────────────── + +function TasksTab({ session, highlightTaskId }: { + session: PaperSession; + highlightTaskId?: string | null; +}) { + const [selectedTask, setSelectedTask] = useState(highlightTaskId ?? null); + + const { data, isLoading } = useQuery({ + queryKey: ['paper-tasks', session.session_name], + queryFn: () => paperApi.tasks(session.session_name), + refetchInterval: 3000, + }); + + const { data: logData } = useQuery({ + queryKey: ['paper-task-log', selectedTask], + queryFn: () => selectedTask ? paperApi.taskLog(selectedTask) : null, + enabled: !!selectedTask, + refetchInterval: (query) => { + const status = query.state.data?.status; + return status === 'running' ? 2000 : false; + }, + }); + + const tasks = data?.tasks ?? []; + + useEffect(() => { + if (highlightTaskId) setSelectedTask(highlightTaskId); + }, [highlightTaskId]); + + return ( +
+ {/* Task list */} +
+
+ Recent Tasks +
+ {isLoading &&
} + {tasks.length === 0 && !isLoading && ( +
+ No tasks yet +
+ )} + {tasks.map(t => ( +
setSelectedTask(t.task_id)} + style={{ + padding: '11px 16px', + cursor: 'pointer', + borderBottom: '1px solid var(--border)', + background: selectedTask === t.task_id ? 'var(--cyan-dim)' : 'transparent', + borderLeft: `3px solid ${selectedTask === t.task_id ? 'var(--cyan)' : 'transparent'}`, + }} + onMouseEnter={e => { if (selectedTask !== t.task_id) e.currentTarget.style.background = 'var(--bg2)'; }} + onMouseLeave={e => { if (selectedTask !== t.task_id) e.currentTarget.style.background = 'transparent'; }} + > +
+ + + {t.operation} + +
+
+ {t.started_at ? new Date(t.started_at).toLocaleString() : t.created_at} +
+ {t.error && ( +
+ {t.error} +
+ )} +
+ ))} +
+ + {/* Log viewer */} +
+ {!selectedTask && ( +
+ Select a task to view its log +
+ )} + {selectedTask && ( + <> + {/* Terminal title bar */} +
+
+
+
+
+
+
+ {logData && } + + {tasks.find(t => t.task_id === selectedTask)?.operation ?? selectedTask} + + {logData?.status === 'running' && ( + (live) + )} +
+
+
${logData?.status === 'running' ? '(waiting for output...)' : 'No output'}`,
+              }}
+            />
+          
+        )}
+      
+
+ ); +} + +// ── Session Detail ──────────────────────────────────────────────────────────── + +type TabId = 'overview' | 'positions' | 'trades' | 'run' | 'tasks'; + +function SessionDetail({ session, onRefresh }: { + session: PaperSession; + onRefresh: () => void; +}) { + const [tab, setTab] = useState('overview'); + const [showClose, setShowClose] = useState(false); + const [latestTask, setLatestTask] = useState(null); + const qc = useQueryClient(); + + const pauseMut = useMutation({ + mutationFn: () => paperApi.pauseSession(session.session_id), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['paper-sessions'] }); onRefresh(); }, + }); + + const resumeMut = useMutation({ + mutationFn: () => paperApi.resumeSession(session.session_id), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['paper-sessions'] }); onRefresh(); }, + }); + + const closeMut = useMutation({ + mutationFn: () => paperApi.closeSession(session.session_id), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['paper-sessions'] }); setShowClose(false); }, + }); + + const TABS: { id: TabId; label: string }[] = [ + { id: 'overview', label: 'Overview' }, + { id: 'positions', label: 'Positions' }, + { id: 'trades', label: 'Trades' }, + { id: 'run', label: 'Run' }, + { id: 'tasks', label: 'Tasks' }, + ]; + + function handleTaskLaunched(task: PaperTask) { + setLatestTask(task); + setTab('tasks'); + } + + return ( +
+ {/* Header */} +
+
+
+
+

{session.session_name}

+ + {session.kill_switch && } +
+
+ {session.config_path} · ID: {session.session_id} · Created {session.created_at.slice(0, 10)} +
+
+
+
+ {session.status === 'active' && ( + + )} + {session.status === 'paused' && ( + + )} + +
+
+ + {/* Tabs */} +
+ {TABS.map(t => ( + + ))} +
+ + {/* Tab content */} +
+ {tab === 'overview' && } + {tab === 'positions' && } + {tab === 'trades' && } + {tab === 'run' && } + {tab === 'tasks' && } +
+ + {showClose && ( + setShowClose(false)} + onConfirm={() => closeMut.mutate()} + /> + )} +
+ ); +} + +// ── Auto Daemon Badge (sidebar) ─────────────────────────────────────────────── + +function AutoDaemonBadge() { + const { data } = useQuery({ + queryKey: ['paper-auto-status'], + queryFn: () => paperApi.autoStatus(), + refetchInterval: 5000, + staleTime: 4000, + }); + if (!data) return null; + return ( + + + {data.running ? 'ON' : 'OFF'} + + ); +} + +// ── Auto Daemon Panel ──────────────────────────────────────────────────────── + +function fmtCountdown(secs: number): string { + if (secs <= 0) return 'now'; + const h = Math.floor(secs / 3600); + const m = Math.floor((secs % 3600) / 60); + const s = Math.floor(secs % 60); + if (h > 0) return `${h}h ${String(m).padStart(2, '0')}m`; + if (m > 0) return `${m}m ${String(s).padStart(2, '0')}s`; + return `${s}s`; +} + +function AutoDaemonPanel({ sessions }: { sessions: string[] }) { + const [showLog, setShowLog] = useState(false); + const [sessionFilter, setSessionFilter] = useState([]); + const [dryRun, setDryRun] = useState(false); + const [sessionDropOpen, setSessionDropOpen] = useState(false); + const logRef = useRef(null); + const qc = useQueryClient(); + + const { data, isLoading, refetch, isFetching } = useQuery({ + queryKey: ['paper-auto-status'], + queryFn: () => paperApi.autoStatus(), + refetchInterval: 5000, + }); + + const { data: logData, refetch: refetchLog } = useQuery({ + queryKey: ['paper-auto-log'], + queryFn: () => paperApi.autoLog(300), + enabled: showLog, + refetchInterval: showLog ? 3000 : false, + }); + + // Auto-scroll log to bottom + useEffect(() => { + if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; + }, [logData?.log]); + + const startMut = useMutation({ + mutationFn: () => paperApi.autoStart(sessionFilter.length > 0 ? sessionFilter : [], dryRun), + onSuccess: () => qc.invalidateQueries({ queryKey: ['paper-auto-status'] }), + onError: (e) => alert((e as Error).message), + }); + + const stopMut = useMutation({ + mutationFn: () => paperApi.autoStop(), + onSuccess: () => qc.invalidateQueries({ queryKey: ['paper-auto-status'] }), + onError: (e) => alert((e as Error).message), + }); + + const running = data?.running ?? false; + const pid = data?.pid; + const schedule = data?.schedule ?? []; + + // Live countdown — re-render every second + const [tick, setTick] = useState(0); + useEffect(() => { + const id = setInterval(() => setTick(t => t + 1), 1000); + return () => clearInterval(id); + }, []); + + const nextEvent = schedule.find(e => !e.past); + + return ( +
+ {/* Status + controls */} +
+ {/* Status card */} +
+
+ + + {running ? 'RUNNING' : 'STOPPED'} + + {running && ( + PID {pid} + )} + +
+ {running && nextEvent && ( +
+ Next: {nextEvent.label} +
+ + {nextEvent.et_time} · in{' '} + + {fmtCountdown(nextEvent.wait_secs - tick)} + + +
+ )} + {!running && ( +
Auto daemon not running
+ )} +
+ + {/* Controls */} +
+
+ Controls +
+
+ {/* Session filter */} + {!running && ( + <> +
+
+ Sessions (비워두면 모든 active 세션) +
+
+
setSessionDropOpen(!sessionDropOpen)} + style={{ ...inputStyle, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: 13 }} + > + + {sessionFilter.length ? sessionFilter.join(', ') : 'All active sessions'} + + +
+ {sessionDropOpen && ( +
+
{ setSessionFilter([]); setSessionDropOpen(false); }} + style={{ padding: '8px 12px', fontSize: 13, cursor: 'pointer', color: !sessionFilter.length ? 'var(--cyan)' : 'var(--text2)', fontFamily: 'var(--font-mono)', borderBottom: '1px solid var(--border)' }} + > + All active sessions +
+ {sessions.map(s => ( +
{ + setSessionFilter(prev => + prev.includes(s) ? prev.filter(x => x !== s) : [...prev, s] + ); + }} + style={{ + padding: '8px 12px', fontSize: 13, cursor: 'pointer', fontFamily: 'var(--font-mono)', + display: 'flex', alignItems: 'center', gap: 8, + color: sessionFilter.includes(s) ? 'var(--cyan)' : 'var(--text1)', + background: sessionFilter.includes(s) ? 'var(--cyan-dim)' : 'transparent', + }} + > + + {s} +
+ ))} +
+ )} +
+
+ + + )} +
+ {!running && ( + + )} + {running && ( + + )} + +
+
+
+ + {/* Schedule */} +
+
+ Today's Schedule (ET) +
+ {schedule.map(ev => ( +
+
+
+
{ev.label}
+
{ev.et_time}
+
+
+ {ev.past ? '✓' : fmtCountdown(ev.wait_secs - tick)} +
+
+ ))} +
+
+ + {/* Log viewer */} + {showLog && ( +
+ {/* Terminal title bar */} +
+
+
+
+
+
+ + paper_auto.log + + {logData?.source && logData.source !== 'none' && !logData.tty && ( + + · {logData.source} + + )} + {logData && !logData.tty && ( + + · {logData.lines} lines + + )} + {running && !logData?.tty && ( + (live) + )} + {logData?.tty && ( + + (terminal output — cannot capture) + + )} +
+
${isLoading ? 'Loading…' : 'No log output yet.'}`,
+            }}
+          />
+        
+ )} +
+ ); +} + +// ── Main Page ──────────────────────────────────────────────────────────────── + +export function PaperTradingPage() { + const [selectedId, setSelectedId] = useState(null); + const [view, setView] = useState<'session' | 'auto'>('session'); + const [showCreate, setShowCreate] = useState(false); + const qc = useQueryClient(); + + const { data, isLoading, error, refetch, isFetching } = useQuery({ + queryKey: ['paper-sessions'], + queryFn: () => paperApi.sessions(), + refetchInterval: 30_000, + }); + + const sessions = data?.sessions ?? []; + const selected = sessions.find(s => s.session_id === selectedId || s.session_name === selectedId) ?? null; + + // Auto-select first session + useEffect(() => { + if (!selectedId && sessions.length > 0) { + setSelectedId(sessions[0].session_id); + } + }, [sessions]); + + function handleCreated(s: { session_id: string; name: string }) { + setShowCreate(false); + qc.invalidateQueries({ queryKey: ['paper-sessions'] }); + setSelectedId(s.session_id); + } + + const activeCount = sessions.filter(s => s.status === 'active').length; + + return ( +
+ + + {/* Session sidebar */} +
+ {/* Sidebar header */} +
+
+
+ +

Paper Trading

+
+ +
+
+ + {sessions.length} sessions · {activeCount} active + + +
+
+ + {/* Auto daemon button */} +
setView(view === 'auto' ? 'session' : 'auto')} + style={{ + padding: '11px 16px', + cursor: 'pointer', + borderBottom: '1px solid var(--border)', + display: 'flex', alignItems: 'center', gap: 8, + background: view === 'auto' ? 'var(--cyan-dim)' : 'transparent', + borderLeft: `3px solid ${view === 'auto' ? 'var(--cyan)' : 'transparent'}`, + transition: 'all 0.1s', + }} + onMouseEnter={e => { if (view !== 'auto') e.currentTarget.style.background = 'var(--bg2)'; }} + onMouseLeave={e => { if (view !== 'auto') e.currentTarget.style.background = 'transparent'; }} + > + + + Auto Daemon + + +
+ + {/* Session list */} +
+ {isLoading &&
} + {error &&
} + {!isLoading && sessions.length === 0 && ( +
+ No sessions yet.
+ +
+ )} + {sessions.map(s => ( + { setView('session'); setSelectedId(s.session_id); }} + /> + ))} +
+
+ + {/* Main content area */} +
+ {/* Auto daemon view */} + {view === 'auto' && ( +
+
+ +

Auto Daemon

+ + ET 장 스케줄 자동 실행 + +
+ s.status === 'active').map(s => s.session_name)} + /> +
+ )} + + {/* Session view */} + {view === 'session' && !selected && !isLoading && ( +
+ +
Select a session
+
+ or{' '} + +
+
+ )} + {view === 'session' && selected && ( + { + qc.invalidateQueries({ queryKey: ['paper-sessions'] }); + qc.invalidateQueries({ queryKey: ['paper-session', selected.session_id] }); + }} + /> + )} +
+ + {showCreate && ( + setShowCreate(false)} + onCreated={handleCreated} + /> + )} +
+ ); +}