fixtureqa 0.4.6__tar.gz → 0.4.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fixtureqa-0.4.6/fixtureqa.egg-info → fixtureqa-0.4.7}/PKG-INFO +1 -1
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/perf_engine.py +10 -5
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/perf_models.py +8 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/perf_stats.py +28 -0
- fixtureqa-0.4.6/fixture/static/assets/index-FGgej6RF.js → fixtureqa-0.4.7/fixture/static/assets/index-DISOjvhu.js +1 -1
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/static/index.html +1 -1
- {fixtureqa-0.4.6 → fixtureqa-0.4.7/fixtureqa.egg-info}/PKG-INFO +1 -1
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixtureqa.egg-info/SOURCES.txt +1 -1
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/pyproject.toml +1 -1
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_perf_engine.py +82 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/LICENSE +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/README.md +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/__init__.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/__main__.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/__init__.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/app.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/connection_manager.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/deps.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/routers/__init__.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/routers/admin.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/routers/auth.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/routers/branding.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/routers/custom_tags.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/routers/fix_spec.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/routers/messages.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/routers/perf.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/routers/scenarios.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/routers/sessions.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/routers/setup.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/routers/spec_overlay.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/routers/templates.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/routers/ws.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/api/schemas.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/config/__init__.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/__init__.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/atomic_io.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/auth.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/config_store.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/custom_tag_store.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/db_migrations.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/events.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/exec_csv_writer.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/fix_application.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/fix_builder.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/fix_parser.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/fix_spec_parser.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/fix_tags.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/fix_time.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/housekeeping.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/inbound.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/json_store.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/message_log.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/message_store.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/models.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/perf_payload.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/perf_store.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/perf_writer.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/scenario_runner.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/scenario_store.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/session.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/session_manager.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/spec_overlay_store.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/template_store.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/user_store.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/core/venue_responses.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/fix_specs/FIX42.xml +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/fix_specs/FIX44.xml +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/server.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/static/assets/index-BwQf-cei.css +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/static/assets/index-CyNOPa0n.js +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/static/favicon.svg +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixture/ui/__init__.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixtureqa.egg-info/dependency_links.txt +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixtureqa.egg-info/entry_points.txt +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixtureqa.egg-info/requires.txt +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/fixtureqa.egg-info/top_level.txt +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/setup.cfg +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_atomic_io.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_auth.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_config_store.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_connection_manager.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_db_migrations.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_fix_builder.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_health.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_inbound.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_inbound_validation.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_message_store.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_perf_api.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_perf_models.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_perf_payload.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_perf_rehydrate.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_scenarios.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_session_lifecycle.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_session_manager_concurrency.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_sessions.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_templates.py +0 -0
- {fixtureqa-0.4.6 → fixtureqa-0.4.7}/tests/test_ws.py +0 -0
|
@@ -35,7 +35,7 @@ from .perf_models import (
|
|
|
35
35
|
RunConfig, RunStatus, LiveSnapshot, ClientLeg, VenueLeg, SnapshotErrors, LatencyStats,
|
|
36
36
|
)
|
|
37
37
|
from .perf_payload import PayloadFactory, ScenarioSelector
|
|
38
|
-
from .perf_stats import PerfStats, TokenBucket
|
|
38
|
+
from .perf_stats import BurstGate, PerfStats, TokenBucket
|
|
39
39
|
from .perf_writer import PerfWriter
|
|
40
40
|
from .session_manager import SessionManager
|
|
41
41
|
|
|
@@ -115,10 +115,15 @@ class PerfRun:
|
|
|
115
115
|
if config.payload.scenarios else None)
|
|
116
116
|
|
|
117
117
|
r = config.rate
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
|
|
119
|
+
def _pacer(per_window: int, window_ms: int, mode: str):
|
|
120
|
+
if mode == "burst":
|
|
121
|
+
return BurstGate(per_window, window_ms)
|
|
122
|
+
return TokenBucket(per_window, window_ms,
|
|
123
|
+
r.allow_burst, r.max_burst_multiplier)
|
|
124
|
+
|
|
125
|
+
self._order_bucket = _pacer(r.orders_per_window, r.order_window_ms, r.dispatch)
|
|
126
|
+
self._fill_bucket = _pacer(r.fills_per_window, r.fill_window_ms, r.fill_dispatch)
|
|
122
127
|
|
|
123
128
|
self._client_sub = None
|
|
124
129
|
self._venue_sub = None
|
|
@@ -24,9 +24,17 @@ RunState = Literal["pending", "running", "completed", "stopped", "error", "satur
|
|
|
24
24
|
Sequence = Literal["parallel", "sequential", "sequential_timed"]
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
DispatchMode = Literal["smooth", "burst"]
|
|
28
|
+
|
|
29
|
+
|
|
27
30
|
class RateConfig(BaseModel):
|
|
28
31
|
orders_per_window: int = Field(gt=0)
|
|
29
32
|
order_window_ms: int = Field(gt=0)
|
|
33
|
+
# smooth: continuous-refill token bucket — sends evenly spaced at the
|
|
34
|
+
# average rate. burst: up to per_window sends back-to-back per window
|
|
35
|
+
# boundary, then silence until the next window (sawtooth throughput).
|
|
36
|
+
dispatch: DispatchMode = "smooth" # order leg
|
|
37
|
+
fill_dispatch: DispatchMode = "smooth" # venue exec leg
|
|
30
38
|
fills_per_window: int = Field(gt=0)
|
|
31
39
|
fill_window_ms: int = Field(gt=0)
|
|
32
40
|
fill_ratio: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
@@ -40,6 +40,34 @@ class TokenBucket:
|
|
|
40
40
|
await asyncio.sleep((n - self._tokens) / self._rate_per_s)
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
class BurstGate:
|
|
44
|
+
"""Window-boundary pacer (same `acquire()` contract as TokenBucket).
|
|
45
|
+
|
|
46
|
+
Allows `per_window` acquires back-to-back within each window, then blocks
|
|
47
|
+
everyone until the next boundary — a sawtooth instead of the bucket's even
|
|
48
|
+
spacing. Boundaries stay aligned to the original cadence (no drift).
|
|
49
|
+
Note: it shapes a ceiling — bursts only show when demand inside a window
|
|
50
|
+
exceeds it (e.g. injector loop, or a venue fill backlog)."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, per_window: int, window_ms: int):
|
|
53
|
+
self._per_window = per_window
|
|
54
|
+
self._window_s = window_ms / 1000.0
|
|
55
|
+
self._window_end = time.monotonic() + self._window_s
|
|
56
|
+
self._used = 0
|
|
57
|
+
|
|
58
|
+
async def acquire(self, n: int = 1) -> None:
|
|
59
|
+
while True:
|
|
60
|
+
now = time.monotonic()
|
|
61
|
+
if now >= self._window_end:
|
|
62
|
+
missed = int((now - self._window_end) / self._window_s) + 1
|
|
63
|
+
self._window_end += missed * self._window_s
|
|
64
|
+
self._used = 0
|
|
65
|
+
if self._used + n <= self._per_window:
|
|
66
|
+
self._used += n
|
|
67
|
+
return
|
|
68
|
+
await asyncio.sleep(max(0.0, self._window_end - now))
|
|
69
|
+
|
|
70
|
+
|
|
43
71
|
class _Reservoir:
|
|
44
72
|
"""Reservoir sampler over latency values (microseconds). Exact count/mean/max;
|
|
45
73
|
approximate percentiles from a fixed-size representative sample."""
|
|
@@ -99,4 +99,4 @@ Please change the parent <Route path="${b}"> to <Route path="${b==="/"?"*":`${b}
|
|
|
99
99
|
]
|
|
100
100
|
}
|
|
101
101
|
]
|
|
102
|
-
}`;function Kz({onClose:e}){const[t,r]=h.useState(""),[n,a]=h.useState(""),[i,o]=h.useState(!1);h.useEffect(()=>{G.specOverlay.get().then(l=>r(JSON.stringify(l,null,2))).catch(console.error)},[]);async function s(){a("");let l;try{l=JSON.parse(t)}catch{a("Invalid JSON");return}o(!0);try{await G.specOverlay.set(l),e()}catch(u){a(u instanceof Error?u.message:"Save failed")}finally{o(!1)}}return c.jsx("div",{className:"modal-overlay",onClick:e,children:c.jsxs("div",{className:"modal",style:{width:660,maxHeight:"85vh",display:"flex",flexDirection:"column"},onClick:l=>l.stopPropagation(),children:[c.jsxs("div",{className:"modal-header",children:[c.jsx("span",{children:"Spec Overlay — Custom Message Types"}),c.jsx("button",{className:"btn-icon",onClick:e,children:"✕"})]}),c.jsx("div",{style:{padding:"10px 16px 4px",fontSize:12,color:"var(--text2)"},children:"Define custom / proprietary message types (e.g. 35=U1). Fields reference standard tag numbers; use Custom Tags for non-standard tag names. Field names and types are resolved from the spec at runtime."}),c.jsx("div",{style:{flex:1,display:"flex",flexDirection:"column",padding:"8px 16px",minHeight:0},children:c.jsx("textarea",{style:{flex:1,minHeight:300,fontFamily:"var(--font)",fontSize:12,background:"var(--bg3)",color:"var(--text)",border:"1px solid var(--border)",borderRadius:4,padding:"8px 10px",resize:"none"},value:t,onChange:l=>r(l.target.value),spellCheck:!1,placeholder:Zz})}),c.jsxs("div",{style:{padding:"0 16px 8px",fontSize:11,color:"var(--text2)"},children:["Schema: ",c.jsx("code",{children:'{ "messages": [{ "msg_type", "name", "category", "fields": [{ "tag", "required" }] }] }'})]}),n&&c.jsx("div",{className:"tpl-error",style:{margin:"0 16px 8px"},children:n}),c.jsxs("div",{className:"modal-footer",children:[c.jsx("button",{className:"btn btn-ghost",onClick:e,children:"Cancel"}),c.jsx("button",{className:"btn btn-primary",onClick:s,disabled:i,children:i?"Saving…":"Save overlay"})]})]})})}const Iw="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgdmlld0JveD0iMCAwIDY4MCAxNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgCgogIDxwb2x5Z29uIHBvaW50cz0iMTg4LDI4IDIzOCwyOCAyNTgsNzggMjM4LDEyOCAxODgsMTI4IiBmaWxsPSIjMWEzYTVjIiBzdHlsZT0iZmlsbDpyZ2IoMjYsIDU4LCA5Mik7c3Ryb2tlOm5vbmU7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGNpcmNsZSBjeD0iMjA3IiBjeT0iNzgiIHI9IjEwIiBmaWxsPSIjMDBiNGM4IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMTgwLCAyMDApO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxjaXJjbGUgY3g9IjIwNyIgY3k9Ijc4IiByPSI2IiBmaWxsPSIjMWEzYTVjIiBzdHlsZT0iZmlsbDpyZ2IoMjYsIDU4LCA5Mik7c3Ryb2tlOm5vbmU7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9IjY1IiB4Mj0iMjQ4IiB5Mj0iNjUiIHN0cm9rZT0iIzAwYjRjOCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0eWxlPSJmaWxsOnJnYigwLCAwLCAwKTtzdHJva2U6cmdiKDAsIDE4MCwgMjAwKTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjRweDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9Ijc4IiB4Mj0iMjUyIiB5Mj0iNzgiIHN0cm9rZT0iIzAwYjRjOCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9IjAuNyIgc3R5bGU9ImZpbGw6cmdiKDAsIDAsIDApO3N0cm9rZTpyZ2IoMCwgMTgwLCAyMDApO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6NHB4O3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjAuNztmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxsaW5lIHgxPSIyMjAiIHkxPSI5MSIgeDI9IjI0NCIgeTI9IjkxIiBzdHJva2U9IiMwMGI0YzgiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBvcGFjaXR5PSIwLjQ1IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMCwgMCk7c3Ryb2tlOnJnYigwLCAxODAsIDIwMCk7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDo0cHg7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MC40NTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgoKICA8dGV4dCB4PSIyNzIiIHk9IjEwNSIgZm9udC1mYW1pbHk9InZhcigtLWZvbnQtbW9ubykiIGZvbnQtc2l6ZT0iNjYiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMwMGI0YzgiIGxldHRlci1zcGFjaW5nPSItMSIgc3R5bGU9ImZpbGw6cmdiKDAsIDE4MCwgMjAwKTtzdHJva2U6bm9uZTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjE7Zm9udC1mYW1pbHk6dWktbW9ub3NwYWNlLCBtb25vc3BhY2U7Zm9udC1zaXplOjY2cHg7Zm9udC13ZWlnaHQ6NzAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iPkZJWDwvdGV4dD4KICA8dGV4dCB4PSIzODYiIHk9IjEwNSIgZm9udC1mYW1pbHk9InZhcigtLWZvbnQtc2FucykiIGZvbnQtc2l6ZT0iNTAiIGZvbnQtd2VpZ2h0PSIzMDAiIGxldHRlci1zcGFjaW5nPSItMSIgc3R5bGU9ImZpbGw6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6NTBweDtmb250LXdlaWdodDozMDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byI+dHVyZTwvdGV4dD4KPC9zdmc+",kw="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgdmlld0JveD0iMCAwIDY4MCAxNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CgogIDxwb2x5Z29uIHBvaW50cz0iMTg4LDI4IDIzOCwyOCAyNTgsNzggMjM4LDEyOCAxODgsMTI4IiBmaWxsPSIjMDBiNGM4IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMTgwLCAyMDApO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxjaXJjbGUgY3g9IjIwNyIgY3k9Ijc4IiByPSIxMCIgZmlsbD0iIzFhM2E1YyIgc3R5bGU9ImZpbGw6cmdiKDI2LCA1OCwgOTIpO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxjaXJjbGUgY3g9IjIwNyIgY3k9Ijc4IiByPSI2IiBmaWxsPSIjMDBiNGM4IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMTgwLCAyMDApO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxsaW5lIHgxPSIyMjAiIHkxPSI2NSIgeDI9IjI0OCIgeTI9IjY1IiBzdHJva2U9IiMxYTNhNWMiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHlsZT0iZmlsbDpyZ2IoMCwgMCwgMCk7c3Ryb2tlOnJnYigyNiwgNTgsIDkyKTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjRweDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9Ijc4IiB4Mj0iMjUyIiB5Mj0iNzgiIHN0cm9rZT0iIzFhM2E1YyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9IjAuNyIgc3R5bGU9ImZpbGw6cmdiKDAsIDAsIDApO3N0cm9rZTpyZ2IoMjYsIDU4LCA5Mik7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDo0cHg7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MC43O2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9IjkxIiB4Mj0iMjQ0IiB5Mj0iOTEiIHN0cm9rZT0iIzFhM2E1YyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9IjAuNDUiIHN0eWxlPSJmaWxsOnJnYigwLCAwLCAwKTtzdHJva2U6cmdiKDI2LCA1OCwgOTIpO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6NHB4O3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjAuNDU7Zm9udC1mYW1pbHk6JnF1b3Q7QW50aHJvcGljIFNhbnMmcXVvdDssIC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJnF1b3Q7U2Vnb2UgVUkmcXVvdDssIHNhbnMtc2VyaWY7Zm9udC1zaXplOjE2cHg7Zm9udC13ZWlnaHQ6NDAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iLz4KCiAgPHRleHQgeD0iMjcyIiB5PSIxMDUiIGZvbnQtZmFtaWx5PSJ2YXIoLS1mb250LW1vbm8pIiBmb250LXNpemU9IjY2IiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWEzYTVjIiBsZXR0ZXItc3BhY2luZz0iLTEiIHN0eWxlPSJmaWxsOnJnYigyNiwgNTgsIDkyKTtzdHJva2U6bm9uZTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjE7Zm9udC1mYW1pbHk6dWktbW9ub3NwYWNlLCBtb25vc3BhY2U7Zm9udC1zaXplOjY2cHg7Zm9udC13ZWlnaHQ6NzAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iPkZJWDwvdGV4dD4KICA8dGV4dCB4PSIzODYiIHk9IjEwNSIgZm9udC1mYW1pbHk9InZhcigtLWZvbnQtc2FucykiIGZvbnQtc2l6ZT0iNTAiIGZvbnQtd2VpZ2h0PSIzMDAiIGZpbGw9IiMwMGI0YzgiIGxldHRlci1zcGFjaW5nPSItMSIgc3R5bGU9ImZpbGw6cmdiKDAsIDE4MCwgMjAwKTtzdHJva2U6bm9uZTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjE7Zm9udC1mYW1pbHk6JnF1b3Q7QW50aHJvcGljIFNhbnMmcXVvdDssIHNhbnMtc2VyaWY7Zm9udC1zaXplOjUwcHg7Zm9udC13ZWlnaHQ6MzAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iPnR1cmU8L3RleHQ+Cjwvc3ZnPg==";function Us(){const e=X(r=>r.theme),t=X(r=>r.branding);return e==="dark"?(t==null?void 0:t.logo_dark)||Iw:(t==null?void 0:t.logo_light)||kw}function qz(){const e=X(g=>g.setSessions),t=X(g=>g.theme),r=X(g=>g.toggleTheme),n=X(g=>g.user),a=X(g=>g.logout),i=X(g=>g.openTabs),o=X(g=>g.activeTabId),s=X(g=>g.branding),l=Us(),u=s!=null&&s.prefix?`${s.prefix} FIXture`:"FIXture",[f,d]=h.useState(!1),[m,p]=h.useState(!1),[v,y]=h.useState(!1);return Sv(),h.useEffect(()=>{document.documentElement.setAttribute("data-theme",t)},[t]),h.useEffect(()=>{G.sessions.list().then(e).catch(console.error)},[]),h.useEffect(()=>{if(!n){n1();return}G.customTags.list().then(jv).catch(console.error)},[n]),c.jsxs("div",{className:"app",children:[c.jsxs("header",{className:"app-header",children:[c.jsx("img",{src:l,alt:u,className:"app-logo"}),c.jsxs("div",{className:"header-right",children:[n&&c.jsx("span",{className:"header-user",children:n.username}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>d(!0),children:"Scenarios"}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p(!0),children:"Custom Tags"}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>y(!0),children:"Spec Overlay"}),(n==null?void 0:n.role)==="platform_admin"&&c.jsx(qr,{to:"/perf",className:"btn btn-sm btn-ghost",title:"Performance testing",children:"Perf"}),(n==null?void 0:n.role)==="platform_admin"&&c.jsx(qr,{to:"/admin/users",className:"btn-icon",title:"Admin",children:"⚙"}),c.jsx("button",{className:"btn-icon theme-toggle",onClick:r,title:"Toggle theme",children:t==="dark"?"☀":"🌙"}),c.jsx("button",{className:"btn-icon",onClick:a,title:"Sign out",children:"⎋"})]})]}),c.jsxs("div",{className:"app-body",children:[c.jsx(g1,{}),c.jsx("div",{className:"tab-area",children:i.length===0?c.jsx("div",{className:"tab-area-empty",children:"Select a session to open it."}):c.jsxs(c.Fragment,{children:[c.jsx(Az,{}),c.jsx("div",{className:"tab-panels",children:i.map(g=>c.jsx("div",{className:`tab-panel${g===o?" active":""}`,children:c.jsx(Cz,{sessionId:g})},g))})]})})]}),f&&c.jsx(Bz,{onClose:()=>d(!1)}),m&&c.jsx(Gz,{onClose:()=>p(!1)}),v&&c.jsx(Kz,{onClose:()=>y(!1)})]})}function Yz(){const[e,t]=h.useState(""),[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),[l,u]=h.useState(!1),{login:f}=X(),d=Us(),m=an();h.useEffect(()=>{fetch("/api/auth/register/status").then(v=>v.json()).then(v=>u(v.enabled)).catch(()=>{})},[]);async function p(v){v.preventDefault(),i(""),s(!0);try{const y=await fetch("/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:r})});if(!y.ok){const b=await y.json().catch(()=>({}));throw new Error(b.detail??"Login failed")}const g=await y.json();f(g.access_token,g.user),m("/",{replace:!0})}catch(y){i(y.message)}finally{s(!1)}}return c.jsx("div",{className:"auth-page",children:c.jsxs("div",{className:"auth-card",children:[c.jsx("img",{src:d,alt:"FIXture",className:"auth-logo"}),c.jsx("h2",{children:"Sign in"}),c.jsxs("form",{onSubmit:p,className:"auth-form",children:[c.jsxs("label",{children:["Username",c.jsx("input",{type:"text",value:e,onChange:v=>t(v.target.value),autoComplete:"username",required:!0})]}),c.jsxs("label",{children:["Password",c.jsx("input",{type:"password",value:r,onChange:v=>n(v.target.value),autoComplete:"current-password",required:!0})]}),a&&c.jsx("p",{className:"auth-error",children:a}),c.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Signing in…":"Sign in"})]}),l&&c.jsxs("p",{className:"auth-hint",style:{marginTop:12},children:["No account? ",c.jsx(qr,{to:"/register",children:"Register"})]})]})})}function Xz(){const[e,t]=h.useState(""),[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),l=Us(),u=an();async function f(d){d.preventDefault(),i(""),s(!0);try{const m=await fetch("/api/setup/create-admin",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:r})});if(!m.ok){const p=await m.json().catch(()=>({}));throw new Error(p.detail??"Setup failed")}u("/login",{replace:!0})}catch(m){i(m.message)}finally{s(!1)}}return c.jsx("div",{className:"auth-page",children:c.jsxs("div",{className:"auth-card",children:[c.jsx("img",{src:l,alt:"FIXture",className:"auth-logo"}),c.jsx("h2",{children:"Create admin account"}),c.jsx("p",{className:"auth-hint",children:"First-time setup — create the platform admin."}),c.jsxs("form",{onSubmit:f,className:"auth-form",children:[c.jsxs("label",{children:["Username",c.jsx("input",{type:"text",value:e,onChange:d=>t(d.target.value),autoComplete:"username",required:!0})]}),c.jsxs("label",{children:["Password",c.jsx("input",{type:"password",value:r,onChange:d=>n(d.target.value),autoComplete:"new-password",minLength:8,required:!0})]}),a&&c.jsx("p",{className:"auth-error",children:a}),c.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Creating…":"Create admin"})]})]})})}function Vz(){const[e,t]=h.useState(""),[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),[l,u]=h.useState(null),{login:f,theme:d}=X(),m=an();h.useEffect(()=>{fetch("/api/auth/register/status").then(y=>y.json()).then(u).catch(()=>u({enabled:!1,max_users:0,current_users:0}))},[]);async function p(y){y.preventDefault(),i(""),s(!0);try{const g=await fetch("/api/auth/register",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:r})});if(!g.ok){const x=await g.json().catch(()=>({}));throw new Error(x.detail??"Registration failed")}const b=await g.json();f(b.access_token,b.user),m("/",{replace:!0})}catch(g){i(g.message)}finally{s(!1)}}const v=l&&l.current_users>=l.max_users;return c.jsx("div",{className:"auth-page",children:c.jsxs("div",{className:"auth-card",children:[c.jsx("img",{src:d==="dark"?Iw:kw,alt:"FIXture",className:"auth-logo"}),c.jsx("h2",{children:"Create account"}),l===null&&c.jsx("p",{className:"auth-hint",children:"Checking registration status…"}),l&&!l.enabled&&c.jsx("p",{className:"auth-error",children:"Registration is currently closed. Contact an administrator."}),l&&l.enabled&&v&&c.jsx("p",{className:"auth-error",children:"Registration limit reached. Contact an administrator."}),l&&l.enabled&&!v&&c.jsxs("form",{onSubmit:p,className:"auth-form",children:[c.jsxs("label",{children:["Username",c.jsx("input",{type:"text",value:e,onChange:y=>t(y.target.value),autoComplete:"username",required:!0})]}),c.jsxs("label",{children:["Password",c.jsx("input",{type:"password",value:r,onChange:y=>n(y.target.value),autoComplete:"new-password",minLength:8,required:!0})]}),a&&c.jsx("p",{className:"auth-error",children:a}),c.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Creating account…":"Create account"})]}),c.jsxs("p",{className:"auth-hint",style:{marginTop:12},children:["Already have an account? ",c.jsx(qr,{to:"/login",children:"Sign in"})]})]})})}const Jz=["accent","bg","bg2","bg3","border","text","text2"];function Tw(e){let t=document.getElementById("branding-style");t||(t=document.createElement("style"),t.id="branding-style",document.head.appendChild(t));const r=Jz.filter(n=>e[n]).map(n=>`--${n}: ${e[n]};`).join(" ");if(r?t.textContent=`:root { ${r} } [data-theme="light"] { ${r} }`:t.textContent="",e.favicon){let n=document.querySelector('link[rel="icon"]');n||(n=document.createElement("link"),n.rel="icon",document.head.appendChild(n)),n.href=e.favicon}document.title=e.prefix?`${e.prefix} FIXture`:"FIXture"}function Sl(e){return new Promise((t,r)=>{const n=new FileReader;n.onload=()=>t(n.result),n.onerror=r,n.readAsDataURL(e)})}const Mw=["user","user_admin","platform_admin"];function Qz(e){return new Date(e*1e3).toLocaleString()}function eW({onClose:e,onCreated:t}){const[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState("user"),[l,u]=h.useState(""),[f,d]=h.useState(!1);async function m(p){p.preventDefault(),u(""),d(!0);try{const v=await G.admin.users.create({username:r,password:a,role:o});t(v),e()}catch(v){u(v.message)}finally{d(!1)}}return c.jsx("div",{className:"modal-overlay",onClick:e,children:c.jsxs("div",{className:"modal",onClick:p=>p.stopPropagation(),children:[c.jsxs("div",{className:"modal-header",children:[c.jsx("span",{children:"Create User"}),c.jsx("button",{className:"btn-icon",onClick:e,children:"✕"})]}),c.jsxs("form",{onSubmit:m,children:[c.jsxs("div",{className:"form-grid",children:[c.jsx("label",{children:"Username"}),c.jsx("input",{value:r,onChange:p=>n(p.target.value),required:!0}),c.jsx("label",{children:"Password"}),c.jsx("input",{type:"password",value:a,onChange:p=>i(p.target.value),minLength:8,required:!0}),c.jsx("label",{children:"Role"}),c.jsx("select",{value:o,onChange:p=>s(p.target.value),children:Mw.map(p=>c.jsx("option",{value:p,children:p},p))})]}),l&&c.jsx("div",{className:"error-msg",children:l}),c.jsxs("div",{className:"modal-footer",children:[c.jsx("button",{type:"button",className:"btn btn-secondary",onClick:e,children:"Cancel"}),c.jsx("button",{type:"submit",className:"btn btn-primary",disabled:f,children:f?"Creating…":"Create"})]})]})]})})}function tW({user:e,onClose:t}){const[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),[l,u]=h.useState(!1);async function f(d){d.preventDefault(),i(""),s(!0);try{await G.admin.users.update(e.uid,{password:r}),u(!0)}catch(m){i(m.message)}finally{s(!1)}}return c.jsx("div",{className:"modal-overlay",onClick:t,children:c.jsxs("div",{className:"modal",onClick:d=>d.stopPropagation(),children:[c.jsxs("div",{className:"modal-header",children:[c.jsxs("span",{children:["Reset Password — ",e.username]}),c.jsx("button",{className:"btn-icon",onClick:t,children:"✕"})]}),l?c.jsxs("div",{style:{padding:16},children:[c.jsx("p",{style:{color:"var(--green)"},children:"Password updated successfully."}),c.jsx("div",{className:"modal-footer",children:c.jsx("button",{className:"btn btn-primary",onClick:t,children:"Close"})})]}):c.jsxs("form",{onSubmit:f,children:[c.jsxs("div",{className:"form-grid",children:[c.jsx("label",{children:"New Password"}),c.jsx("input",{type:"password",value:r,onChange:d=>n(d.target.value),minLength:8,required:!0})]}),a&&c.jsx("div",{className:"error-msg",children:a}),c.jsxs("div",{className:"modal-footer",children:[c.jsx("button",{type:"button",className:"btn btn-secondary",onClick:t,children:"Cancel"}),c.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Saving…":"Save"})]})]})]})})}function rW(){const e=X(T=>T.user),t=an(),[r,n]=h.useState([]),[a,i]=h.useState(null),[o,s]=h.useState(null),[l,u]=h.useState(null),[f,d]=h.useState(!1),[m,p]=h.useState(null),[v,y]=h.useState(!1),{setBranding:g}=X(),[b,x]=h.useState(!1),[w,S]=h.useState(null),[_,j]=h.useState("");async function O(){try{const[T,R,z,W]=await Promise.all([G.admin.users.list(),G.admin.settings.get(),G.admin.housekeeping.get(),G.admin.branding.get()]);n(T),i(R),s(z),p(W)}catch(T){j(T.message)}}h.useEffect(()=>{O()},[]);async function N(T){try{const R=await G.admin.users.update(T.uid,{is_active:!T.is_active});n(z=>z.map(W=>W.uid===T.uid?R:W))}catch(R){j(R.message)}}async function A(T,R){try{const z=await G.admin.users.update(T.uid,{role:R});n(W=>W.map(V=>V.uid===T.uid?z:V))}catch(z){j(z.message)}}async function k(T){if(confirm(`Delete user "${T.username}"? This cannot be undone.`))try{await G.admin.users.delete(T.uid),n(R=>R.filter(z=>z.uid!==T.uid))}catch(R){j(R.message)}}async function M(){if(a)try{const T=await G.admin.settings.update({registration_enabled:!a.enabled});i(T)}catch(T){j(T.message)}}async function P(T){const R=parseInt(T,10);if(!(isNaN(R)||R<1))try{const z=await G.admin.settings.update({max_users:R});i(z)}catch(z){j(z.message)}}async function H(){if(o)try{const T=await G.admin.housekeeping.update({enabled:!o.enabled});s(T)}catch(T){j(T.message)}}async function L(T,R){const z=parseInt(R,10);if(!(isNaN(z)||z<0))try{const W=await G.admin.housekeeping.update({[T]:z});s(W)}catch(W){j(W.message)}}async function U(){d(!0),u(null);try{const T=await G.admin.housekeeping.runNow();u(`Done — deleted ${T.msgs_deleted} messages, ${T.logs_deleted} log files`)}catch(T){j(T.message)}finally{d(!1)}}return c.jsxs("div",{className:"admin-page",children:[c.jsxs("div",{className:"admin-header",children:[c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[c.jsx("button",{className:"btn-icon",onClick:()=>t("/"),title:"Back",children:"←"}),c.jsx("h1",{children:"User Management"})]}),c.jsx("button",{className:"btn btn-primary btn-sm",onClick:()=>x(!0),children:"+ Create User"})]}),_&&c.jsx("div",{className:"error-msg",style:{margin:"0 0 12px"},children:_}),a&&c.jsxs("div",{className:"admin-settings-bar",children:[c.jsx("span",{className:"admin-settings-label",children:"Self-registration:"}),c.jsx("button",{className:`btn btn-sm ${a.enabled?"btn-stop":"btn-start"}`,onClick:M,children:a.enabled?"Enabled — click to disable":"Disabled — click to enable"}),c.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Max users:"}),c.jsx("input",{type:"number",className:"admin-max-users-input",defaultValue:a.max_users,min:1,onBlur:T=>P(T.target.value)}),c.jsxs("span",{className:"admin-settings-label",style:{color:"var(--text2)"},children:["(",a.current_users," / ",a.max_users," registered)"]})]}),o&&c.jsxs("div",{className:"admin-settings-bar",children:[c.jsx("span",{className:"admin-settings-label",children:"Housekeeping:"}),c.jsx("button",{className:`btn btn-sm ${o.enabled?"btn-stop":"btn-start"}`,onClick:H,children:o.enabled?"Auto — click to disable":"Manual only — click to enable"}),c.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Keep messages (days):"}),c.jsx("input",{type:"number",className:"admin-max-users-input",defaultValue:o.msg_retention_days,min:0,title:"0 = keep forever",onBlur:T=>L("msg_retention_days",T.target.value)}),c.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Keep logs (days):"}),c.jsx("input",{type:"number",className:"admin-max-users-input",defaultValue:o.log_retention_days,min:0,title:"0 = keep forever",onBlur:T=>L("log_retention_days",T.target.value)}),c.jsx("button",{className:"btn btn-sm btn-ghost",style:{marginLeft:16},onClick:U,disabled:f,children:f?"Running…":"Run Now"}),l&&c.jsx("span",{style:{marginLeft:8,color:"var(--green)",fontSize:12},children:l})]}),m&&c.jsxs("div",{className:"admin-settings-bar",style:{flexDirection:"column",alignItems:"flex-start",gap:10},children:[c.jsx("span",{className:"admin-settings-label",style:{fontWeight:700},children:"Branding"}),c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[c.jsx("span",{className:"admin-settings-label",children:"Brand prefix:"}),c.jsx("input",{className:"admin-max-users-input",style:{width:160},value:m.prefix,placeholder:"e.g. Acme (shown as Acme FIXture)",onChange:T=>p({...m,prefix:T.target.value})}),c.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Accent colour:"}),c.jsx("input",{type:"color",value:m.accent||"#388bfd",onChange:T=>p({...m,accent:T.target.value}),title:"Primary accent colour"}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,accent:""}),title:"Reset to default blue",children:"Reset"})]}),c.jsx("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[["bg","Page background","#0d1117"],["bg2","Panel background","#161b22"],["bg3","Input background","#21262d"],["border","Border colour","#30363d"],["text","Primary text","#e6edf3"],["text2","Secondary text","#8b949e"]].map(([T,R,z])=>c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:4},children:[c.jsxs("span",{className:"admin-settings-label",children:[R,":"]}),c.jsx("input",{type:"color",value:m[T]||z,onChange:W=>p({...m,[T]:W.target.value}),title:R}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,[T]:""}),title:"Reset to default",children:"↺"})]},T))}),c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[c.jsx("span",{className:"admin-settings-label",children:"Logo (dark theme):"}),m.logo_dark&&c.jsx("img",{src:m.logo_dark,style:{height:28,background:"#161b22",borderRadius:4,padding:"2px 6px"},alt:"dark logo preview"}),c.jsx("input",{type:"file",accept:"image/*",onChange:async T=>{var z;const R=(z=T.target.files)==null?void 0:z[0];R&&p({...m,logo_dark:await Sl(R)})}}),m.logo_dark&&c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,logo_dark:""}),children:"Clear"})]}),c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[c.jsx("span",{className:"admin-settings-label",children:"Logo (light theme):"}),m.logo_light&&c.jsx("img",{src:m.logo_light,style:{height:28,background:"#f0f2f5",borderRadius:4,padding:"2px 6px"},alt:"light logo preview"}),c.jsx("input",{type:"file",accept:"image/*",onChange:async T=>{var z;const R=(z=T.target.files)==null?void 0:z[0];R&&p({...m,logo_light:await Sl(R)})}}),m.logo_light&&c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,logo_light:""}),children:"Clear"})]}),c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[c.jsx("span",{className:"admin-settings-label",children:"Favicon:"}),m.favicon&&c.jsx("img",{src:m.favicon,style:{height:20,width:20},alt:"favicon preview"}),c.jsx("input",{type:"file",accept:"image/png,image/svg+xml,image/x-icon",onChange:async T=>{var z;const R=(z=T.target.files)==null?void 0:z[0];R&&p({...m,favicon:await Sl(R)})}}),m.favicon&&c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,favicon:""}),children:"Clear"})]}),c.jsx("button",{className:"btn btn-sm btn-primary",disabled:v,onClick:async()=>{y(!0);try{const T=await G.admin.branding.update(m);p(T),g(T),Tw(T)}catch(T){j(T.message)}finally{y(!1)}},children:v?"Saving…":"Save Branding"})]}),c.jsxs("table",{className:"admin-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{children:"Username"}),c.jsx("th",{children:"Role"}),c.jsx("th",{children:"Status"}),c.jsx("th",{children:"Created"}),c.jsx("th",{children:"Actions"})]})}),c.jsx("tbody",{children:r.map(T=>c.jsxs("tr",{className:T.is_active?"":"admin-row-inactive",children:[c.jsxs("td",{children:[T.username,T.uid===(e==null?void 0:e.uid)&&c.jsx("span",{className:"admin-you-badge",children:" (you)"})]}),c.jsx("td",{children:c.jsx("select",{value:T.role,onChange:R=>A(T,R.target.value),className:"admin-role-select",disabled:T.uid===(e==null?void 0:e.uid),children:Mw.map(R=>c.jsx("option",{value:R,children:R},R))})}),c.jsx("td",{children:c.jsx("span",{style:{color:T.is_active?"var(--green)":"var(--text2)"},children:T.is_active?"Active":"Inactive"})}),c.jsx("td",{style:{color:"var(--text2)",fontSize:11},children:Qz(T.created_at)}),c.jsx("td",{children:c.jsxs("div",{className:"admin-row-actions",children:[c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>N(T),disabled:T.uid===(e==null?void 0:e.uid),title:T.is_active?"Deactivate":"Activate",children:T.is_active?"Deactivate":"Activate"}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>S(T),title:"Reset password",children:"Reset PW"}),c.jsx("button",{className:"btn btn-sm btn-danger",onClick:()=>k(T),disabled:T.uid===(e==null?void 0:e.uid),title:"Delete user",children:"✕"})]})})]},T.uid))})]}),b&&c.jsx(eW,{onClose:()=>x(!1),onCreated:T=>n(R=>[...R,T])}),w&&c.jsx(tW,{user:w,onClose:()=>S(null)})]})}const Up={name:"smoke",mode:"loopback",client_session_id:null,venue_session_id:null,correlation_tag:376,exec_id_tag:25116,rate:{orders_per_window:50,order_window_ms:100,fills_per_window:50,fill_window_ms:100,fill_ratio:1,allow_burst:!1,max_burst_multiplier:2},test:{duration:30,max_orders:0,scenario_timeout_ms:3e4,max_pending:1e5,on_saturation:"pause",output:"results",record_execs:!1},payload:{variables:{},order:{symbols:["AAPL","MSFT","GOOGL"],side:"alternate",ord_type:"limit",quantity_min:100,quantity_max:1e3,price_min:99,price_max:101,time_in_force:"0"},fill:{fill_type:"full",send_ack:!0,fills_per_order:1,partial_fill_pct_min:50,partial_fill_pct_max:99,price_variance_ticks:0},order_template_id:null,exec_template_id:null,auto_expiry:!0,expiry_offset:1,expiry_unit:"days",gen_rules:{}}},nW=["loopback","manager","client","venue"];function Hp(e,t){return t?Math.round(e/t*1e3):0}function aW({onStarted:e}){var mt,Cr;const t=X(C=>C.sessions),[r,n]=h.useState(Up),[a,i]=h.useState(Up.payload.order.symbols.join(", ")),[o,s]=h.useState(!1),[l,u]=h.useState(""),[f,d]=h.useState(!1),[m,p]=h.useState([]),[v,y]=h.useState(""),[g,b]=h.useState([]),x=Object.values(t),w=r.mode==="loopback"||r.mode==="manager"||r.mode==="client",S=r.mode==="loopback"||r.mode==="manager"||r.mode==="venue",_=r.client_session_id?t[r.client_session_id]:void 0,j=r.venue_session_id?t[r.venue_session_id]:void 0,O=!w||(_==null?void 0:_.status)==="LOGGED_ON",N=!S||(j==null?void 0:j.status)==="LOGGED_ON",A=(!w||!!r.client_session_id)&&(!S||!!r.venue_session_id),k=(()=>{if(!r.name.trim())return"Name is required";const C=r.rate;if(C.max_burst_multiplier<1)return"Max burst multiplier must be ≥ 1";if(r.test.scenario_timeout_ms<1)return"Order timeout must be > 0";if(r.test.max_pending<1)return"Max pending must be > 0";if(w){if(C.orders_per_window<1||C.order_window_ms<1)return"Order rate must be > 0";if(a.split(",").map(J=>J.trim()).filter(Boolean).length===0)return"At least one symbol is required";const I=r.payload.order;if(I.quantity_min<1||I.quantity_max<1||I.quantity_min>I.quantity_max)return"Check quantity min/max";if(I.price_min<=0||I.price_max<=0||I.price_min>I.price_max)return"Check price min/max"}if(S){if(C.fills_per_window<1||C.fill_window_ms<1)return"Fill rate must be > 0";if(r.payload.fill.fills_per_order<1)return"Fills per order must be ≥ 1";if(C.fill_ratio<0||C.fill_ratio>1)return"Fill ratio must be between 0 and 1"}return null})(),M=A&&O&&N&&!k&&!f;async function P(){try{p(await G.perf.configs.list())}catch{}}h.useEffect(()=>{P()},[]),h.useEffect(()=>{G.templates.list().then(b).catch(()=>{})},[]);const H=g.filter(C=>C.msg_type==="D"),L=g.filter(C=>C.msg_type==="8"),U=!!r.payload.order_template_id,T=!!r.payload.exec_template_id,R=((mt=H.find(C=>C.id===r.payload.order_template_id))==null?void 0:mt.name)??"selected",z=((Cr=L.find(C=>C.id===r.payload.exec_template_id))==null?void 0:Cr.name)??"selected",W=C=>n(I=>({...I,...C})),V=C=>n(I=>({...I,rate:{...I.rate,...C}})),se=C=>n(I=>({...I,test:{...I.test,...C}})),Q=C=>n(I=>({...I,payload:{...I.payload,order:{...I.payload.order,...C}}})),ye=C=>n(I=>({...I,payload:{...I.payload,fill:{...I.payload.fill,...C}}})),de=C=>n(I=>({...I,payload:{...I.payload,...C}})),[Ne,$]=h.useState([]),ae=C=>{$(C),de({gen_rules:Object.fromEntries(C.filter(I=>I.tag.trim()).map(I=>[I.tag.trim(),I.value]))})},ne=(C,I)=>ae(Ne.map((J,he)=>he===C?{...J,...I}:J));function B(){const C=a.split(",").map(I=>I.trim()).filter(Boolean);return{...r,client_session_id:w?r.client_session_id:null,venue_session_id:S?r.venue_session_id:null,payload:{...r.payload,order:{...r.payload.order,symbols:C}}}}async function F(){u(""),d(!0);try{const{run_id:C}=await G.perf.runs.create(B());e(C)}catch(C){u(C.message??"Failed to start run")}finally{d(!1)}}async function ee(){u("");try{await G.perf.configs.save(B()),await P()}catch(C){u(C.message??"Failed to save config")}}async function D(C){if(y(C),!!C){u("");try{const I=await G.perf.configs.get(C);n(I),i(I.payload.order.symbols.join(", ")),$(Object.entries(I.payload.gen_rules??{}).map(([J,he])=>({tag:J,value:he})))}catch(I){u(I.message??"Failed to load config")}}}async function Te(){if(v&&window.confirm("Delete this saved config?"))try{await G.perf.configs.delete(v),y(""),await P()}catch(C){u(C.message??"Failed to delete config")}}const oe=C=>{const I=t[C];return I?`${I.display_name||`${I.sender_comp_id} → ${I.target_comp_id}`} (${I.status})`:C},ze=x.filter(C=>C.session_role!=="venue"),We=x.filter(C=>C.session_role!=="client");return c.jsxs("div",{className:"perf-form",children:[l&&c.jsx("div",{className:"error-msg",children:l}),c.jsxs("div",{className:"perf-form-section",children:[c.jsx("div",{className:"perf-section-title",children:"Saved configs"}),c.jsxs("div",{className:"perf-field-row",children:[c.jsxs("select",{value:v,onChange:C=>D(C.target.value),children:[c.jsx("option",{value:"",children:"(load a saved config…)"}),m.map(C=>c.jsxs("option",{value:C.config_id,children:[C.name," — ",C.mode]},C.config_id))]}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:ee,children:"Save current"}),c.jsx("button",{className:"btn btn-sm btn-ghost btn-danger-ghost",onClick:Te,disabled:!v,children:"Delete"})]})]}),c.jsxs("div",{className:"perf-form-section",children:[c.jsx("div",{className:"perf-section-title",children:"Run"}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Name"}),c.jsx("input",{value:r.name,onChange:C=>W({name:C.target.value})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Mode"}),c.jsx("select",{value:r.mode,onChange:C=>W({mode:C.target.value}),children:nW.map(C=>c.jsx("option",{value:C,children:C},C))})]}),w&&c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Client session"}),c.jsxs("select",{value:r.client_session_id??"",onChange:C=>W({client_session_id:C.target.value||null}),children:[c.jsx("option",{value:"",children:"(select…)"}),ze.map(C=>c.jsx("option",{value:C.session_id,children:oe(C.session_id)},C.session_id))]}),_&&c.jsx(Gp,{status:_.status})]}),S&&c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Venue session"}),c.jsxs("select",{value:r.venue_session_id??"",onChange:C=>W({venue_session_id:C.target.value||null}),children:[c.jsx("option",{value:"",children:"(select…)"}),We.map(C=>c.jsx("option",{value:C.session_id,children:oe(C.session_id)},C.session_id))]}),j&&c.jsx(Gp,{status:j.status})]})]}),c.jsxs("div",{className:"perf-form-section",children:[c.jsx("div",{className:"perf-section-title",children:"Rate"}),c.jsxs("div",{className:"perf-field-grid",children:[w&&c.jsxs(c.Fragment,{children:[c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Orders / window"}),c.jsx("input",{type:"number",min:1,value:r.rate.orders_per_window,onChange:C=>V({orders_per_window:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Order window (ms)"}),c.jsx("input",{type:"number",min:1,value:r.rate.order_window_ms,onChange:C=>V({order_window_ms:Number(C.target.value)})})]})]}),S&&c.jsxs(c.Fragment,{children:[c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Fills / window"}),c.jsx("input",{type:"number",min:1,value:r.rate.fills_per_window,onChange:C=>V({fills_per_window:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Fill window (ms)"}),c.jsx("input",{type:"number",min:1,value:r.rate.fill_window_ms,onChange:C=>V({fill_window_ms:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Fill ratio"}),c.jsx("input",{type:"number",min:0,max:1,step:.05,value:r.rate.fill_ratio,onChange:C=>V({fill_ratio:Number(C.target.value)})})]})]})]}),c.jsxs("div",{className:"perf-hint",children:[w&&c.jsxs(c.Fragment,{children:["≈ ",Hp(r.rate.orders_per_window,r.rate.order_window_ms).toLocaleString()," orders/s"]}),w&&S&&" · ",S&&c.jsxs(c.Fragment,{children:["≈ ",Hp(r.rate.fills_per_window,r.rate.fill_window_ms).toLocaleString()," fills/s"]})," ","Smaller windows smooth the injection."]})]}),c.jsxs("div",{className:"perf-form-section",children:[c.jsx("div",{className:"perf-section-title",children:"Test"}),c.jsxs("div",{className:"perf-field-grid",children:[c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Duration (s, 0 = no time limit)"}),c.jsx("input",{type:"number",min:0,value:r.test.duration,onChange:C=>se({duration:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Max orders (0 = no count limit)"}),c.jsx("input",{type:"number",min:0,value:r.test.max_orders,onChange:C=>se({max_orders:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Order timeout (ms)"}),c.jsx("input",{type:"number",min:1,step:1e3,value:r.test.scenario_timeout_ms,onChange:C=>se({scenario_timeout_ms:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"On saturation"}),c.jsxs("select",{value:r.test.on_saturation,onChange:C=>se({on_saturation:C.target.value}),children:[c.jsx("option",{value:"pause",children:"pause (preserves latency accuracy)"}),c.jsx("option",{value:"drop_oldest",children:"drop_oldest"})]})]})]}),w&&c.jsxs("label",{className:"checkbox-label",children:[c.jsx("input",{type:"checkbox",checked:r.test.record_execs,onChange:C=>se({record_execs:C.target.checked})}),"Record per-exec detail — one CSV row per ack/fill with latency + inter-report gap (downloads as csv.gz after the run)"]}),c.jsxs("div",{className:"perf-hint",children:["An order not fully filled within the order timeout is counted ",c.jsx("strong",{children:"Lost"}),". Raise it if rate-limited fills are being flagged lost on full-fill runs."]})]}),w&&c.jsxs("div",{className:`perf-form-section ${U?"perf-section-layered":""}`,children:[c.jsxs("div",{className:"perf-section-title",children:["Order payload",U&&c.jsx("span",{className:"perf-layer-badge",children:"values for template"})]}),U&&c.jsxs("div",{className:"perf-hint perf-layer-note",children:["Order template ",c.jsx("strong",{children:R})," defines the message shape. These fields supply the values it references via ",c.jsx("code",{children:"{symbol}"})," ",c.jsx("code",{children:"{side}"})," ",c.jsx("code",{children:"{qty}"})," ",c.jsx("code",{children:"{price}"})," tokens. Value generators (",c.jsx("code",{children:"uuid()"}),", ",c.jsx("code",{children:"random_str(n)"}),", ",c.jsx("code",{children:"random_int(a,b)"}),","," ",c.jsx("code",{children:"seq(width)"}),", ",c.jsx("code",{children:"timestamp()"}),", ",c.jsx("code",{children:"date()"}),") are evaluated fresh per order and mix with text as ",c.jsx("code",{children:"ORD-{seq(8)}"}),"; ClOrdID(11) is always made unique per order. ",c.jsx("strong",{children:"Order type"})," is taken from the template."]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Symbols (comma-separated)"}),c.jsx("input",{value:a,onChange:C=>i(C.target.value)})]}),c.jsxs("div",{className:"perf-field-grid",children:[c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Side"}),c.jsxs("select",{value:r.payload.order.side,onChange:C=>Q({side:C.target.value}),children:[c.jsx("option",{value:"alternate",children:"alternate"}),c.jsx("option",{value:"fixed_buy",children:"fixed_buy"}),c.jsx("option",{value:"fixed_sell",children:"fixed_sell"}),c.jsx("option",{value:"random",children:"random"})]})]}),c.jsxs("label",{className:`perf-field ${U?"perf-field-overridden":""}`,children:[c.jsxs("span",{children:["Order type ",U&&c.jsx("em",{className:"perf-field-tag",children:"from template"})]}),c.jsxs("select",{value:r.payload.order.ord_type,disabled:U,onChange:C=>Q({ord_type:C.target.value}),children:[c.jsx("option",{value:"limit",children:"limit"}),c.jsx("option",{value:"market",children:"market"})]})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Qty min"}),c.jsx("input",{type:"number",min:1,value:r.payload.order.quantity_min,onChange:C=>Q({quantity_min:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Qty max"}),c.jsx("input",{type:"number",min:1,value:r.payload.order.quantity_max,onChange:C=>Q({quantity_max:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Price min"}),c.jsx("input",{type:"number",min:0,step:.01,value:r.payload.order.price_min,onChange:C=>Q({price_min:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Price max"}),c.jsx("input",{type:"number",min:0,step:.01,value:r.payload.order.price_max,onChange:C=>Q({price_max:Number(C.target.value)})})]})]})]}),w&&c.jsxs("div",{className:"perf-form-section",children:[c.jsx("div",{className:"perf-section-title",children:"Send options"}),c.jsxs("div",{className:"perf-field-row",children:[c.jsxs("label",{className:"checkbox-label",children:[c.jsx("input",{type:"checkbox",checked:r.payload.auto_expiry??!0,onChange:C=>de({auto_expiry:C.target.checked})}),"Auto expiry (tags 126 / 432, only if present)"]}),(r.payload.auto_expiry??!0)&&c.jsxs(c.Fragment,{children:[c.jsx("input",{type:"number",min:0,style:{width:64},value:r.payload.expiry_offset??1,onChange:C=>de({expiry_offset:Number(C.target.value)})}),c.jsxs("select",{value:r.payload.expiry_unit??"days",onChange:C=>de({expiry_unit:C.target.value}),children:[c.jsx("option",{value:"minutes",children:"minutes"}),c.jsx("option",{value:"hours",children:"hours"}),c.jsx("option",{value:"days",children:"days"})]}),c.jsx("span",{children:"into the future"})]})]}),Ne.map((C,I)=>c.jsxs("div",{className:"perf-field-row",children:[c.jsx("input",{style:{width:72},placeholder:"tag",value:C.tag,onChange:J=>ne(I,{tag:J.target.value.replace(/[^0-9]/g,"")})}),c.jsx("span",{children:"="}),c.jsx("input",{style:{flex:1},placeholder:"uuid() / seq(8) / ORD-{seq(8)} / literal",value:C.value,onChange:J=>ne(I,{value:J.target.value})}),c.jsx("button",{className:"btn btn-sm btn-ghost btn-danger-ghost",onClick:()=>ae(Ne.filter((J,he)=>he!==I)),children:"✕"})]},I)),c.jsx("div",{className:"perf-field-row",children:c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>ae([...Ne,{tag:"",value:""}]),children:"+ Add tag rule"})}),c.jsxs("div",{className:"perf-hint",children:["Rules set the tag on every order/scenario message (insert or replace). Generators: ",c.jsx("code",{children:"uuid()"})," · ",c.jsx("code",{children:"random_str(n)"})," · ",c.jsx("code",{children:"random_int(a,b)"})," ·"," ",c.jsx("code",{children:"seq(width[,start])"})," · ",c.jsx("code",{children:"timestamp()"})," · ",c.jsx("code",{children:"date()"})," — mix with text as ",c.jsx("code",{children:"ORD-{seq(8)}"}),". ClOrdID(11) always stays unique per order."]})]}),S&&c.jsxs("div",{className:`perf-form-section ${T?"perf-section-layered":""}`,children:[c.jsxs("div",{className:"perf-section-title",children:["Fill payload",T&&c.jsx("span",{className:"perf-layer-badge",children:"behavior for template"})]}),T&&c.jsxs("div",{className:"perf-hint perf-layer-note",children:["Exec template ",c.jsx("strong",{children:z})," reshapes each report (custom tags). These still control fill ",c.jsx("strong",{children:"behavior"})," — how many reports, the fill ratio, and the ack — while the engine sets the standard exec tags."]}),c.jsxs("div",{className:"perf-field-grid",children:[c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Fill type"}),c.jsxs("select",{value:r.payload.fill.fill_type,onChange:C=>ye({fill_type:C.target.value}),children:[c.jsx("option",{value:"full",children:"full"}),c.jsx("option",{value:"partial",children:"partial"}),c.jsx("option",{value:"random",children:"random"})]})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Fills per order"}),c.jsx("input",{type:"number",min:1,value:r.payload.fill.fills_per_order,onChange:C=>ye({fills_per_order:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field perf-field-check",children:[c.jsx("input",{type:"checkbox",checked:r.payload.fill.send_ack,onChange:C=>ye({send_ack:C.target.checked})}),c.jsx("span",{children:"Send ack before fills (35=8|150=0|39=0)"})]}),r.payload.fill.fill_type!=="full"&&c.jsxs(c.Fragment,{children:[c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Partial % min"}),c.jsx("input",{type:"number",min:1,max:100,value:r.payload.fill.partial_fill_pct_min,onChange:C=>ye({partial_fill_pct_min:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Partial % max"}),c.jsx("input",{type:"number",min:1,max:100,value:r.payload.fill.partial_fill_pct_max,onChange:C=>ye({partial_fill_pct_max:Number(C.target.value)})})]})]})]})]}),(w||S)&&c.jsxs("div",{className:"perf-form-section",children:[c.jsx("div",{className:"perf-section-title",children:"Templates (optional)"}),c.jsxs("div",{className:"perf-field-grid",children:[w&&c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Order template"}),c.jsxs("select",{value:r.payload.order_template_id??"",onChange:C=>de({order_template_id:C.target.value||null}),children:[c.jsx("option",{value:"",children:"— built-in —"}),H.map(C=>c.jsx("option",{value:C.id,children:C.name},C.id))]})]}),S&&c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Exec report template"}),c.jsxs("select",{value:r.payload.exec_template_id??"",onChange:C=>de({exec_template_id:C.target.value||null}),children:[c.jsx("option",{value:"",children:"— built-in —"}),L.map(C=>c.jsx("option",{value:C.id,children:C.name},C.id))]})]})]}),c.jsx("div",{className:"perf-hint",children:"Optional FIX templates from your Templates library shape the order / execution report (custom & venue-specific tags). Standard tags (correlation, OrderQty, CumQty, ExecType…) are always set by the engine. Leave as built-in for the default messages."})]}),c.jsxs("div",{className:"perf-form-section",children:[c.jsxs("button",{className:"btn btn-sm btn-ghost",onClick:()=>s(C=>!C),children:[o?"▾":"▸"," Advanced"]}),o&&c.jsxs("div",{className:"perf-field-grid",style:{marginTop:8},children:[c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Correlation tag"}),c.jsx("input",{type:"number",min:1,value:r.correlation_tag,onChange:C=>W({correlation_tag:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Exec ID tag"}),c.jsx("input",{type:"number",min:1,value:r.exec_id_tag,onChange:C=>W({exec_id_tag:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Max pending"}),c.jsx("input",{type:"number",min:1,value:r.test.max_pending,onChange:C=>se({max_pending:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field perf-field-check",children:[c.jsx("input",{type:"checkbox",checked:r.rate.allow_burst,onChange:C=>V({allow_burst:C.target.checked})}),c.jsx("span",{children:"Allow burst"})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Max burst multiplier"}),c.jsx("input",{type:"number",min:1,step:.5,value:r.rate.max_burst_multiplier,onChange:C=>V({max_burst_multiplier:Number(C.target.value)})})]})]})]}),c.jsxs("div",{className:"perf-form-actions",children:[k?c.jsx("span",{className:"perf-not-ready",children:k}):A&&!(O&&N)?c.jsxs("span",{className:"perf-not-ready",children:["Selected session",w&&S?"s":""," must be LOGGED_ON before starting."]}):null,c.jsx("button",{className:"btn btn-primary",onClick:F,disabled:!M,title:A?k||(O&&N?void 0:"Session not logged on yet"):"Select the required session(s)",children:f?"Starting…":"▶ Start run"})]})]})}function Gp({status:e}){const t=e==="LOGGED_ON";return c.jsxs("span",{className:`perf-status-chip ${t?"ok":"warn"}`,children:[t?"● ":"○ ",e]})}const Zp=600,iW=5e3,oW=new Set(["completed","stopped","error"]);function sW(e){const[t,r]=h.useState(null),[n,a]=h.useState([]),[i,o]=h.useState(!1);return h.useEffect(()=>{if(r(null),a([]),o(!1),!e)return;let s=!1,l=null,u,f=0,d=!1;async function m(){if(s||d||!e)return;let v;try{({ticket:v}=await G.perf.runs.ticket(e))}catch{p();return}if(s)return;const g=`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/api/perf/runs/${e}/live?ticket=${v}`;l=new WebSocket(g),l.onopen=()=>{s||(f=0,o(!0))},l.onmessage=b=>{let x;try{x=JSON.parse(b.data)}catch{return}r(x),a(w=>{var j,O,N,A,k,M,P,H;const S={t:x.elapsed_s,ops:((j=x.client)==null?void 0:j.ops_live)??0,fps:((O=x.venue)==null?void 0:O.fps_live)??0,tps:(((N=x.client)==null?void 0:N.ops_live)??0)+(((A=x.venue)==null?void 0:A.fps_live)??0),p50:(((k=x.response_latency)==null?void 0:k.p50_us)??0)/1e3,p99:(((M=x.response_latency)==null?void 0:M.p99_us)??0)/1e3,fc50:(((P=x.fill_completion_latency)==null?void 0:P.p50_us)??0)/1e3,fc99:(((H=x.fill_completion_latency)==null?void 0:H.p99_us)??0)/1e3},_=[...w,S];return _.length>Zp?_.slice(-Zp):_}),oW.has(x.status)&&(d=!0,l==null||l.close())},l.onclose=()=>{s||(o(!1),d||p())},l.onerror=()=>{l==null||l.close()}}function p(){if(s||d)return;f+=1;const v=Math.min(iW,500*2**Math.min(f,4));u=setTimeout(m,v)}return m(),()=>{s=!0,clearTimeout(u),l==null||l.close()}},[e]),{latest:t,series:n,connected:i}}const Dw="img-export-exclude";async function lW(e,t,r){const{toPng:n,toJpeg:a}=await Cv(async()=>{const{toPng:u,toJpeg:f}=await import("./index-CyNOPa0n.js");return{toPng:u,toJpeg:f}},[]),i=getComputedStyle(document.body).backgroundColor||"#111",s=await(r==="png"?n:a)(e,{backgroundColor:i,pixelRatio:2,quality:.95,filter:u=>!(u instanceof HTMLElement&&u.classList.contains(Dw))}),l=document.createElement("a");l.href=s,l.download=t,l.click()}function cW(e,t){const r=new Date().toISOString().slice(0,19).replace(/[:T]/g,"-");return`${e}-${r}.${t}`}const _l=new Set(["completed","stopped","error"]);function wn(e){return e?(e/1e3).toFixed(3):"—"}function Rw(e,t){return t<=0?"—":(e/t).toFixed(2)}function Kp({title:e,sub:t,stats:r}){return c.jsxs("div",{className:"perf-lat-card",children:[c.jsxs("div",{className:"perf-lat-title",children:[e,t&&c.jsx("span",{className:"perf-lat-sub",children:t})]}),c.jsx("div",{className:"perf-lat-grid",children:["p50_us","p95_us","p99_us","max_us"].map(n=>c.jsxs("div",{className:"perf-lat-cell",children:[c.jsx("span",{className:"perf-lat-val",children:wn(r[n])}),c.jsxs("span",{className:"perf-lat-lbl",children:[n.replace("_us","")," ms"]})]},n))})]})}function uW({runId:e}){var w;const{latest:t,series:r,connected:n}=sW(e),[a,i]=h.useState(null),[o,s]=h.useState(!1),l=h.useRef(null);h.useEffect(()=>{let S=!0;i(null);let _;const j=async()=>{try{const O=await G.perf.runs.get(e);if(!S)return;i(O),_l.has(O.status)&&clearInterval(_)}catch{}};return _=setInterval(j,2e3),j(),()=>{S=!1,clearInterval(_)}},[e]);const u=t?_l.has(t.status):a?_l.has(a.status):!1,f=!u;async function d(){try{await G.perf.runs.stop(e)}catch{}}async function m(S){if(!(!l.current||o)){s(!0);try{const _=S==="png"?"png":"jpg";await lW(l.current,cW(`perf-${e.slice(0,8)}`,_),S)}catch(_){console.error("image export failed",_)}finally{s(!1)}}}const p=t==null?void 0:t.client,v=t==null?void 0:t.venue,y=t==null?void 0:t.errors,g=(y==null?void 0:y.saturated)||(t==null?void 0:t.status)==="saturated",b=t==null?void 0:t.rate_config,x=[];return p&&(x.push({label:"orders/s",value:Math.round(p.ops_live).toLocaleString()}),(b==null?void 0:b.orders_per_window)!=null&&(b==null?void 0:b.order_window_ms)!=null&&x.push({label:"order rate (cfg)",value:`${b.orders_per_window.toLocaleString()} / ${b.order_window_ms}ms`}),x.push({label:"orders sent",value:p.orders_sent.toLocaleString()},{label:"pending",value:p.pending.toLocaleString()},{label:"dropped",value:p.dropped.toLocaleString()})),v&&(x.push({label:"fills/s",value:Math.round(v.fps_live).toLocaleString()}),(b==null?void 0:b.fills_per_window)!=null&&(b==null?void 0:b.fill_window_ms)!=null&&x.push({label:"fill rate (cfg)",value:`${b.fills_per_window.toLocaleString()} / ${b.fill_window_ms}ms`}),x.push({label:"fills",value:v.fills_sent.toLocaleString()},{label:"fills/order",value:Rw(v.fills_sent,v.orders_received-v.unfilled)},{label:"fill ratio",value:v.fill_ratio.toFixed(3)},{label:"recv",value:v.orders_received.toLocaleString()},{label:"acks",value:v.acks_sent.toLocaleString()},{label:"unfilled",value:v.unfilled.toLocaleString()})),y&&x.push({label:"lost",value:y.lost_timeout.toLocaleString()},{label:"rejected",value:y.rejected.toLocaleString()}),c.jsxs("div",{className:"perf-dashboard",ref:l,children:[c.jsxs("div",{className:"perf-dash-header",children:[c.jsxs("div",{className:"perf-dash-status",children:[c.jsx("span",{className:`badge perf-state-${(t==null?void 0:t.status)??(a==null?void 0:a.status)??"pending"}`,children:(t==null?void 0:t.status)??(a==null?void 0:a.status)??"pending"}),t&&c.jsxs("span",{className:"perf-dash-elapsed",children:[Math.round(t.elapsed_s),"s",t.duration_s?` / ${t.duration_s}s`:""," · ",t.mode]}),c.jsx("span",{className:`perf-conn ${n?"on":"off"}`,children:n?"● live":"○ offline"})]}),c.jsxs("div",{className:`perf-dash-actions ${Dw}`,children:[f&&c.jsx("button",{className:"btn btn-sm btn-danger",onClick:d,children:"■ Stop"}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>G.perf.runs.export(e,"messages"),children:"Export CSV"}),u&&c.jsx("button",{className:"btn btn-sm btn-ghost",title:"Per-exec detail (requires record_execs)",onClick:()=>G.perf.runs.export(e,"execs"),children:"Execs CSV"}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>G.perf.runs.export(e,"both"),children:"Export ZIP"}),c.jsx("button",{className:"btn btn-sm btn-ghost",disabled:o,onClick:()=>m("png"),children:o?"…":"PNG"}),c.jsx("button",{className:"btn btn-sm btn-ghost",disabled:o,onClick:()=>m("jpeg"),children:o?"…":"JPG"})]})]}),g&&c.jsxs("div",{className:"perf-saturated-banner",children:["⚠ SATURATED — injection paused to protect latency measurement.",p&&c.jsxs(c.Fragment,{children:[" Pending ",p.pending.toLocaleString(),", dropped ",p.dropped.toLocaleString(),"."]})]}),(w=a==null?void 0:a.warnings)==null?void 0:w.map((S,_)=>c.jsxs("div",{className:"perf-warning-banner",children:["⚠ ",S]},_)),!t&&!u&&c.jsx("div",{className:"loading-msg",children:"Waiting for first snapshot…"}),x.length>0&&c.jsx("div",{className:"stat-cards",children:x.map(({label:S,value:_})=>c.jsxs("div",{className:"stat-card",children:[c.jsx("span",{className:"stat-card-value",children:_}),c.jsx("span",{className:"stat-card-label",children:S})]},S))}),t&&c.jsxs("div",{className:"perf-lat-row",children:[c.jsx(Kp,{title:"Response latency",sub:"order-2-ack",stats:t.response_latency}),c.jsx(Kp,{title:"Fill-completion latency",sub:"order-2-last-fill",stats:t.fill_completion_latency})]}),r.length>1&&c.jsxs(c.Fragment,{children:[c.jsx("div",{className:"analysis-section-title",children:"Throughput"}),c.jsx("div",{className:"analysis-chart-wrap",children:c.jsx(Pn,{width:"100%",height:200,children:c.jsxs(Ii,{data:r,margin:{top:4,right:16,bottom:4,left:8},children:[c.jsx(Gr,{strokeDasharray:"3 3",stroke:"var(--border)"}),c.jsx(Zr,{dataKey:"t",tick:{fontSize:11,fill:"var(--text2)"},tickFormatter:S=>`${Math.round(S)}s`}),c.jsx(Kr,{tick:{fontSize:11,fill:"var(--text2)"},allowDecimals:!1}),c.jsx(En,{contentStyle:{background:"var(--bg3)",border:"1px solid var(--border)",fontSize:12},labelStyle:{color:"var(--text)"},labelFormatter:S=>`${Math.round(Number(S))}s`}),c.jsx(pa,{wrapperStyle:{fontSize:11}}),c.jsx(_t,{type:"monotone",dataKey:"ops",name:"orders/s",stroke:"#00b4c8",dot:!1,isAnimationActive:!1}),c.jsx(_t,{type:"monotone",dataKey:"fps",name:"fills/s",stroke:"#a78bfa",dot:!1,isAnimationActive:!1}),c.jsx(_t,{type:"monotone",dataKey:"tps",name:"total msgs/s",stroke:"#fbbf24",dot:!1,isAnimationActive:!1})]})})}),c.jsx("div",{className:"analysis-section-title",children:"Response latency (ms)"}),c.jsx("div",{className:"analysis-chart-wrap",children:c.jsx(Pn,{width:"100%",height:200,children:c.jsxs(Ii,{data:r,margin:{top:4,right:16,bottom:4,left:8},children:[c.jsx(Gr,{strokeDasharray:"3 3",stroke:"var(--border)"}),c.jsx(Zr,{dataKey:"t",tick:{fontSize:11,fill:"var(--text2)"},tickFormatter:S=>`${Math.round(S)}s`}),c.jsx(Kr,{tick:{fontSize:11,fill:"var(--text2)"}}),c.jsx(En,{contentStyle:{background:"var(--bg3)",border:"1px solid var(--border)",fontSize:12},labelStyle:{color:"var(--text)"},labelFormatter:S=>`${Math.round(Number(S))}s`}),c.jsx(pa,{wrapperStyle:{fontSize:11}}),c.jsx(_t,{type:"monotone",dataKey:"p50",name:"p50",stroke:"#4ade80",dot:!1,isAnimationActive:!1}),c.jsx(_t,{type:"monotone",dataKey:"p99",name:"p99",stroke:"#f87171",dot:!1,isAnimationActive:!1})]})})}),c.jsx("div",{className:"analysis-section-title",children:"Fill-completion latency (ms)"}),c.jsx("div",{className:"analysis-chart-wrap",children:c.jsx(Pn,{width:"100%",height:200,children:c.jsxs(Ii,{data:r,margin:{top:4,right:16,bottom:4,left:8},children:[c.jsx(Gr,{strokeDasharray:"3 3",stroke:"var(--border)"}),c.jsx(Zr,{dataKey:"t",tick:{fontSize:11,fill:"var(--text2)"},tickFormatter:S=>`${Math.round(S)}s`}),c.jsx(Kr,{tick:{fontSize:11,fill:"var(--text2)"}}),c.jsx(En,{contentStyle:{background:"var(--bg3)",border:"1px solid var(--border)",fontSize:12},labelStyle:{color:"var(--text)"},labelFormatter:S=>`${Math.round(Number(S))}s`}),c.jsx(pa,{wrapperStyle:{fontSize:11}}),c.jsx(_t,{type:"monotone",dataKey:"fc50",name:"p50",stroke:"#4ade80",dot:!1,isAnimationActive:!1}),c.jsx(_t,{type:"monotone",dataKey:"fc99",name:"p99",stroke:"#f87171",dot:!1,isAnimationActive:!1})]})})})]}),u&&(a==null?void 0:a.summary)&&c.jsx(dW,{status:a})]})}function dW({status:e}){const t=e.summary,r=[["Orders sent",t.orders_sent.toLocaleString()],["Acks sent",t.acks_sent.toLocaleString()],["Fills sent",t.fills_sent.toLocaleString()],["Responses",t.responses.toLocaleString()],["Completions (fully filled)",t.completions.toLocaleString()],["Fills per order (completed)",Rw(t.fills_sent,t.completions)],["Fill ratio (completed)",t.fill_ratio.toFixed(3)],["Lost / dropped / rejected",`${t.lost_timeout} / ${t.dropped} / ${t.rejected}`],["Response p50 / p99 (ms)",`${wn(t.response_latency.p50_us)} / ${wn(t.response_latency.p99_us)}`],["Response max (ms)",wn(t.response_latency.max_us)],["Fill-completion p50 / p99 (ms)",`${wn(t.fill_completion_latency.p50_us)} / ${wn(t.fill_completion_latency.p99_us)}`]];return c.jsxs("div",{className:"perf-summary-card",children:[c.jsxs("div",{className:"perf-section-title",children:["Run summary — ",e.status]}),c.jsx("table",{className:"perf-summary-table",children:c.jsx("tbody",{children:r.map(([n,a])=>c.jsxs("tr",{children:[c.jsx("td",{children:n}),c.jsx("td",{className:"mono",children:a})]},n))})})]})}const fW=new Set(["completed","stopped","error"]);function hW(e){if(!e)return"—";try{return new Date(e*1e3).toLocaleTimeString()}catch{return"—"}}function mW({activeRunId:e,onSelect:t,refreshKey:r}){const[n,a]=h.useState([]);h.useEffect(()=>{let o=!0;const s=()=>G.perf.runs.list().then(u=>{o&&a(u)}).catch(()=>{});s();const l=setInterval(s,2e3);return()=>{o=!1,clearInterval(l)}},[r]);async function i(o,s){o.stopPropagation();try{await G.perf.runs.stop(s)}catch{}}return n.length===0?c.jsx("div",{className:"perf-runlist-empty",children:"No runs yet. Configure and start one."}):c.jsxs("table",{className:"perf-runlist",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{children:"Name"}),c.jsx("th",{children:"Mode"}),c.jsx("th",{children:"Status"}),c.jsx("th",{children:"Started"}),c.jsx("th",{children:"Elapsed"}),c.jsx("th",{children:"Orders"}),c.jsx("th",{children:"Fill"}),c.jsx("th",{})]})}),c.jsx("tbody",{children:n.map(o=>c.jsxs("tr",{className:o.run_id===e?"perf-run-active":"",onClick:()=>t(o.run_id),children:[c.jsx("td",{children:o.name}),c.jsx("td",{children:o.mode}),c.jsx("td",{children:c.jsx("span",{className:`badge perf-state-${o.status}`,children:o.status})}),c.jsx("td",{children:hW(o.started_at)}),c.jsxs("td",{className:"mono",children:[Math.round(o.elapsed_s),"s"]}),c.jsx("td",{className:"mono",children:o.summary?o.summary.orders_sent.toLocaleString():"—"}),c.jsx("td",{className:"mono",children:o.summary?o.summary.fill_ratio.toFixed(2):"—"}),c.jsx("td",{className:"perf-runlist-actions",children:!fW.has(o.status)&&c.jsx("button",{className:"btn btn-sm btn-danger",onClick:s=>i(s,o.run_id),children:"Stop"})})]},o.run_id))})]})}const Ol=200,_n=(e,t,r=120)=>({field:e,headerName:t,width:r,type:"numericColumn",valueFormatter:n=>n.value==null?"":Number(n.value).toLocaleString()}),Pc=(e,t)=>({field:e,headerName:t,width:150,type:"numericColumn",valueFormatter:r=>r.value==null?"":`${Math.round(Number(r.value)).toLocaleString()}`}),pW=[{field:"clordid",headerName:"ClOrdID",width:150},{field:"corr_id",headerName:"Corr ID",width:150},{field:"msg_type",headerName:"Type",width:80},{field:"symbol",headerName:"Symbol",width:90},{field:"side",headerName:"Side",width:70},_n("qty","Qty",90),_n("price","Price",100),Pc("response_latency_us","Resp µs"),Pc("fill_latency_us","Fill µs"),_n("fill_qty","Fill qty",100),_n("fill_price","Fill price",110),{field:"status",headerName:"Status",width:110,cellStyle:e=>e.value==="lost"||e.value==="dropped"?{color:"#facc15",fontWeight:"bold"}:void 0}],vW=[{field:"scenario_name",headerName:"Scenario",flex:1,minWidth:160},Pc("latency_us","Latency µs"),_n("msg_count","Msgs",90),_n("fill_ratio","Fill ratio",110),{field:"status",headerName:"Status",width:110,cellStyle:e=>e.value==="lost"||e.value==="dropped"?{color:"#facc15",fontWeight:"bold"}:void 0}];function gW({runId:e}){const t=X(g=>g.theme),[r,n]=h.useState("messages"),[a,i]=h.useState(0),[o,s]=h.useState([]),[l,u]=h.useState(0),[f,d]=h.useState(!1);h.useEffect(()=>{i(0)},[r,e]),h.useEffect(()=>{let g=!0;return d(!0),(r==="messages"?G.perf.runs.messages:G.perf.runs.scenarios)(e,{limit:Ol,offset:a*Ol}).then(x=>{g&&(s(x.items),u(x.total))}).catch(()=>{g&&(s([]),u(0))}).finally(()=>{g&&d(!1)}),()=>{g=!1}},[e,r,a]);const m=h.useMemo(()=>r==="messages"?pW:vW,[r]),p=h.useMemo(()=>({sortable:!0,filter:!0,resizable:!0}),[]),v=Math.max(0,Math.ceil(l/Ol)-1),y=t==="dark"?"ag-theme-quartz-dark":"ag-theme-quartz";return c.jsxs("div",{className:"perf-results",children:[c.jsxs("div",{className:"perf-results-toolbar",children:[c.jsxs("div",{className:"perf-results-tabs",children:[c.jsx("button",{className:`btn btn-sm ${r==="messages"?"btn-primary":"btn-ghost"}`,onClick:()=>n("messages"),children:"Messages"}),c.jsx("button",{className:`btn btn-sm ${r==="scenarios"?"btn-primary":"btn-ghost"}`,onClick:()=>n("scenarios"),children:"Scenarios"})]}),c.jsxs("div",{className:"perf-results-pager",children:[c.jsxs("span",{children:[l.toLocaleString()," rows"]}),c.jsx("button",{className:"btn btn-sm btn-ghost",disabled:a<=0||f,onClick:()=>i(g=>Math.max(0,g-1)),children:"‹ Prev"}),c.jsxs("span",{children:["Page ",a+1," / ",v+1]}),c.jsx("button",{className:"btn btn-sm btn-ghost",disabled:a>=v||f,onClick:()=>i(g=>Math.min(v,g+1)),children:"Next ›"})]})]}),c.jsx("div",{className:`${y} perf-results-grid`,children:c.jsx(Nc,{rowData:o,columnDefs:m,defaultColDef:p,animateRows:!1,suppressCellFocus:!0})})]})}function yW(){const{theme:e,toggleTheme:t,branding:r,setSessions:n}=X(),a=Us(),i=r!=null&&r.prefix?`${r.prefix} FIXture`:"FIXture";Sv(),h.useEffect(()=>{G.sessions.list().then(n).catch(()=>{})},[]);const[o,s]=h.useState("configure"),[l,u]=h.useState(null),[f,d]=h.useState(0);function m(y){u(y),d(g=>g+1),s("live")}function p(y){u(y),s("live")}const v=[{id:"configure",label:"Configure"},{id:"live",label:"Live",disabled:!l},{id:"results",label:"Results",disabled:!l},{id:"history",label:"History"}];return c.jsxs("div",{className:"app perf-page",children:[c.jsxs("header",{className:"app-header",children:[c.jsx("img",{src:a,alt:i,className:"app-logo"}),c.jsx("div",{className:"perf-page-title",children:"Performance Testing"}),c.jsxs("div",{className:"header-right",children:[c.jsx(qr,{to:"/",className:"btn btn-sm btn-ghost",children:"← Sessions"}),c.jsx("button",{className:"btn-icon theme-toggle",onClick:t,title:"Toggle theme",children:e==="dark"?"☀":"🌙"})]})]}),c.jsx("div",{className:"perf-tabbar",children:v.map(y=>c.jsx("button",{className:`perf-tab${o===y.id?" active":""}`,disabled:y.disabled,onClick:()=>s(y.id),children:y.label},y.id))}),c.jsxs("div",{className:"perf-body",children:[o==="configure"&&c.jsx("div",{className:"perf-pane perf-pane-form",children:c.jsx(aW,{onStarted:m})}),o==="live"&&l&&c.jsx("div",{className:"perf-pane",children:c.jsx(uW,{runId:l},l)}),o==="results"&&l&&c.jsx("div",{className:"perf-pane",children:c.jsx(gW,{runId:l},l)}),o==="history"&&c.jsx("div",{className:"perf-pane",children:c.jsx(mW,{activeRunId:l,onSelect:p,refreshKey:f})})]})]})}function bW(){return X(t=>t.token)?c.jsx(uv,{}):c.jsx(Mi,{to:"/login",replace:!0})}function xW(){const e=X(t=>t.user);return e?e.role!=="platform_admin"?c.jsx(Mi,{to:"/",replace:!0}):c.jsx(uv,{}):c.jsx(Mi,{to:"/login",replace:!0})}function wW(){const e=X(t=>t.setBranding);h.useEffect(()=>{fetch("/api/branding").then(t=>t.json()).then(t=>{e(t),Tw(t)}).catch(()=>{})},[])}function jW(){const e=an(),t=X(r=>r.theme);return wW(),h.useEffect(()=>{document.documentElement.setAttribute("data-theme",t)},[t]),h.useEffect(()=>{fetch("/api/setup/status").then(r=>r.json()).then(r=>{r.setup_required&&e("/setup",{replace:!0})}).catch(()=>{})},[]),c.jsxs(Zj,{children:[c.jsx(Lt,{path:"/setup",element:c.jsx(Xz,{})}),c.jsx(Lt,{path:"/login",element:c.jsx(Yz,{})}),c.jsx(Lt,{path:"/register",element:c.jsx(Vz,{})}),c.jsxs(Lt,{element:c.jsx(bW,{}),children:[c.jsx(Lt,{path:"/",element:c.jsx(qz,{})}),c.jsxs(Lt,{element:c.jsx(xW,{}),children:[c.jsx(Lt,{path:"/admin/users",element:c.jsx(rW,{})}),c.jsx(Lt,{path:"/perf",element:c.jsx(yW,{})})]})]}),c.jsx(Lt,{path:"*",element:c.jsx(Mi,{to:"/",replace:!0})})]})}Pl.createRoot(document.getElementById("root")).render(c.jsx(qp.StrictMode,{children:c.jsx(vS,{children:c.jsx(jW,{})})}));
|
|
102
|
+
}`;function Kz({onClose:e}){const[t,r]=h.useState(""),[n,a]=h.useState(""),[i,o]=h.useState(!1);h.useEffect(()=>{G.specOverlay.get().then(l=>r(JSON.stringify(l,null,2))).catch(console.error)},[]);async function s(){a("");let l;try{l=JSON.parse(t)}catch{a("Invalid JSON");return}o(!0);try{await G.specOverlay.set(l),e()}catch(u){a(u instanceof Error?u.message:"Save failed")}finally{o(!1)}}return c.jsx("div",{className:"modal-overlay",onClick:e,children:c.jsxs("div",{className:"modal",style:{width:660,maxHeight:"85vh",display:"flex",flexDirection:"column"},onClick:l=>l.stopPropagation(),children:[c.jsxs("div",{className:"modal-header",children:[c.jsx("span",{children:"Spec Overlay — Custom Message Types"}),c.jsx("button",{className:"btn-icon",onClick:e,children:"✕"})]}),c.jsx("div",{style:{padding:"10px 16px 4px",fontSize:12,color:"var(--text2)"},children:"Define custom / proprietary message types (e.g. 35=U1). Fields reference standard tag numbers; use Custom Tags for non-standard tag names. Field names and types are resolved from the spec at runtime."}),c.jsx("div",{style:{flex:1,display:"flex",flexDirection:"column",padding:"8px 16px",minHeight:0},children:c.jsx("textarea",{style:{flex:1,minHeight:300,fontFamily:"var(--font)",fontSize:12,background:"var(--bg3)",color:"var(--text)",border:"1px solid var(--border)",borderRadius:4,padding:"8px 10px",resize:"none"},value:t,onChange:l=>r(l.target.value),spellCheck:!1,placeholder:Zz})}),c.jsxs("div",{style:{padding:"0 16px 8px",fontSize:11,color:"var(--text2)"},children:["Schema: ",c.jsx("code",{children:'{ "messages": [{ "msg_type", "name", "category", "fields": [{ "tag", "required" }] }] }'})]}),n&&c.jsx("div",{className:"tpl-error",style:{margin:"0 16px 8px"},children:n}),c.jsxs("div",{className:"modal-footer",children:[c.jsx("button",{className:"btn btn-ghost",onClick:e,children:"Cancel"}),c.jsx("button",{className:"btn btn-primary",onClick:s,disabled:i,children:i?"Saving…":"Save overlay"})]})]})})}const Iw="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgdmlld0JveD0iMCAwIDY4MCAxNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgCgogIDxwb2x5Z29uIHBvaW50cz0iMTg4LDI4IDIzOCwyOCAyNTgsNzggMjM4LDEyOCAxODgsMTI4IiBmaWxsPSIjMWEzYTVjIiBzdHlsZT0iZmlsbDpyZ2IoMjYsIDU4LCA5Mik7c3Ryb2tlOm5vbmU7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGNpcmNsZSBjeD0iMjA3IiBjeT0iNzgiIHI9IjEwIiBmaWxsPSIjMDBiNGM4IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMTgwLCAyMDApO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxjaXJjbGUgY3g9IjIwNyIgY3k9Ijc4IiByPSI2IiBmaWxsPSIjMWEzYTVjIiBzdHlsZT0iZmlsbDpyZ2IoMjYsIDU4LCA5Mik7c3Ryb2tlOm5vbmU7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9IjY1IiB4Mj0iMjQ4IiB5Mj0iNjUiIHN0cm9rZT0iIzAwYjRjOCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0eWxlPSJmaWxsOnJnYigwLCAwLCAwKTtzdHJva2U6cmdiKDAsIDE4MCwgMjAwKTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjRweDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9Ijc4IiB4Mj0iMjUyIiB5Mj0iNzgiIHN0cm9rZT0iIzAwYjRjOCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9IjAuNyIgc3R5bGU9ImZpbGw6cmdiKDAsIDAsIDApO3N0cm9rZTpyZ2IoMCwgMTgwLCAyMDApO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6NHB4O3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjAuNztmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxsaW5lIHgxPSIyMjAiIHkxPSI5MSIgeDI9IjI0NCIgeTI9IjkxIiBzdHJva2U9IiMwMGI0YzgiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBvcGFjaXR5PSIwLjQ1IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMCwgMCk7c3Ryb2tlOnJnYigwLCAxODAsIDIwMCk7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDo0cHg7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MC40NTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgoKICA8dGV4dCB4PSIyNzIiIHk9IjEwNSIgZm9udC1mYW1pbHk9InZhcigtLWZvbnQtbW9ubykiIGZvbnQtc2l6ZT0iNjYiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMwMGI0YzgiIGxldHRlci1zcGFjaW5nPSItMSIgc3R5bGU9ImZpbGw6cmdiKDAsIDE4MCwgMjAwKTtzdHJva2U6bm9uZTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjE7Zm9udC1mYW1pbHk6dWktbW9ub3NwYWNlLCBtb25vc3BhY2U7Zm9udC1zaXplOjY2cHg7Zm9udC13ZWlnaHQ6NzAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iPkZJWDwvdGV4dD4KICA8dGV4dCB4PSIzODYiIHk9IjEwNSIgZm9udC1mYW1pbHk9InZhcigtLWZvbnQtc2FucykiIGZvbnQtc2l6ZT0iNTAiIGZvbnQtd2VpZ2h0PSIzMDAiIGxldHRlci1zcGFjaW5nPSItMSIgc3R5bGU9ImZpbGw6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6NTBweDtmb250LXdlaWdodDozMDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byI+dHVyZTwvdGV4dD4KPC9zdmc+",kw="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgdmlld0JveD0iMCAwIDY4MCAxNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CgogIDxwb2x5Z29uIHBvaW50cz0iMTg4LDI4IDIzOCwyOCAyNTgsNzggMjM4LDEyOCAxODgsMTI4IiBmaWxsPSIjMDBiNGM4IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMTgwLCAyMDApO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxjaXJjbGUgY3g9IjIwNyIgY3k9Ijc4IiByPSIxMCIgZmlsbD0iIzFhM2E1YyIgc3R5bGU9ImZpbGw6cmdiKDI2LCA1OCwgOTIpO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxjaXJjbGUgY3g9IjIwNyIgY3k9Ijc4IiByPSI2IiBmaWxsPSIjMDBiNGM4IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMTgwLCAyMDApO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxsaW5lIHgxPSIyMjAiIHkxPSI2NSIgeDI9IjI0OCIgeTI9IjY1IiBzdHJva2U9IiMxYTNhNWMiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHlsZT0iZmlsbDpyZ2IoMCwgMCwgMCk7c3Ryb2tlOnJnYigyNiwgNTgsIDkyKTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjRweDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9Ijc4IiB4Mj0iMjUyIiB5Mj0iNzgiIHN0cm9rZT0iIzFhM2E1YyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9IjAuNyIgc3R5bGU9ImZpbGw6cmdiKDAsIDAsIDApO3N0cm9rZTpyZ2IoMjYsIDU4LCA5Mik7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDo0cHg7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MC43O2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9IjkxIiB4Mj0iMjQ0IiB5Mj0iOTEiIHN0cm9rZT0iIzFhM2E1YyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9IjAuNDUiIHN0eWxlPSJmaWxsOnJnYigwLCAwLCAwKTtzdHJva2U6cmdiKDI2LCA1OCwgOTIpO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6NHB4O3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjAuNDU7Zm9udC1mYW1pbHk6JnF1b3Q7QW50aHJvcGljIFNhbnMmcXVvdDssIC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJnF1b3Q7U2Vnb2UgVUkmcXVvdDssIHNhbnMtc2VyaWY7Zm9udC1zaXplOjE2cHg7Zm9udC13ZWlnaHQ6NDAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iLz4KCiAgPHRleHQgeD0iMjcyIiB5PSIxMDUiIGZvbnQtZmFtaWx5PSJ2YXIoLS1mb250LW1vbm8pIiBmb250LXNpemU9IjY2IiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWEzYTVjIiBsZXR0ZXItc3BhY2luZz0iLTEiIHN0eWxlPSJmaWxsOnJnYigyNiwgNTgsIDkyKTtzdHJva2U6bm9uZTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjE7Zm9udC1mYW1pbHk6dWktbW9ub3NwYWNlLCBtb25vc3BhY2U7Zm9udC1zaXplOjY2cHg7Zm9udC13ZWlnaHQ6NzAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iPkZJWDwvdGV4dD4KICA8dGV4dCB4PSIzODYiIHk9IjEwNSIgZm9udC1mYW1pbHk9InZhcigtLWZvbnQtc2FucykiIGZvbnQtc2l6ZT0iNTAiIGZvbnQtd2VpZ2h0PSIzMDAiIGZpbGw9IiMwMGI0YzgiIGxldHRlci1zcGFjaW5nPSItMSIgc3R5bGU9ImZpbGw6cmdiKDAsIDE4MCwgMjAwKTtzdHJva2U6bm9uZTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjE7Zm9udC1mYW1pbHk6JnF1b3Q7QW50aHJvcGljIFNhbnMmcXVvdDssIHNhbnMtc2VyaWY7Zm9udC1zaXplOjUwcHg7Zm9udC13ZWlnaHQ6MzAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iPnR1cmU8L3RleHQ+Cjwvc3ZnPg==";function Us(){const e=X(r=>r.theme),t=X(r=>r.branding);return e==="dark"?(t==null?void 0:t.logo_dark)||Iw:(t==null?void 0:t.logo_light)||kw}function qz(){const e=X(g=>g.setSessions),t=X(g=>g.theme),r=X(g=>g.toggleTheme),n=X(g=>g.user),a=X(g=>g.logout),i=X(g=>g.openTabs),o=X(g=>g.activeTabId),s=X(g=>g.branding),l=Us(),u=s!=null&&s.prefix?`${s.prefix} FIXture`:"FIXture",[f,d]=h.useState(!1),[m,p]=h.useState(!1),[v,y]=h.useState(!1);return Sv(),h.useEffect(()=>{document.documentElement.setAttribute("data-theme",t)},[t]),h.useEffect(()=>{G.sessions.list().then(e).catch(console.error)},[]),h.useEffect(()=>{if(!n){n1();return}G.customTags.list().then(jv).catch(console.error)},[n]),c.jsxs("div",{className:"app",children:[c.jsxs("header",{className:"app-header",children:[c.jsx("img",{src:l,alt:u,className:"app-logo"}),c.jsxs("div",{className:"header-right",children:[n&&c.jsx("span",{className:"header-user",children:n.username}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>d(!0),children:"Scenarios"}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p(!0),children:"Custom Tags"}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>y(!0),children:"Spec Overlay"}),(n==null?void 0:n.role)==="platform_admin"&&c.jsx(qr,{to:"/perf",className:"btn btn-sm btn-ghost",title:"Performance testing",children:"Perf"}),(n==null?void 0:n.role)==="platform_admin"&&c.jsx(qr,{to:"/admin/users",className:"btn-icon",title:"Admin",children:"⚙"}),c.jsx("button",{className:"btn-icon theme-toggle",onClick:r,title:"Toggle theme",children:t==="dark"?"☀":"🌙"}),c.jsx("button",{className:"btn-icon",onClick:a,title:"Sign out",children:"⎋"})]})]}),c.jsxs("div",{className:"app-body",children:[c.jsx(g1,{}),c.jsx("div",{className:"tab-area",children:i.length===0?c.jsx("div",{className:"tab-area-empty",children:"Select a session to open it."}):c.jsxs(c.Fragment,{children:[c.jsx(Az,{}),c.jsx("div",{className:"tab-panels",children:i.map(g=>c.jsx("div",{className:`tab-panel${g===o?" active":""}`,children:c.jsx(Cz,{sessionId:g})},g))})]})})]}),f&&c.jsx(Bz,{onClose:()=>d(!1)}),m&&c.jsx(Gz,{onClose:()=>p(!1)}),v&&c.jsx(Kz,{onClose:()=>y(!1)})]})}function Yz(){const[e,t]=h.useState(""),[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),[l,u]=h.useState(!1),{login:f}=X(),d=Us(),m=an();h.useEffect(()=>{fetch("/api/auth/register/status").then(v=>v.json()).then(v=>u(v.enabled)).catch(()=>{})},[]);async function p(v){v.preventDefault(),i(""),s(!0);try{const y=await fetch("/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:r})});if(!y.ok){const b=await y.json().catch(()=>({}));throw new Error(b.detail??"Login failed")}const g=await y.json();f(g.access_token,g.user),m("/",{replace:!0})}catch(y){i(y.message)}finally{s(!1)}}return c.jsx("div",{className:"auth-page",children:c.jsxs("div",{className:"auth-card",children:[c.jsx("img",{src:d,alt:"FIXture",className:"auth-logo"}),c.jsx("h2",{children:"Sign in"}),c.jsxs("form",{onSubmit:p,className:"auth-form",children:[c.jsxs("label",{children:["Username",c.jsx("input",{type:"text",value:e,onChange:v=>t(v.target.value),autoComplete:"username",required:!0})]}),c.jsxs("label",{children:["Password",c.jsx("input",{type:"password",value:r,onChange:v=>n(v.target.value),autoComplete:"current-password",required:!0})]}),a&&c.jsx("p",{className:"auth-error",children:a}),c.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Signing in…":"Sign in"})]}),l&&c.jsxs("p",{className:"auth-hint",style:{marginTop:12},children:["No account? ",c.jsx(qr,{to:"/register",children:"Register"})]})]})})}function Xz(){const[e,t]=h.useState(""),[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),l=Us(),u=an();async function f(d){d.preventDefault(),i(""),s(!0);try{const m=await fetch("/api/setup/create-admin",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:r})});if(!m.ok){const p=await m.json().catch(()=>({}));throw new Error(p.detail??"Setup failed")}u("/login",{replace:!0})}catch(m){i(m.message)}finally{s(!1)}}return c.jsx("div",{className:"auth-page",children:c.jsxs("div",{className:"auth-card",children:[c.jsx("img",{src:l,alt:"FIXture",className:"auth-logo"}),c.jsx("h2",{children:"Create admin account"}),c.jsx("p",{className:"auth-hint",children:"First-time setup — create the platform admin."}),c.jsxs("form",{onSubmit:f,className:"auth-form",children:[c.jsxs("label",{children:["Username",c.jsx("input",{type:"text",value:e,onChange:d=>t(d.target.value),autoComplete:"username",required:!0})]}),c.jsxs("label",{children:["Password",c.jsx("input",{type:"password",value:r,onChange:d=>n(d.target.value),autoComplete:"new-password",minLength:8,required:!0})]}),a&&c.jsx("p",{className:"auth-error",children:a}),c.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Creating…":"Create admin"})]})]})})}function Vz(){const[e,t]=h.useState(""),[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),[l,u]=h.useState(null),{login:f,theme:d}=X(),m=an();h.useEffect(()=>{fetch("/api/auth/register/status").then(y=>y.json()).then(u).catch(()=>u({enabled:!1,max_users:0,current_users:0}))},[]);async function p(y){y.preventDefault(),i(""),s(!0);try{const g=await fetch("/api/auth/register",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:r})});if(!g.ok){const x=await g.json().catch(()=>({}));throw new Error(x.detail??"Registration failed")}const b=await g.json();f(b.access_token,b.user),m("/",{replace:!0})}catch(g){i(g.message)}finally{s(!1)}}const v=l&&l.current_users>=l.max_users;return c.jsx("div",{className:"auth-page",children:c.jsxs("div",{className:"auth-card",children:[c.jsx("img",{src:d==="dark"?Iw:kw,alt:"FIXture",className:"auth-logo"}),c.jsx("h2",{children:"Create account"}),l===null&&c.jsx("p",{className:"auth-hint",children:"Checking registration status…"}),l&&!l.enabled&&c.jsx("p",{className:"auth-error",children:"Registration is currently closed. Contact an administrator."}),l&&l.enabled&&v&&c.jsx("p",{className:"auth-error",children:"Registration limit reached. Contact an administrator."}),l&&l.enabled&&!v&&c.jsxs("form",{onSubmit:p,className:"auth-form",children:[c.jsxs("label",{children:["Username",c.jsx("input",{type:"text",value:e,onChange:y=>t(y.target.value),autoComplete:"username",required:!0})]}),c.jsxs("label",{children:["Password",c.jsx("input",{type:"password",value:r,onChange:y=>n(y.target.value),autoComplete:"new-password",minLength:8,required:!0})]}),a&&c.jsx("p",{className:"auth-error",children:a}),c.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Creating account…":"Create account"})]}),c.jsxs("p",{className:"auth-hint",style:{marginTop:12},children:["Already have an account? ",c.jsx(qr,{to:"/login",children:"Sign in"})]})]})})}const Jz=["accent","bg","bg2","bg3","border","text","text2"];function Tw(e){let t=document.getElementById("branding-style");t||(t=document.createElement("style"),t.id="branding-style",document.head.appendChild(t));const r=Jz.filter(n=>e[n]).map(n=>`--${n}: ${e[n]};`).join(" ");if(r?t.textContent=`:root { ${r} } [data-theme="light"] { ${r} }`:t.textContent="",e.favicon){let n=document.querySelector('link[rel="icon"]');n||(n=document.createElement("link"),n.rel="icon",document.head.appendChild(n)),n.href=e.favicon}document.title=e.prefix?`${e.prefix} FIXture`:"FIXture"}function Sl(e){return new Promise((t,r)=>{const n=new FileReader;n.onload=()=>t(n.result),n.onerror=r,n.readAsDataURL(e)})}const Mw=["user","user_admin","platform_admin"];function Qz(e){return new Date(e*1e3).toLocaleString()}function eW({onClose:e,onCreated:t}){const[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState("user"),[l,u]=h.useState(""),[f,d]=h.useState(!1);async function m(p){p.preventDefault(),u(""),d(!0);try{const v=await G.admin.users.create({username:r,password:a,role:o});t(v),e()}catch(v){u(v.message)}finally{d(!1)}}return c.jsx("div",{className:"modal-overlay",onClick:e,children:c.jsxs("div",{className:"modal",onClick:p=>p.stopPropagation(),children:[c.jsxs("div",{className:"modal-header",children:[c.jsx("span",{children:"Create User"}),c.jsx("button",{className:"btn-icon",onClick:e,children:"✕"})]}),c.jsxs("form",{onSubmit:m,children:[c.jsxs("div",{className:"form-grid",children:[c.jsx("label",{children:"Username"}),c.jsx("input",{value:r,onChange:p=>n(p.target.value),required:!0}),c.jsx("label",{children:"Password"}),c.jsx("input",{type:"password",value:a,onChange:p=>i(p.target.value),minLength:8,required:!0}),c.jsx("label",{children:"Role"}),c.jsx("select",{value:o,onChange:p=>s(p.target.value),children:Mw.map(p=>c.jsx("option",{value:p,children:p},p))})]}),l&&c.jsx("div",{className:"error-msg",children:l}),c.jsxs("div",{className:"modal-footer",children:[c.jsx("button",{type:"button",className:"btn btn-secondary",onClick:e,children:"Cancel"}),c.jsx("button",{type:"submit",className:"btn btn-primary",disabled:f,children:f?"Creating…":"Create"})]})]})]})})}function tW({user:e,onClose:t}){const[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),[l,u]=h.useState(!1);async function f(d){d.preventDefault(),i(""),s(!0);try{await G.admin.users.update(e.uid,{password:r}),u(!0)}catch(m){i(m.message)}finally{s(!1)}}return c.jsx("div",{className:"modal-overlay",onClick:t,children:c.jsxs("div",{className:"modal",onClick:d=>d.stopPropagation(),children:[c.jsxs("div",{className:"modal-header",children:[c.jsxs("span",{children:["Reset Password — ",e.username]}),c.jsx("button",{className:"btn-icon",onClick:t,children:"✕"})]}),l?c.jsxs("div",{style:{padding:16},children:[c.jsx("p",{style:{color:"var(--green)"},children:"Password updated successfully."}),c.jsx("div",{className:"modal-footer",children:c.jsx("button",{className:"btn btn-primary",onClick:t,children:"Close"})})]}):c.jsxs("form",{onSubmit:f,children:[c.jsxs("div",{className:"form-grid",children:[c.jsx("label",{children:"New Password"}),c.jsx("input",{type:"password",value:r,onChange:d=>n(d.target.value),minLength:8,required:!0})]}),a&&c.jsx("div",{className:"error-msg",children:a}),c.jsxs("div",{className:"modal-footer",children:[c.jsx("button",{type:"button",className:"btn btn-secondary",onClick:t,children:"Cancel"}),c.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Saving…":"Save"})]})]})]})})}function rW(){const e=X(T=>T.user),t=an(),[r,n]=h.useState([]),[a,i]=h.useState(null),[o,s]=h.useState(null),[l,u]=h.useState(null),[f,d]=h.useState(!1),[m,p]=h.useState(null),[v,y]=h.useState(!1),{setBranding:g}=X(),[b,x]=h.useState(!1),[w,S]=h.useState(null),[_,j]=h.useState("");async function O(){try{const[T,R,z,W]=await Promise.all([G.admin.users.list(),G.admin.settings.get(),G.admin.housekeeping.get(),G.admin.branding.get()]);n(T),i(R),s(z),p(W)}catch(T){j(T.message)}}h.useEffect(()=>{O()},[]);async function N(T){try{const R=await G.admin.users.update(T.uid,{is_active:!T.is_active});n(z=>z.map(W=>W.uid===T.uid?R:W))}catch(R){j(R.message)}}async function A(T,R){try{const z=await G.admin.users.update(T.uid,{role:R});n(W=>W.map(V=>V.uid===T.uid?z:V))}catch(z){j(z.message)}}async function k(T){if(confirm(`Delete user "${T.username}"? This cannot be undone.`))try{await G.admin.users.delete(T.uid),n(R=>R.filter(z=>z.uid!==T.uid))}catch(R){j(R.message)}}async function M(){if(a)try{const T=await G.admin.settings.update({registration_enabled:!a.enabled});i(T)}catch(T){j(T.message)}}async function P(T){const R=parseInt(T,10);if(!(isNaN(R)||R<1))try{const z=await G.admin.settings.update({max_users:R});i(z)}catch(z){j(z.message)}}async function H(){if(o)try{const T=await G.admin.housekeeping.update({enabled:!o.enabled});s(T)}catch(T){j(T.message)}}async function L(T,R){const z=parseInt(R,10);if(!(isNaN(z)||z<0))try{const W=await G.admin.housekeeping.update({[T]:z});s(W)}catch(W){j(W.message)}}async function U(){d(!0),u(null);try{const T=await G.admin.housekeeping.runNow();u(`Done — deleted ${T.msgs_deleted} messages, ${T.logs_deleted} log files`)}catch(T){j(T.message)}finally{d(!1)}}return c.jsxs("div",{className:"admin-page",children:[c.jsxs("div",{className:"admin-header",children:[c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[c.jsx("button",{className:"btn-icon",onClick:()=>t("/"),title:"Back",children:"←"}),c.jsx("h1",{children:"User Management"})]}),c.jsx("button",{className:"btn btn-primary btn-sm",onClick:()=>x(!0),children:"+ Create User"})]}),_&&c.jsx("div",{className:"error-msg",style:{margin:"0 0 12px"},children:_}),a&&c.jsxs("div",{className:"admin-settings-bar",children:[c.jsx("span",{className:"admin-settings-label",children:"Self-registration:"}),c.jsx("button",{className:`btn btn-sm ${a.enabled?"btn-stop":"btn-start"}`,onClick:M,children:a.enabled?"Enabled — click to disable":"Disabled — click to enable"}),c.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Max users:"}),c.jsx("input",{type:"number",className:"admin-max-users-input",defaultValue:a.max_users,min:1,onBlur:T=>P(T.target.value)}),c.jsxs("span",{className:"admin-settings-label",style:{color:"var(--text2)"},children:["(",a.current_users," / ",a.max_users," registered)"]})]}),o&&c.jsxs("div",{className:"admin-settings-bar",children:[c.jsx("span",{className:"admin-settings-label",children:"Housekeeping:"}),c.jsx("button",{className:`btn btn-sm ${o.enabled?"btn-stop":"btn-start"}`,onClick:H,children:o.enabled?"Auto — click to disable":"Manual only — click to enable"}),c.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Keep messages (days):"}),c.jsx("input",{type:"number",className:"admin-max-users-input",defaultValue:o.msg_retention_days,min:0,title:"0 = keep forever",onBlur:T=>L("msg_retention_days",T.target.value)}),c.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Keep logs (days):"}),c.jsx("input",{type:"number",className:"admin-max-users-input",defaultValue:o.log_retention_days,min:0,title:"0 = keep forever",onBlur:T=>L("log_retention_days",T.target.value)}),c.jsx("button",{className:"btn btn-sm btn-ghost",style:{marginLeft:16},onClick:U,disabled:f,children:f?"Running…":"Run Now"}),l&&c.jsx("span",{style:{marginLeft:8,color:"var(--green)",fontSize:12},children:l})]}),m&&c.jsxs("div",{className:"admin-settings-bar",style:{flexDirection:"column",alignItems:"flex-start",gap:10},children:[c.jsx("span",{className:"admin-settings-label",style:{fontWeight:700},children:"Branding"}),c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[c.jsx("span",{className:"admin-settings-label",children:"Brand prefix:"}),c.jsx("input",{className:"admin-max-users-input",style:{width:160},value:m.prefix,placeholder:"e.g. Acme (shown as Acme FIXture)",onChange:T=>p({...m,prefix:T.target.value})}),c.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Accent colour:"}),c.jsx("input",{type:"color",value:m.accent||"#388bfd",onChange:T=>p({...m,accent:T.target.value}),title:"Primary accent colour"}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,accent:""}),title:"Reset to default blue",children:"Reset"})]}),c.jsx("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[["bg","Page background","#0d1117"],["bg2","Panel background","#161b22"],["bg3","Input background","#21262d"],["border","Border colour","#30363d"],["text","Primary text","#e6edf3"],["text2","Secondary text","#8b949e"]].map(([T,R,z])=>c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:4},children:[c.jsxs("span",{className:"admin-settings-label",children:[R,":"]}),c.jsx("input",{type:"color",value:m[T]||z,onChange:W=>p({...m,[T]:W.target.value}),title:R}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,[T]:""}),title:"Reset to default",children:"↺"})]},T))}),c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[c.jsx("span",{className:"admin-settings-label",children:"Logo (dark theme):"}),m.logo_dark&&c.jsx("img",{src:m.logo_dark,style:{height:28,background:"#161b22",borderRadius:4,padding:"2px 6px"},alt:"dark logo preview"}),c.jsx("input",{type:"file",accept:"image/*",onChange:async T=>{var z;const R=(z=T.target.files)==null?void 0:z[0];R&&p({...m,logo_dark:await Sl(R)})}}),m.logo_dark&&c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,logo_dark:""}),children:"Clear"})]}),c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[c.jsx("span",{className:"admin-settings-label",children:"Logo (light theme):"}),m.logo_light&&c.jsx("img",{src:m.logo_light,style:{height:28,background:"#f0f2f5",borderRadius:4,padding:"2px 6px"},alt:"light logo preview"}),c.jsx("input",{type:"file",accept:"image/*",onChange:async T=>{var z;const R=(z=T.target.files)==null?void 0:z[0];R&&p({...m,logo_light:await Sl(R)})}}),m.logo_light&&c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,logo_light:""}),children:"Clear"})]}),c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[c.jsx("span",{className:"admin-settings-label",children:"Favicon:"}),m.favicon&&c.jsx("img",{src:m.favicon,style:{height:20,width:20},alt:"favicon preview"}),c.jsx("input",{type:"file",accept:"image/png,image/svg+xml,image/x-icon",onChange:async T=>{var z;const R=(z=T.target.files)==null?void 0:z[0];R&&p({...m,favicon:await Sl(R)})}}),m.favicon&&c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,favicon:""}),children:"Clear"})]}),c.jsx("button",{className:"btn btn-sm btn-primary",disabled:v,onClick:async()=>{y(!0);try{const T=await G.admin.branding.update(m);p(T),g(T),Tw(T)}catch(T){j(T.message)}finally{y(!1)}},children:v?"Saving…":"Save Branding"})]}),c.jsxs("table",{className:"admin-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{children:"Username"}),c.jsx("th",{children:"Role"}),c.jsx("th",{children:"Status"}),c.jsx("th",{children:"Created"}),c.jsx("th",{children:"Actions"})]})}),c.jsx("tbody",{children:r.map(T=>c.jsxs("tr",{className:T.is_active?"":"admin-row-inactive",children:[c.jsxs("td",{children:[T.username,T.uid===(e==null?void 0:e.uid)&&c.jsx("span",{className:"admin-you-badge",children:" (you)"})]}),c.jsx("td",{children:c.jsx("select",{value:T.role,onChange:R=>A(T,R.target.value),className:"admin-role-select",disabled:T.uid===(e==null?void 0:e.uid),children:Mw.map(R=>c.jsx("option",{value:R,children:R},R))})}),c.jsx("td",{children:c.jsx("span",{style:{color:T.is_active?"var(--green)":"var(--text2)"},children:T.is_active?"Active":"Inactive"})}),c.jsx("td",{style:{color:"var(--text2)",fontSize:11},children:Qz(T.created_at)}),c.jsx("td",{children:c.jsxs("div",{className:"admin-row-actions",children:[c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>N(T),disabled:T.uid===(e==null?void 0:e.uid),title:T.is_active?"Deactivate":"Activate",children:T.is_active?"Deactivate":"Activate"}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>S(T),title:"Reset password",children:"Reset PW"}),c.jsx("button",{className:"btn btn-sm btn-danger",onClick:()=>k(T),disabled:T.uid===(e==null?void 0:e.uid),title:"Delete user",children:"✕"})]})})]},T.uid))})]}),b&&c.jsx(eW,{onClose:()=>x(!1),onCreated:T=>n(R=>[...R,T])}),w&&c.jsx(tW,{user:w,onClose:()=>S(null)})]})}const Up={name:"smoke",mode:"loopback",client_session_id:null,venue_session_id:null,correlation_tag:376,exec_id_tag:25116,rate:{orders_per_window:50,order_window_ms:100,dispatch:"smooth",fill_dispatch:"smooth",fills_per_window:50,fill_window_ms:100,fill_ratio:1,allow_burst:!1,max_burst_multiplier:2},test:{duration:30,max_orders:0,scenario_timeout_ms:3e4,max_pending:1e5,on_saturation:"pause",output:"results",record_execs:!1},payload:{variables:{},order:{symbols:["AAPL","MSFT","GOOGL"],side:"alternate",ord_type:"limit",quantity_min:100,quantity_max:1e3,price_min:99,price_max:101,time_in_force:"0"},fill:{fill_type:"full",send_ack:!0,fills_per_order:1,partial_fill_pct_min:50,partial_fill_pct_max:99,price_variance_ticks:0},order_template_id:null,exec_template_id:null,auto_expiry:!0,expiry_offset:1,expiry_unit:"days",gen_rules:{}}},nW=["loopback","manager","client","venue"];function Hp(e,t){return t?Math.round(e/t*1e3):0}function aW({onStarted:e}){var mt,Cr;const t=X(C=>C.sessions),[r,n]=h.useState(Up),[a,i]=h.useState(Up.payload.order.symbols.join(", ")),[o,s]=h.useState(!1),[l,u]=h.useState(""),[f,d]=h.useState(!1),[m,p]=h.useState([]),[v,y]=h.useState(""),[g,b]=h.useState([]),x=Object.values(t),w=r.mode==="loopback"||r.mode==="manager"||r.mode==="client",S=r.mode==="loopback"||r.mode==="manager"||r.mode==="venue",_=r.client_session_id?t[r.client_session_id]:void 0,j=r.venue_session_id?t[r.venue_session_id]:void 0,O=!w||(_==null?void 0:_.status)==="LOGGED_ON",N=!S||(j==null?void 0:j.status)==="LOGGED_ON",A=(!w||!!r.client_session_id)&&(!S||!!r.venue_session_id),k=(()=>{if(!r.name.trim())return"Name is required";const C=r.rate;if(C.max_burst_multiplier<1)return"Max burst multiplier must be ≥ 1";if(r.test.scenario_timeout_ms<1)return"Order timeout must be > 0";if(r.test.max_pending<1)return"Max pending must be > 0";if(w){if(C.orders_per_window<1||C.order_window_ms<1)return"Order rate must be > 0";if(a.split(",").map(J=>J.trim()).filter(Boolean).length===0)return"At least one symbol is required";const I=r.payload.order;if(I.quantity_min<1||I.quantity_max<1||I.quantity_min>I.quantity_max)return"Check quantity min/max";if(I.price_min<=0||I.price_max<=0||I.price_min>I.price_max)return"Check price min/max"}if(S){if(C.fills_per_window<1||C.fill_window_ms<1)return"Fill rate must be > 0";if(r.payload.fill.fills_per_order<1)return"Fills per order must be ≥ 1";if(C.fill_ratio<0||C.fill_ratio>1)return"Fill ratio must be between 0 and 1"}return null})(),M=A&&O&&N&&!k&&!f;async function P(){try{p(await G.perf.configs.list())}catch{}}h.useEffect(()=>{P()},[]),h.useEffect(()=>{G.templates.list().then(b).catch(()=>{})},[]);const H=g.filter(C=>C.msg_type==="D"),L=g.filter(C=>C.msg_type==="8"),U=!!r.payload.order_template_id,T=!!r.payload.exec_template_id,R=((mt=H.find(C=>C.id===r.payload.order_template_id))==null?void 0:mt.name)??"selected",z=((Cr=L.find(C=>C.id===r.payload.exec_template_id))==null?void 0:Cr.name)??"selected",W=C=>n(I=>({...I,...C})),V=C=>n(I=>({...I,rate:{...I.rate,...C}})),se=C=>n(I=>({...I,test:{...I.test,...C}})),Q=C=>n(I=>({...I,payload:{...I.payload,order:{...I.payload.order,...C}}})),ye=C=>n(I=>({...I,payload:{...I.payload,fill:{...I.payload.fill,...C}}})),de=C=>n(I=>({...I,payload:{...I.payload,...C}})),[Ne,$]=h.useState([]),ae=C=>{$(C),de({gen_rules:Object.fromEntries(C.filter(I=>I.tag.trim()).map(I=>[I.tag.trim(),I.value]))})},ne=(C,I)=>ae(Ne.map((J,he)=>he===C?{...J,...I}:J));function B(){const C=a.split(",").map(I=>I.trim()).filter(Boolean);return{...r,client_session_id:w?r.client_session_id:null,venue_session_id:S?r.venue_session_id:null,payload:{...r.payload,order:{...r.payload.order,symbols:C}}}}async function F(){u(""),d(!0);try{const{run_id:C}=await G.perf.runs.create(B());e(C)}catch(C){u(C.message??"Failed to start run")}finally{d(!1)}}async function ee(){u("");try{await G.perf.configs.save(B()),await P()}catch(C){u(C.message??"Failed to save config")}}async function D(C){if(y(C),!!C){u("");try{const I=await G.perf.configs.get(C);n(I),i(I.payload.order.symbols.join(", ")),$(Object.entries(I.payload.gen_rules??{}).map(([J,he])=>({tag:J,value:he})))}catch(I){u(I.message??"Failed to load config")}}}async function Te(){if(v&&window.confirm("Delete this saved config?"))try{await G.perf.configs.delete(v),y(""),await P()}catch(C){u(C.message??"Failed to delete config")}}const oe=C=>{const I=t[C];return I?`${I.display_name||`${I.sender_comp_id} → ${I.target_comp_id}`} (${I.status})`:C},ze=x.filter(C=>C.session_role!=="venue"),We=x.filter(C=>C.session_role!=="client");return c.jsxs("div",{className:"perf-form",children:[l&&c.jsx("div",{className:"error-msg",children:l}),c.jsxs("div",{className:"perf-form-section",children:[c.jsx("div",{className:"perf-section-title",children:"Saved configs"}),c.jsxs("div",{className:"perf-field-row",children:[c.jsxs("select",{value:v,onChange:C=>D(C.target.value),children:[c.jsx("option",{value:"",children:"(load a saved config…)"}),m.map(C=>c.jsxs("option",{value:C.config_id,children:[C.name," — ",C.mode]},C.config_id))]}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:ee,children:"Save current"}),c.jsx("button",{className:"btn btn-sm btn-ghost btn-danger-ghost",onClick:Te,disabled:!v,children:"Delete"})]})]}),c.jsxs("div",{className:"perf-form-section",children:[c.jsx("div",{className:"perf-section-title",children:"Run"}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Name"}),c.jsx("input",{value:r.name,onChange:C=>W({name:C.target.value})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Mode"}),c.jsx("select",{value:r.mode,onChange:C=>W({mode:C.target.value}),children:nW.map(C=>c.jsx("option",{value:C,children:C},C))})]}),w&&c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Client session"}),c.jsxs("select",{value:r.client_session_id??"",onChange:C=>W({client_session_id:C.target.value||null}),children:[c.jsx("option",{value:"",children:"(select…)"}),ze.map(C=>c.jsx("option",{value:C.session_id,children:oe(C.session_id)},C.session_id))]}),_&&c.jsx(Gp,{status:_.status})]}),S&&c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Venue session"}),c.jsxs("select",{value:r.venue_session_id??"",onChange:C=>W({venue_session_id:C.target.value||null}),children:[c.jsx("option",{value:"",children:"(select…)"}),We.map(C=>c.jsx("option",{value:C.session_id,children:oe(C.session_id)},C.session_id))]}),j&&c.jsx(Gp,{status:j.status})]})]}),c.jsxs("div",{className:"perf-form-section",children:[c.jsx("div",{className:"perf-section-title",children:"Rate"}),c.jsxs("div",{className:"perf-field-grid",children:[w&&c.jsxs(c.Fragment,{children:[c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Orders / window"}),c.jsx("input",{type:"number",min:1,value:r.rate.orders_per_window,onChange:C=>V({orders_per_window:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Order window (ms)"}),c.jsx("input",{type:"number",min:1,value:r.rate.order_window_ms,onChange:C=>V({order_window_ms:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Order dispatch"}),c.jsxs("select",{value:r.rate.dispatch??"smooth",onChange:C=>V({dispatch:C.target.value}),children:[c.jsx("option",{value:"smooth",children:"smooth (evenly spaced)"}),c.jsx("option",{value:"burst",children:"burst (all at window start)"})]})]})]}),S&&c.jsxs(c.Fragment,{children:[c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Fills / window"}),c.jsx("input",{type:"number",min:1,value:r.rate.fills_per_window,onChange:C=>V({fills_per_window:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Fill window (ms)"}),c.jsx("input",{type:"number",min:1,value:r.rate.fill_window_ms,onChange:C=>V({fill_window_ms:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Fill ratio"}),c.jsx("input",{type:"number",min:0,max:1,step:.05,value:r.rate.fill_ratio,onChange:C=>V({fill_ratio:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Fill dispatch"}),c.jsxs("select",{value:r.rate.fill_dispatch??"smooth",onChange:C=>V({fill_dispatch:C.target.value}),children:[c.jsx("option",{value:"smooth",children:"smooth (evenly spaced)"}),c.jsx("option",{value:"burst",children:"burst (all at window start)"})]})]})]})]}),c.jsxs("div",{className:"perf-hint",children:[w&&c.jsxs(c.Fragment,{children:["≈ ",Hp(r.rate.orders_per_window,r.rate.order_window_ms).toLocaleString()," orders/s"]}),w&&S&&" · ",S&&c.jsxs(c.Fragment,{children:["≈ ",Hp(r.rate.fills_per_window,r.rate.fill_window_ms).toLocaleString()," fills/s"]})," ",r.rate.dispatch==="burst"?c.jsxs(c.Fragment,{children:["Burst: ",r.rate.orders_per_window.toLocaleString()," orders fire back-to-back every ",r.rate.order_window_ms,"ms, then silence — sawtooth load."]}):c.jsx(c.Fragment,{children:"Smaller windows smooth the injection."})]})]}),c.jsxs("div",{className:"perf-form-section",children:[c.jsx("div",{className:"perf-section-title",children:"Test"}),c.jsxs("div",{className:"perf-field-grid",children:[c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Duration (s, 0 = no time limit)"}),c.jsx("input",{type:"number",min:0,value:r.test.duration,onChange:C=>se({duration:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Max orders (0 = no count limit)"}),c.jsx("input",{type:"number",min:0,value:r.test.max_orders,onChange:C=>se({max_orders:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Order timeout (ms)"}),c.jsx("input",{type:"number",min:1,step:1e3,value:r.test.scenario_timeout_ms,onChange:C=>se({scenario_timeout_ms:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"On saturation"}),c.jsxs("select",{value:r.test.on_saturation,onChange:C=>se({on_saturation:C.target.value}),children:[c.jsx("option",{value:"pause",children:"pause (preserves latency accuracy)"}),c.jsx("option",{value:"drop_oldest",children:"drop_oldest"})]})]})]}),w&&c.jsxs("label",{className:"checkbox-label",children:[c.jsx("input",{type:"checkbox",checked:r.test.record_execs,onChange:C=>se({record_execs:C.target.checked})}),"Record per-exec detail — one CSV row per ack/fill with latency + inter-report gap (downloads as csv.gz after the run)"]}),c.jsxs("div",{className:"perf-hint",children:["An order not fully filled within the order timeout is counted ",c.jsx("strong",{children:"Lost"}),". Raise it if rate-limited fills are being flagged lost on full-fill runs."]})]}),w&&c.jsxs("div",{className:`perf-form-section ${U?"perf-section-layered":""}`,children:[c.jsxs("div",{className:"perf-section-title",children:["Order payload",U&&c.jsx("span",{className:"perf-layer-badge",children:"values for template"})]}),U&&c.jsxs("div",{className:"perf-hint perf-layer-note",children:["Order template ",c.jsx("strong",{children:R})," defines the message shape. These fields supply the values it references via ",c.jsx("code",{children:"{symbol}"})," ",c.jsx("code",{children:"{side}"})," ",c.jsx("code",{children:"{qty}"})," ",c.jsx("code",{children:"{price}"})," tokens. Value generators (",c.jsx("code",{children:"uuid()"}),", ",c.jsx("code",{children:"random_str(n)"}),", ",c.jsx("code",{children:"random_int(a,b)"}),","," ",c.jsx("code",{children:"seq(width)"}),", ",c.jsx("code",{children:"timestamp()"}),", ",c.jsx("code",{children:"date()"}),") are evaluated fresh per order and mix with text as ",c.jsx("code",{children:"ORD-{seq(8)}"}),"; ClOrdID(11) is always made unique per order. ",c.jsx("strong",{children:"Order type"})," is taken from the template."]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Symbols (comma-separated)"}),c.jsx("input",{value:a,onChange:C=>i(C.target.value)})]}),c.jsxs("div",{className:"perf-field-grid",children:[c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Side"}),c.jsxs("select",{value:r.payload.order.side,onChange:C=>Q({side:C.target.value}),children:[c.jsx("option",{value:"alternate",children:"alternate"}),c.jsx("option",{value:"fixed_buy",children:"fixed_buy"}),c.jsx("option",{value:"fixed_sell",children:"fixed_sell"}),c.jsx("option",{value:"random",children:"random"})]})]}),c.jsxs("label",{className:`perf-field ${U?"perf-field-overridden":""}`,children:[c.jsxs("span",{children:["Order type ",U&&c.jsx("em",{className:"perf-field-tag",children:"from template"})]}),c.jsxs("select",{value:r.payload.order.ord_type,disabled:U,onChange:C=>Q({ord_type:C.target.value}),children:[c.jsx("option",{value:"limit",children:"limit"}),c.jsx("option",{value:"market",children:"market"})]})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Qty min"}),c.jsx("input",{type:"number",min:1,value:r.payload.order.quantity_min,onChange:C=>Q({quantity_min:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Qty max"}),c.jsx("input",{type:"number",min:1,value:r.payload.order.quantity_max,onChange:C=>Q({quantity_max:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Price min"}),c.jsx("input",{type:"number",min:0,step:.01,value:r.payload.order.price_min,onChange:C=>Q({price_min:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Price max"}),c.jsx("input",{type:"number",min:0,step:.01,value:r.payload.order.price_max,onChange:C=>Q({price_max:Number(C.target.value)})})]})]})]}),w&&c.jsxs("div",{className:"perf-form-section",children:[c.jsx("div",{className:"perf-section-title",children:"Send options"}),c.jsxs("div",{className:"perf-field-row",children:[c.jsxs("label",{className:"checkbox-label",children:[c.jsx("input",{type:"checkbox",checked:r.payload.auto_expiry??!0,onChange:C=>de({auto_expiry:C.target.checked})}),"Auto expiry (tags 126 / 432, only if present)"]}),(r.payload.auto_expiry??!0)&&c.jsxs(c.Fragment,{children:[c.jsx("input",{type:"number",min:0,style:{width:64},value:r.payload.expiry_offset??1,onChange:C=>de({expiry_offset:Number(C.target.value)})}),c.jsxs("select",{value:r.payload.expiry_unit??"days",onChange:C=>de({expiry_unit:C.target.value}),children:[c.jsx("option",{value:"minutes",children:"minutes"}),c.jsx("option",{value:"hours",children:"hours"}),c.jsx("option",{value:"days",children:"days"})]}),c.jsx("span",{children:"into the future"})]})]}),Ne.map((C,I)=>c.jsxs("div",{className:"perf-field-row",children:[c.jsx("input",{style:{width:72},placeholder:"tag",value:C.tag,onChange:J=>ne(I,{tag:J.target.value.replace(/[^0-9]/g,"")})}),c.jsx("span",{children:"="}),c.jsx("input",{style:{flex:1},placeholder:"uuid() / seq(8) / ORD-{seq(8)} / literal",value:C.value,onChange:J=>ne(I,{value:J.target.value})}),c.jsx("button",{className:"btn btn-sm btn-ghost btn-danger-ghost",onClick:()=>ae(Ne.filter((J,he)=>he!==I)),children:"✕"})]},I)),c.jsx("div",{className:"perf-field-row",children:c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>ae([...Ne,{tag:"",value:""}]),children:"+ Add tag rule"})}),c.jsxs("div",{className:"perf-hint",children:["Rules set the tag on every order/scenario message (insert or replace). Generators: ",c.jsx("code",{children:"uuid()"})," · ",c.jsx("code",{children:"random_str(n)"})," · ",c.jsx("code",{children:"random_int(a,b)"})," ·"," ",c.jsx("code",{children:"seq(width[,start])"})," · ",c.jsx("code",{children:"timestamp()"})," · ",c.jsx("code",{children:"date()"})," — mix with text as ",c.jsx("code",{children:"ORD-{seq(8)}"}),". ClOrdID(11) always stays unique per order."]})]}),S&&c.jsxs("div",{className:`perf-form-section ${T?"perf-section-layered":""}`,children:[c.jsxs("div",{className:"perf-section-title",children:["Fill payload",T&&c.jsx("span",{className:"perf-layer-badge",children:"behavior for template"})]}),T&&c.jsxs("div",{className:"perf-hint perf-layer-note",children:["Exec template ",c.jsx("strong",{children:z})," reshapes each report (custom tags). These still control fill ",c.jsx("strong",{children:"behavior"})," — how many reports, the fill ratio, and the ack — while the engine sets the standard exec tags."]}),c.jsxs("div",{className:"perf-field-grid",children:[c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Fill type"}),c.jsxs("select",{value:r.payload.fill.fill_type,onChange:C=>ye({fill_type:C.target.value}),children:[c.jsx("option",{value:"full",children:"full"}),c.jsx("option",{value:"partial",children:"partial"}),c.jsx("option",{value:"random",children:"random"})]})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Fills per order"}),c.jsx("input",{type:"number",min:1,value:r.payload.fill.fills_per_order,onChange:C=>ye({fills_per_order:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field perf-field-check",children:[c.jsx("input",{type:"checkbox",checked:r.payload.fill.send_ack,onChange:C=>ye({send_ack:C.target.checked})}),c.jsx("span",{children:"Send ack before fills (35=8|150=0|39=0)"})]}),r.payload.fill.fill_type!=="full"&&c.jsxs(c.Fragment,{children:[c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Partial % min"}),c.jsx("input",{type:"number",min:1,max:100,value:r.payload.fill.partial_fill_pct_min,onChange:C=>ye({partial_fill_pct_min:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Partial % max"}),c.jsx("input",{type:"number",min:1,max:100,value:r.payload.fill.partial_fill_pct_max,onChange:C=>ye({partial_fill_pct_max:Number(C.target.value)})})]})]})]})]}),(w||S)&&c.jsxs("div",{className:"perf-form-section",children:[c.jsx("div",{className:"perf-section-title",children:"Templates (optional)"}),c.jsxs("div",{className:"perf-field-grid",children:[w&&c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Order template"}),c.jsxs("select",{value:r.payload.order_template_id??"",onChange:C=>de({order_template_id:C.target.value||null}),children:[c.jsx("option",{value:"",children:"— built-in —"}),H.map(C=>c.jsx("option",{value:C.id,children:C.name},C.id))]})]}),S&&c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Exec report template"}),c.jsxs("select",{value:r.payload.exec_template_id??"",onChange:C=>de({exec_template_id:C.target.value||null}),children:[c.jsx("option",{value:"",children:"— built-in —"}),L.map(C=>c.jsx("option",{value:C.id,children:C.name},C.id))]})]})]}),c.jsx("div",{className:"perf-hint",children:"Optional FIX templates from your Templates library shape the order / execution report (custom & venue-specific tags). Standard tags (correlation, OrderQty, CumQty, ExecType…) are always set by the engine. Leave as built-in for the default messages."})]}),c.jsxs("div",{className:"perf-form-section",children:[c.jsxs("button",{className:"btn btn-sm btn-ghost",onClick:()=>s(C=>!C),children:[o?"▾":"▸"," Advanced"]}),o&&c.jsxs("div",{className:"perf-field-grid",style:{marginTop:8},children:[c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Correlation tag"}),c.jsx("input",{type:"number",min:1,value:r.correlation_tag,onChange:C=>W({correlation_tag:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Exec ID tag"}),c.jsx("input",{type:"number",min:1,value:r.exec_id_tag,onChange:C=>W({exec_id_tag:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Max pending"}),c.jsx("input",{type:"number",min:1,value:r.test.max_pending,onChange:C=>se({max_pending:Number(C.target.value)})})]}),c.jsxs("label",{className:"perf-field perf-field-check",children:[c.jsx("input",{type:"checkbox",checked:r.rate.allow_burst,onChange:C=>V({allow_burst:C.target.checked})}),c.jsx("span",{children:"Allow burst"})]}),c.jsxs("label",{className:"perf-field",children:[c.jsx("span",{children:"Max burst multiplier"}),c.jsx("input",{type:"number",min:1,step:.5,value:r.rate.max_burst_multiplier,onChange:C=>V({max_burst_multiplier:Number(C.target.value)})})]})]})]}),c.jsxs("div",{className:"perf-form-actions",children:[k?c.jsx("span",{className:"perf-not-ready",children:k}):A&&!(O&&N)?c.jsxs("span",{className:"perf-not-ready",children:["Selected session",w&&S?"s":""," must be LOGGED_ON before starting."]}):null,c.jsx("button",{className:"btn btn-primary",onClick:F,disabled:!M,title:A?k||(O&&N?void 0:"Session not logged on yet"):"Select the required session(s)",children:f?"Starting…":"▶ Start run"})]})]})}function Gp({status:e}){const t=e==="LOGGED_ON";return c.jsxs("span",{className:`perf-status-chip ${t?"ok":"warn"}`,children:[t?"● ":"○ ",e]})}const Zp=600,iW=5e3,oW=new Set(["completed","stopped","error"]);function sW(e){const[t,r]=h.useState(null),[n,a]=h.useState([]),[i,o]=h.useState(!1);return h.useEffect(()=>{if(r(null),a([]),o(!1),!e)return;let s=!1,l=null,u,f=0,d=!1;async function m(){if(s||d||!e)return;let v;try{({ticket:v}=await G.perf.runs.ticket(e))}catch{p();return}if(s)return;const g=`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/api/perf/runs/${e}/live?ticket=${v}`;l=new WebSocket(g),l.onopen=()=>{s||(f=0,o(!0))},l.onmessage=b=>{let x;try{x=JSON.parse(b.data)}catch{return}r(x),a(w=>{var j,O,N,A,k,M,P,H;const S={t:x.elapsed_s,ops:((j=x.client)==null?void 0:j.ops_live)??0,fps:((O=x.venue)==null?void 0:O.fps_live)??0,tps:(((N=x.client)==null?void 0:N.ops_live)??0)+(((A=x.venue)==null?void 0:A.fps_live)??0),p50:(((k=x.response_latency)==null?void 0:k.p50_us)??0)/1e3,p99:(((M=x.response_latency)==null?void 0:M.p99_us)??0)/1e3,fc50:(((P=x.fill_completion_latency)==null?void 0:P.p50_us)??0)/1e3,fc99:(((H=x.fill_completion_latency)==null?void 0:H.p99_us)??0)/1e3},_=[...w,S];return _.length>Zp?_.slice(-Zp):_}),oW.has(x.status)&&(d=!0,l==null||l.close())},l.onclose=()=>{s||(o(!1),d||p())},l.onerror=()=>{l==null||l.close()}}function p(){if(s||d)return;f+=1;const v=Math.min(iW,500*2**Math.min(f,4));u=setTimeout(m,v)}return m(),()=>{s=!0,clearTimeout(u),l==null||l.close()}},[e]),{latest:t,series:n,connected:i}}const Dw="img-export-exclude";async function lW(e,t,r){const{toPng:n,toJpeg:a}=await Cv(async()=>{const{toPng:u,toJpeg:f}=await import("./index-CyNOPa0n.js");return{toPng:u,toJpeg:f}},[]),i=getComputedStyle(document.body).backgroundColor||"#111",s=await(r==="png"?n:a)(e,{backgroundColor:i,pixelRatio:2,quality:.95,filter:u=>!(u instanceof HTMLElement&&u.classList.contains(Dw))}),l=document.createElement("a");l.href=s,l.download=t,l.click()}function cW(e,t){const r=new Date().toISOString().slice(0,19).replace(/[:T]/g,"-");return`${e}-${r}.${t}`}const _l=new Set(["completed","stopped","error"]);function wn(e){return e?(e/1e3).toFixed(3):"—"}function Rw(e,t){return t<=0?"—":(e/t).toFixed(2)}function Kp({title:e,sub:t,stats:r}){return c.jsxs("div",{className:"perf-lat-card",children:[c.jsxs("div",{className:"perf-lat-title",children:[e,t&&c.jsx("span",{className:"perf-lat-sub",children:t})]}),c.jsx("div",{className:"perf-lat-grid",children:["p50_us","p95_us","p99_us","max_us"].map(n=>c.jsxs("div",{className:"perf-lat-cell",children:[c.jsx("span",{className:"perf-lat-val",children:wn(r[n])}),c.jsxs("span",{className:"perf-lat-lbl",children:[n.replace("_us","")," ms"]})]},n))})]})}function uW({runId:e}){var w;const{latest:t,series:r,connected:n}=sW(e),[a,i]=h.useState(null),[o,s]=h.useState(!1),l=h.useRef(null);h.useEffect(()=>{let S=!0;i(null);let _;const j=async()=>{try{const O=await G.perf.runs.get(e);if(!S)return;i(O),_l.has(O.status)&&clearInterval(_)}catch{}};return _=setInterval(j,2e3),j(),()=>{S=!1,clearInterval(_)}},[e]);const u=t?_l.has(t.status):a?_l.has(a.status):!1,f=!u;async function d(){try{await G.perf.runs.stop(e)}catch{}}async function m(S){if(!(!l.current||o)){s(!0);try{const _=S==="png"?"png":"jpg";await lW(l.current,cW(`perf-${e.slice(0,8)}`,_),S)}catch(_){console.error("image export failed",_)}finally{s(!1)}}}const p=t==null?void 0:t.client,v=t==null?void 0:t.venue,y=t==null?void 0:t.errors,g=(y==null?void 0:y.saturated)||(t==null?void 0:t.status)==="saturated",b=t==null?void 0:t.rate_config,x=[];return p&&(x.push({label:"orders/s",value:Math.round(p.ops_live).toLocaleString()}),(b==null?void 0:b.orders_per_window)!=null&&(b==null?void 0:b.order_window_ms)!=null&&x.push({label:"order rate (cfg)",value:`${b.orders_per_window.toLocaleString()} / ${b.order_window_ms}ms`}),x.push({label:"orders sent",value:p.orders_sent.toLocaleString()},{label:"pending",value:p.pending.toLocaleString()},{label:"dropped",value:p.dropped.toLocaleString()})),v&&(x.push({label:"fills/s",value:Math.round(v.fps_live).toLocaleString()}),(b==null?void 0:b.fills_per_window)!=null&&(b==null?void 0:b.fill_window_ms)!=null&&x.push({label:"fill rate (cfg)",value:`${b.fills_per_window.toLocaleString()} / ${b.fill_window_ms}ms`}),x.push({label:"fills",value:v.fills_sent.toLocaleString()},{label:"fills/order",value:Rw(v.fills_sent,v.orders_received-v.unfilled)},{label:"fill ratio",value:v.fill_ratio.toFixed(3)},{label:"recv",value:v.orders_received.toLocaleString()},{label:"acks",value:v.acks_sent.toLocaleString()},{label:"unfilled",value:v.unfilled.toLocaleString()})),y&&x.push({label:"lost",value:y.lost_timeout.toLocaleString()},{label:"rejected",value:y.rejected.toLocaleString()}),c.jsxs("div",{className:"perf-dashboard",ref:l,children:[c.jsxs("div",{className:"perf-dash-header",children:[c.jsxs("div",{className:"perf-dash-status",children:[c.jsx("span",{className:`badge perf-state-${(t==null?void 0:t.status)??(a==null?void 0:a.status)??"pending"}`,children:(t==null?void 0:t.status)??(a==null?void 0:a.status)??"pending"}),t&&c.jsxs("span",{className:"perf-dash-elapsed",children:[Math.round(t.elapsed_s),"s",t.duration_s?` / ${t.duration_s}s`:""," · ",t.mode]}),c.jsx("span",{className:`perf-conn ${n?"on":"off"}`,children:n?"● live":"○ offline"})]}),c.jsxs("div",{className:`perf-dash-actions ${Dw}`,children:[f&&c.jsx("button",{className:"btn btn-sm btn-danger",onClick:d,children:"■ Stop"}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>G.perf.runs.export(e,"messages"),children:"Export CSV"}),u&&c.jsx("button",{className:"btn btn-sm btn-ghost",title:"Per-exec detail (requires record_execs)",onClick:()=>G.perf.runs.export(e,"execs"),children:"Execs CSV"}),c.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>G.perf.runs.export(e,"both"),children:"Export ZIP"}),c.jsx("button",{className:"btn btn-sm btn-ghost",disabled:o,onClick:()=>m("png"),children:o?"…":"PNG"}),c.jsx("button",{className:"btn btn-sm btn-ghost",disabled:o,onClick:()=>m("jpeg"),children:o?"…":"JPG"})]})]}),g&&c.jsxs("div",{className:"perf-saturated-banner",children:["⚠ SATURATED — injection paused to protect latency measurement.",p&&c.jsxs(c.Fragment,{children:[" Pending ",p.pending.toLocaleString(),", dropped ",p.dropped.toLocaleString(),"."]})]}),(w=a==null?void 0:a.warnings)==null?void 0:w.map((S,_)=>c.jsxs("div",{className:"perf-warning-banner",children:["⚠ ",S]},_)),!t&&!u&&c.jsx("div",{className:"loading-msg",children:"Waiting for first snapshot…"}),x.length>0&&c.jsx("div",{className:"stat-cards",children:x.map(({label:S,value:_})=>c.jsxs("div",{className:"stat-card",children:[c.jsx("span",{className:"stat-card-value",children:_}),c.jsx("span",{className:"stat-card-label",children:S})]},S))}),t&&c.jsxs("div",{className:"perf-lat-row",children:[c.jsx(Kp,{title:"Response latency",sub:"order-2-ack",stats:t.response_latency}),c.jsx(Kp,{title:"Fill-completion latency",sub:"order-2-last-fill",stats:t.fill_completion_latency})]}),r.length>1&&c.jsxs(c.Fragment,{children:[c.jsx("div",{className:"analysis-section-title",children:"Throughput"}),c.jsx("div",{className:"analysis-chart-wrap",children:c.jsx(Pn,{width:"100%",height:200,children:c.jsxs(Ii,{data:r,margin:{top:4,right:16,bottom:4,left:8},children:[c.jsx(Gr,{strokeDasharray:"3 3",stroke:"var(--border)"}),c.jsx(Zr,{dataKey:"t",tick:{fontSize:11,fill:"var(--text2)"},tickFormatter:S=>`${Math.round(S)}s`}),c.jsx(Kr,{tick:{fontSize:11,fill:"var(--text2)"},allowDecimals:!1}),c.jsx(En,{contentStyle:{background:"var(--bg3)",border:"1px solid var(--border)",fontSize:12},labelStyle:{color:"var(--text)"},labelFormatter:S=>`${Math.round(Number(S))}s`}),c.jsx(pa,{wrapperStyle:{fontSize:11}}),c.jsx(_t,{type:"monotone",dataKey:"ops",name:"orders/s",stroke:"#00b4c8",dot:!1,isAnimationActive:!1}),c.jsx(_t,{type:"monotone",dataKey:"fps",name:"fills/s",stroke:"#a78bfa",dot:!1,isAnimationActive:!1}),c.jsx(_t,{type:"monotone",dataKey:"tps",name:"total msgs/s",stroke:"#fbbf24",dot:!1,isAnimationActive:!1})]})})}),c.jsx("div",{className:"analysis-section-title",children:"Response latency (ms)"}),c.jsx("div",{className:"analysis-chart-wrap",children:c.jsx(Pn,{width:"100%",height:200,children:c.jsxs(Ii,{data:r,margin:{top:4,right:16,bottom:4,left:8},children:[c.jsx(Gr,{strokeDasharray:"3 3",stroke:"var(--border)"}),c.jsx(Zr,{dataKey:"t",tick:{fontSize:11,fill:"var(--text2)"},tickFormatter:S=>`${Math.round(S)}s`}),c.jsx(Kr,{tick:{fontSize:11,fill:"var(--text2)"}}),c.jsx(En,{contentStyle:{background:"var(--bg3)",border:"1px solid var(--border)",fontSize:12},labelStyle:{color:"var(--text)"},labelFormatter:S=>`${Math.round(Number(S))}s`}),c.jsx(pa,{wrapperStyle:{fontSize:11}}),c.jsx(_t,{type:"monotone",dataKey:"p50",name:"p50",stroke:"#4ade80",dot:!1,isAnimationActive:!1}),c.jsx(_t,{type:"monotone",dataKey:"p99",name:"p99",stroke:"#f87171",dot:!1,isAnimationActive:!1})]})})}),c.jsx("div",{className:"analysis-section-title",children:"Fill-completion latency (ms)"}),c.jsx("div",{className:"analysis-chart-wrap",children:c.jsx(Pn,{width:"100%",height:200,children:c.jsxs(Ii,{data:r,margin:{top:4,right:16,bottom:4,left:8},children:[c.jsx(Gr,{strokeDasharray:"3 3",stroke:"var(--border)"}),c.jsx(Zr,{dataKey:"t",tick:{fontSize:11,fill:"var(--text2)"},tickFormatter:S=>`${Math.round(S)}s`}),c.jsx(Kr,{tick:{fontSize:11,fill:"var(--text2)"}}),c.jsx(En,{contentStyle:{background:"var(--bg3)",border:"1px solid var(--border)",fontSize:12},labelStyle:{color:"var(--text)"},labelFormatter:S=>`${Math.round(Number(S))}s`}),c.jsx(pa,{wrapperStyle:{fontSize:11}}),c.jsx(_t,{type:"monotone",dataKey:"fc50",name:"p50",stroke:"#4ade80",dot:!1,isAnimationActive:!1}),c.jsx(_t,{type:"monotone",dataKey:"fc99",name:"p99",stroke:"#f87171",dot:!1,isAnimationActive:!1})]})})})]}),u&&(a==null?void 0:a.summary)&&c.jsx(dW,{status:a})]})}function dW({status:e}){const t=e.summary,r=[["Orders sent",t.orders_sent.toLocaleString()],["Acks sent",t.acks_sent.toLocaleString()],["Fills sent",t.fills_sent.toLocaleString()],["Responses",t.responses.toLocaleString()],["Completions (fully filled)",t.completions.toLocaleString()],["Fills per order (completed)",Rw(t.fills_sent,t.completions)],["Fill ratio (completed)",t.fill_ratio.toFixed(3)],["Lost / dropped / rejected",`${t.lost_timeout} / ${t.dropped} / ${t.rejected}`],["Response p50 / p99 (ms)",`${wn(t.response_latency.p50_us)} / ${wn(t.response_latency.p99_us)}`],["Response max (ms)",wn(t.response_latency.max_us)],["Fill-completion p50 / p99 (ms)",`${wn(t.fill_completion_latency.p50_us)} / ${wn(t.fill_completion_latency.p99_us)}`]];return c.jsxs("div",{className:"perf-summary-card",children:[c.jsxs("div",{className:"perf-section-title",children:["Run summary — ",e.status]}),c.jsx("table",{className:"perf-summary-table",children:c.jsx("tbody",{children:r.map(([n,a])=>c.jsxs("tr",{children:[c.jsx("td",{children:n}),c.jsx("td",{className:"mono",children:a})]},n))})})]})}const fW=new Set(["completed","stopped","error"]);function hW(e){if(!e)return"—";try{return new Date(e*1e3).toLocaleTimeString()}catch{return"—"}}function mW({activeRunId:e,onSelect:t,refreshKey:r}){const[n,a]=h.useState([]);h.useEffect(()=>{let o=!0;const s=()=>G.perf.runs.list().then(u=>{o&&a(u)}).catch(()=>{});s();const l=setInterval(s,2e3);return()=>{o=!1,clearInterval(l)}},[r]);async function i(o,s){o.stopPropagation();try{await G.perf.runs.stop(s)}catch{}}return n.length===0?c.jsx("div",{className:"perf-runlist-empty",children:"No runs yet. Configure and start one."}):c.jsxs("table",{className:"perf-runlist",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{children:"Name"}),c.jsx("th",{children:"Mode"}),c.jsx("th",{children:"Status"}),c.jsx("th",{children:"Started"}),c.jsx("th",{children:"Elapsed"}),c.jsx("th",{children:"Orders"}),c.jsx("th",{children:"Fill"}),c.jsx("th",{})]})}),c.jsx("tbody",{children:n.map(o=>c.jsxs("tr",{className:o.run_id===e?"perf-run-active":"",onClick:()=>t(o.run_id),children:[c.jsx("td",{children:o.name}),c.jsx("td",{children:o.mode}),c.jsx("td",{children:c.jsx("span",{className:`badge perf-state-${o.status}`,children:o.status})}),c.jsx("td",{children:hW(o.started_at)}),c.jsxs("td",{className:"mono",children:[Math.round(o.elapsed_s),"s"]}),c.jsx("td",{className:"mono",children:o.summary?o.summary.orders_sent.toLocaleString():"—"}),c.jsx("td",{className:"mono",children:o.summary?o.summary.fill_ratio.toFixed(2):"—"}),c.jsx("td",{className:"perf-runlist-actions",children:!fW.has(o.status)&&c.jsx("button",{className:"btn btn-sm btn-danger",onClick:s=>i(s,o.run_id),children:"Stop"})})]},o.run_id))})]})}const Ol=200,_n=(e,t,r=120)=>({field:e,headerName:t,width:r,type:"numericColumn",valueFormatter:n=>n.value==null?"":Number(n.value).toLocaleString()}),Pc=(e,t)=>({field:e,headerName:t,width:150,type:"numericColumn",valueFormatter:r=>r.value==null?"":`${Math.round(Number(r.value)).toLocaleString()}`}),pW=[{field:"clordid",headerName:"ClOrdID",width:150},{field:"corr_id",headerName:"Corr ID",width:150},{field:"msg_type",headerName:"Type",width:80},{field:"symbol",headerName:"Symbol",width:90},{field:"side",headerName:"Side",width:70},_n("qty","Qty",90),_n("price","Price",100),Pc("response_latency_us","Resp µs"),Pc("fill_latency_us","Fill µs"),_n("fill_qty","Fill qty",100),_n("fill_price","Fill price",110),{field:"status",headerName:"Status",width:110,cellStyle:e=>e.value==="lost"||e.value==="dropped"?{color:"#facc15",fontWeight:"bold"}:void 0}],vW=[{field:"scenario_name",headerName:"Scenario",flex:1,minWidth:160},Pc("latency_us","Latency µs"),_n("msg_count","Msgs",90),_n("fill_ratio","Fill ratio",110),{field:"status",headerName:"Status",width:110,cellStyle:e=>e.value==="lost"||e.value==="dropped"?{color:"#facc15",fontWeight:"bold"}:void 0}];function gW({runId:e}){const t=X(g=>g.theme),[r,n]=h.useState("messages"),[a,i]=h.useState(0),[o,s]=h.useState([]),[l,u]=h.useState(0),[f,d]=h.useState(!1);h.useEffect(()=>{i(0)},[r,e]),h.useEffect(()=>{let g=!0;return d(!0),(r==="messages"?G.perf.runs.messages:G.perf.runs.scenarios)(e,{limit:Ol,offset:a*Ol}).then(x=>{g&&(s(x.items),u(x.total))}).catch(()=>{g&&(s([]),u(0))}).finally(()=>{g&&d(!1)}),()=>{g=!1}},[e,r,a]);const m=h.useMemo(()=>r==="messages"?pW:vW,[r]),p=h.useMemo(()=>({sortable:!0,filter:!0,resizable:!0}),[]),v=Math.max(0,Math.ceil(l/Ol)-1),y=t==="dark"?"ag-theme-quartz-dark":"ag-theme-quartz";return c.jsxs("div",{className:"perf-results",children:[c.jsxs("div",{className:"perf-results-toolbar",children:[c.jsxs("div",{className:"perf-results-tabs",children:[c.jsx("button",{className:`btn btn-sm ${r==="messages"?"btn-primary":"btn-ghost"}`,onClick:()=>n("messages"),children:"Messages"}),c.jsx("button",{className:`btn btn-sm ${r==="scenarios"?"btn-primary":"btn-ghost"}`,onClick:()=>n("scenarios"),children:"Scenarios"})]}),c.jsxs("div",{className:"perf-results-pager",children:[c.jsxs("span",{children:[l.toLocaleString()," rows"]}),c.jsx("button",{className:"btn btn-sm btn-ghost",disabled:a<=0||f,onClick:()=>i(g=>Math.max(0,g-1)),children:"‹ Prev"}),c.jsxs("span",{children:["Page ",a+1," / ",v+1]}),c.jsx("button",{className:"btn btn-sm btn-ghost",disabled:a>=v||f,onClick:()=>i(g=>Math.min(v,g+1)),children:"Next ›"})]})]}),c.jsx("div",{className:`${y} perf-results-grid`,children:c.jsx(Nc,{rowData:o,columnDefs:m,defaultColDef:p,animateRows:!1,suppressCellFocus:!0})})]})}function yW(){const{theme:e,toggleTheme:t,branding:r,setSessions:n}=X(),a=Us(),i=r!=null&&r.prefix?`${r.prefix} FIXture`:"FIXture";Sv(),h.useEffect(()=>{G.sessions.list().then(n).catch(()=>{})},[]);const[o,s]=h.useState("configure"),[l,u]=h.useState(null),[f,d]=h.useState(0);function m(y){u(y),d(g=>g+1),s("live")}function p(y){u(y),s("live")}const v=[{id:"configure",label:"Configure"},{id:"live",label:"Live",disabled:!l},{id:"results",label:"Results",disabled:!l},{id:"history",label:"History"}];return c.jsxs("div",{className:"app perf-page",children:[c.jsxs("header",{className:"app-header",children:[c.jsx("img",{src:a,alt:i,className:"app-logo"}),c.jsx("div",{className:"perf-page-title",children:"Performance Testing"}),c.jsxs("div",{className:"header-right",children:[c.jsx(qr,{to:"/",className:"btn btn-sm btn-ghost",children:"← Sessions"}),c.jsx("button",{className:"btn-icon theme-toggle",onClick:t,title:"Toggle theme",children:e==="dark"?"☀":"🌙"})]})]}),c.jsx("div",{className:"perf-tabbar",children:v.map(y=>c.jsx("button",{className:`perf-tab${o===y.id?" active":""}`,disabled:y.disabled,onClick:()=>s(y.id),children:y.label},y.id))}),c.jsxs("div",{className:"perf-body",children:[o==="configure"&&c.jsx("div",{className:"perf-pane perf-pane-form",children:c.jsx(aW,{onStarted:m})}),o==="live"&&l&&c.jsx("div",{className:"perf-pane",children:c.jsx(uW,{runId:l},l)}),o==="results"&&l&&c.jsx("div",{className:"perf-pane",children:c.jsx(gW,{runId:l},l)}),o==="history"&&c.jsx("div",{className:"perf-pane",children:c.jsx(mW,{activeRunId:l,onSelect:p,refreshKey:f})})]})]})}function bW(){return X(t=>t.token)?c.jsx(uv,{}):c.jsx(Mi,{to:"/login",replace:!0})}function xW(){const e=X(t=>t.user);return e?e.role!=="platform_admin"?c.jsx(Mi,{to:"/",replace:!0}):c.jsx(uv,{}):c.jsx(Mi,{to:"/login",replace:!0})}function wW(){const e=X(t=>t.setBranding);h.useEffect(()=>{fetch("/api/branding").then(t=>t.json()).then(t=>{e(t),Tw(t)}).catch(()=>{})},[])}function jW(){const e=an(),t=X(r=>r.theme);return wW(),h.useEffect(()=>{document.documentElement.setAttribute("data-theme",t)},[t]),h.useEffect(()=>{fetch("/api/setup/status").then(r=>r.json()).then(r=>{r.setup_required&&e("/setup",{replace:!0})}).catch(()=>{})},[]),c.jsxs(Zj,{children:[c.jsx(Lt,{path:"/setup",element:c.jsx(Xz,{})}),c.jsx(Lt,{path:"/login",element:c.jsx(Yz,{})}),c.jsx(Lt,{path:"/register",element:c.jsx(Vz,{})}),c.jsxs(Lt,{element:c.jsx(bW,{}),children:[c.jsx(Lt,{path:"/",element:c.jsx(qz,{})}),c.jsxs(Lt,{element:c.jsx(xW,{}),children:[c.jsx(Lt,{path:"/admin/users",element:c.jsx(rW,{})}),c.jsx(Lt,{path:"/perf",element:c.jsx(yW,{})})]})]}),c.jsx(Lt,{path:"*",element:c.jsx(Mi,{to:"/",replace:!0})})]})}Pl.createRoot(document.getElementById("root")).render(c.jsx(qp.StrictMode,{children:c.jsx(vS,{children:c.jsx(jW,{})})}));
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>FIXture</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DISOjvhu.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/ag-grid-_QKprVdm.js">
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/assets/index-BwQf-cei.css">
|
|
11
11
|
</head>
|
|
@@ -65,7 +65,7 @@ fixture/static/index.html
|
|
|
65
65
|
fixture/static/assets/ag-grid-_QKprVdm.js
|
|
66
66
|
fixture/static/assets/index-BwQf-cei.css
|
|
67
67
|
fixture/static/assets/index-CyNOPa0n.js
|
|
68
|
-
fixture/static/assets/index-
|
|
68
|
+
fixture/static/assets/index-DISOjvhu.js
|
|
69
69
|
fixture/static/assets/react-vendor-2eF0YfZT.js
|
|
70
70
|
fixture/ui/__init__.py
|
|
71
71
|
fixtureqa.egg-info/PKG-INFO
|
|
@@ -415,3 +415,85 @@ async def test_record_execs_off_writes_nothing(tmp_path):
|
|
|
415
415
|
await run._main
|
|
416
416
|
assert run.stats.completions > 0
|
|
417
417
|
assert not (tmp_path / "execoff1.csv.gz").exists()
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# --------------------------------------------------------------------------
|
|
421
|
+
# Burst dispatch
|
|
422
|
+
# --------------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
class TimestampingSM(FakeSM):
|
|
425
|
+
"""FakeSM that records a perf_counter_ns per send, by session."""
|
|
426
|
+
|
|
427
|
+
def __init__(self, client_id: str, venue_id: str):
|
|
428
|
+
super().__init__(client_id, venue_id)
|
|
429
|
+
self.client_send_ns: list[int] = []
|
|
430
|
+
self.venue_sends: list[tuple[int, dict]] = [] # (ns, parsed fields)
|
|
431
|
+
|
|
432
|
+
async def send_message(self, sid: str, msg) -> bool:
|
|
433
|
+
if sid == "c":
|
|
434
|
+
self.client_send_ns.append(time.perf_counter_ns())
|
|
435
|
+
elif sid == "v":
|
|
436
|
+
self.venue_sends.append((time.perf_counter_ns(), _fields_from_message(msg)))
|
|
437
|
+
return await super().send_message(sid, msg)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
async def test_burst_dispatch_clusters_at_window_boundaries():
|
|
441
|
+
sm = TimestampingSM("c", "v")
|
|
442
|
+
cfg = _cfg(
|
|
443
|
+
rate=RateConfig(orders_per_window=5, order_window_ms=400,
|
|
444
|
+
fills_per_window=1000, fill_window_ms=100,
|
|
445
|
+
dispatch="burst"),
|
|
446
|
+
test=RunTestConfig(duration=1),
|
|
447
|
+
)
|
|
448
|
+
run = PerfRun("burst1", cfg, sm, FakeWriter())
|
|
449
|
+
run.start()
|
|
450
|
+
await run._main
|
|
451
|
+
|
|
452
|
+
ts = sm.client_send_ns
|
|
453
|
+
# windows at ~0 / 400 / 800 ms → 3 bursts of 5
|
|
454
|
+
assert len(ts) == 15, f"expected 3 bursts of 5, got {len(ts)} sends"
|
|
455
|
+
gaps_ms = [(b - a) / 1e6 for a, b in zip(ts, ts[1:])]
|
|
456
|
+
big = [g for g in gaps_ms if g > 200] # inter-burst pauses
|
|
457
|
+
small = [g for g in gaps_ms if g < 50] # back-to-back within a burst
|
|
458
|
+
assert len(big) == 2, f"expected 2 window pauses, gaps: {[round(g) for g in gaps_ms]}"
|
|
459
|
+
assert len(small) == 12, f"expected 12 in-burst gaps, gaps: {[round(g) for g in gaps_ms]}"
|
|
460
|
+
assert run.stats.completions == 15 # loopback drain still fills everything
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
async def test_burst_dispatch_respects_max_orders():
|
|
464
|
+
cfg = _cfg(
|
|
465
|
+
rate=RateConfig(orders_per_window=10, order_window_ms=100,
|
|
466
|
+
fills_per_window=1000, fill_window_ms=100,
|
|
467
|
+
dispatch="burst"),
|
|
468
|
+
test=RunTestConfig(duration=0, max_orders=23),
|
|
469
|
+
)
|
|
470
|
+
run = _run(cfg)
|
|
471
|
+
run.start()
|
|
472
|
+
await asyncio.wait_for(run._main, timeout=10)
|
|
473
|
+
assert run.status == "completed"
|
|
474
|
+
assert run.stats.orders_sent == 23 # stops mid-burst, exact
|
|
475
|
+
assert run.stats.completions == 23
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
async def test_fill_burst_dispatch_clusters():
|
|
479
|
+
# Orders outpace the fill rate so a venue backlog forms; with
|
|
480
|
+
# fill_dispatch=burst the gate then releases fills in window clumps.
|
|
481
|
+
sm = TimestampingSM("c", "v")
|
|
482
|
+
cfg = _cfg(
|
|
483
|
+
rate=RateConfig(orders_per_window=20, order_window_ms=100, # 200/s in
|
|
484
|
+
fills_per_window=30, fill_window_ms=300, # 100/s out
|
|
485
|
+
fill_dispatch="burst"),
|
|
486
|
+
test=RunTestConfig(duration=1),
|
|
487
|
+
)
|
|
488
|
+
run = PerfRun("fburst1", cfg, sm, FakeWriter())
|
|
489
|
+
run.start()
|
|
490
|
+
await asyncio.wait_for(run._main, timeout=20)
|
|
491
|
+
|
|
492
|
+
fill_ts = [ns for ns, f in sm.venue_sends if f.get(150) in ("1", "2")]
|
|
493
|
+
assert len(fill_ts) > 60
|
|
494
|
+
gaps_ms = [(b - a) / 1e6 for a, b in zip(fill_ts, fill_ts[1:])]
|
|
495
|
+
big = [g for g in gaps_ms if g > 150] # inter-window pauses
|
|
496
|
+
small = [g for g in gaps_ms if g < 50]
|
|
497
|
+
assert len(big) >= 2, f"expected window pauses, biggest gaps: {sorted(gaps_ms)[-5:]}"
|
|
498
|
+
assert len(small) >= 0.7 * len(gaps_ms) # fills overwhelmingly back-to-back
|
|
499
|
+
assert run.stats.completions == run.stats.orders_sent # drain still fills all
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|