From 1d2489332652acc9012a27d479961a7de570436a Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Mon, 4 May 2026 22:40:46 -0700 Subject: [PATCH] Add centralized event logging + Logs/Health UI with PEAD/ORB tabs - New EventsStore (SQLite WAL) captures all structlog + stdlib events - ORB engine: 13 _emit() calls for orders, errors, kill-switch, circuit breaker - ORB daemon: configures structlog sink so engine emits reach events.db - ORB scheduler: phase lifecycle events (phase_started/completed) with job_run_id - PEAD scheduler: same lifecycle pattern, pipeline stdout capture improved - New /api/events + /api/health endpoints - Logs/Health page: All / PEAD / ORB tabs, health cards, event table, detail panel Co-Authored-By: Claude Sonnet 4.6 --- apps/orb_trader/daemon.py | 14 + apps/orb_trader/engine.py | 1217 ++++++++++++++++- apps/web/main.py | 22 +- apps/web/orb_trading_service.py | 138 +- apps/web/routers/events.py | 69 + apps/web/services/events_store.py | 576 ++++++++ apps/web/static/assets/index-BdTCIlkw.js | 133 -- apps/web/static/assets/index-BpOBWdYW.js | 133 ++ apps/web/static/index.html | 2 +- apps/web_frontend/src/App.tsx | 4 + apps/web_frontend/src/api/client.ts | 130 ++ .../src/components/layout/Sidebar.tsx | 4 + apps/web_frontend/src/pages/Logs.tsx | 606 ++++++++ 13 files changed, 2864 insertions(+), 184 deletions(-) create mode 100644 apps/web/routers/events.py create mode 100644 apps/web/services/events_store.py delete mode 100644 apps/web/static/assets/index-BdTCIlkw.js create mode 100644 apps/web/static/assets/index-BpOBWdYW.js create mode 100644 apps/web_frontend/src/pages/Logs.tsx diff --git a/apps/orb_trader/daemon.py b/apps/orb_trader/daemon.py index 9fe2412..94e1392 100644 --- a/apps/orb_trader/daemon.py +++ b/apps/orb_trader/daemon.py @@ -33,6 +33,20 @@ def main() -> None: file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")) logging.root.addHandler(file_handler) logging.root.setLevel(logging.INFO) + # Mirror all ORB daemon logs to events.db so the Logs/Health UI can surface them. + # Two channels: + # 1. structlog sink processor — captures structured engine emits (orb_engine_*) + # 2. EventsLoggingHandler — bridges plain stdlib log.info(...) lines + try: + from apps.web.services.events_store import EventsStore, EventsLoggingHandler + from libs.common.logging import configure_logging + EventsStore.get().ensure_schema() + configure_logging(enable_events_sink=True) + _events_handler = EventsLoggingHandler() + _events_handler.setLevel(logging.INFO) + logging.root.addHandler(_events_handler) + except Exception: + pass # events.db mirror is best-effort; don't break the daemon sessions = [s.strip() for s in args.sessions.split(",") if s.strip()] trigger_dir = Path(args.db_path).parent diff --git a/apps/orb_trader/engine.py b/apps/orb_trader/engine.py index 2d871db..0da27e3 100644 --- a/apps/orb_trader/engine.py +++ b/apps/orb_trader/engine.py @@ -19,6 +19,7 @@ import datetime as dt import logging import time import uuid +from pathlib import Path from typing import Any from zoneinfo import ZoneInfo @@ -36,12 +37,42 @@ from apps.orb_trader.screener import ( ) from apps.orb_trader.state import ORBStateManager from libs.intraday.features import enrich_daily_bars -from libs.intraday.orb_simulator import _aggregate_bars, compute_orb_candidates +from libs.intraday.orb_simulator import ( + _aggregate_bars, + _bar_close_location, + _bar_return_pct, + _first_regular_bar, + _linear_range_scaler, + _linear_scaler, + _opening_breadth_stats, + compute_orb_candidates, +) from libs.intraday.simulator import _parse_ts, filter_market_hours from libs.oracle_client.alpaca import get_snapshots log = logging.getLogger(__name__) +# Structured logger — events flow to journal/events.db via libs.common.logging +# sink processor (configured by the daemon at startup). Used at trade/error +# sites so the Logs/Health UI gets ticker, fill_price, qty, etc. as fields, +# not embedded in a free-text message. +try: + from libs.common.logging import get_logger as _get_struct_logger + structured_log = _get_struct_logger("apps.orb_trader.engine") +except Exception: # pragma: no cover — defensive; structlog should always import + structured_log = None # type: ignore[assignment] + + +def _emit(event: str, **fields: Any) -> None: + """Emit a structured event if structlog is available; no-op otherwise.""" + if structured_log is None: + return + level = fields.pop("_level", "info") + try: + getattr(structured_log, level)(event, **fields) + except Exception: + pass + _ET = ZoneInfo("America/New_York") _ACCOUNT_CIRCUIT_BREAKER_PCT = 25.0 # halt if equity drops >25% from peak @@ -64,16 +95,16 @@ class ORBTradingEngine: self._session = session self._broker = broker self._state = state - self._params = params + # Keep live execution overrides local to this engine instance. Strategy + # filters must remain identical to the backtest config; otherwise a + # session named "v49.86" can make different candidate decisions live. + self._params = params.model_copy(deep=True) if hasattr(params, "model_copy") else copy.copy(params) self._log_callback = log_callback # optional scheduler._log for UI visibility - # Live trading overrides + # Live execution details. These affect paper/live order handling, not + # strategy candidate selection. self._params.settlement_days = 0 # paper trading; no real T+1 settlement self._params.slippage_bps = 0.0 # real fills, no simulated slippage - # IEX feed has ~1-3% market share vs SIP; RVOL computed from IEX ORB volume - # relative to SIP avg_daily_vol would be ~0.01-0.03 (min_rvol=1.0 would filter - # everything). Disable the threshold filter; RVOL is still used for ranking. - self._params.min_rvol = 0.0 # Per-day in-memory state (reset each day) self._date_str: str = "" @@ -83,6 +114,13 @@ class ORBTradingEngine: self._pending_cands: list[dict] = [] # not yet filled (for breakout checks) # Pre-screened tickers: None = pre_screen not yet run, [] = ran but nothing passed self._pre_screened_tickers: list[str] | None = None + self._day_size_scale: float = 1.0 + self._soft_day_reason: str | None = None + self._market_orb_quality_max_trades: int | None = None + self._market_orb_quality_reason: str | None = None + self._market_thrust_breadth_override_active: bool = False + self._market_thrust_index_breadth_override_active: bool = False + self._market_thrust_opening_breadth_override_active: bool = False def _get_equity(self) -> float: """Current equity = last snapshot equity, or initial if no snapshots.""" @@ -109,6 +147,599 @@ class ORBTradingEngine: except Exception: pass + def _market_context_tickers(self) -> list[str]: + """Tickers needed for live regime/quality gates even if not trade candidates.""" + tickers: list[str] = [] + regime_ticker = getattr(self._params, "market_regime_ticker", None) or "QQQ" + tickers.append(regime_ticker) + for attr in ( + "market_regime_gap_ticker", + "market_orb_quality_ticker", + "market_orb_quality_secondary_ticker", + ): + ticker = getattr(self._params, attr, None) + if ticker: + tickers.append(str(ticker)) + return list(dict.fromkeys(t.upper() for t in tickers if t)) + + def _with_market_context_tickers(self, tickers: list[str]) -> list[str]: + """Fetch market context bars without expanding the tradable universe.""" + return list(dict.fromkeys([*self._market_context_tickers(), *tickers])) + + def _reset_day_context(self) -> None: + self._day_size_scale = 1.0 + self._soft_day_reason = None + self._market_orb_quality_max_trades = None + self._market_orb_quality_reason = None + self._market_thrust_breadth_override_active = False + self._market_thrust_index_breadth_override_active = False + self._market_thrust_opening_breadth_override_active = False + + @staticmethod + def _copy_params_with_updates(params: Any, updates: dict[str, Any]) -> Any: + """Return a params copy with updates for parity scans.""" + if not updates: + return params + if hasattr(params, "model_copy"): + return params.model_copy(update=updates) + copied = copy.copy(params) + for key, value in updates.items(): + setattr(copied, key, value) + return copied + + @staticmethod + def _meets_min(value: float | None, threshold: Any) -> bool: + return threshold is None or (value is not None and value >= float(threshold)) + + def _apply_market_thrust_breadth_override( + self, + bars_by_ticker: dict[str, list[dict]], + date_str: str, + *, + soft_day_reason_parts: list[str], + regime_gap_pct: float | None, + breadth_ratio: float | None, + breadth_scaler: float, + ) -> tuple[float, list[str]]: + """Mirror the backtest gate that enables market-thrust auxiliary sleeves.""" + index_override_enabled = bool( + getattr(self._params, "market_thrust_breadth_override_enabled", False) + ) + opening_breadth_override_enabled = bool( + getattr(self._params, "market_thrust_opening_breadth_override_enabled", False) + ) + if not (index_override_enabled or opening_breadth_override_enabled): + self._market_thrust_breadth_override_active = False + self._market_thrust_index_breadth_override_active = False + self._market_thrust_opening_breadth_override_active = False + return breadth_scaler, soft_day_reason_parts + + market_open = dt.datetime.fromisoformat(f"{date_str}T09:30:00").replace(tzinfo=_ET) + primary_ticker = ( + getattr(self._params, "market_orb_quality_ticker", None) + or getattr(self._params, "market_regime_ticker", None) + or "SPY" + ) + secondary_ticker = getattr(self._params, "market_orb_quality_secondary_ticker", None) + primary_bar = _first_regular_bar(bars_by_ticker.get(str(primary_ticker), []), market_open) + secondary_bar = ( + _first_regular_bar(bars_by_ticker.get(str(secondary_ticker), []), market_open) + if secondary_ticker + else None + ) + primary_close_loc = _bar_close_location(primary_bar) + secondary_close_loc = _bar_close_location(secondary_bar) + primary_ret = _bar_return_pct(primary_bar) + secondary_ret = _bar_return_pct(secondary_bar) + + quality_blocks_thrust = self._market_quality_blocks_thrust( + primary_close_loc, + secondary_close_loc, + ) + active = ( + index_override_enabled + and "breadth" in soft_day_reason_parts + and "hard_breadth" not in soft_day_reason_parts + and not quality_blocks_thrust + and self._meets_min( + primary_close_loc, + getattr( + self._params, + "market_thrust_breadth_override_min_primary_close_location", + None, + ), + ) + and self._meets_min( + secondary_close_loc, + getattr( + self._params, + "market_thrust_breadth_override_min_secondary_close_location", + None, + ), + ) + and self._meets_min( + primary_ret, + getattr( + self._params, + "market_thrust_breadth_override_min_primary_return_pct", + None, + ), + ) + and self._meets_min( + secondary_ret, + getattr( + self._params, + "market_thrust_breadth_override_min_secondary_return_pct", + None, + ), + ) + and self._meets_min( + regime_gap_pct, + getattr(self._params, "market_thrust_breadth_override_min_regime_gap_pct", None), + ) + and self._meets_min( + breadth_ratio, + getattr(self._params, "market_thrust_breadth_override_min_breadth_ratio", None), + ) + ) + opening_breadth_active = False + if opening_breadth_override_enabled: + opening_reason_allowed = bool(soft_day_reason_parts) and ( + ( + "market_regime" in soft_day_reason_parts + and bool( + getattr( + self._params, + "market_thrust_opening_breadth_override_allow_regime_soft_day", + True, + ) + ) + ) + or ( + "breadth" in soft_day_reason_parts + and bool( + getattr( + self._params, + "market_thrust_opening_breadth_override_allow_breadth_soft_day", + True, + ) + ) + ) + or ( + "hard_breadth" in soft_day_reason_parts + and bool( + getattr( + self._params, + "market_thrust_opening_breadth_override_allow_hard_breadth", + False, + ) + ) + ) + ) + opening_stats = _opening_breadth_stats( + bars_by_ticker, + date_str, + min_first_bar_dollar_vol=getattr( + self._params, + "market_thrust_opening_breadth_override_min_first_bar_dollar_vol", + None, + ), + strong_close_location=float( + getattr( + self._params, + "market_thrust_opening_breadth_override_strong_close_location", + 0.65, + ) + or 0.65 + ), + ) + opening_total_count = int(opening_stats.get("total_count") or 0) + opening_positive_ratio = opening_stats.get("positive_ratio") + opening_avg_return = opening_stats.get("avg_return_pct") + opening_strong_ratio = opening_stats.get("strong_close_location_ratio") + opening_breadth_active = ( + opening_reason_allowed + and opening_total_count + >= int( + getattr( + self._params, + "market_thrust_opening_breadth_override_min_total_count", + 100, + ) + or 0 + ) + and self._meets_min( + opening_positive_ratio, + getattr( + self._params, + "market_thrust_opening_breadth_override_min_positive_ratio", + None, + ), + ) + and self._meets_min( + opening_avg_return, + getattr( + self._params, + "market_thrust_opening_breadth_override_min_avg_return_pct", + None, + ), + ) + and self._meets_min( + opening_strong_ratio, + getattr( + self._params, + "market_thrust_opening_breadth_override_min_strong_close_location_ratio", + None, + ), + ) + ) + self._log( + "Opening-breadth thrust override: " + f"active={opening_breadth_active}, count={opening_total_count}, " + f"positive={opening_positive_ratio if opening_positive_ratio is not None else 'NA'}, " + f"avg_ret={opening_avg_return if opening_avg_return is not None else 'NA'}, " + f"strong_close={opening_strong_ratio if opening_strong_ratio is not None else 'NA'}" + ) + + self._market_thrust_index_breadth_override_active = active + self._market_thrust_opening_breadth_override_active = opening_breadth_active + self._market_thrust_breadth_override_active = active or opening_breadth_active + if active: + floor = max( + 0.0, + float( + getattr(self._params, "market_thrust_breadth_override_size_scale_floor", 1.0) + or 0.0 + ), + ) + breadth_scaler = max(breadth_scaler, floor) + if ( + bool(getattr(self._params, "market_thrust_breadth_override_clear_soft_day", False)) + and breadth_scaler >= getattr(self._params, "soft_day_scaler_threshold", 1.0) + ): + soft_day_reason_parts = [ + reason for reason in soft_day_reason_parts if reason != "breadth" + ] + if opening_breadth_active: + regime_floor = max( + 0.0, + float( + getattr( + self._params, + "market_thrust_opening_breadth_override_regime_size_scale_floor", + 1.0, + ) + or 0.0 + ), + ) + breadth_floor = max( + 0.0, + float( + getattr( + self._params, + "market_thrust_opening_breadth_override_breadth_size_scale_floor", + 1.0, + ) + or 0.0 + ), + ) + # The caller has already applied regime_scaler, so live can only lift + # breadth sizing here. Backtest parity for regime-floor lifting is + # handled before combined sizing in the simulator. + if regime_floor > 0: + breadth_scaler = max(breadth_scaler, min(regime_floor, breadth_floor)) + else: + breadth_scaler = max(breadth_scaler, breadth_floor) + if getattr(self._params, "market_thrust_opening_breadth_override_clear_soft_day", False): + soft_day_reason_parts = [ + reason + for reason in soft_day_reason_parts + if not ( + ( + reason == "market_regime" + and getattr( + self._params, + "market_thrust_opening_breadth_override_allow_regime_soft_day", + True, + ) + ) + or ( + reason == "breadth" + and getattr( + self._params, + "market_thrust_opening_breadth_override_allow_breadth_soft_day", + True, + ) + ) + or ( + reason == "hard_breadth" + and getattr( + self._params, + "market_thrust_opening_breadth_override_allow_hard_breadth", + False, + ) + ) + ) + ] + self._log( + "Market thrust breadth override: " + f"active={active}, {primary_ticker} close_loc={primary_close_loc if primary_close_loc is not None else 'NA'} " + f"ret={primary_ret if primary_ret is not None else 'NA'}, " + f"{secondary_ticker or '-'} close_loc={secondary_close_loc if secondary_close_loc is not None else 'NA'} " + f"ret={secondary_ret if secondary_ret is not None else 'NA'}, " + f"breadth={breadth_ratio if breadth_ratio is not None else 'NA'}, " + f"regime_gap={regime_gap_pct if regime_gap_pct is not None else 'NA'}" + ) + return breadth_scaler, soft_day_reason_parts + + def _market_quality_blocks_thrust( + self, + primary_close_loc: float | None, + secondary_close_loc: float | None, + ) -> bool: + """Backtest market-thrust override is blocked by defensive quality states.""" + primary_strong_above = getattr(self._params, "market_orb_quality_primary_strong_above", None) + secondary_weak_below = getattr(self._params, "market_orb_quality_secondary_weak_below", None) + secondary_weak_above = getattr(self._params, "market_orb_quality_secondary_weak_above", None) + divergence = ( + primary_strong_above is not None + and secondary_weak_below is not None + and primary_close_loc is not None + and secondary_close_loc is not None + and primary_close_loc >= primary_strong_above + and secondary_close_loc <= secondary_weak_below + and (secondary_weak_above is None or secondary_close_loc >= secondary_weak_above) + ) + + primary_weak_below = getattr(self._params, "market_orb_quality_primary_weak_below", None) + primary_weak_above = getattr(self._params, "market_orb_quality_primary_weak_above", None) + secondary_strong_above = getattr(self._params, "market_orb_quality_secondary_strong_above", None) + primary_weak_secondary_strong = ( + primary_weak_below is not None + and secondary_strong_above is not None + and primary_close_loc is not None + and secondary_close_loc is not None + and primary_close_loc <= primary_weak_below + and (primary_weak_above is None or primary_close_loc >= primary_weak_above) + and secondary_close_loc >= secondary_strong_above + ) + + primary_lag_above = getattr(self._params, "market_orb_quality_primary_lag_above", None) + primary_lag_below = getattr(self._params, "market_orb_quality_primary_lag_below", None) + secondary_lead_above = getattr(self._params, "market_orb_quality_secondary_lead_above", None) + secondary_lead_below = getattr(self._params, "market_orb_quality_secondary_lead_below", None) + primary_lag_secondary_lead = ( + primary_lag_below is not None + and secondary_lead_above is not None + and primary_close_loc is not None + and secondary_close_loc is not None + and primary_close_loc <= primary_lag_below + and (primary_lag_above is None or primary_close_loc >= primary_lag_above) + and secondary_close_loc >= secondary_lead_above + and (secondary_lead_below is None or secondary_close_loc <= secondary_lead_below) + ) + + joint_weak_primary_below = getattr(self._params, "market_orb_quality_joint_weak_primary_below", None) + joint_weak_primary_above = getattr(self._params, "market_orb_quality_joint_weak_primary_above", None) + joint_weak_secondary_below = getattr(self._params, "market_orb_quality_joint_weak_secondary_below", None) + joint_weak_secondary_above = getattr(self._params, "market_orb_quality_joint_weak_secondary_above", None) + joint_weak = ( + joint_weak_primary_below is not None + and joint_weak_secondary_below is not None + and primary_close_loc is not None + and secondary_close_loc is not None + and primary_close_loc <= joint_weak_primary_below + and (joint_weak_primary_above is None or primary_close_loc >= joint_weak_primary_above) + and secondary_close_loc <= joint_weak_secondary_below + and (joint_weak_secondary_above is None or secondary_close_loc >= joint_weak_secondary_above) + ) + + joint_panic_primary_below = getattr(self._params, "market_orb_quality_joint_panic_primary_below", None) + joint_panic_secondary_below = getattr(self._params, "market_orb_quality_joint_panic_secondary_below", None) + joint_panic = ( + joint_panic_primary_below is not None + and joint_panic_secondary_below is not None + and primary_close_loc is not None + and secondary_close_loc is not None + and primary_close_loc <= joint_panic_primary_below + and secondary_close_loc <= joint_panic_secondary_below + ) + + return bool( + divergence + or primary_weak_secondary_strong + or primary_lag_secondary_lead + or joint_weak + or joint_panic + ) + + def _apply_market_orb_quality( + self, + bars_by_ticker: dict[str, list[dict]], + date_str: str, + ) -> None: + """Mirror the backtest market-ORB-quality day scaler in live trading.""" + use_quality = any( + getattr(self._params, attr, None) is not None + for attr in ( + "market_orb_quality_size_scale_low", + "market_orb_quality_size_scale_high", + "market_orb_quality_primary_strong_above", + "market_orb_quality_secondary_weak_above", + "market_orb_quality_secondary_weak_below", + "market_orb_quality_divergence_scale", + "market_orb_quality_primary_weak_below", + "market_orb_quality_primary_weak_above", + "market_orb_quality_secondary_strong_above", + "market_orb_quality_primary_weak_secondary_strong_scale", + "market_orb_quality_primary_lag_above", + "market_orb_quality_primary_lag_below", + "market_orb_quality_secondary_lead_above", + "market_orb_quality_secondary_lead_below", + "market_orb_quality_primary_lag_secondary_lead_scale", + "market_orb_quality_joint_weak_primary_below", + "market_orb_quality_joint_weak_primary_above", + "market_orb_quality_joint_weak_secondary_below", + "market_orb_quality_joint_weak_secondary_above", + "market_orb_quality_joint_weak_scale", + "market_orb_quality_joint_panic_primary_below", + "market_orb_quality_joint_panic_secondary_below", + "market_orb_quality_joint_panic_scale", + ) + ) + if not use_quality: + return + + market_open = dt.datetime.fromisoformat(f"{date_str}T09:30:00").replace(tzinfo=_ET) + primary_ticker = ( + getattr(self._params, "market_orb_quality_ticker", None) + or getattr(self._params, "market_regime_ticker", None) + or "SPY" + ) + secondary_ticker = getattr(self._params, "market_orb_quality_secondary_ticker", None) + primary_bar = _first_regular_bar(bars_by_ticker.get(str(primary_ticker), []), market_open) + secondary_bar = ( + _first_regular_bar(bars_by_ticker.get(str(secondary_ticker), []), market_open) + if secondary_ticker + else None + ) + primary_close_loc = _bar_close_location(primary_bar) + secondary_close_loc = _bar_close_location(secondary_bar) + primary_ret = _bar_return_pct(primary_bar) + secondary_ret = _bar_return_pct(secondary_bar) + + scaler = 1.0 + reasons: list[str] = [] + max_trades: int | None = None + + low = getattr(self._params, "market_orb_quality_size_scale_low", None) + high = getattr(self._params, "market_orb_quality_size_scale_high", None) + if low is not None and high is not None: + scaler *= _linear_range_scaler( + primary_close_loc, + low, + high, + getattr(self._params, "market_orb_quality_size_scale_min", 1.0), + getattr(self._params, "market_orb_quality_size_scale_max", 1.0), + ) + + def _cap_trades(raw: Any) -> None: + nonlocal max_trades + if raw is None: + return + cap = max(0, int(raw)) + max_trades = cap if max_trades is None else min(max_trades, cap) + + primary_strong_above = getattr(self._params, "market_orb_quality_primary_strong_above", None) + secondary_weak_above = getattr(self._params, "market_orb_quality_secondary_weak_above", None) + secondary_weak_below = getattr(self._params, "market_orb_quality_secondary_weak_below", None) + divergence = ( + primary_strong_above is not None + and secondary_weak_above is not None + and primary_close_loc is not None + and secondary_close_loc is not None + and primary_close_loc >= primary_strong_above + and secondary_close_loc >= secondary_weak_above + and (secondary_weak_below is None or secondary_close_loc <= secondary_weak_below) + ) + if divergence: + scale = getattr(self._params, "market_orb_quality_divergence_scale", None) + if scale is not None: + scaler *= max(0.0, float(scale)) + _cap_trades(getattr(self._params, "market_orb_quality_divergence_max_trades", None)) + reasons.append("divergence") + + primary_weak_below = getattr(self._params, "market_orb_quality_primary_weak_below", None) + primary_weak_above = getattr(self._params, "market_orb_quality_primary_weak_above", None) + secondary_strong_above = getattr(self._params, "market_orb_quality_secondary_strong_above", None) + primary_weak_secondary_strong = ( + primary_weak_below is not None + and secondary_strong_above is not None + and primary_close_loc is not None + and secondary_close_loc is not None + and primary_close_loc <= primary_weak_below + and (primary_weak_above is None or primary_close_loc >= primary_weak_above) + and secondary_close_loc >= secondary_strong_above + ) + if primary_weak_secondary_strong: + scale = getattr(self._params, "market_orb_quality_primary_weak_secondary_strong_scale", None) + if scale is not None: + scaler *= max(0.0, float(scale)) + _cap_trades(getattr(self._params, "market_orb_quality_primary_weak_secondary_strong_max_trades", None)) + reasons.append("primary_weak_secondary_strong") + + primary_lag_above = getattr(self._params, "market_orb_quality_primary_lag_above", None) + primary_lag_below = getattr(self._params, "market_orb_quality_primary_lag_below", None) + secondary_lead_above = getattr(self._params, "market_orb_quality_secondary_lead_above", None) + secondary_lead_below = getattr(self._params, "market_orb_quality_secondary_lead_below", None) + primary_lag_secondary_lead = ( + primary_lag_below is not None + and secondary_lead_above is not None + and primary_close_loc is not None + and secondary_close_loc is not None + and primary_close_loc <= primary_lag_below + and (primary_lag_above is None or primary_close_loc >= primary_lag_above) + and secondary_close_loc >= secondary_lead_above + and (secondary_lead_below is None or secondary_close_loc <= secondary_lead_below) + ) + if primary_lag_secondary_lead: + scale = getattr(self._params, "market_orb_quality_primary_lag_secondary_lead_scale", None) + if scale is not None: + scaler *= max(0.0, float(scale)) + _cap_trades(getattr(self._params, "market_orb_quality_primary_lag_secondary_lead_max_trades", None)) + reasons.append("primary_lag_secondary_lead") + + joint_weak_primary_below = getattr(self._params, "market_orb_quality_joint_weak_primary_below", None) + joint_weak_primary_above = getattr(self._params, "market_orb_quality_joint_weak_primary_above", None) + joint_weak_secondary_below = getattr(self._params, "market_orb_quality_joint_weak_secondary_below", None) + joint_weak_secondary_above = getattr(self._params, "market_orb_quality_joint_weak_secondary_above", None) + joint_weak = ( + joint_weak_primary_below is not None + and joint_weak_secondary_below is not None + and primary_close_loc is not None + and secondary_close_loc is not None + and primary_close_loc <= joint_weak_primary_below + and (joint_weak_primary_above is None or primary_close_loc >= joint_weak_primary_above) + and secondary_close_loc <= joint_weak_secondary_below + and (joint_weak_secondary_above is None or secondary_close_loc >= joint_weak_secondary_above) + ) + if joint_weak: + scale = getattr(self._params, "market_orb_quality_joint_weak_scale", None) + if scale is not None: + scaler *= max(0.0, float(scale)) + _cap_trades(getattr(self._params, "market_orb_quality_joint_weak_max_trades", None)) + reasons.append("joint_weak") + + joint_panic_primary_below = getattr(self._params, "market_orb_quality_joint_panic_primary_below", None) + joint_panic_secondary_below = getattr(self._params, "market_orb_quality_joint_panic_secondary_below", None) + joint_panic = ( + joint_panic_primary_below is not None + and joint_panic_secondary_below is not None + and primary_close_loc is not None + and secondary_close_loc is not None + and primary_close_loc <= joint_panic_primary_below + and secondary_close_loc <= joint_panic_secondary_below + ) + if joint_panic: + scale = getattr(self._params, "market_orb_quality_joint_panic_scale", None) + if scale is not None: + scaler *= max(0.0, float(scale)) + _cap_trades(getattr(self._params, "market_orb_quality_joint_panic_max_trades", None)) + reasons.append("joint_panic") + + self._day_size_scale *= max(0.0, scaler) + self._market_orb_quality_max_trades = max_trades + self._market_orb_quality_reason = "+".join(reasons) if reasons else None + self._log( + "Market ORB quality: " + f"{primary_ticker} close_loc={primary_close_loc if primary_close_loc is not None else 'NA'} " + f"ret={primary_ret if primary_ret is not None else 'NA'}, " + f"{secondary_ticker or '-'} close_loc={secondary_close_loc if secondary_close_loc is not None else 'NA'} " + f"ret={secondary_ret if secondary_ret is not None else 'NA'}, " + f"scale={scaler:.3f}, max_trades={max_trades}, " + f"reason={self._market_orb_quality_reason or '-'}" + ) + # ── Phase 1: Pre-market Screening (daily bars + enrichment + quality filter) ─ def run_pre_screen(self, date_str: str) -> dict[str, Any]: @@ -144,6 +775,12 @@ class ORBTradingEngine: "CRITICAL: Oracle is unreachable — bar fetches will likely fail. " "Start Oracle before 09:20 ET on live trading days." ) + _emit( + "orb_engine_oracle_unreachable", + _level="error", + session_id=self._session.session_id, + session_name=self._session.session_name, + ) universe_source = getattr(self._params, "_universe_source", "midlarge") universe_symbols_file = getattr(self._params, "_universe_symbols_file", None) @@ -166,6 +803,14 @@ class ORBTradingEngine: raw_bars.update(self._broker.get_bars(chunk, start, bars_end)) except Exception as e: self._log(f" WARNING: daily bars chunk {i//chunk_size+1} failed ({e}) — skipping") + _emit( + "orb_engine_bars_chunk_failed", + _level="warning", + session_id=self._session.session_id, + chunk_kind="daily", + chunk_idx=i // chunk_size + 1, + error=str(e), + ) daily_bars_dict = bars_to_enrichment_format(raw_bars) @@ -211,6 +856,7 @@ class ORBTradingEngine: Returns summary dict. """ self._date_str = date_str + self._reset_day_context() self._state.update_daily_state( self._session.session_id, date_str, phase="orb_detection" ) @@ -255,6 +901,15 @@ class ORBTradingEngine: f"CIRCUIT BREAKER: account drawdown {account_dd_pct:.1f}% " f">= {_ACCOUNT_CIRCUIT_BREAKER_PCT}% — session halted" ) + _emit( + "orb_engine_circuit_breaker", + _level="error", + session_id=self._session.session_id, + drawdown_pct=round(float(account_dd_pct), 2), + threshold_pct=_ACCOUNT_CIRCUIT_BREAKER_PCT, + peak_equity=round(float(peak_eq), 2), + current_equity=round(float(equity_now), 2), + ) self._state.set_session_status(self._session.session_id, "paused") self._state.update_daily_state( self._session.session_id, date_str, phase="done" @@ -268,10 +923,12 @@ class ORBTradingEngine: # ── Determine intraday_tickers: use pre-screen cache or fetch daily bars ─ if self._enrichment and self._pre_screened_tickers is not None: # Pre-screen already ran — skip daily bars fetch - intraday_tickers = self._pre_screened_tickers + candidate_tickers = list(self._pre_screened_tickers) + candidate_ticker_set = set(candidate_tickers) + intraday_tickers = self._with_market_context_tickers(candidate_tickers) daily_bars_count = len([s for s, b in self._daily_bars.items() if b]) self._log( - f"Pre-screened universe 사용: {len(intraday_tickers)} tickers " + f"Pre-screened universe 사용: {len(candidate_tickers)} tickers " f"(daily bars 캐시됨)" ) else: @@ -314,7 +971,16 @@ class ORBTradingEngine: self._enrichment = enrich_daily_bars(daily_bars_dict, [date_str]) self._daily_bars = daily_bars_dict daily_bars_count = len([s for s, b in raw_bars.items() if b]) - intraday_tickers = tickers # full universe (no pre-screen) + candidate_tickers = list(tickers) + candidate_ticker_set = set(candidate_tickers) + intraday_tickers = self._with_market_context_tickers(candidate_tickers) + + context_added = len(intraday_tickers) - len(candidate_ticker_set) + if context_added > 0: + self._log( + f"Market context bars 포함: {context_added} tickers " + f"({', '.join(self._market_context_tickers())})" + ) # ── Fetch 5-min intraday bars for (pre-screened or full) universe ────── today = dt.date.fromisoformat(date_str) @@ -338,22 +1004,45 @@ class ORBTradingEngine: intraday_raw.update(chunk_bars) except Exception as e: self._log(f" WARNING: intraday bars chunk {i//chunk_size+1} failed ({e}) — skipping") + _emit( + "orb_engine_bars_chunk_failed", + _level="warning", + session_id=self._session.session_id, + chunk_kind="intraday", + chunk_idx=i // chunk_size + 1, + error=str(e), + ) bars_by_ticker = intraday_bars_to_format(intraday_raw) - intraday_count = len(bars_by_ticker) + market_bars_by_ticker = { + ticker: mkt_bars + for ticker, bars in bars_by_ticker.items() + if (mkt_bars := filter_market_hours(bars)) + } + intraday_count = len(market_bars_by_ticker) self._log( f"Daily bars: {daily_bars_count} tickers | " f"Intraday bars: {intraday_count} tickers" ) if daily_bars_count == 0: self._log("WARNING: no daily bars fetched — enrichment will be empty; check Oracle/Alpaca connection") + _emit( + "orb_engine_no_daily_bars", + _level="error", + session_id=self._session.session_id, + ) if intraday_count == 0: self._log("WARNING: no intraday bars fetched — zero candidates will be produced") + _emit( + "orb_engine_no_intraday_bars", + _level="error", + session_id=self._session.session_id, + ) # Patch today_open in enrichment with actual first-bar open from intraday data. # The pre_screen synthetic row uses prev_close as today_open (gap=0), which breaks # market_regime_spy_threshold and breadth filters. Overwrite with real opening price. - for ticker, ticker_bars in bars_by_ticker.items(): + for ticker, ticker_bars in market_bars_by_ticker.items(): if not ticker_bars: continue first_bar = ticker_bars[0] @@ -372,19 +1061,27 @@ class ORBTradingEngine: self._enrichment[ticker][date_str]["today_open"] = real_open break - # Market regime check (mirrors simulate_day:1678-1693) + # Market regime / breadth checks mirror simulate_orb_day's soft-day fallback. + regime_scaler = 1.0 + breadth_scaler = 1.0 + regime_gap_pct: float | None = None + breadth_ratio: float | None = None + soft_day_reason_parts: list[str] = [] + regime_thresh = getattr(self._params, "market_regime_spy_threshold", None) - if regime_thresh is not None: + if regime_thresh is not None or getattr(self._params, "regime_size_scale_low", None) is not None: regime_ticker = getattr(self._params, "market_regime_ticker", None) or "QQQ" regime_enrich = self._enrichment.get(regime_ticker, {}).get(date_str, {}) regime_prev_close = regime_enrich.get("prev_close") regime_today_open = regime_enrich.get("today_open") if regime_prev_close and regime_today_open and regime_prev_close > 0: regime_gap = (regime_today_open - regime_prev_close) / regime_prev_close - if regime_gap < regime_thresh: + regime_gap_pct = regime_gap + regime_skip_below = getattr(self._params, "regime_skip_below", None) + if regime_skip_below is not None and regime_gap < regime_skip_below: self._log( f"Regime filter: {regime_ticker} gap {regime_gap:.3%} " - f"< {regime_thresh:.3%} — skipping today" + f"< hard floor {regime_skip_below:.3%} — skipping today" ) self._state.update_daily_state( self._session.session_id, date_str, phase="done" @@ -394,10 +1091,55 @@ class ORBTradingEngine: "intraday_bars": intraday_count, "orb_candidates": 0, "long": 0, "short": 0, "skip_reason": "market_regime", } + if ( + getattr(self._params, "regime_size_scale_low", None) is None + and regime_thresh is not None + and regime_gap < regime_thresh + ): + if getattr(self._params, "soft_day_fallback_on_regime_skip", False): + regime_scaler = max( + 0.0, + float( + getattr( + self._params, + "soft_day_regime_skip_size_scale", + 1.0, + ) + or 0.0 + ), + ) + soft_day_reason_parts.append("market_regime") + self._log( + f"Regime soft fallback: {regime_ticker} gap {regime_gap:.3%} " + f"< {regime_thresh:.3%}; size_scale={regime_scaler:.3f}" + ) + else: + self._log( + f"Regime filter: {regime_ticker} gap {regime_gap:.3%} " + f"< {regime_thresh:.3%} — skipping today" + ) + self._state.update_daily_state( + self._session.session_id, date_str, phase="done" + ) + return { + "universe_size": intraday_count, "daily_bars": daily_bars_count, + "intraday_bars": intraday_count, "orb_candidates": 0, + "long": 0, "short": 0, "skip_reason": "market_regime", + } + regime_low = getattr(self._params, "regime_size_scale_low", None) + regime_high = getattr(self._params, "regime_size_scale_high", None) + if regime_low is not None and regime_high is not None: + regime_scaler = _linear_scaler( + regime_gap, + regime_low, + regime_high, + getattr(self._params, "regime_size_scale_min", 0.0), + invert=True, + ) - # Breadth filter (mirrors simulate_day:1706-1727) + # Breadth filter min_breadth = getattr(self._params, "min_candidate_breadth", None) - if min_breadth is not None: + if min_breadth is not None or getattr(self._params, "breadth_size_scale_low", None) is not None: pos_gap_count = 0 total_with_data = 0 for ticker in bars_by_ticker: @@ -410,26 +1152,289 @@ class ORBTradingEngine: pos_gap_count += 1 if total_with_data > 0: breadth_ratio = pos_gap_count / total_with_data - if breadth_ratio < min_breadth: - self._log( - f"Breadth filter: {breadth_ratio:.1%} positive gaps " - f"< {min_breadth:.1%} — skipping today" + hard_breadth_fallback_active = False + breadth_skip_below = getattr(self._params, "breadth_skip_below", None) + if breadth_skip_below is not None and breadth_ratio < breadth_skip_below: + fallback_min = getattr( + self._params, + "hard_breadth_soft_fallback_min_breadth", + None, ) - self._state.update_daily_state( - self._session.session_id, date_str, phase="done" + if ( + getattr(self._params, "hard_breadth_soft_fallback_enabled", False) + and (fallback_min is None or breadth_ratio >= float(fallback_min)) + ): + breadth_scaler = max( + 0.0, + float( + getattr( + self._params, + "hard_breadth_soft_fallback_size_scale", + 0.03, + ) + or 0.0 + ), + ) + soft_day_reason_parts.append("hard_breadth") + hard_breadth_fallback_active = True + self._log( + f"Hard breadth soft fallback: {breadth_ratio:.1%} " + f"< {breadth_skip_below:.1%}; size_scale={breadth_scaler:.3f}" + ) + else: + self._log( + f"Breadth filter: {breadth_ratio:.1%} positive gaps " + f"< hard floor {breadth_skip_below:.1%} — skipping today" + ) + self._state.update_daily_state( + self._session.session_id, date_str, phase="done" + ) + return { + "universe_size": intraday_count, "daily_bars": daily_bars_count, + "intraday_bars": intraday_count, "orb_candidates": 0, + "long": 0, "short": 0, "skip_reason": "breadth", + } + if ( + getattr(self._params, "breadth_size_scale_low", None) is None + and min_breadth is not None + and breadth_ratio < min_breadth + and not hard_breadth_fallback_active + ): + if getattr(self._params, "soft_day_fallback_on_breadth_skip", False): + breadth_scaler = max( + 0.0, + float( + getattr( + self._params, + "soft_day_breadth_skip_size_scale", + 1.0, + ) + or 0.0 + ), + ) + soft_day_reason_parts.append("breadth") + self._log( + f"Breadth soft fallback: {breadth_ratio:.1%} " + f"< {min_breadth:.1%}; size_scale={breadth_scaler:.3f}" + ) + else: + self._log( + f"Breadth filter: {breadth_ratio:.1%} positive gaps " + f"< {min_breadth:.1%} — skipping today" + ) + self._state.update_daily_state( + self._session.session_id, date_str, phase="done" + ) + return { + "universe_size": intraday_count, "daily_bars": daily_bars_count, + "intraday_bars": intraday_count, "orb_candidates": 0, + "long": 0, "short": 0, "skip_reason": "breadth", + } + breadth_low = getattr(self._params, "breadth_size_scale_low", None) + breadth_high = getattr(self._params, "breadth_size_scale_high", None) + if breadth_low is not None and breadth_high is not None: + breadth_scaler = _linear_scaler( + breadth_ratio, + breadth_low, + breadth_high, + getattr(self._params, "breadth_size_scale_min", 0.0), + invert=True, ) - return { - "universe_size": intraday_count, "daily_bars": daily_bars_count, - "intraday_bars": intraday_count, "orb_candidates": 0, - "long": 0, "short": 0, "skip_reason": "breadth", - } - self._candidates = compute_orb_candidates( + breadth_scaler, soft_day_reason_parts = self._apply_market_thrust_breadth_override( + market_bars_by_ticker, + date_str, + soft_day_reason_parts=soft_day_reason_parts, + regime_gap_pct=regime_gap_pct, + breadth_ratio=breadth_ratio, + breadth_scaler=breadth_scaler, + ) + self._day_size_scale = max(0.0, regime_scaler * breadth_scaler) + if ( + soft_day_reason_parts + and getattr(self._params, "soft_day_combined_size_scale_floor", None) is not None + ): + self._day_size_scale = max( + self._day_size_scale, + max( + 0.0, + float(getattr(self._params, "soft_day_combined_size_scale_floor")), + ), + ) + if soft_day_reason_parts: + self._soft_day_reason = "+".join(soft_day_reason_parts) + self._log( + f"Soft-day active: reason={self._soft_day_reason}, " + f"day_size_scale={self._day_size_scale:.3f}" + ) + + self._apply_market_orb_quality(market_bars_by_ticker, date_str) + if self._day_size_scale <= 0: + self._log("Day sizing disabled by market ORB quality — candidates may be recorded, but no entries will be placed") + + broad_gapup_fallback_mode = bool( + getattr(self._params, "broad_gapup_continuation_enabled", False) + ) + market_thrust_liquid_mode = bool( + getattr(self._params, "market_thrust_liquid_continuation_enabled", False) + ) + market_thrust_opening_impulse_mode = bool( + getattr(self._params, "market_thrust_opening_impulse_reclaim_enabled", False) + ) + base_candidate_updates: dict[str, Any] = {} + if broad_gapup_fallback_mode: + base_candidate_updates["broad_gapup_continuation_enabled"] = False + if market_thrust_liquid_mode: + base_candidate_updates["market_thrust_liquid_continuation_enabled"] = False + if market_thrust_opening_impulse_mode: + base_candidate_updates["market_thrust_opening_impulse_reclaim_enabled"] = False + candidate_params = self._copy_params_with_updates( + self._params, + base_candidate_updates, + ) + computed_candidates = compute_orb_candidates( bars_by_ticker=bars_by_ticker, date_str=date_str, - params=self._params, + params=candidate_params, enrichment=self._enrichment, ) + normal_candidates = [ + cand for cand in computed_candidates + if cand["ticker"] in candidate_ticker_set + ] + broad_candidates: list[dict[str, Any]] = [] + allow_broad_candidates = ( + broad_gapup_fallback_mode + and ( + normal_candidates == [] + or not getattr( + self._params, + "broad_gapup_continuation_only_when_no_primary_entries", + False, + ) + ) + ) + if allow_broad_candidates: + broad_scan_params = self._copy_params_with_updates( + self._params, + { + "max_candidates": max( + int(getattr(self._params, "max_candidates", 0) or 0), + len(candidate_ticker_set), + ), + "max_candidates_per_sector": None, + "market_thrust_liquid_continuation_enabled": False, + "market_thrust_opening_impulse_reclaim_enabled": False, + }, + ) + broad_candidates = [ + cand for cand in compute_orb_candidates( + bars_by_ticker=bars_by_ticker, + date_str=date_str, + params=broad_scan_params, + enrichment=self._enrichment, + ) + if cand["ticker"] in candidate_ticker_set + and cand.get("broad_gapup_continuation") + ] + + used_tickers = {cand["ticker"] for cand in normal_candidates + broad_candidates} + market_thrust_liquid_candidates: list[dict[str, Any]] = [] + opening_breadth_only_thrust = ( + self._market_thrust_opening_breadth_override_active + and not self._market_thrust_index_breadth_override_active + ) + liquid_allowed_on_opening_breadth = bool( + getattr( + self._params, + "market_thrust_opening_breadth_override_activate_liquid_continuation", + True, + ) + ) + market_thrust_liquid_scan_active = ( + market_thrust_liquid_mode + and self._market_thrust_breadth_override_active + and ( + not opening_breadth_only_thrust + or liquid_allowed_on_opening_breadth + ) + ) + if market_thrust_liquid_scan_active: + market_thrust_scan_params = self._copy_params_with_updates( + self._params, + { + "max_candidates": max( + int(getattr(self._params, "max_candidates", 0) or 0), + len(candidate_ticker_set), + ), + "max_candidates_per_sector": None, + "broad_gapup_continuation_enabled": False, + "market_thrust_opening_impulse_reclaim_enabled": False, + }, + ) + market_thrust_liquid_candidates = [ + cand for cand in compute_orb_candidates( + bars_by_ticker=bars_by_ticker, + date_str=date_str, + params=market_thrust_scan_params, + enrichment=self._enrichment, + ) + if cand["ticker"] in candidate_ticker_set + and cand.get("market_thrust_liquid_continuation") + and cand["ticker"] not in used_tickers + ] + max_market_thrust_candidates = getattr( + self._params, + "market_thrust_liquid_continuation_max_candidates", + None, + ) + if max_market_thrust_candidates is not None: + market_thrust_liquid_candidates = market_thrust_liquid_candidates[ + : max(0, int(max_market_thrust_candidates)) + ] + used_tickers.update(cand["ticker"] for cand in market_thrust_liquid_candidates) + + market_thrust_opening_impulse_candidates: list[dict[str, Any]] = [] + if market_thrust_opening_impulse_mode and self._market_thrust_breadth_override_active: + market_thrust_impulse_params = self._copy_params_with_updates( + self._params, + { + "max_candidates": max( + int(getattr(self._params, "max_candidates", 0) or 0), + len(candidate_ticker_set), + ), + "max_candidates_per_sector": None, + "broad_gapup_continuation_enabled": False, + "market_thrust_liquid_continuation_enabled": False, + }, + ) + market_thrust_opening_impulse_candidates = [ + cand for cand in compute_orb_candidates( + bars_by_ticker=bars_by_ticker, + date_str=date_str, + params=market_thrust_impulse_params, + enrichment=self._enrichment, + ) + if cand["ticker"] in candidate_ticker_set + and cand.get("market_thrust_opening_impulse_reclaim") + and cand["ticker"] not in used_tickers + ] + max_market_thrust_impulse_candidates = getattr( + self._params, + "market_thrust_opening_impulse_reclaim_max_candidates", + None, + ) + if max_market_thrust_impulse_candidates is not None: + market_thrust_opening_impulse_candidates = market_thrust_opening_impulse_candidates[ + : max(0, int(max_market_thrust_impulse_candidates)) + ] + + self._candidates = ( + normal_candidates + + broad_candidates + + market_thrust_liquid_candidates + + market_thrust_opening_impulse_candidates + ) # Save candidates to DB for cand in self._candidates: @@ -495,6 +1500,12 @@ class ORBTradingEngine: ) if daily_state.kill_switch: self._log("Kill switch active — skipping breakout check") + _emit( + "orb_engine_kill_switch_active", + _level="warning", + session_id=self._session.session_id, + phase="breakout", + ) return {"checked": 0, "filled": 0, "remaining": 0, "kill_switch": True} equity = self._get_equity() @@ -525,6 +1536,16 @@ class ORBTradingEngine: self._session.session_id, date_str, ticker, "filled" ) continue + quality_max_trades = getattr(self, "_market_orb_quality_max_trades", None) + if quality_max_trades is not None and len(open_positions) >= quality_max_trades: + self._log( + f" {ticker}: market ORB quality max_trades={quality_max_trades} " + f"reached ({self._market_orb_quality_reason or 'quality_gate'}) — cancelling" + ) + self._state.update_candidate_status( + self._session.session_id, date_str, ticker, "cancelled" + ) + continue max_sim = getattr(self._params, "max_simultaneous_entries", None) if max_sim is not None and len(open_positions) >= max_sim: still_pending.append(cand) @@ -551,7 +1572,22 @@ class ORBTradingEngine: still_pending.append(cand) continue - sizing_capital = self._compute_sizing_capital(equity) + day_size_scale = max(0.0, float(getattr(self, "_day_size_scale", 1.0) or 0.0)) + candidate_size_scale = self._candidate_size_scale(cand) + sizing_capital = ( + self._compute_sizing_capital(equity) + * day_size_scale + * candidate_size_scale + ) + if sizing_capital <= 0: + self._log( + f" {ticker}: day_size_scale={day_size_scale:.3f}, " + f"candidate_size_scale={candidate_size_scale:.3f}; sizing disabled" + ) + self._state.update_candidate_status( + self._session.session_id, date_str, ticker, "cancelled" + ) + continue risk_dollars = sizing_capital * self._params.risk_per_trade_pct shares_from_risk = risk_dollars / stop_distance @@ -587,14 +1623,31 @@ class ORBTradingEngine: f" {ticker}: {direction} breakout → {shares} shares " f"(order {order.id})" ) + _emit( + "orb_engine_buy_submitted", + session_id=self._session.session_id, + ticker=ticker, direction=direction, qty=shares, + entry_price_est=round(entry_price_est, 4), + order_id=order.id, + category="order", + ) except Exception as e: self._log(f" {ticker}: order failed: {e}") + _emit( + "orb_engine_buy_rejected", + _level="error", + session_id=self._session.session_id, + ticker=ticker, direction=direction, qty=shares, + error=str(e), + category="order", + ) still_pending.append(cand) continue # Wait for fill (poll up to 30s) fill_price = entry_price_est order_rejected = False + reject_status: str | None = None for _ in range(6): time.sleep(5) try: @@ -605,15 +1658,33 @@ class ORBTradingEngine: if filled_order.status in ("cancelled", "rejected", "expired"): self._log(f" {ticker}: order {filled_order.status} — no position created") order_rejected = True + reject_status = filled_order.status break except Exception: pass if order_rejected: + _emit( + "orb_engine_buy_rejected", + _level="error", + session_id=self._session.session_id, + ticker=ticker, qty=shares, + order_id=order.id, + status=reject_status or "unknown", + category="order", + ) self._state.update_candidate_status( self._session.session_id, date_str, ticker, "cancelled" ) continue + _emit( + "orb_engine_buy_filled", + session_id=self._session.session_id, + ticker=ticker, direction=direction, qty=shares, + fill_price=round(float(fill_price), 4), + order_id=order.id, + category="order", + ) # Record position initial_stop = ( @@ -651,6 +1722,12 @@ class ORBTradingEngine: ) if daily_state.kill_switch: self._log("Kill switch triggered — stopping breakout monitoring") + _emit( + "orb_engine_kill_switch_triggered", + _level="error", + session_id=self._session.session_id, + phase="breakout", + ) break self._pending_cands = still_pending @@ -820,8 +1897,29 @@ class ORBTradingEngine: self._log( f" {ticker}: stop hit ({exit_reason}) @ {exit_price:.2f}" ) + _emit( + "orb_engine_close_filled", + session_id=self._session.session_id, + ticker=ticker, qty=int(pos.shares), + exit_price=round(float(exit_price), 4), + entry_price=round(float(pos.entry_price), 4), + reason="stop", + exit_subreason=exit_reason, + order_id=close_order.id, + category="order", + ) except Exception as e: self._log(f" {ticker}: close error: {e}") + _emit( + "orb_engine_close_failed", + _level="error", + session_id=self._session.session_id, + ticker=ticker, qty=int(pos.shares), + reason="stop", + exit_subreason=exit_reason, + error=str(e), + category="order", + ) self._record_trade(pos, exit_price, exit_bar["timestamp"], exit_reason, equity) stops_hit += 1 @@ -848,6 +1946,14 @@ class ORBTradingEngine: ) if kill: self._log("Kill switch triggered!") + _emit( + "orb_engine_kill_switch_triggered", + _level="error", + session_id=self._session.session_id, + phase="stop_check", + cumulative_loss=round(float(new_cum_loss), 2), + stops_hit=int(new_stops), + ) break self._log(f"Stop check: {len(positions)} positions, {stops_hit} stops hit") @@ -904,9 +2010,28 @@ class ORBTradingEngine: self._record_trade(pos, exit_price, now_str, "close", equity) self._log(f" EOD close: {pos.ticker} @ {exit_price:.2f}") + _emit( + "orb_engine_close_filled", + session_id=self._session.session_id, + ticker=pos.ticker, qty=int(pos.shares), + exit_price=round(float(exit_price), 4), + entry_price=round(float(pos.entry_price), 4), + reason="eod", + order_id=close_order.id, + category="order", + ) closed += 1 except Exception as e: self._log(f" EOD close error {pos.ticker}: {e}") + _emit( + "orb_engine_close_failed", + _level="error", + session_id=self._session.session_id, + ticker=pos.ticker, qty=int(pos.shares), + reason="eod", + error=str(e), + category="order", + ) # Mark as closed in DB anyway to prevent zombie positions self._state.close_position_record( self._session.session_id, date_str, pos.ticker @@ -1065,6 +2190,26 @@ class ORBTradingEngine: return sizing + def _candidate_size_scale(self, cand: dict[str, Any]) -> float: + """Per-candidate live sizing scale for sleeves that survive in memory/DB.""" + if not bool(getattr(self._params, "broad_gapup_continuation_enabled", False)): + return 1.0 + + is_broad_gapup = bool(cand.get("broad_gapup_continuation")) + if not is_broad_gapup: + max_gap = getattr(self._params, "max_gap_pct", None) + gap_pct = cand.get("gap_pct") + is_broad_gapup = ( + max_gap is not None + and gap_pct is not None + and float(gap_pct) > float(max_gap) + ) + if not is_broad_gapup: + return 1.0 + + raw_scale = getattr(self._params, "broad_gapup_continuation_size_scale", 1.0) + return max(0.0, min(1.0, float(raw_scale or 0.0))) + def _rebuild_pending_candidates(self, date_str: str) -> list[dict]: """Reconstruct pending candidates from DB (after server restart).""" db_cands = self._state.list_candidates(self._session.session_id, date_str) @@ -1110,10 +2255,9 @@ def make_orb_engine( broker_override: pass a MockORBBroker (or any duck-typed broker) to avoid real Alpaca API calls during testing. """ - import yaml - from apps.orb_trader.state import ORBStateManager from libs.intraday.domain import IntradayConfig + from apps.intraday_bt.run import _load_config_yaml if broker_override is not None: broker = broker_override @@ -1123,9 +2267,8 @@ def make_orb_engine( state = ORBStateManager(db_path) - # Load strategy params from YAML config - with open(session.config_path) as f: - raw = yaml.safe_load(f) + # Load strategy params from YAML config (resolves `extends` inheritance) + raw = _load_config_yaml(Path(session.config_path)) or {} # Strip _meta and other non-model keys config_data = {k: v for k, v in raw.items() if not k.startswith("_")} config = IntradayConfig(**config_data) diff --git a/apps/web/main.py b/apps/web/main.py index 69aa9c3..048a1c2 100644 --- a/apps/web/main.py +++ b/apps/web/main.py @@ -43,7 +43,8 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles -from apps.web.routers import backtest, docs, experiments, intraday, leaderboard, orb_trading, paper_trading, runs, sqs +from apps.web.routers import advisor, backtest, docs, experiments, intraday, leaderboard, orb_trading, paper_trading, runs, sqs +from apps.web.routers.events import router as events_router, health_router _STATIC_DIR = Path(__file__).parent / "static" @@ -56,6 +57,16 @@ async def lifespan(app: FastAPI): # type: ignore[type-arg] project_root = Path(__file__).parent.parent.parent os.chdir(project_root) + # ── Centralized event store: schema first, then sink + stdlib bridge ───── + from apps.web.services.events_store import EventsStore, EventsLoggingHandler + _events_store = EventsStore.get() + _events_store.ensure_schema() # must precede configure_logging so table exists before first write + from libs.common.logging import configure_logging + configure_logging(enable_events_sink=True) + _events_handler = EventsLoggingHandler() + _events_handler.setLevel(_logging.INFO) + _logging.root.addHandler(_events_handler) + # Auto-restart PEAD auto scheduler if it was running before last shutdown from apps.web.paper_trading_service import auto_scheduler, load_saved_state _db_env = os.environ.get("PAPER_TRADER_DB", "paper_trading.db") @@ -91,13 +102,15 @@ async def lifespan(app: FastAPI): # type: ignore[type-arg] if _orb_sessions: try: from apps.orb_trader.state import ORBStateManager as _ORBState + _orb_state = _ORBState(_orb_db_path) _existing = [ s for s in _orb_sessions - if _ORBState(_orb_db_path).get_session(s) is not None + if (session := _orb_state.get_session(s)) is not None + and session.status == "active" ] if not _existing: _logging.getLogger(__name__).warning( - "ORB auto-restart: saved sessions %s not found in DB; " + "ORB auto-restart: saved sessions %s not active in DB; " "will use all active sessions instead", _orb_sessions ) _orb_sessions = [] @@ -160,6 +173,9 @@ def create_app() -> FastAPI: app.include_router(paper_trading.router, prefix=api_prefix) app.include_router(sqs.router, prefix=api_prefix) app.include_router(docs.router, prefix=api_prefix) + app.include_router(advisor.router, prefix=api_prefix) + app.include_router(events_router, prefix=api_prefix) + app.include_router(health_router, prefix=api_prefix) # Serve built frontend (production) if _STATIC_DIR.exists(): diff --git a/apps/web/orb_trading_service.py b/apps/web/orb_trading_service.py index 0bcaa28..c746947 100644 --- a/apps/web/orb_trading_service.py +++ b/apps/web/orb_trading_service.py @@ -37,6 +37,43 @@ from zoneinfo import ZoneInfo log = logging.getLogger(__name__) +# Mirror scheduler activity into journal/events.db so the Logs/Health UI can +# group ORB phases by job_run_id alongside PEAD events. We write directly to +# EventsStore rather than via structlog because the scheduler runs in the +# daemon process where source inference from a PrintLogger is unreliable. +try: + from libs.common.logging import bind_job_run_id as _bind_job_run_id +except Exception: # pragma: no cover + _bind_job_run_id = None # type: ignore[assignment] + + +def _scheduler_emit(event: str, level: str = "INFO", **fields: Any) -> None: + try: + from apps.web.services.events_store import EventsStore + payload: dict[str, Any] = { + "ts_utc": datetime.now(timezone.utc).isoformat(), + "source": "auto_scheduler", + "level": level, + "event": event, + "scheduler": "orb", + } + payload.update(fields) + EventsStore.get().write(payload) + except Exception: + pass + + +# kind → public phase name (distinct from PEAD's run_open/run_close so health +# card rows do not collide). +_PHASE_NAME_MAP = { + "pre_screen": "orb_pre_screen", + "orb_detect": "orb_detect", + "breakout": "orb_breakout", + "stop_check": "orb_stop", + "eod_exit": "orb_eod", + "post_close": "orb_post_close", +} + _TZ_ET = ZoneInfo("America/New_York") _TZ_PHOENIX = ZoneInfo("America/Phoenix") # UTC-7 always (no DST) _DEFAULT_DB = "data/paper/orb.db" @@ -98,12 +135,13 @@ def build_schedule( "et_dt": t, }) - # ── ORB detection at window close (fetch bars + rank candidates) ────────── + # ── ORB detection: one bar after window close (bars are published ~5 min late) + detect_dt = orb_end + dt.timedelta(minutes=5) events.append({ "name": "orb_detect", - "label": f"ORB 감지 ({orb_end.strftime('%H:%M')} ET)", + "label": f"ORB 감지 ({detect_dt.strftime('%H:%M')} ET)", "kind": "orb_detect", - "et_dt": orb_end, + "et_dt": detect_dt, }) # ── Breakout checks: every sim_bar_minutes from first bar close to timeout ── @@ -151,12 +189,12 @@ def _load_session_params(db_path: str, session_name: str) -> dict[str, Any]: """ defaults = {"orb_minutes": 10, "sim_bar_minutes": 90, "order_timeout_minutes": 45} try: - import yaml from apps.orb_trader.state import ORBStateManager + from apps.intraday_bt.run import _load_config_yaml session = ORBStateManager(db_path).get_session(session_name) if session is None: return defaults - raw = yaml.safe_load(Path(session.config_path).read_text()) or {} + raw = _load_config_yaml(Path(session.config_path)) or {} orb = raw.get("orb_strategy", {}) return { "orb_minutes": orb.get("orb_minutes", defaults["orb_minutes"]), @@ -380,6 +418,9 @@ class ORBAutoScheduler: f.write(line + "\n") except Exception: pass + # Mirror to events.db with source=auto_scheduler so the Logs/Health UI + # can show scheduler activity inline with engine events. + _scheduler_emit("scheduler_log", message=msg) def _now_et(self) -> dt.datetime: return dt.datetime.now(tz=_TZ_ET) @@ -423,6 +464,13 @@ class ORBAutoScheduler: except Exception: return [] + def _mark_past_events_completed(self, now_et: dt.datetime) -> None: + """Mark already-past events as completed after a schedule rebuild.""" + for ev in self._today_schedule: + if ev["et_dt"] <= now_et and ev["name"] not in self._completed: + self._completed.add(ev["name"]) + self._log(f"Past (skipped): {ev['label']}") + def _build_today_schedule(self, date: dt.date, sessions: list[str]) -> list[dict[str, Any]]: """Build per-session schedules and merge into one sorted timeline. @@ -513,7 +561,25 @@ class ORBAutoScheduler: if engine is None: continue + # Bind a fresh job_run_id per (session, kind) so every engine event + # in this run groups into a single timeline in the Logs/Health UI. + phase_name = _PHASE_NAME_MAP.get(kind, kind) + phase_run_id = str(uuid.uuid4()) + if _bind_job_run_id is not None: + try: + _bind_job_run_id(phase_run_id) + except Exception: + pass + phase_started = time.monotonic() + _scheduler_emit( + "phase_started", + job_run_id=phase_run_id, + phase=phase_name, session_id=session.session_id, + session_name=session_name, category="lifecycle", + ) + self._log(f" ▶ {kind} — {session_name}") + phase_status = "success" try: def _run_sync(e=engine, k=kind, d=date_str) -> dict[str, Any]: import asyncio as _asyncio @@ -540,8 +606,28 @@ class ORBAutoScheduler: self._log(f" ✓ {session_name}: {summary}") except Exception as exc: tb = traceback.format_exc() + phase_status = "failed" self._log(f" ERROR {session_name}: {exc}") log.error("ORB engine error: %s\n%s", exc, tb) + _scheduler_emit( + "phase_completed", + level="ERROR", + job_run_id=phase_run_id, + phase=phase_name, status="failed", + session_id=session.session_id, session_name=session_name, + category="lifecycle", + duration_s=round(time.monotonic() - phase_started, 3), + error=str(exc), + ) + continue + _scheduler_emit( + "phase_completed", + job_run_id=phase_run_id, + phase=phase_name, status=phase_status, + session_id=session.session_id, session_name=session_name, + category="lifecycle", + duration_s=round(time.monotonic() - phase_started, 3), + ) # ── Run-now: manually trigger detection for a late-added session ───────── @@ -665,10 +751,7 @@ class ORBAutoScheduler: else: self._today_schedule = self._build_today_schedule(today, resolved) # Skip events already past - for ev in self._today_schedule: - if ev["et_dt"] <= now_et: - self._completed.add(ev["name"]) - self._log(f"Past (skipped): {ev['label']}") + self._mark_past_events_completed(now_et) self._save_schedule_state() if not self._is_trading_day(today): @@ -679,6 +762,23 @@ class ORBAutoScheduler: await asyncio.sleep(300) continue + # If the daemon was started with an explicit but now-stale session + # list, do not spend the whole day scheduling inactive/deleted + # sessions. This commonly happens when a user creates/replaces an + # ORB session while the long-lived daemon is already running. + if self._sessions: + active_now = self._get_active_sessions() + active_resolved = [s for s in resolved if s in active_now] + if not active_resolved and active_now: + self._log( + "Configured ORB sessions are no longer active; " + f"refreshing to active sessions: {', '.join(active_now)}" + ) + resolved = active_now + self._today_schedule = self._build_today_schedule(today, resolved) + self._mark_past_events_completed(now_et) + self._save_schedule_state() + pending = [ev for ev in self._today_schedule if ev["name"] not in self._completed] if not pending: @@ -795,7 +895,25 @@ class ORBDaemonController: def start(self, sessions: list[str], db_path: str, dry_run: bool = False) -> None: self._db_path = db_path # set FIRST so _pid_file() uses the right dir if self.running: - return # daemon already alive — no-op (web server may have restarted) + requested_sessions = [s.strip() for s in sessions if s and s.strip()] + saved = load_orb_saved_state(self._db_path) or {} + current_sessions = [ + str(s).strip() + for s in saved.get("sessions", []) + if str(s).strip() + ] + current_dry_run = bool(saved.get("dry_run", False)) + if current_sessions == requested_sessions and current_dry_run == dry_run: + return # daemon already alive with the requested configuration + + # A no-op here leaves the old daemon pinned to stale sessions. Restart + # so "start selected sessions" from the UI actually updates trading. + self.stop() + deadline = time.time() + 5.0 + while self.running and time.time() < deadline: + time.sleep(0.1) + if self.running: + raise RuntimeError("ORB daemon is still running after stop request") cmd = [ sys.executable, "-m", "apps.orb_trader.daemon", diff --git a/apps/web/routers/events.py b/apps/web/routers/events.py new file mode 100644 index 0000000..64effd6 --- /dev/null +++ b/apps/web/routers/events.py @@ -0,0 +1,69 @@ +"""Events and health monitoring router. + +Provides endpoints for the centralized trading event log and live health summary. +""" +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, Query + +router = APIRouter(prefix="/events", tags=["events"]) + + +def _get_store(): + from apps.web.services.events_store import EventsStore + return EventsStore.get() + + +@router.get("") +def list_events( + source: str | None = Query(None), + level: str | None = Query(None), + category: str | None = Query(None), + session_id: str | None = Query(None), + job_run_id: str | None = Query(None), + event_name: str | None = Query(None), + q: str | None = Query(None), + since: str | None = Query(None), + until: str | None = Query(None), + limit: int = Query(200, ge=1, le=1000), + offset: int = Query(0, ge=0), + system: str | None = Query(None, description="'pead' or 'orb' to scope to one system"), +) -> dict[str, Any]: + store = _get_store() + rows, total = store.query( + source=source, level=level, category=category, + session_id=session_id, job_run_id=job_run_id, event_name=event_name, + q=q, since=since, until=until, + limit=limit, offset=offset, + system=system, + ) + next_offset = offset + limit if offset + limit < total else None + return {"rows": rows, "total": total, "offset": offset, "next_offset": next_offset} + + +@router.get("/runs") +def list_runs(limit: int = Query(20, ge=1, le=100)) -> list[dict[str, Any]]: + return _get_store().recent_runs(limit=limit) + + +@router.get("/sources") +def list_sources() -> list[str]: + return _get_store().distinct_sources() + + +@router.delete("") +def purge_events(before: str = Query(..., description="ISO-8601 datetime; delete events older than this")) -> dict[str, Any]: + deleted = _get_store().purge_before(before) + return {"deleted": deleted} + + +# ── Health endpoint (mounted at /health to match plan, kept in events router) ── + +health_router = APIRouter(tags=["health"]) + + +@health_router.get("/health") +def get_health() -> dict[str, Any]: + return _get_store().health_summary() diff --git a/apps/web/services/events_store.py b/apps/web/services/events_store.py new file mode 100644 index 0000000..d89dbe5 --- /dev/null +++ b/apps/web/services/events_store.py @@ -0,0 +1,576 @@ +"""Centralized structured event store for live trading monitoring. + +All structlog events from the PEAD engine, ORB engine, pipeline subprocesses, +and AutoScheduler flow into a single SQLite table here. This gives the user +a queryable log of every fallback, timeout, error, and phase lifecycle event +without touching any engine emit sites. +""" +from __future__ import annotations + +import json +import logging +import queue +import sqlite3 +import threading +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +_DB_PATH = "journal/events.db" +_SCHEMA = """ +PRAGMA journal_mode=WAL; + +CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts_utc TEXT NOT NULL, + job_run_id TEXT, + source TEXT NOT NULL, + level TEXT NOT NULL, + category TEXT, + event_name TEXT NOT NULL, + session_id TEXT, + ticker TEXT, + message TEXT, + details TEXT +); + +CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts_utc DESC); +CREATE INDEX IF NOT EXISTS idx_events_run ON events(job_run_id, ts_utc DESC); +CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id, ts_utc DESC); +CREATE INDEX IF NOT EXISTS idx_events_cat_lvl ON events(category, level, ts_utc DESC); +CREATE INDEX IF NOT EXISTS idx_events_name ON events(event_name, ts_utc DESC); +""" + +# event_name suffix → level promotion to WARN +_WARN_SUFFIXES = ( + "_fallback", "_skipped", "_timeout", "_stale", "_failed", + "_unavailable", "_not_found", "_soft_fallback", +) + +# event_name prefix → category +_CATEGORY_MAP: list[tuple[str, str]] = [ + ("paper_engine_macro_", "macro"), + ("paper_engine_clock_", "health"), + ("paper_engine_kill_switch_", "kill_switch"), + ("paper_engine_buy_", "order"), + ("paper_engine_close_", "order"), + ("paper_engine_no_bar", "order"), + ("paper_engine_", "engine"), + ("orb_engine_buy_", "order"), + ("orb_engine_close_", "order"), + ("orb_engine_kill_switch_", "kill_switch"), + ("orb_engine_circuit_breaker", "kill_switch"), + ("orb_engine_oracle_", "broker"), + ("orb_engine_bars_", "broker"), + ("orb_engine_no_", "health"), + ("orb_engine_", "engine"), + ("snapshot_store_", "snapshot"), + ("incremental_update_canonical_", "snapshot"), + ("event_detector_", "snapshot"), + ("oracle_", "broker"), + ("multi_daily_bars", "broker"), + ("multi_intraday_bars", "broker"), + ("phase_started", "lifecycle"), + ("phase_completed", "lifecycle"), + ("scheduler_", "lifecycle"), + ("pipeline_", "pipeline"), +] + + +def _infer_category(event_name: str, explicit: str | None) -> str | None: + if explicit: + return explicit + for prefix, cat in _CATEGORY_MAP: + if event_name.startswith(prefix): + return cat + return None + + +def _promote_level(level: str, event_name: str, category: str | None) -> str: + """Promote INFO → WARN for known-fallback event names.""" + if level in ("warning", "warn", "WARN", "WARNING"): + return "WARN" + if level in ("error", "critical", "ERROR", "CRITICAL"): + return "ERROR" + low = event_name.lower() + if any(low.endswith(s) for s in _WARN_SUFFIXES): + return "WARN" + # fallback-category items that are INFO → WARN + if category in ("kill_switch",) and level in ("info", "INFO"): + return "WARN" + return "INFO" + + +class EventsStore: + """Thread-safe event store backed by SQLite. + + Uses a background writer thread and a bounded queue so that SQLite I/O + never blocks the trading hot-path. The writer thread is resilient to + transient SQLite errors (locked / disk full) — it drops the failing row + and counts the drop rather than dying. + """ + + _instance: "EventsStore | None" = None + _lock = threading.Lock() + + @classmethod + def get(cls) -> "EventsStore": + """Return the process-level singleton, creating it on first call.""" + if cls._instance is None: + with cls._lock: + if cls._instance is None: + cls._instance = cls(_DB_PATH) + return cls._instance + + def __init__(self, db_path: str = _DB_PATH) -> None: + self._db_path = str(Path(db_path)) + self._queue: queue.Queue[dict[str, Any] | None] = queue.Queue(maxsize=10_000) + self._drop_count = 0 + self._schema_done = False + self._writer = threading.Thread(target=self._writer_loop, daemon=True, name="events-writer") + self._writer.start() + + def ensure_schema(self) -> None: + """Create DB tables if they don't exist. Safe to call multiple times.""" + if self._schema_done: + return + try: + Path(self._db_path).parent.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(self._db_path, timeout=10) + conn.executescript(_SCHEMA) + conn.commit() + conn.close() + self._schema_done = True + except Exception as exc: + logging.getLogger(__name__).error("events_store schema init failed: %s", exc) + + def write(self, record: dict[str, Any]) -> None: + """Enqueue a record for background writing. Non-blocking; drops on full queue.""" + try: + self._queue.put_nowait(record) + except queue.Full: + self._drop_count += 1 + if self._drop_count % 100 == 1: + print(f"[events_store] queue full, {self._drop_count} drops", flush=True, file=__import__("sys").stderr) + + def _writer_loop(self) -> None: + conn: sqlite3.Connection | None = None + BATCH = 50 + FLUSH_EVERY = 2.0 # seconds + + def _connect() -> sqlite3.Connection | None: + try: + Path(self._db_path).parent.mkdir(parents=True, exist_ok=True) + c = sqlite3.connect(self._db_path, timeout=15, check_same_thread=False) + c.execute("PRAGMA journal_mode=WAL") + return c + except Exception as exc: + print(f"[events_store] connect failed: {exc}", file=__import__("sys").stderr, flush=True) + return None + + pending: list[dict[str, Any]] = [] + last_flush = time.monotonic() + + while True: + # Drain up to BATCH items from the queue + try: + record = self._queue.get(timeout=FLUSH_EVERY) + if record is None: # poison pill + break + pending.append(record) + # Drain additional items without waiting + while len(pending) < BATCH: + try: + r = self._queue.get_nowait() + if r is None: + break + pending.append(r) + except queue.Empty: + break + except queue.Empty: + pass + + now = time.monotonic() + if not pending and now - last_flush < FLUSH_EVERY: + continue + + if not pending: + last_flush = now + continue + + if conn is None: + conn = _connect() + if conn is None: + pending.clear() + continue + + rows_to_insert = [] + for rec in pending: + try: + rows_to_insert.append(_build_row(rec)) + except Exception: + pass + pending.clear() + + if not rows_to_insert: + last_flush = now + continue + + try: + conn.executemany( + "INSERT INTO events (ts_utc,job_run_id,source,level,category," + "event_name,session_id,ticker,message,details) " + "VALUES (?,?,?,?,?,?,?,?,?,?)", + rows_to_insert, + ) + conn.commit() + except Exception as exc: + self._drop_count += len(rows_to_insert) + print(f"[events_store] write failed ({len(rows_to_insert)} rows dropped): {exc}", + file=__import__("sys").stderr, flush=True) + try: + conn.close() + except Exception: + pass + conn = None + + last_flush = time.monotonic() + + def query( + self, + source: str | None = None, + level: str | None = None, + category: str | None = None, + session_id: str | None = None, + job_run_id: str | None = None, + event_name: str | None = None, + q: str | None = None, + since: str | None = None, + until: str | None = None, + limit: int = 200, + offset: int = 0, + system: str | None = None, + ) -> tuple[list[dict[str, Any]], int]: + """Return (rows, total_count) matching the given filters.""" + clauses: list[str] = [] + params: list[Any] = [] + + if system == "pead": + srcs = ("paper_engine", "snapshot_store", "event_detector", "oracle", "pipeline") + ph = ",".join("?" * len(srcs)) + clauses.append(f"(source IN ({ph}) OR (source='auto_scheduler' AND details NOT LIKE ?))") + params.extend(srcs) + params.append('%"scheduler": "orb"%') + elif system == "orb": + clauses.append("(source IN (?) OR (source='auto_scheduler' AND details LIKE ?))") + params.append("orb_engine") + params.append('%"scheduler": "orb"%') + + if source: + clauses.append("source = ?") + params.append(source) + if level: + clauses.append("level = ?") + params.append(level.upper()) + if category: + clauses.append("category = ?") + params.append(category) + if session_id: + clauses.append("session_id = ?") + params.append(session_id) + if job_run_id: + clauses.append("job_run_id = ?") + params.append(job_run_id) + if event_name: + clauses.append("event_name LIKE ?") + params.append(f"%{event_name}%") + if q: + clauses.append("(message LIKE ? OR event_name LIKE ? OR details LIKE ?)") + params += [f"%{q}%", f"%{q}%", f"%{q}%"] + if since: + clauses.append("ts_utc >= ?") + params.append(since) + if until: + clauses.append("ts_utc <= ?") + params.append(until) + + where = ("WHERE " + " AND ".join(clauses)) if clauses else "" + + try: + conn = sqlite3.connect(self._db_path, timeout=10) + conn.row_factory = sqlite3.Row + total = conn.execute(f"SELECT COUNT(*) FROM events {where}", params).fetchone()[0] + rows = conn.execute( + f"SELECT * FROM events {where} ORDER BY ts_utc DESC LIMIT ? OFFSET ?", + params + [limit, offset], + ).fetchall() + conn.close() + return [dict(r) for r in rows], total + except Exception as exc: + logging.getLogger(__name__).warning("events_store query failed: %s", exc) + return [], 0 + + def recent_runs(self, limit: int = 20) -> list[dict[str, Any]]: + """Return recent job_run_id summaries ordered by start time.""" + sql = """ + SELECT + job_run_id, + MIN(ts_utc) AS started_at, + MAX(ts_utc) AS ended_at, + COUNT(*) AS event_count, + SUM(CASE WHEN level='ERROR' THEN 1 ELSE 0 END) AS error_count, + SUM(CASE WHEN level='WARN' THEN 1 ELSE 0 END) AS warn_count, + GROUP_CONCAT(DISTINCT source) AS sources, + MAX(CASE WHEN event_name='phase_started' THEN json_extract(details,'$.phase') END) AS phase + FROM events + WHERE job_run_id IS NOT NULL AND job_run_id != '' + GROUP BY job_run_id + ORDER BY started_at DESC + LIMIT ? + """ + try: + conn = sqlite3.connect(self._db_path, timeout=10) + conn.row_factory = sqlite3.Row + rows = conn.execute(sql, [limit]).fetchall() + conn.close() + return [dict(r) for r in rows] + except Exception as exc: + logging.getLogger(__name__).warning("events_store recent_runs failed: %s", exc) + return [] + + def health_summary(self) -> dict[str, Any]: + """Aggregate today's events into a health snapshot for the UI.""" + today_start = datetime.now(timezone.utc).strftime("%Y-%m-%dT00:00:00") + + phase_sql = """ + SELECT + json_extract(details,'$.phase') AS phase, + json_extract(details,'$.status') AS status, + MAX(ts_utc) AS last_ts + FROM events + WHERE event_name='phase_completed' AND ts_utc >= ? + GROUP BY phase, status + """ + fallback_sql = """ + SELECT category, level, COUNT(*) AS cnt + FROM events + WHERE ts_utc >= ? + AND ( + level IN ('WARN','ERROR') + OR category IN ('fallback','snapshot','macro','regime','broker','kill_switch') + ) + AND category NOT IN ('lifecycle','pipeline') + GROUP BY category, level + """ + recent_errors_sql = """ + SELECT id, ts_utc, source, event_name, message, session_id, job_run_id + FROM events + WHERE level='ERROR' AND ts_utc >= ? + ORDER BY ts_utc DESC + LIMIT 5 + """ + snapshot_sql = """ + SELECT session_id, + json_extract(details,'$.snapshot_id') AS snapshot_id, + MAX(ts_utc) AS last_update + FROM events + WHERE event_name LIKE 'incremental_update_canonical%done' + AND ts_utc >= date('now','-7 days') + GROUP BY session_id, snapshot_id + """ + + try: + conn = sqlite3.connect(self._db_path, timeout=10) + conn.row_factory = sqlite3.Row + + phases = [dict(r) for r in conn.execute(phase_sql, [today_start]).fetchall()] + fallbacks = [dict(r) for r in conn.execute(fallback_sql, [today_start]).fetchall()] + recent_errors = [dict(r) for r in conn.execute(recent_errors_sql, [today_start]).fetchall()] + snapshots = [dict(r) for r in conn.execute(snapshot_sql, []).fetchall()] + conn.close() + except Exception as exc: + logging.getLogger(__name__).warning("events_store health_summary failed: %s", exc) + phases, fallbacks, recent_errors, snapshots = [], [], [], [] + + phase_map: dict[str, dict[str, Any]] = {} + for row in phases: + ph = row["phase"] or "unknown" + if ph not in phase_map or row["last_ts"] > phase_map[ph].get("last_ts", ""): + phase_map[ph] = row + + fallback_map: dict[str, dict[str, Any]] = {} + for row in fallbacks: + cat = row["category"] or "other" + if cat not in fallback_map: + fallback_map[cat] = {"warn": 0, "error": 0} + if row["level"] == "ERROR": + fallback_map[cat]["error"] += row["cnt"] + else: + fallback_map[cat]["warn"] += row["cnt"] + + return { + "phases": phase_map, + "fallbacks_today": fallback_map, + "recent_errors": recent_errors, + "snapshot_freshness": snapshots, + "drop_count": self._drop_count, + } + + def distinct_sources(self) -> list[str]: + try: + conn = sqlite3.connect(self._db_path, timeout=10) + rows = conn.execute( + "SELECT DISTINCT source FROM events ORDER BY source" + ).fetchall() + conn.close() + return [r[0] for r in rows] + except Exception: + return [] + + def purge_before(self, before_iso: str) -> int: + try: + conn = sqlite3.connect(self._db_path, timeout=10) + cur = conn.execute("DELETE FROM events WHERE ts_utc < ?", [before_iso]) + conn.commit() + deleted = cur.rowcount + conn.execute("VACUUM") + conn.close() + return deleted + except Exception as exc: + logging.getLogger(__name__).warning("events_store purge failed: %s", exc) + return 0 + + +def _build_row(rec: dict[str, Any]) -> tuple: + """Convert a structlog event_dict or freeform dict into a DB row tuple.""" + ts = rec.get("timestamp") or rec.get("ts_utc") or datetime.now(timezone.utc).isoformat() + event_name = str(rec.get("event") or rec.get("event_name") or "") + raw_level = str(rec.get("level") or "info").lower() + explicit_cat = rec.get("category") + category = _infer_category(event_name, explicit_cat) + level = _promote_level(raw_level, event_name, category) + + # "fallback" refinement: any event_name containing "fallback" → category fallback + if "fallback" in event_name.lower() and category not in ("lifecycle",): + category = "fallback" + + message = rec.get("message") or rec.get("msg") or event_name + details_dict = {k: v for k, v in rec.items() + if k not in ("timestamp", "level", "event", "event_name", + "message", "msg", "ts_utc", "source", + "job_run_id", "session_id", "ticker", "category")} + try: + details = json.dumps(details_dict, default=str) + except Exception: + details = str(details_dict) + + return ( + ts, + rec.get("job_run_id") or "", + str(rec.get("source") or "unknown"), + level, + category, + event_name, + rec.get("session_id"), + rec.get("ticker"), + message, + details, + ) + + +# ── structlog processor ─────────────────────────────────────────────────────── + +def structlog_sink_processor(logger: Any, method: str, event_dict: dict[str, Any]) -> dict[str, Any]: + """structlog processor that tees events to EventsStore without blocking.""" + try: + store = EventsStore.get() + # Infer source from logger name if not already set + source = event_dict.get("source") + if not source: + logger_name = str(logger.name if hasattr(logger, "name") else "") + source = _infer_source(logger_name, event_dict.get("event", "")) + record = dict(event_dict) + record["source"] = source + store.write(record) + except Exception: + pass + return event_dict + + +def _infer_source(logger_name: str, event_name: str) -> str: + if "paper_trader" in logger_name or event_name.startswith("paper_engine_"): + return "paper_engine" + if "orb_trader" in logger_name or event_name.startswith("orb_"): + return "orb_engine" + if "snapshot_store" in logger_name or event_name.startswith("snapshot_store_"): + return "snapshot_store" + if "event_detector" in logger_name or event_name.startswith("event_detector_"): + return "event_detector" + if "oracle" in logger_name or event_name.startswith(("oracle_", "multi_")): + return "oracle" + if "canonical_snapshot" in logger_name or event_name.startswith("incremental_update"): + return "snapshot_store" + return logger_name.split(".")[-1] if logger_name else "unknown" + + +# ── stdlib logging bridge ───────────────────────────────────────────────────── + +_STDLIB_LEVEL_MAP = { + logging.DEBUG: "INFO", + logging.INFO: "INFO", + logging.WARNING: "WARN", + logging.ERROR: "ERROR", + logging.CRITICAL: "ERROR", +} + + +class EventsLoggingHandler(logging.Handler): + """stdlib logging.Handler that writes to EventsStore. + + Captures ORB engine (stdlib), oracle client (stdlib), and web.main + auto-restart warnings that structlog doesn't see. + """ + + # Loggers to skip (already handled by structlog or too noisy) + _SKIP_LOGGERS = { + "uvicorn", "uvicorn.error", "uvicorn.access", + "fastapi", "asyncio", "multiprocessing", + "sqlalchemy", + } + + def emit(self, record: logging.LogRecord) -> None: + logger_name = record.name or "" + # Skip loggers that are too noisy or already handled by structlog + root = logger_name.split(".")[0] + if root in self._SKIP_LOGGERS: + return + # Skip DEBUG unless it's a known important logger + if record.levelno < logging.WARNING and record.levelno == logging.DEBUG: + return + + try: + store = EventsStore.get() + event_name = f"stdlib_{logger_name.replace('.', '_')}" + message = record.getMessage() + level = _STDLIB_LEVEL_MAP.get(record.levelno, "INFO") + + # Infer a better event_name from known patterns in the message + low_msg = message.lower() + if "fallback" in low_msg: + event_name = "stdlib_fallback" + elif "failed" in low_msg or "error" in low_msg: + event_name = f"stdlib_error_{root}" + elif "auto-restart" in low_msg or "auto_restart" in low_msg: + event_name = "scheduler_auto_restart" + + store.write({ + "ts_utc": datetime.now(timezone.utc).isoformat(), + "source": _infer_source(logger_name, event_name), + "level": level, + "event": event_name, + "message": message, + "logger": logger_name, + }) + except Exception: + pass diff --git a/apps/web/static/assets/index-BdTCIlkw.js b/apps/web/static/assets/index-BdTCIlkw.js deleted file mode 100644 index adf20a9..0000000 --- a/apps/web/static/assets/index-BdTCIlkw.js +++ /dev/null @@ -1,133 +0,0 @@ -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(1z||(e.current=R[z],R[z]=null,z--)}function V(e,t){z++,R[z]=e.current,e.current=t}var ne=te(null),re=te(null),ie=te(null),ae=te(null);function oe(e,t){switch(V(ie,t),V(re,e),V(ne,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?qd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=qd(t),e=Jd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}B(ne),V(ne,e)}function se(){B(ne),B(re),B(ie)}function ce(e){e.memoizedState!==null&&V(ae,e);var t=ne.current,n=Jd(t,e.type);t!==n&&(V(re,e),V(ne,n))}function le(e){re.current===e&&(B(ne),B(re)),ae.current===e&&(B(ae),ip._currentValue=L)}var ue,de;function fe(e){if(ue===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);ue=t&&t[1]||``,de=-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{pe=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?fe(n):``}function he(e,t){switch(e.tag){case 26:case 27:case 5:return fe(e.type);case 16:return fe(`Lazy`);case 13:return e.child!==t&&t!==null?fe(`Suspense Fallback`):fe(`Suspense`);case 19:return fe(`SuspenseList`);case 0:case 15:return me(e.type,!1);case 11:return me(e.type.render,!1);case 1:return me(e.type,!0);case 31:return fe(`Activity`);default:return``}}function ge(e){try{var t=``,n=null;do t+=he(e,n),n=e,e=e.return;while(e);return t}catch(e){return` -Error generating stack: `+e.message+` -`+e.stack}}var _e=Object.prototype.hasOwnProperty,ve=t.unstable_scheduleCallback,ye=t.unstable_cancelCallback,be=t.unstable_shouldYield,xe=t.unstable_requestPaint,Se=t.unstable_now,Ce=t.unstable_getCurrentPriorityLevel,we=t.unstable_ImmediatePriority,Te=t.unstable_UserBlockingPriority,Ee=t.unstable_NormalPriority,De=t.unstable_LowPriority,Oe=t.unstable_IdlePriority,ke=t.log,Ae=t.unstable_setDisableYieldValue,je=null,Me=null;function Ne(e){if(typeof ke==`function`&&Ae(e),Me&&typeof Me.setStrictMode==`function`)try{Me.setStrictMode(je,e)}catch{}}var Pe=Math.clz32?Math.clz32:Le,Fe=Math.log,Ie=Math.LN2;function Le(e){return e>>>=0,e===0?32:31-(Fe(e)/Ie|0)|0}var Re=256,ze=262144,Be=4194304;function Ve(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 He(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=Ve(n))):i=Ve(o):i=Ve(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Ve(n))):i=Ve(o)):i=Ve(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 Ue(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function We(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 Ge(){var e=Be;return Be<<=1,!(Be&62914560)&&(Be=4194304),e}function Ke(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function qe(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Je(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),sn=!1;if(on)try{var cn={};Object.defineProperty(cn,`passive`,{get:function(){sn=!0}}),window.addEventListener(`test`,cn,cn),window.removeEventListener(`test`,cn,cn)}catch{sn=!1}var ln=null,un=null,dn=null;function fn(){if(dn)return dn;var e,t=un,n=t.length,r,i=`value`in ln?ln.value:ln.textContent,a=i.length;for(e=0;e=Wn),qn=` `,Jn=!1;function Yn(e,t){switch(e){case`keyup`:return Hn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Xn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Zn=!1;function Qn(e,t){switch(e){case`compositionend`:return Xn(t);case`keypress`:return t.which===32?(Jn=!0,qn):null;case`textInput`:return e=t.data,e===qn&&Jn?null:e;default:return null}}function $n(e,t){if(Zn)return e===`compositionend`||!Un&&Yn(e,t)?(e=fn(),dn=un=ln=null,Zn=!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=xr(n)}}function Cr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Cr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function wr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Nt(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=Nt(e.document)}return t}function Tr(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 Er=on&&`documentMode`in document&&11>=document.documentMode,Dr=null,Or=null,kr=null,Ar=!1;function jr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ar||Dr==null||Dr!==Nt(r)||(r=Dr,`selectionStart`in r&&Tr(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}),kr&&br(kr,r)||(kr=r,r=jd(Or,`onSelect`),0>=o,i-=o,xi=1<<32-Pe(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),ki&&Ci(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),ki&&Ci(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 ki&&Ci(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)}),ki&&Ci(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&&Ca(l)===r.type){n(e,r.sibling),c=a(r,o.props),ka(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=li(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=ci(o.type,o.key,o.props,null,e.mode,c),ka(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=fi(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=Ca(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,Oa(o),c);if(o.$$typeof===C)return b(e,r,Zi(e,o),c);Aa(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=ui(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Da=0;var i=b(e,t,n,r);return Ea=null,i}catch(t){if(t===_a||t===ya)throw t;var a=ii(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ma=ja(!0),Na=ja(!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,Ll&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=ti(e),ei(e,null,n),t}return Zr(e,r,t,n),ti(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,Xe(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=ca;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?(Bl&f)===f:(r&f)===f){f!==0&&f===sa&&(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),Jl|=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,As(e,!1,t,n);try{var c=i(),l=F.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?ks(e,t,da(c,r),gu(e)):ks(e,t,r,gu(e))}catch(n){ks(e,t,{then:function(){},status:`rejected`,reason:n},gu())}finally{I.p=a,o!==null&&s.types!==null&&(o.types=s.types),F.T=o}}function ys(){}function bs(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=xs(e).queue;vs(e,a,t,L,n===null?ys:function(){return Ss(e),n(r)})}function xs(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:L,baseState:L,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:jo,lastRenderedState:L},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:jo,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Ss(e){var t=xs(e);t.next===null&&(t=e.alternate.memoizedState),ks(e,t.next.queue,{},gu())}function Cs(){return Xi(ip)}function ws(){return Eo().memoizedState}function Ts(){return Eo().memoizedState}function Es(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=gu();e=La(n);var r=Ra(t,e,n);r!==null&&(vu(r,t,n),za(r,t,n)),t={cache:ra()},e.payload=t;return}t=t.return}}function Ds(e,t,n){var r=gu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},js(e)?Ms(t,n):(n=Qr(e,t,n,r),n!==null&&(vu(n,e,r),Ns(n,t,r)))}function Os(e,t,n){ks(e,t,n,gu())}function ks(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(js(e))Ms(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,yr(s,o))return Zr(e,t,i,0),Rl===null&&H(),!1}catch{}if(n=Qr(e,t,i,r),n!==null)return vu(n,e,r),Ns(n,t,r),!0}return!1}function As(e,t,n,r){if(r={lane:2,revertLane:hd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},js(e)){if(t)throw Error(i(479))}else t=Qr(e,n,r,2),t!==null&&vu(t,e,2)}function js(e){var t=e.alternate;return e===ao||t!==null&&t===ao}function Ms(e,t){lo=co=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ns(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Xe(e,n)}}var Ps={readContext:Xi,use:ko,useCallback:go,useContext:go,useEffect:go,useImperativeHandle:go,useLayoutEffect:go,useInsertionEffect:go,useMemo:go,useReducer:go,useRef:go,useState:go,useDebugValue:go,useDeferredValue:go,useTransition:go,useSyncExternalStore:go,useId:go,useHostTransitionStatus:go,useFormState:go,useActionState:go,useOptimistic:go,useMemoCache:go,useCacheRefresh:go};Ps.useEffectEvent=go;var Fs={readContext:Xi,use:ko,useCallback:function(e,t){return To().memoizedState=[e,t===void 0?null:t],e},useContext:Xi,useEffect:as,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),rs(4194308,4,ds.bind(null,t,e),n)},useLayoutEffect:function(e,t){return rs(4194308,4,e,t)},useInsertionEffect:function(e,t){rs(4,2,e,t)},useMemo:function(e,t){var n=To();t=t===void 0?null:t;var r=e();if(uo){Ne(!0);try{e()}finally{Ne(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=To();if(n!==void 0){var i=n(t);if(uo){Ne(!0);try{n(t)}finally{Ne(!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=Ds.bind(null,ao,e),[r.memoizedState,e]},useRef:function(e){var t=To();return e={current:e},t.memoizedState=e},useState:function(e){e=Vo(e);var t=e.queue,n=Os.bind(null,ao,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:ps,useDeferredValue:function(e,t){return gs(To(),e,t)},useTransition:function(){var e=Vo(!1);return e=vs.bind(null,ao,e.queue,!0,!1),To().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=ao,a=To();if(ki){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Rl===null)throw Error(i(349));Bl&127||Io(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,as(Ro.bind(null,r,o,e),[e]),r.flags|=2048,ts(9,{destroy:void 0},Lo.bind(null,r,o,n,t),null),n},useId:function(){var e=To(),t=Rl.identifierPrefix;if(ki){var n=Si,r=xi;n=(r&~(1<<32-Pe(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=fo++,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[rt]=t,o[it]=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(Bd(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&&kc(t)}}return Pc(t),Ac(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&kc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=ie.current,Ii(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Di,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[rt]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Ld(e.nodeValue,n)),e||Ni(t,!0)}else e=Kd(e).createTextNode(r),e[rt]=t,t.stateNode=e}return Pc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Ii(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[rt]=t}else Li(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Pc(t),e=!1}else n=Ri(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(to(t),t):(to(t),null);if(t.flags&128)throw Error(i(558))}return Pc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Ii(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[rt]=t}else Li(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Pc(t),a=!1}else a=Ri(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(to(t),t):(to(t),null)}return to(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),Mc(t,t.updateQueue),Pc(t),null);case 4:return se(),e===null&&Dd(t.stateNode.containerInfo),Pc(t),null;case 10:return Wi(t.type),Pc(t),null;case 19:if(B(no),r=t.memoizedState,r===null)return Pc(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Nc(r,!1);else{if(ql!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=ro(e),o!==null){for(t.flags|=128,Nc(r,!1),e=o.updateQueue,t.updateQueue=e,Mc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)si(n,e),n=n.sibling;return V(no,no.current&1|2),ki&&Ci(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Se()>iu&&(t.flags|=128,a=!0,Nc(r,!1),t.lanes=4194304)}else{if(!a)if(e=ro(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Mc(t,e),Nc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!ki)return Pc(t),null}else 2*Se()-r.renderingStartTime>iu&&n!==536870912&&(t.flags|=128,a=!0,Nc(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?(Pc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Se(),e.sibling=null,n=no.current,V(no,a?n&1|2:n&1),ki&&Ci(t,r.treeForkCount),e);case 22:case 23:return to(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)&&(Pc(t),t.subtreeFlags&6&&(t.flags|=8192)):Pc(t),n=t.updateQueue,n!==null&&Mc(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&&B(pa),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Wi(na),Pc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Ic(e,t){switch(Ti(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Wi(na),se(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return le(t),null;case 31:if(t.memoizedState!==null){if(to(t),t.alternate===null)throw Error(i(340));Li()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(to(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Li()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return B(no),null;case 4:return se(),null;case 10:return Wi(t.type),null;case 22:case 23:return to(t),Xa(),e!==null&&B(pa),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Wi(na),null;case 25:return null;default:return null}}function Lc(e,t){switch(Ti(t),t.tag){case 3:Wi(na),se();break;case 26:case 27:case 5:le(t);break;case 4:se();break;case 31:t.memoizedState!==null&&to(t);break;case 13:to(t);break;case 19:B(no);break;case 10:Wi(t.type);break;case 22:case 23:to(t),Xa(),e!==null&&B(pa);break;case 24:Wi(na)}}function Rc(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){Ju(t,t.return,e)}}function zc(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){Ju(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Ju(t,t.return,e)}}function Bc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Ga(t,n)}catch(t){Ju(e,e.return,t)}}}function Vc(e,t,n){n.props=Hs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Ju(e,t,n)}}function Hc(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){Ju(e,t,n)}}function Uc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Ju(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){Ju(e,t,n)}else n.current=null}function Wc(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){Ju(e,e.return,t)}}function Gc(e,t,n){try{var r=e.stateNode;Vd(r,e.type,n,t),r[it]=t}catch(t){Ju(e,e.return,t)}}function Kc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&rf(e.type)||e.tag===4}function qc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Kc(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&&rf(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 Jc(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=Xt));else if(r!==4&&(r===27&&rf(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Jc(e,t,n),e=e.sibling;e!==null;)Jc(e,t,n),e=e.sibling}function Yc(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&&rf(e.type)&&(n=e.stateNode),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){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Bd(t,r,n),t[rt]=e,t[it]=n}catch(t){Ju(e,e.return,t)}}var Zc=!1,Qc=!1,$c=!1,el=typeof WeakSet==`function`?WeakSet:Set,tl=null;function nl(e,t){if(e=e.containerInfo,Wd=pp,e=wr(e),Tr(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(Gd={focusedElem:e,selectionRange:n},pp=!1,tl=t;tl!==null;)if(t=tl,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,tl=e;else for(;tl!==null;){switch(t=tl,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`))),Bd(o,r,n),o[rt]=e,gt(o),r=o;break a;case`link`:var s=qf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=Sr(s,h),v=Sr(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=fu,fu=null;var o=cu,s=uu;if(su=0,lu=cu=null,uu=0,Ll&6)throw Error(i(331));var c=Ll;if(Ll|=4,Ml(o.current),wl(o,o.current,s,n),Ll=c,cd(0,!1),Me&&typeof Me.onPostCommitFiberRoot==`function`)try{Me.onPostCommitFiberRoot(je,o)}catch{}return!0}finally{I.p=a,F.T=r,Wu(e,t)}}function qu(e,t,n){t=U(n,t),t=Js(e.stateNode,t,2),e=Ra(e,t,2),e!==null&&(qe(e,2),sd(e))}function Ju(e,t,n){if(e.tag===3)qu(e,e,n);else for(;t!==null;){if(t.tag===3){qu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(ou===null||!ou.has(r))){e=U(n,e),n=Ys(2),r=Ra(t,n,2),r!==null&&(Xs(n,r,t,e),qe(r,2),sd(r));break}}t=t.return}}function Yu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Il;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)||(Gl=!0,i.add(n),e=Xu.bind(null,e,t,n),t.then(e,e))}function Xu(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Rl===e&&(Bl&n)===n&&(ql===4||ql===3&&(Bl&62914560)===Bl&&300>Se()-nu?!(Ll&2)&&Tu(e,0):Xl|=n,Ql===Bl&&(Ql=0)),sd(e)}function Zu(e,t){t===0&&(t=Ge()),e=$r(e,t),e!==null&&(qe(e,t),sd(e))}function Qu(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Zu(e,n)}function $u(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),Zu(e,n)}function ed(e,t){return ve(e,t)}var td=null,nd=null,rd=!1,id=!1,ad=!1,od=0;function sd(e){e!==nd&&e.next===null&&(nd===null?td=nd=e:nd=nd.next=e),id=!0,rd||(rd=!0,md())}function cd(e,t){if(!ad&&id){ad=!0;do for(var n=!1,r=td;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-Pe(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,pd(r,a))}else a=Bl,a=He(r,r===Rl?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Ue(r,a)||(n=!0,pd(r,a));r=r.next}while(n);ad=!1}}function ld(){ud()}function ud(){id=rd=!1;var e=0;od!==0&&Zd()&&(e=od);for(var t=Se(),n=null,r=td;r!==null;){var i=r.next,a=dd(r,t);a===0?(r.next=null,n===null?td=i:n.next=i,i===null&&(nd=n)):(n=r,(e!==0||a&3)&&(id=!0)),r=i}su!==0&&su!==5||cd(e,!1),od!==0&&(od=0)}function dd(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&&Hd(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function Df(e,t,n){var r=Ef;if(r&&typeof t==`string`&&t){var i=Ft(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),xf.has(i)||(xf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Bd(t,`link`,e),gt(t),r.head.appendChild(t)))}}function Of(e){Cf.D(e),Df(`dns-prefetch`,e,null)}function kf(e,t){Cf.C(e,t),Df(`preconnect`,e,t)}function Af(e,t,n){Cf.L(e,t,n);var r=Ef;if(r&&e&&t){var i=`link[rel="preload"][as="`+Ft(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Ft(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Ft(n.imageSizes)+`"]`)):i+=`[href="`+Ft(e)+`"]`;var a=i;switch(t){case`style`:a=If(e);break;case`script`:a=Bf(e)}bf.has(a)||(e=p({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),bf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Lf(a))||t===`script`&&r.querySelector(Vf(a))||(t=r.createElement(`link`),Bd(t,`link`,e),gt(t),r.head.appendChild(t)))}}function jf(e,t){Cf.m(e,t);var n=Ef;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Ft(r)+`"][href="`+Ft(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Bf(e)}if(!bf.has(a)&&(e=p({rel:`modulepreload`,href:e},t),bf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Vf(a)))return}r=n.createElement(`link`),Bd(r,`link`,e),gt(r),n.head.appendChild(r)}}}function Mf(e,t,n){Cf.S(e,t,n);var r=Ef;if(r&&e){var i=ht(r).hoistableStyles,a=If(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Lf(a)))s.loading=5;else{e=p({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=bf.get(a))&&Wf(e,n);var c=o=r.createElement(`link`);gt(c),Bd(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,Uf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Nf(e,t){Cf.X(e,t);var n=Ef;if(n&&e){var r=ht(n).hoistableScripts,i=Bf(e),a=r.get(i);a||(a=n.querySelector(Vf(i)),a||(e=p({src:e,async:!0},t),(t=bf.get(i))&&Gf(e,t),a=n.createElement(`script`),gt(a),Bd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Pf(e,t){Cf.M(e,t);var n=Ef;if(n&&e){var r=ht(n).hoistableScripts,i=Bf(e),a=r.get(i);a||(a=n.querySelector(Vf(i)),a||(e=p({src:e,async:!0,type:`module`},t),(t=bf.get(i))&&Gf(e,t),a=n.createElement(`script`),gt(a),Bd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Ff(e,t,n,r){var a=(a=ie.current)?Sf(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=If(n.href),n=ht(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=If(n.href);var o=ht(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(Lf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),bf.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},bf.set(e,n),o||zf(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=Bf(n),n=ht(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 If(e){return`href="`+Ft(e)+`"`}function Lf(e){return`link[rel="stylesheet"][`+e+`]`}function Rf(e){return p({},e,{"data-precedence":e.precedence,precedence:null})}function zf(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}),Bd(t,`link`,n),gt(t),e.head.appendChild(t))}function Bf(e){return`[src="`+Ft(e)+`"]`}function Vf(e){return`script[async]`+e}function Hf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Ft(n.href)+`"]`);if(r)return t.instance=r,gt(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`),gt(r),Bd(r,`style`,a),Uf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=If(n.href);var o=e.querySelector(Lf(a));if(o)return t.state.loading|=4,t.instance=o,gt(o),o;r=Rf(n),(a=bf.get(a))&&Wf(r,a),o=(e.ownerDocument||e).createElement(`link`),gt(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Bd(o,`link`,r),t.state.loading|=4,Uf(o,n.precedence,e),t.instance=o;case`script`:return o=Bf(n.src),(a=e.querySelector(Vf(o)))?(t.instance=a,gt(a),a):(r=n,(a=bf.get(o))&&(r=p({},n),Gf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),gt(a),Bd(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,Uf(r,n.precedence,e));return t.instance}function Uf(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 Yf(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 Xf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Zf(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=If(r.href),a=t.querySelector(Lf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=ep.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,gt(a);return}a=t.ownerDocument||t,r=Rf(r),(i=bf.get(i))&&Wf(r,i),a=a.createElement(`link`),gt(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Bd(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=ep.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Qf=0;function $f(e,t){return e.stylesheets&&e.count===0&&np(e,e.stylesheets),0Qf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function ep(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)np(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var tp=null;function np(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,tp=new Map,t.forEach(rp,e),tp=null,ep.call(e))}function rp(e,t){if(!(t.state.loading&4)){var n=tp.get(e);if(n)var r=n.get(null);else{n=new Map,tp.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 L(e,t,n,!1)}function L(e,t,n,r){let i=me((typeof t==`string`?P(t):t).pathname||`/`,n);if(i==null)return null;let a=z(e);B(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=Se([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}".`),z(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:ce(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of te(e.path))a(e,t,!0,n)}),t}function te(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=te(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 B(e){e.sort((e,t)=>e.score===t.score?le(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var V=/^:[\w-]+$/,ne=3,re=2,ie=1,ae=10,oe=-2,se=e=>e===`*`;function ce(e,t){let n=e.split(`/`),r=n.length;return n.some(se)&&(r+=oe),t&&(r+=re),n.filter(e=>!se(e)).reduce((e,t)=>e+(V.test(t)?ne:t===``?ie:ae),r)}function le(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 ue(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 fe(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 pe(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 me(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 he=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function ge(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(`/`)?_e(n.substring(1),`/`):_e(n,t)):a=t,{pathname:a,search:we(r),hash:Te(i)}}function _e(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 ve(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 ye(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function be(e){let t=ye(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function xe(e,t,n,r=!1){let i;typeof e==`string`?i=P(e):(i={...e},O(!i.pathname||!i.pathname.includes(`?`),ve(`?`,`pathname`,`search`,i)),O(!i.pathname||!i.pathname.includes(`#`),ve(`#`,`pathname`,`hash`,i)),O(!i.search||!i.search.includes(`#`),ve(`#`,`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=ge(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var Se=e=>e.join(`/`).replace(/\/\/+/g,`/`),Ce=e=>e.replace(/\/+$/,``).replace(/^\/*/,`/`),we=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,Te=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,Ee=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 De(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Oe(e){return e.map(e=>e.route.path).filter(Boolean).join(`/`).replace(/\/\/*/g,`/`)||`/`}var ke=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Ae(e,t){let n=e;if(typeof n!=`string`||!he.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(ke)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=me(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 je=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(je);var Me=[`GET`,...je];new Set(Me);var Ne=y.createContext(null);Ne.displayName=`DataRouter`;var Pe=y.createContext(null);Pe.displayName=`DataRouterState`;var Fe=y.createContext(!1),Ie=y.createContext({isTransitioning:!1});Ie.displayName=`ViewTransition`;var Le=y.createContext(new Map);Le.displayName=`Fetchers`;var Re=y.createContext(null);Re.displayName=`Await`;var ze=y.createContext(null);ze.displayName=`Navigation`;var Be=y.createContext(null);Be.displayName=`Location`;var Ve=y.createContext({outlet:null,matches:[],isDataRoute:!1});Ve.displayName=`Route`;var He=y.createContext(null);He.displayName=`RouteError`;var Ue=`REACT_ROUTER_ERROR`,We=`REDIRECT`,Ge=`ROUTE_ERROR_RESPONSE`;function Ke(e){if(e.startsWith(`${Ue}:${We}:{`))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 qe(e){if(e.startsWith(`${Ue}:${Ge}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new Ee(t.status,t.statusText,t.data)}catch{}}function Je(e,{relative:t}={}){O(Ye(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=y.useContext(ze),{hash:i,pathname:a,search:o}=nt(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:Se([n,a])),r.createHref({pathname:s,search:o,hash:i})}function Ye(){return y.useContext(Be)!=null}function Xe(){return O(Ye(),`useLocation() may be used only in the context of a component.`),y.useContext(Be).location}var Ze=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function Qe(e){y.useContext(ze).static||y.useLayoutEffect(e)}function $e(){let{isDataRoute:e}=y.useContext(Ve);return e?xt():et()}function et(){O(Ye(),`useNavigate() may be used only in the context of a component.`);let e=y.useContext(Ne),{basename:t,navigator:n}=y.useContext(ze),{matches:r}=y.useContext(Ve),{pathname:i}=Xe(),a=JSON.stringify(be(r)),o=y.useRef(!1);return Qe(()=>{o.current=!0}),y.useCallback((r,s={})=>{if(k(o.current,Ze),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=xe(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:Se([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}y.createContext(null);function tt(){let{matches:e}=y.useContext(Ve),t=e[e.length-1];return t?t.params:{}}function nt(e,{relative:t}={}){let{matches:n}=y.useContext(Ve),{pathname:r}=Xe(),i=JSON.stringify(be(n));return y.useMemo(()=>xe(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function rt(e,t){return it(e,t)}function it(e,t,n){O(Ye(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=y.useContext(ze),{matches:i}=y.useContext(Ve),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||``;Ct(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=Xe(),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=dt(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:Se([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:Se([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(Be.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,unstable_mask:void 0,...d},navigationType:`POP`}},h):h}function at(){let e=bt(),t=De(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 ot=y.createElement(at,null),st=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=qe(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:y.createElement(Ve.Provider,{value:this.props.routeContext},y.createElement(He.Provider,{value:e,children:this.props.component}));return this.context?y.createElement(lt,{error:e},t):t}};st.contextType=Fe;var ct=new WeakMap;function lt({children:e,error:t}){let{basename:n}=y.useContext(ze);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=Ke(t.digest);if(e){let r=ct.get(t);if(r)throw r;let i=Ae(e.location,n);if(ke&&!ct.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 ct.set(t,n),n}return y.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function ut({routeContext:e,match:t,children:n}){let r=y.useContext(Ne);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),y.createElement(Ve.Provider,{value:e},n)}function dt(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:Oe(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||ot,o&&(s<0&&c===0?(Ct(`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(ut,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?y.createElement(st,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function ft(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function pt(e){let t=y.useContext(Ne);return O(t,ft(e)),t}function mt(e){let t=y.useContext(Pe);return O(t,ft(e)),t}function ht(e){let t=y.useContext(Ve);return O(t,ft(e)),t}function gt(e){let t=ht(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 _t(){return gt(`useRouteId`)}function vt(){return mt(`useNavigation`).navigation}function yt(){let{matches:e,loaderData:t}=mt(`useMatches`);return y.useMemo(()=>e.map(e=>R(e,t)),[e,t])}function bt(){let e=y.useContext(He),t=mt(`useRouteError`),n=gt(`useRouteError`);return e===void 0?t.errors?.[n]:e}function xt(){let{router:e}=pt(`useNavigate`),t=gt(`useNavigate`),n=y.useRef(!1);return Qe(()=>{n.current=!0}),y.useCallback(async(r,i={})=>{k(n.current,Ze),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var St={};function Ct(e,t,n){!t&&!St[e]&&(St[e]=!0,k(!1,n))}y.useOptimistic,y.memo(wt);function wt({routes:e,future:t,state:n,isStatic:r,onError:i}){return it(e,void 0,{state:n,isStatic:r,onError:i,future:t})}function Tt(e){O(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Et({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,unstable_useTransitions:o}){O(!Ye(),`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=me(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(ze.Provider,{value:c},y.createElement(Be.Provider,{children:t,value:h}))}function Dt({children:e,location:t}){return rt(Ot(e),t)}y.Component;function Ot(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,Ot(e.props.children,i));return}O(e.type===Tt,`[${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=Ot(e.props.children,i)),n.push(a)}),n}var kt=`get`,At=`application/x-www-form-urlencoded`;function jt(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Mt(e){return jt(e)&&e.tagName.toLowerCase()===`button`}function Nt(e){return jt(e)&&e.tagName.toLowerCase()===`form`}function Pt(e){return jt(e)&&e.tagName.toLowerCase()===`input`}function Ft(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function It(e,t){return e.button===0&&(!t||t===`_self`)&&!Ft(e)}function Lt(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 Rt(e,t){let n=Lt(e);return t&&t.forEach((e,r)=>{n.has(r)||t.getAll(r).forEach(e=>{n.append(r,e)})}),n}var zt=null;function Bt(){if(zt===null)try{new FormData(document.createElement(`form`),0),zt=!1}catch{zt=!0}return zt}var Vt=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Ht(e){return e!=null&&!Vt.has(e)?(k(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${At}"`),null):e}function Ut(e,t){let n,r,i,a,o;if(Nt(e)){let o=e.getAttribute(`action`);r=o?me(o,t):null,n=e.getAttribute(`method`)||kt,i=Ht(e.getAttribute(`enctype`))||At,a=new FormData(e)}else if(Mt(e)||Pt(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a + + +
+
+
+ + +
+
+ {event.event_name} +
+ {event.message && ( +
{event.message}
+ )} +
+ Time: {fmtTs(event.ts_utc)} + Source: {event.source} + {event.session_id && Session: {event.session_id}} + {event.ticker && Ticker: {event.ticker}} +
+ {event.job_run_id && ( +
+ Run: {event.job_run_id.slice(0, 16)}… +
+ )} +
+ + {parsedDetails && Object.keys(parsedDetails).length > 0 && ( +
+
Details
+
+              {JSON.stringify(parsedDetails, null, 2)}
+            
+
+ )} + + {event.job_run_id && relatedEvents.length > 1 && ( +
+
+ Run Timeline ({relatedEvents.length} events) +
+
+ {[...relatedEvents].sort((a, b) => a.ts_utc.localeCompare(b.ts_utc)).map(e => ( +
+
+
+
+ {e.ts_utc.slice(11, 19)} +
+
+ {e.event_name.length > 40 ? e.event_name.slice(0, 40) + '…' : e.event_name} +
+
+
+ ))} +
+
+ )} +
+
+ ); +} + +// ── Filter bar ──────────────────────────────────────────────────────────────── + +const LEVEL_OPTIONS = ['', 'ERROR', 'WARN', 'INFO']; +const CAT_OPTIONS = ['', 'fallback', 'snapshot', 'macro', 'broker', 'kill_switch', 'lifecycle', 'order', 'engine', 'pipeline']; +const TIME_OPTIONS = [ + { label: 'Today', since: () => new Date().toISOString().slice(0, 10) + 'T00:00:00' }, + { label: '24h', since: () => new Date(Date.now() - 86_400_000).toISOString() }, + { label: '7d', since: () => new Date(Date.now() - 7 * 86_400_000).toISOString() }, + { label: 'All', since: () => '' }, +]; + +function sel() { + return { + padding: '5px 10px', borderRadius: 6, border: '1px solid var(--border)', + background: 'var(--bg2)', color: 'var(--text2)', fontSize: 12, + fontFamily: 'var(--font-mono)', cursor: 'pointer', outline: 'none', + } as React.CSSProperties; +} + +// ── Tab bar ─────────────────────────────────────────────────────────────────── + +const TABS = [ + { key: 'all', label: 'All' }, + { key: 'pead', label: 'PEAD' }, + { key: 'orb', label: 'ORB' }, +] as const; + +type TabKey = typeof TABS[number]['key']; + +// ── Main page ───────────────────────────────────────────────────────────────── + +export function LogsPage() { + const [sp, setSP] = useSearchParams(); + const [selected, setSelected] = useState(null); + const [relatedEvents, setRelatedEvents] = useState([]); + const [timeLabel, setTimeLabel] = useState('Today'); + + // Active tab from URL + const activeTab = ((sp.get('system') || 'all') as TabKey); + + // Filters from URL querystring for deep-linking + const filters = { + level: sp.get('level') ?? '', + category: sp.get('category') ?? '', + source: sp.get('source') ?? '', + session_id: sp.get('session_id') ?? '', + job_run_id: sp.get('job_run_id') ?? '', + q: sp.get('q') ?? '', + since: sp.get('since') ?? TIME_OPTIONS[0].since(), + system: sp.get('system') ?? '', + }; + + const setFilter = useCallback((key: string, val: string) => { + setSP(prev => { + const next = new URLSearchParams(prev); + if (val) next.set(key, val); + else next.delete(key); + return next; + }); + }, [setSP]); + + const { data: health, refetch: refetchHealth } = useQuery({ + queryKey: ['health'], + queryFn: eventsApi.health, + refetchInterval: 5_000, + }); + + const { data: sources } = useQuery({ + queryKey: ['event-sources'], + queryFn: eventsApi.sources, + staleTime: 60_000, + }); + + const queryKey = ['events', filters]; + const { data, isFetching, refetch } = useQuery({ + queryKey, + queryFn: () => eventsApi.list({ + level: filters.level || undefined, + category: filters.category || undefined, + source: filters.source || undefined, + session_id: filters.session_id || undefined, + job_run_id: filters.job_run_id || undefined, + q: filters.q || undefined, + since: filters.since || undefined, + limit: 200, + system: filters.system || undefined, + }), + refetchInterval: 3_000, + }); + + // Load run timeline when an event with job_run_id is selected + useEffect(() => { + if (!selected?.job_run_id) { setRelatedEvents([]); return; } + eventsApi.list({ job_run_id: selected.job_run_id, limit: 1000 }).then(r => setRelatedEvents(r.rows)).catch(() => {}); + }, [selected?.job_run_id]); + + const rows = data?.rows ?? []; + const total = data?.total ?? 0; + + return ( +
+ {/* ── Header ── */} +
+
+ +

Logs / Health

+ {isFetching && } +
+
+ Live event feed from PEAD engine, ORB engine, pipeline, and AutoScheduler +
+
+ + {/* ── Tab bar ── */} +
+ {TABS.map(tab => ( + + ))} +
+ +
+ {/* ── Health Cards ── */} + {health && ( +
+ +
+ )} + + {/* ── Filter bar ── */} +
+ {/* Time range */} +
+ {TIME_OPTIONS.map(opt => ( + + ))} +
+ +
+ + + + + + + + setFilter('q', e.target.value)} + style={{ + padding: '5px 10px', borderRadius: 6, border: '1px solid var(--border)', + background: 'var(--bg2)', color: 'var(--text2)', fontSize: 12, + fontFamily: 'var(--font-mono)', outline: 'none', minWidth: 140, + }} + /> + +
+ + {total.toLocaleString()} events + + +
+
+ + {/* ── Table + Detail ── */} +
+ {/* Event table */} +
+ {rows.length === 0 ? ( +
+ {isFetching ? 'Loading…' : 'No events match the current filters'} +
+ ) : ( + + + + {['Time', 'Level', 'Category', 'Source', 'Event', 'Message', 'Session'].map(h => ( + + ))} + + + + {rows.map(row => ( + setSelected(row.id === selected?.id ? null : row)} + style={{ + cursor: 'pointer', + borderBottom: '1px solid var(--border)', + background: row.id === selected?.id + ? 'var(--cyan-dim)' + : row.level === 'ERROR' ? 'rgba(239,68,68,0.04)' + : row.level === 'WARN' ? 'rgba(234,179,8,0.04)' + : 'transparent', + transition: 'background 0.1s', + }} + onMouseEnter={e => { if (row.id !== selected?.id) (e.currentTarget as HTMLElement).style.background = 'var(--bg1)'; }} + onMouseLeave={e => { if (row.id !== selected?.id) (e.currentTarget as HTMLElement).style.background = row.level === 'ERROR' ? 'rgba(239,68,68,0.04)' : row.level === 'WARN' ? 'rgba(234,179,8,0.04)' : 'transparent'; }} + > + + + + + + + + + ))} + +
{h}
+ {fmtTs(row.ts_utc)} + + + + + + {row.source} + + {row.event_name} + + {row.message} + + {row.session_id ?? '—'} +
+ )} +
+ + {/* Detail panel */} + {selected && ( + setSelected(null)} + relatedEvents={relatedEvents} + /> + )} +
+
+
+ ); +}