fixtureqa 0.4.8__tar.gz → 0.4.10__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.8/fixtureqa.egg-info → fixtureqa-0.4.10}/PKG-INFO +1 -1
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/perf_engine.py +39 -3
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/perf_models.py +2 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/perf_stats.py +4 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/perf_store.py +19 -2
- fixtureqa-0.4.8/fixture/static/assets/index-DrmyYeG0.js → fixtureqa-0.4.10/fixture/static/assets/index-BFPDQdQb.js +1 -1
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/static/index.html +1 -1
- {fixtureqa-0.4.8 → fixtureqa-0.4.10/fixtureqa.egg-info}/PKG-INFO +1 -1
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixtureqa.egg-info/SOURCES.txt +1 -1
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/pyproject.toml +1 -1
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_perf_api.py +29 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_perf_engine.py +75 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/LICENSE +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/README.md +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/__init__.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/__main__.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/__init__.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/app.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/connection_manager.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/deps.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/__init__.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/admin.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/auth.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/branding.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/custom_tags.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/fix_spec.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/messages.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/perf.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/scenarios.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/sessions.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/setup.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/spec_overlay.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/templates.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/ws.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/schemas.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/config/__init__.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/__init__.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/atomic_io.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/auth.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/config_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/custom_tag_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/db_migrations.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/events.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/exec_csv_writer.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/fix_application.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/fix_builder.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/fix_parser.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/fix_spec_parser.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/fix_tags.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/fix_time.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/housekeeping.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/inbound.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/json_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/message_log.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/message_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/models.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/perf_payload.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/perf_writer.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/scenario_runner.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/scenario_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/session.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/session_manager.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/spec_overlay_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/template_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/user_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/venue_responses.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/fix_specs/FIX42.xml +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/fix_specs/FIX44.xml +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/server.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/static/assets/index-BwQf-cei.css +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/static/assets/index-CyNOPa0n.js +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/static/favicon.svg +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/ui/__init__.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixtureqa.egg-info/dependency_links.txt +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixtureqa.egg-info/entry_points.txt +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixtureqa.egg-info/requires.txt +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixtureqa.egg-info/top_level.txt +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/setup.cfg +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_atomic_io.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_auth.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_config_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_connection_manager.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_db_migrations.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_fix_builder.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_health.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_inbound.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_inbound_validation.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_message_store.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_perf_models.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_perf_payload.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_perf_rehydrate.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_scenarios.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_session_lifecycle.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_session_manager_concurrency.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_sessions.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_templates.py +0 -0
- {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_ws.py +0 -0
|
@@ -206,7 +206,7 @@ class PerfRun:
|
|
|
206
206
|
# client mode, count-bounded: external venue may never fill — bounded settle.
|
|
207
207
|
deadline = time.perf_counter() + max(_GRACE_DRAIN_S,
|
|
208
208
|
self.config.test.scenario_timeout_ms / 1000.0)
|
|
209
|
-
while self._pending and time.perf_counter() < deadline:
|
|
209
|
+
while self._pending and time.perf_counter() < deadline and not self._stopped_by_user:
|
|
210
210
|
await asyncio.sleep(0.02)
|
|
211
211
|
else:
|
|
212
212
|
await asyncio.sleep(_GRACE_DRAIN_S)
|
|
@@ -252,7 +252,9 @@ class PerfRun:
|
|
|
252
252
|
last_activity = time.perf_counter()
|
|
253
253
|
while True:
|
|
254
254
|
now = time.perf_counter()
|
|
255
|
-
|
|
255
|
+
# Stop pressed mid-drain must win immediately — the backlog gets
|
|
256
|
+
# finalized as lost instead of ground through at the fill rate.
|
|
257
|
+
if self._stopped_by_user or not self._pending or now >= cap:
|
|
256
258
|
break
|
|
257
259
|
cur_sent = self.stats.fills_sent + self.stats.acks_sent
|
|
258
260
|
if cur_sent != last_sent:
|
|
@@ -437,6 +439,15 @@ class PerfRun:
|
|
|
437
439
|
cfg = self.config
|
|
438
440
|
self.stats.amends_received += 1
|
|
439
441
|
if of.get(cfg.correlation_tag) is None:
|
|
442
|
+
# Can't reply — there is nothing to echo for the client to correlate.
|
|
443
|
+
# Classic cause: the OMS rebuilds the 35=G downstream and drops the
|
|
444
|
+
# custom tag it passes through fine on NewOrderSingle.
|
|
445
|
+
self.stats.amends_uncorrelated += 1
|
|
446
|
+
if self.stats.amends_uncorrelated == 1:
|
|
447
|
+
self.warnings.append(
|
|
448
|
+
f"venue received 35=G without correlation tag "
|
|
449
|
+
f"{cfg.correlation_tag} — no replace ack sent; check the "
|
|
450
|
+
f"counterparty passes the tag through on amends")
|
|
440
451
|
return
|
|
441
452
|
order_qty = _to_int(of.get(T_ORDERQTY), 0)
|
|
442
453
|
px = of.get(T_PRICE) or "0"
|
|
@@ -538,10 +549,33 @@ class PerfRun:
|
|
|
538
549
|
sub = self._client_sub
|
|
539
550
|
while True:
|
|
540
551
|
inb = await sub.get()
|
|
552
|
+
if inb.msg_type == "9":
|
|
553
|
+
self._on_cancel_reject(inb.fields)
|
|
554
|
+
continue
|
|
541
555
|
if inb.msg_type != "8":
|
|
542
556
|
continue
|
|
543
557
|
self._on_exec_report(inb.fields, inb.recv_perf_ns or time.perf_counter_ns())
|
|
544
558
|
|
|
559
|
+
def _on_cancel_reject(self, f: dict) -> None:
|
|
560
|
+
"""OrderCancelReject (35=9): the counterparty refused the amend. Count
|
|
561
|
+
it (these would otherwise be invisible — only 35=8 is correlated) and
|
|
562
|
+
release the in-flight amend so the order finalizes normally instead of
|
|
563
|
+
being reaped as amends_lost."""
|
|
564
|
+
self.stats.amend_rejects += 1
|
|
565
|
+
if self.stats.amend_rejects == 1:
|
|
566
|
+
self.warnings.append(
|
|
567
|
+
"counterparty rejected an amend (35=9 OrderCancelReject) — "
|
|
568
|
+
"it may not match OrigClOrdID(41) against its own order chain")
|
|
569
|
+
corr = f.get(self.config.correlation_tag)
|
|
570
|
+
entry = self._pending.get(corr) if corr is not None else None
|
|
571
|
+
if entry is None or not entry.get("amend_inflight"):
|
|
572
|
+
return
|
|
573
|
+
entry["amend_inflight"] = False
|
|
574
|
+
entry["pending_clordid"] = None # chain tip stays at the last accepted
|
|
575
|
+
if entry.get("completed"):
|
|
576
|
+
self._pending.pop(corr, None)
|
|
577
|
+
self._finalize(entry, "filled")
|
|
578
|
+
|
|
545
579
|
def _on_exec_report(self, f: dict, now_ns: int) -> None:
|
|
546
580
|
cfg = self.config
|
|
547
581
|
corr = f.get(cfg.correlation_tag)
|
|
@@ -720,6 +754,7 @@ class PerfRun:
|
|
|
720
754
|
amends_sent=self.stats.amends_sent,
|
|
721
755
|
replaces_received=self.stats.replaces_received,
|
|
722
756
|
amends_lost=self.stats.amends_lost,
|
|
757
|
+
amend_rejects=self.stats.amend_rejects,
|
|
723
758
|
aps_live=round(aps, 1),
|
|
724
759
|
scenarios_dispatched=self.stats.scenarios_dispatched,
|
|
725
760
|
scenarios_per_sec=round(sps, 1),
|
|
@@ -733,7 +768,8 @@ class PerfRun:
|
|
|
733
768
|
fills_sent=self.stats.fills_sent, fps_live=round(fps, 1),
|
|
734
769
|
fill_ratio=round(self.stats.fill_ratio(), 3),
|
|
735
770
|
unfilled=max(0, self.stats.orders_sent - self.stats.completions),
|
|
736
|
-
amends_received=self.stats.amends_received
|
|
771
|
+
amends_received=self.stats.amends_received,
|
|
772
|
+
amends_uncorrelated=self.stats.amends_uncorrelated)
|
|
737
773
|
snap = LiveSnapshot(
|
|
738
774
|
run_id=self.run_id, status=self.status, mode=self.config.mode,
|
|
739
775
|
elapsed_s=round(self._elapsed_s(), 1), duration_s=self.config.test.duration,
|
|
@@ -199,6 +199,7 @@ class ClientLeg(BaseModel):
|
|
|
199
199
|
amends_sent: int = 0
|
|
200
200
|
replaces_received: int = 0
|
|
201
201
|
amends_lost: int = 0
|
|
202
|
+
amend_rejects: int = 0
|
|
202
203
|
aps_live: float = 0.0
|
|
203
204
|
scenarios_dispatched: int = 0
|
|
204
205
|
scenarios_per_sec: float = 0.0
|
|
@@ -215,6 +216,7 @@ class VenueLeg(BaseModel):
|
|
|
215
216
|
unfilled: int = 0
|
|
216
217
|
venue_proc_us_p50: float = 0.0
|
|
217
218
|
amends_received: int = 0
|
|
219
|
+
amends_uncorrelated: int = 0
|
|
218
220
|
|
|
219
221
|
|
|
220
222
|
class SnapshotErrors(BaseModel):
|
|
@@ -122,7 +122,9 @@ class PerfStats:
|
|
|
122
122
|
self.amends_sent = 0 # client sent a 35=G
|
|
123
123
|
self.replaces_received = 0 # client got a Replaced ack (150=5)
|
|
124
124
|
self.amends_lost = 0 # amend in flight when its order was reaped
|
|
125
|
+
self.amend_rejects = 0 # client got an OrderCancelReject (35=9)
|
|
125
126
|
self.amends_received = 0 # venue saw an inbound 35=G
|
|
127
|
+
self.amends_uncorrelated = 0 # inbound 35=G lacked the correlation tag (no reply)
|
|
126
128
|
self.lost_timeout = 0
|
|
127
129
|
self.dropped = 0
|
|
128
130
|
self.rejected = 0
|
|
@@ -179,7 +181,9 @@ class PerfStats:
|
|
|
179
181
|
"amends_sent": self.amends_sent,
|
|
180
182
|
"replaces_received": self.replaces_received,
|
|
181
183
|
"amends_lost": self.amends_lost,
|
|
184
|
+
"amend_rejects": self.amend_rejects,
|
|
182
185
|
"amends_received": self.amends_received,
|
|
186
|
+
"amends_uncorrelated": self.amends_uncorrelated,
|
|
183
187
|
"lost_timeout": self.lost_timeout,
|
|
184
188
|
"dropped": self.dropped,
|
|
185
189
|
"rejected": self.rejected,
|
|
@@ -6,6 +6,7 @@ parameter is unused.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
|
+
import uuid
|
|
9
10
|
from typing import Optional
|
|
10
11
|
|
|
11
12
|
from .json_store import JsonListStore
|
|
@@ -28,8 +29,24 @@ class PerfStore(JsonListStore):
|
|
|
28
29
|
return item["config"] if item else None
|
|
29
30
|
|
|
30
31
|
def save(self, config: dict) -> str:
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
"""Upsert by name: saving a config whose name already exists overwrites
|
|
33
|
+
it in place (same config_id), so the dropdown never accumulates
|
|
34
|
+
duplicates. Pre-existing duplicates of that name are collapsed into the
|
|
35
|
+
first one. One locked load-modify-save so the find+replace is atomic."""
|
|
36
|
+
name = config.get("name", "")
|
|
37
|
+
with self._lock:
|
|
38
|
+
items = self._load("")
|
|
39
|
+
existing = [it for it in items if it.get("name") == name]
|
|
40
|
+
if existing:
|
|
41
|
+
keep = existing[0]
|
|
42
|
+
keep["config"] = config
|
|
43
|
+
items = [it for it in items if it.get("name") != name or it is keep]
|
|
44
|
+
self._save("", items)
|
|
45
|
+
return keep["config_id"]
|
|
46
|
+
item = {"name": name, "config": config, "config_id": str(uuid.uuid4())}
|
|
47
|
+
items.append(item)
|
|
48
|
+
self._save("", items)
|
|
49
|
+
return item["config_id"]
|
|
33
50
|
|
|
34
51
|
def delete(self, config_id: str) -> bool:
|
|
35
52
|
return self.delete_item("", config_id)
|
|
@@ -99,4 +99,4 @@ Please change the parent <Route path="${b}"> to <Route path="${b==="/"?"*":`${b}
|
|
|
99
99
|
]
|
|
100
100
|
}
|
|
101
101
|
]
|
|
102
|
-
}`;function qz({onClose:e}){const[t,r]=h.useState(""),[n,a]=h.useState(""),[i,o]=h.useState(!1);h.useEffect(()=>{Z.specOverlay.get().then(c=>r(JSON.stringify(c,null,2))).catch(console.error)},[]);async function s(){a("");let c;try{c=JSON.parse(t)}catch{a("Invalid JSON");return}o(!0);try{await Z.specOverlay.set(c),e()}catch(u){a(u instanceof Error?u.message:"Save failed")}finally{o(!1)}}return l.jsx("div",{className:"modal-overlay",onClick:e,children:l.jsxs("div",{className:"modal",style:{width:660,maxHeight:"85vh",display:"flex",flexDirection:"column"},onClick:c=>c.stopPropagation(),children:[l.jsxs("div",{className:"modal-header",children:[l.jsx("span",{children:"Spec Overlay — Custom Message Types"}),l.jsx("button",{className:"btn-icon",onClick:e,children:"✕"})]}),l.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."}),l.jsx("div",{style:{flex:1,display:"flex",flexDirection:"column",padding:"8px 16px",minHeight:0},children:l.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:c=>r(c.target.value),spellCheck:!1,placeholder:Kz})}),l.jsxs("div",{style:{padding:"0 16px 8px",fontSize:11,color:"var(--text2)"},children:["Schema: ",l.jsx("code",{children:'{ "messages": [{ "msg_type", "name", "category", "fields": [{ "tag", "required" }] }] }'})]}),n&&l.jsx("div",{className:"tpl-error",style:{margin:"0 16px 8px"},children:n}),l.jsxs("div",{className:"modal-footer",children:[l.jsx("button",{className:"btn btn-ghost",onClick:e,children:"Cancel"}),l.jsx("button",{className:"btn btn-primary",onClick:s,disabled:i,children:i?"Saving…":"Save overlay"})]})]})})}const Mw="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgdmlld0JveD0iMCAwIDY4MCAxNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgCgogIDxwb2x5Z29uIHBvaW50cz0iMTg4LDI4IDIzOCwyOCAyNTgsNzggMjM4LDEyOCAxODgsMTI4IiBmaWxsPSIjMWEzYTVjIiBzdHlsZT0iZmlsbDpyZ2IoMjYsIDU4LCA5Mik7c3Ryb2tlOm5vbmU7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGNpcmNsZSBjeD0iMjA3IiBjeT0iNzgiIHI9IjEwIiBmaWxsPSIjMDBiNGM4IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMTgwLCAyMDApO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxjaXJjbGUgY3g9IjIwNyIgY3k9Ijc4IiByPSI2IiBmaWxsPSIjMWEzYTVjIiBzdHlsZT0iZmlsbDpyZ2IoMjYsIDU4LCA5Mik7c3Ryb2tlOm5vbmU7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9IjY1IiB4Mj0iMjQ4IiB5Mj0iNjUiIHN0cm9rZT0iIzAwYjRjOCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0eWxlPSJmaWxsOnJnYigwLCAwLCAwKTtzdHJva2U6cmdiKDAsIDE4MCwgMjAwKTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjRweDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9Ijc4IiB4Mj0iMjUyIiB5Mj0iNzgiIHN0cm9rZT0iIzAwYjRjOCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9IjAuNyIgc3R5bGU9ImZpbGw6cmdiKDAsIDAsIDApO3N0cm9rZTpyZ2IoMCwgMTgwLCAyMDApO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6NHB4O3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjAuNztmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxsaW5lIHgxPSIyMjAiIHkxPSI5MSIgeDI9IjI0NCIgeTI9IjkxIiBzdHJva2U9IiMwMGI0YzgiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBvcGFjaXR5PSIwLjQ1IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMCwgMCk7c3Ryb2tlOnJnYigwLCAxODAsIDIwMCk7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDo0cHg7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MC40NTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgoKICA8dGV4dCB4PSIyNzIiIHk9IjEwNSIgZm9udC1mYW1pbHk9InZhcigtLWZvbnQtbW9ubykiIGZvbnQtc2l6ZT0iNjYiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMwMGI0YzgiIGxldHRlci1zcGFjaW5nPSItMSIgc3R5bGU9ImZpbGw6cmdiKDAsIDE4MCwgMjAwKTtzdHJva2U6bm9uZTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjE7Zm9udC1mYW1pbHk6dWktbW9ub3NwYWNlLCBtb25vc3BhY2U7Zm9udC1zaXplOjY2cHg7Zm9udC13ZWlnaHQ6NzAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iPkZJWDwvdGV4dD4KICA8dGV4dCB4PSIzODYiIHk9IjEwNSIgZm9udC1mYW1pbHk9InZhcigtLWZvbnQtc2FucykiIGZvbnQtc2l6ZT0iNTAiIGZvbnQtd2VpZ2h0PSIzMDAiIGxldHRlci1zcGFjaW5nPSItMSIgc3R5bGU9ImZpbGw6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6NTBweDtmb250LXdlaWdodDozMDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byI+dHVyZTwvdGV4dD4KPC9zdmc+",Dw="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgdmlld0JveD0iMCAwIDY4MCAxNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CgogIDxwb2x5Z29uIHBvaW50cz0iMTg4LDI4IDIzOCwyOCAyNTgsNzggMjM4LDEyOCAxODgsMTI4IiBmaWxsPSIjMDBiNGM4IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMTgwLCAyMDApO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxjaXJjbGUgY3g9IjIwNyIgY3k9Ijc4IiByPSIxMCIgZmlsbD0iIzFhM2E1YyIgc3R5bGU9ImZpbGw6cmdiKDI2LCA1OCwgOTIpO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxjaXJjbGUgY3g9IjIwNyIgY3k9Ijc4IiByPSI2IiBmaWxsPSIjMDBiNGM4IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMTgwLCAyMDApO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxsaW5lIHgxPSIyMjAiIHkxPSI2NSIgeDI9IjI0OCIgeTI9IjY1IiBzdHJva2U9IiMxYTNhNWMiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHlsZT0iZmlsbDpyZ2IoMCwgMCwgMCk7c3Ryb2tlOnJnYigyNiwgNTgsIDkyKTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjRweDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9Ijc4IiB4Mj0iMjUyIiB5Mj0iNzgiIHN0cm9rZT0iIzFhM2E1YyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9IjAuNyIgc3R5bGU9ImZpbGw6cmdiKDAsIDAsIDApO3N0cm9rZTpyZ2IoMjYsIDU4LCA5Mik7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDo0cHg7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MC43O2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9IjkxIiB4Mj0iMjQ0IiB5Mj0iOTEiIHN0cm9rZT0iIzFhM2E1YyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9IjAuNDUiIHN0eWxlPSJmaWxsOnJnYigwLCAwLCAwKTtzdHJva2U6cmdiKDI2LCA1OCwgOTIpO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6NHB4O3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjAuNDU7Zm9udC1mYW1pbHk6JnF1b3Q7QW50aHJvcGljIFNhbnMmcXVvdDssIC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJnF1b3Q7U2Vnb2UgVUkmcXVvdDssIHNhbnMtc2VyaWY7Zm9udC1zaXplOjE2cHg7Zm9udC13ZWlnaHQ6NDAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iLz4KCiAgPHRleHQgeD0iMjcyIiB5PSIxMDUiIGZvbnQtZmFtaWx5PSJ2YXIoLS1mb250LW1vbm8pIiBmb250LXNpemU9IjY2IiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWEzYTVjIiBsZXR0ZXItc3BhY2luZz0iLTEiIHN0eWxlPSJmaWxsOnJnYigyNiwgNTgsIDkyKTtzdHJva2U6bm9uZTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjE7Zm9udC1mYW1pbHk6dWktbW9ub3NwYWNlLCBtb25vc3BhY2U7Zm9udC1zaXplOjY2cHg7Zm9udC13ZWlnaHQ6NzAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iPkZJWDwvdGV4dD4KICA8dGV4dCB4PSIzODYiIHk9IjEwNSIgZm9udC1mYW1pbHk9InZhcigtLWZvbnQtc2FucykiIGZvbnQtc2l6ZT0iNTAiIGZvbnQtd2VpZ2h0PSIzMDAiIGZpbGw9IiMwMGI0YzgiIGxldHRlci1zcGFjaW5nPSItMSIgc3R5bGU9ImZpbGw6cmdiKDAsIDE4MCwgMjAwKTtzdHJva2U6bm9uZTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjE7Zm9udC1mYW1pbHk6JnF1b3Q7QW50aHJvcGljIFNhbnMmcXVvdDssIHNhbnMtc2VyaWY7Zm9udC1zaXplOjUwcHg7Zm9udC13ZWlnaHQ6MzAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iPnR1cmU8L3RleHQ+Cjwvc3ZnPg==";function Gs(){const e=V(r=>r.theme),t=V(r=>r.branding);return e==="dark"?(t==null?void 0:t.logo_dark)||Mw:(t==null?void 0:t.logo_light)||Dw}function Yz(){const e=V(g=>g.setSessions),t=V(g=>g.theme),r=V(g=>g.toggleTheme),n=V(g=>g.user),a=V(g=>g.logout),i=V(g=>g.openTabs),o=V(g=>g.activeTabId),s=V(g=>g.branding),c=Gs(),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 Pv(),h.useEffect(()=>{document.documentElement.setAttribute("data-theme",t)},[t]),h.useEffect(()=>{Z.sessions.list().then(e).catch(console.error)},[]),h.useEffect(()=>{if(!n){a_();return}Z.customTags.list().then(Ov).catch(console.error)},[n]),l.jsxs("div",{className:"app",children:[l.jsxs("header",{className:"app-header",children:[l.jsx("img",{src:c,alt:u,className:"app-logo"}),l.jsxs("div",{className:"header-right",children:[n&&l.jsx("span",{className:"header-user",children:n.username}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>d(!0),children:"Scenarios"}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p(!0),children:"Custom Tags"}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>y(!0),children:"Spec Overlay"}),(n==null?void 0:n.role)==="platform_admin"&&l.jsx(Yr,{to:"/perf",className:"btn btn-sm btn-ghost",title:"Performance testing",children:"Perf"}),(n==null?void 0:n.role)==="platform_admin"&&l.jsx(Yr,{to:"/admin/users",className:"btn-icon",title:"Admin",children:"⚙"}),l.jsx("button",{className:"btn-icon theme-toggle",onClick:r,title:"Toggle theme",children:t==="dark"?"☀":"🌙"}),l.jsx("button",{className:"btn-icon",onClick:a,title:"Sign out",children:"⎋"})]})]}),l.jsxs("div",{className:"app-body",children:[l.jsx(y_,{}),l.jsx("div",{className:"tab-area",children:i.length===0?l.jsx("div",{className:"tab-area-empty",children:"Select a session to open it."}):l.jsxs(l.Fragment,{children:[l.jsx(kz,{}),l.jsx("div",{className:"tab-panels",children:i.map(g=>l.jsx("div",{className:`tab-panel${g===o?" active":""}`,children:l.jsx(Ez,{sessionId:g})},g))})]})})]}),f&&l.jsx(zz,{onClose:()=>d(!1)}),m&&l.jsx(Zz,{onClose:()=>p(!1)}),v&&l.jsx(qz,{onClose:()=>y(!1)})]})}function Xz(){const[e,t]=h.useState(""),[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),[c,u]=h.useState(!1),{login:f}=V(),d=Gs(),m=on();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 l.jsx("div",{className:"auth-page",children:l.jsxs("div",{className:"auth-card",children:[l.jsx("img",{src:d,alt:"FIXture",className:"auth-logo"}),l.jsx("h2",{children:"Sign in"}),l.jsxs("form",{onSubmit:p,className:"auth-form",children:[l.jsxs("label",{children:["Username",l.jsx("input",{type:"text",value:e,onChange:v=>t(v.target.value),autoComplete:"username",required:!0})]}),l.jsxs("label",{children:["Password",l.jsx("input",{type:"password",value:r,onChange:v=>n(v.target.value),autoComplete:"current-password",required:!0})]}),a&&l.jsx("p",{className:"auth-error",children:a}),l.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Signing in…":"Sign in"})]}),c&&l.jsxs("p",{className:"auth-hint",style:{marginTop:12},children:["No account? ",l.jsx(Yr,{to:"/register",children:"Register"})]})]})})}function Vz(){const[e,t]=h.useState(""),[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),c=Gs(),u=on();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 l.jsx("div",{className:"auth-page",children:l.jsxs("div",{className:"auth-card",children:[l.jsx("img",{src:c,alt:"FIXture",className:"auth-logo"}),l.jsx("h2",{children:"Create admin account"}),l.jsx("p",{className:"auth-hint",children:"First-time setup — create the platform admin."}),l.jsxs("form",{onSubmit:f,className:"auth-form",children:[l.jsxs("label",{children:["Username",l.jsx("input",{type:"text",value:e,onChange:d=>t(d.target.value),autoComplete:"username",required:!0})]}),l.jsxs("label",{children:["Password",l.jsx("input",{type:"password",value:r,onChange:d=>n(d.target.value),autoComplete:"new-password",minLength:8,required:!0})]}),a&&l.jsx("p",{className:"auth-error",children:a}),l.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Creating…":"Create admin"})]})]})})}function Jz(){const[e,t]=h.useState(""),[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),[c,u]=h.useState(null),{login:f,theme:d}=V(),m=on();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=c&&c.current_users>=c.max_users;return l.jsx("div",{className:"auth-page",children:l.jsxs("div",{className:"auth-card",children:[l.jsx("img",{src:d==="dark"?Mw:Dw,alt:"FIXture",className:"auth-logo"}),l.jsx("h2",{children:"Create account"}),c===null&&l.jsx("p",{className:"auth-hint",children:"Checking registration status…"}),c&&!c.enabled&&l.jsx("p",{className:"auth-error",children:"Registration is currently closed. Contact an administrator."}),c&&c.enabled&&v&&l.jsx("p",{className:"auth-error",children:"Registration limit reached. Contact an administrator."}),c&&c.enabled&&!v&&l.jsxs("form",{onSubmit:p,className:"auth-form",children:[l.jsxs("label",{children:["Username",l.jsx("input",{type:"text",value:e,onChange:y=>t(y.target.value),autoComplete:"username",required:!0})]}),l.jsxs("label",{children:["Password",l.jsx("input",{type:"password",value:r,onChange:y=>n(y.target.value),autoComplete:"new-password",minLength:8,required:!0})]}),a&&l.jsx("p",{className:"auth-error",children:a}),l.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Creating account…":"Create account"})]}),l.jsxs("p",{className:"auth-hint",style:{marginTop:12},children:["Already have an account? ",l.jsx(Yr,{to:"/login",children:"Sign in"})]})]})})}const Qz=["accent","bg","bg2","bg3","border","text","text2"];function Rw(e){let t=document.getElementById("branding-style");t||(t=document.createElement("style"),t.id="branding-style",document.head.appendChild(t));const r=Qz.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 Ol(e){return new Promise((t,r)=>{const n=new FileReader;n.onload=()=>t(n.result),n.onerror=r,n.readAsDataURL(e)})}const $w=["user","user_admin","platform_admin"];function eW(e){return new Date(e*1e3).toLocaleString()}function tW({onClose:e,onCreated:t}){const[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState("user"),[c,u]=h.useState(""),[f,d]=h.useState(!1);async function m(p){p.preventDefault(),u(""),d(!0);try{const v=await Z.admin.users.create({username:r,password:a,role:o});t(v),e()}catch(v){u(v.message)}finally{d(!1)}}return l.jsx("div",{className:"modal-overlay",onClick:e,children:l.jsxs("div",{className:"modal",onClick:p=>p.stopPropagation(),children:[l.jsxs("div",{className:"modal-header",children:[l.jsx("span",{children:"Create User"}),l.jsx("button",{className:"btn-icon",onClick:e,children:"✕"})]}),l.jsxs("form",{onSubmit:m,children:[l.jsxs("div",{className:"form-grid",children:[l.jsx("label",{children:"Username"}),l.jsx("input",{value:r,onChange:p=>n(p.target.value),required:!0}),l.jsx("label",{children:"Password"}),l.jsx("input",{type:"password",value:a,onChange:p=>i(p.target.value),minLength:8,required:!0}),l.jsx("label",{children:"Role"}),l.jsx("select",{value:o,onChange:p=>s(p.target.value),children:$w.map(p=>l.jsx("option",{value:p,children:p},p))})]}),c&&l.jsx("div",{className:"error-msg",children:c}),l.jsxs("div",{className:"modal-footer",children:[l.jsx("button",{type:"button",className:"btn btn-secondary",onClick:e,children:"Cancel"}),l.jsx("button",{type:"submit",className:"btn btn-primary",disabled:f,children:f?"Creating…":"Create"})]})]})]})})}function rW({user:e,onClose:t}){const[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),[c,u]=h.useState(!1);async function f(d){d.preventDefault(),i(""),s(!0);try{await Z.admin.users.update(e.uid,{password:r}),u(!0)}catch(m){i(m.message)}finally{s(!1)}}return l.jsx("div",{className:"modal-overlay",onClick:t,children:l.jsxs("div",{className:"modal",onClick:d=>d.stopPropagation(),children:[l.jsxs("div",{className:"modal-header",children:[l.jsxs("span",{children:["Reset Password — ",e.username]}),l.jsx("button",{className:"btn-icon",onClick:t,children:"✕"})]}),c?l.jsxs("div",{style:{padding:16},children:[l.jsx("p",{style:{color:"var(--green)"},children:"Password updated successfully."}),l.jsx("div",{className:"modal-footer",children:l.jsx("button",{className:"btn btn-primary",onClick:t,children:"Close"})})]}):l.jsxs("form",{onSubmit:f,children:[l.jsxs("div",{className:"form-grid",children:[l.jsx("label",{children:"New Password"}),l.jsx("input",{type:"password",value:r,onChange:d=>n(d.target.value),minLength:8,required:!0})]}),a&&l.jsx("div",{className:"error-msg",children:a}),l.jsxs("div",{className:"modal-footer",children:[l.jsx("button",{type:"button",className:"btn btn-secondary",onClick:t,children:"Cancel"}),l.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Saving…":"Save"})]})]})]})})}function nW(){const e=V(k=>k.user),t=on(),[r,n]=h.useState([]),[a,i]=h.useState(null),[o,s]=h.useState(null),[c,u]=h.useState(null),[f,d]=h.useState(!1),[m,p]=h.useState(null),[v,y]=h.useState(!1),{setBranding:g}=V(),[b,x]=h.useState(!1),[w,_]=h.useState(null),[S,j]=h.useState("");async function O(){try{const[k,$,W,G]=await Promise.all([Z.admin.users.list(),Z.admin.settings.get(),Z.admin.housekeeping.get(),Z.admin.branding.get()]);n(k),i($),s(W),p(G)}catch(k){j(k.message)}}h.useEffect(()=>{O()},[]);async function N(k){try{const $=await Z.admin.users.update(k.uid,{is_active:!k.is_active});n(W=>W.map(G=>G.uid===k.uid?$:G))}catch($){j($.message)}}async function A(k,$){try{const W=await Z.admin.users.update(k.uid,{role:$});n(G=>G.map(J=>J.uid===k.uid?W:J))}catch(W){j(W.message)}}async function I(k){if(confirm(`Delete user "${k.username}"? This cannot be undone.`))try{await Z.admin.users.delete(k.uid),n($=>$.filter(W=>W.uid!==k.uid))}catch($){j($.message)}}async function T(){if(a)try{const k=await Z.admin.settings.update({registration_enabled:!a.enabled});i(k)}catch(k){j(k.message)}}async function C(k){const $=parseInt(k,10);if(!(isNaN($)||$<1))try{const W=await Z.admin.settings.update({max_users:$});i(W)}catch(W){j(W.message)}}async function U(){if(o)try{const k=await Z.admin.housekeeping.update({enabled:!o.enabled});s(k)}catch(k){j(k.message)}}async function L(k,$){const W=parseInt($,10);if(!(isNaN(W)||W<0))try{const G=await Z.admin.housekeeping.update({[k]:W});s(G)}catch(G){j(G.message)}}async function H(){d(!0),u(null);try{const k=await Z.admin.housekeeping.runNow();u(`Done — deleted ${k.msgs_deleted} messages, ${k.logs_deleted} log files`)}catch(k){j(k.message)}finally{d(!1)}}return l.jsxs("div",{className:"admin-page",children:[l.jsxs("div",{className:"admin-header",children:[l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[l.jsx("button",{className:"btn-icon",onClick:()=>t("/"),title:"Back",children:"←"}),l.jsx("h1",{children:"User Management"})]}),l.jsx("button",{className:"btn btn-primary btn-sm",onClick:()=>x(!0),children:"+ Create User"})]}),S&&l.jsx("div",{className:"error-msg",style:{margin:"0 0 12px"},children:S}),a&&l.jsxs("div",{className:"admin-settings-bar",children:[l.jsx("span",{className:"admin-settings-label",children:"Self-registration:"}),l.jsx("button",{className:`btn btn-sm ${a.enabled?"btn-stop":"btn-start"}`,onClick:T,children:a.enabled?"Enabled — click to disable":"Disabled — click to enable"}),l.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Max users:"}),l.jsx("input",{type:"number",className:"admin-max-users-input",defaultValue:a.max_users,min:1,onBlur:k=>C(k.target.value)}),l.jsxs("span",{className:"admin-settings-label",style:{color:"var(--text2)"},children:["(",a.current_users," / ",a.max_users," registered)"]})]}),o&&l.jsxs("div",{className:"admin-settings-bar",children:[l.jsx("span",{className:"admin-settings-label",children:"Housekeeping:"}),l.jsx("button",{className:`btn btn-sm ${o.enabled?"btn-stop":"btn-start"}`,onClick:U,children:o.enabled?"Auto — click to disable":"Manual only — click to enable"}),l.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Keep messages (days):"}),l.jsx("input",{type:"number",className:"admin-max-users-input",defaultValue:o.msg_retention_days,min:0,title:"0 = keep forever",onBlur:k=>L("msg_retention_days",k.target.value)}),l.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Keep logs (days):"}),l.jsx("input",{type:"number",className:"admin-max-users-input",defaultValue:o.log_retention_days,min:0,title:"0 = keep forever",onBlur:k=>L("log_retention_days",k.target.value)}),l.jsx("button",{className:"btn btn-sm btn-ghost",style:{marginLeft:16},onClick:H,disabled:f,children:f?"Running…":"Run Now"}),c&&l.jsx("span",{style:{marginLeft:8,color:"var(--green)",fontSize:12},children:c})]}),m&&l.jsxs("div",{className:"admin-settings-bar",style:{flexDirection:"column",alignItems:"flex-start",gap:10},children:[l.jsx("span",{className:"admin-settings-label",style:{fontWeight:700},children:"Branding"}),l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[l.jsx("span",{className:"admin-settings-label",children:"Brand prefix:"}),l.jsx("input",{className:"admin-max-users-input",style:{width:160},value:m.prefix,placeholder:"e.g. Acme (shown as Acme FIXture)",onChange:k=>p({...m,prefix:k.target.value})}),l.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Accent colour:"}),l.jsx("input",{type:"color",value:m.accent||"#388bfd",onChange:k=>p({...m,accent:k.target.value}),title:"Primary accent colour"}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,accent:""}),title:"Reset to default blue",children:"Reset"})]}),l.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(([k,$,W])=>l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:4},children:[l.jsxs("span",{className:"admin-settings-label",children:[$,":"]}),l.jsx("input",{type:"color",value:m[k]||W,onChange:G=>p({...m,[k]:G.target.value}),title:$}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,[k]:""}),title:"Reset to default",children:"↺"})]},k))}),l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[l.jsx("span",{className:"admin-settings-label",children:"Logo (dark theme):"}),m.logo_dark&&l.jsx("img",{src:m.logo_dark,style:{height:28,background:"#161b22",borderRadius:4,padding:"2px 6px"},alt:"dark logo preview"}),l.jsx("input",{type:"file",accept:"image/*",onChange:async k=>{var W;const $=(W=k.target.files)==null?void 0:W[0];$&&p({...m,logo_dark:await Ol($)})}}),m.logo_dark&&l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,logo_dark:""}),children:"Clear"})]}),l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[l.jsx("span",{className:"admin-settings-label",children:"Logo (light theme):"}),m.logo_light&&l.jsx("img",{src:m.logo_light,style:{height:28,background:"#f0f2f5",borderRadius:4,padding:"2px 6px"},alt:"light logo preview"}),l.jsx("input",{type:"file",accept:"image/*",onChange:async k=>{var W;const $=(W=k.target.files)==null?void 0:W[0];$&&p({...m,logo_light:await Ol($)})}}),m.logo_light&&l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,logo_light:""}),children:"Clear"})]}),l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[l.jsx("span",{className:"admin-settings-label",children:"Favicon:"}),m.favicon&&l.jsx("img",{src:m.favicon,style:{height:20,width:20},alt:"favicon preview"}),l.jsx("input",{type:"file",accept:"image/png,image/svg+xml,image/x-icon",onChange:async k=>{var W;const $=(W=k.target.files)==null?void 0:W[0];$&&p({...m,favicon:await Ol($)})}}),m.favicon&&l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,favicon:""}),children:"Clear"})]}),l.jsx("button",{className:"btn btn-sm btn-primary",disabled:v,onClick:async()=>{y(!0);try{const k=await Z.admin.branding.update(m);p(k),g(k),Rw(k)}catch(k){j(k.message)}finally{y(!1)}},children:v?"Saving…":"Save Branding"})]}),l.jsxs("table",{className:"admin-table",children:[l.jsx("thead",{children:l.jsxs("tr",{children:[l.jsx("th",{children:"Username"}),l.jsx("th",{children:"Role"}),l.jsx("th",{children:"Status"}),l.jsx("th",{children:"Created"}),l.jsx("th",{children:"Actions"})]})}),l.jsx("tbody",{children:r.map(k=>l.jsxs("tr",{className:k.is_active?"":"admin-row-inactive",children:[l.jsxs("td",{children:[k.username,k.uid===(e==null?void 0:e.uid)&&l.jsx("span",{className:"admin-you-badge",children:" (you)"})]}),l.jsx("td",{children:l.jsx("select",{value:k.role,onChange:$=>A(k,$.target.value),className:"admin-role-select",disabled:k.uid===(e==null?void 0:e.uid),children:$w.map($=>l.jsx("option",{value:$,children:$},$))})}),l.jsx("td",{children:l.jsx("span",{style:{color:k.is_active?"var(--green)":"var(--text2)"},children:k.is_active?"Active":"Inactive"})}),l.jsx("td",{style:{color:"var(--text2)",fontSize:11},children:eW(k.created_at)}),l.jsx("td",{children:l.jsxs("div",{className:"admin-row-actions",children:[l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>N(k),disabled:k.uid===(e==null?void 0:e.uid),title:k.is_active?"Deactivate":"Activate",children:k.is_active?"Deactivate":"Activate"}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>_(k),title:"Reset password",children:"Reset PW"}),l.jsx("button",{className:"btn btn-sm btn-danger",onClick:()=>I(k),disabled:k.uid===(e==null?void 0:e.uid),title:"Delete user",children:"✕"})]})})]},k.uid))})]}),b&&l.jsx(tW,{onClose:()=>x(!1),onCreated:k=>n($=>[...$,k])}),w&&l.jsx(rW,{user:w,onClose:()=>_(null)})]})}const Ec={enabled:!1,every_n_fills:1,amend_ratio:1,max_per_order:1,price_offset:.01,pending_replace:!1},Kp={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},amend:Ec,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:{}}},aW=["loopback","manager","client","venue"];function qp(e,t){return t?Math.round(e/t*1e3):0}function iW({onStarted:e}){var ea,D;const t=V(P=>P.sessions),[r,n]=h.useState(Kp),[a,i]=h.useState(Kp.payload.order.symbols.join(", ")),[o,s]=h.useState(!1),[c,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",_=r.mode==="loopback"||r.mode==="manager"||r.mode==="venue",S=r.amend??Ec,j=r.client_session_id?t[r.client_session_id]:void 0,O=r.venue_session_id?t[r.venue_session_id]:void 0,N=!w||(j==null?void 0:j.status)==="LOGGED_ON",A=!_||(O==null?void 0:O.status)==="LOGGED_ON",I=(!w||!!r.client_session_id)&&(!_||!!r.venue_session_id),T=(()=>{if(!r.name.trim())return"Name is required";const P=r.rate;if(P.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(P.orders_per_window<1||P.order_window_ms<1)return"Order rate must be > 0";if(a.split(",").map(it=>it.trim()).filter(Boolean).length===0)return"At least one symbol is required";const B=r.payload.order;if(B.quantity_min<1||B.quantity_max<1||B.quantity_min>B.quantity_max)return"Check quantity min/max";if(B.price_min<=0||B.price_max<=0||B.price_min>B.price_max)return"Check price min/max";if(S.enabled){if(S.every_n_fills<1)return"Amend every-n-fills must be ≥ 1";if(S.amend_ratio<0||S.amend_ratio>1)return"Amend ratio must be between 0 and 1";if(S.max_per_order<0)return"Max amends per order must be ≥ 0";if(S.price_offset<=0)return"Amend price offset must be > 0"}}if(_){if(P.fills_per_window<1||P.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(P.fill_ratio<0||P.fill_ratio>1)return"Fill ratio must be between 0 and 1"}return null})(),C=I&&N&&A&&!T&&!f;async function U(){try{p(await Z.perf.configs.list())}catch{}}h.useEffect(()=>{U()},[]),h.useEffect(()=>{Z.templates.list().then(b).catch(()=>{})},[]);const L=g.filter(P=>P.msg_type==="D"),H=g.filter(P=>P.msg_type==="8"),k=!!r.payload.order_template_id,$=!!r.payload.exec_template_id,W=((ea=L.find(P=>P.id===r.payload.order_template_id))==null?void 0:ea.name)??"selected",G=((D=H.find(P=>P.id===r.payload.exec_template_id))==null?void 0:D.name)??"selected",J=P=>n(B=>({...B,...P})),ae=P=>n(B=>({...B,rate:{...B.rate,...P}})),ee=P=>n(B=>({...B,test:{...B.test,...P}})),me=P=>n(B=>({...B,amend:{...Ec,...B.amend,...P}})),de=P=>n(B=>({...B,payload:{...B.payload,order:{...B.payload.order,...P}}})),Oe=P=>n(B=>({...B,payload:{...B.payload,fill:{...B.payload.fill,...P}}})),R=P=>n(B=>({...B,payload:{...B.payload,...P}})),[ie,ne]=h.useState([]),z=P=>{ne(P),R({gen_rules:Object.fromEntries(P.filter(B=>B.tag.trim()).map(B=>[B.tag.trim(),B.value]))})},F=(P,B)=>z(ie.map((it,Er)=>Er===P?{...it,...B}:it));function Q(){const P=a.split(",").map(B=>B.trim()).filter(Boolean);return{...r,client_session_id:w?r.client_session_id:null,venue_session_id:_?r.venue_session_id:null,payload:{...r.payload,order:{...r.payload.order,symbols:P}}}}async function M(){u(""),d(!0);try{const{run_id:P}=await Z.perf.runs.create(Q());e(P)}catch(P){u(P.message??"Failed to start run")}finally{d(!1)}}async function Ie(){u("");try{await Z.perf.configs.save(Q()),await U()}catch(P){u(P.message??"Failed to save config")}}async function se(P){if(y(P),!!P){u("");try{const B=await Z.perf.configs.get(P);n(B),i(B.payload.order.symbols.join(", ")),ne(Object.entries(B.payload.gen_rules??{}).map(([it,Er])=>({tag:it,value:Er})))}catch(B){u(B.message??"Failed to load config")}}}async function ze(){if(v&&window.confirm("Delete this saved config?"))try{await Z.perf.configs.delete(v),y(""),await U()}catch(P){u(P.message??"Failed to delete config")}}const Te=P=>{const B=t[P];return B?`${B.display_name||`${B.sender_comp_id} → ${B.target_comp_id}`} (${B.status})`:P},_t=x.filter(P=>P.session_role!=="venue"),mn=x.filter(P=>P.session_role!=="client");return l.jsxs("div",{className:"perf-form",children:[c&&l.jsx("div",{className:"error-msg",children:c}),l.jsxs("div",{className:"perf-form-section",children:[l.jsx("div",{className:"perf-section-title",children:"Saved configs"}),l.jsxs("div",{className:"perf-field-row",children:[l.jsxs("select",{value:v,onChange:P=>se(P.target.value),children:[l.jsx("option",{value:"",children:"(load a saved config…)"}),m.map(P=>l.jsxs("option",{value:P.config_id,children:[P.name," — ",P.mode]},P.config_id))]}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:Ie,children:"Save current"}),l.jsx("button",{className:"btn btn-sm btn-ghost btn-danger-ghost",onClick:ze,disabled:!v,children:"Delete"})]})]}),l.jsxs("div",{className:"perf-form-section",children:[l.jsx("div",{className:"perf-section-title",children:"Run"}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Name"}),l.jsx("input",{value:r.name,onChange:P=>J({name:P.target.value})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Mode"}),l.jsx("select",{value:r.mode,onChange:P=>J({mode:P.target.value}),children:aW.map(P=>l.jsx("option",{value:P,children:P},P))})]}),w&&l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Client session"}),l.jsxs("select",{value:r.client_session_id??"",onChange:P=>J({client_session_id:P.target.value||null}),children:[l.jsx("option",{value:"",children:"(select…)"}),_t.map(P=>l.jsx("option",{value:P.session_id,children:Te(P.session_id)},P.session_id))]}),j&&l.jsx(Yp,{status:j.status})]}),_&&l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Venue session"}),l.jsxs("select",{value:r.venue_session_id??"",onChange:P=>J({venue_session_id:P.target.value||null}),children:[l.jsx("option",{value:"",children:"(select…)"}),mn.map(P=>l.jsx("option",{value:P.session_id,children:Te(P.session_id)},P.session_id))]}),O&&l.jsx(Yp,{status:O.status})]})]}),l.jsxs("div",{className:"perf-form-section",children:[l.jsx("div",{className:"perf-section-title",children:"Rate"}),l.jsxs("div",{className:"perf-field-grid",children:[w&&l.jsxs(l.Fragment,{children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Orders / window"}),l.jsx("input",{type:"number",min:1,value:r.rate.orders_per_window,onChange:P=>ae({orders_per_window:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Order window (ms)"}),l.jsx("input",{type:"number",min:1,value:r.rate.order_window_ms,onChange:P=>ae({order_window_ms:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Order dispatch"}),l.jsxs("select",{value:r.rate.dispatch??"smooth",onChange:P=>ae({dispatch:P.target.value}),children:[l.jsx("option",{value:"smooth",children:"smooth (evenly spaced)"}),l.jsx("option",{value:"burst",children:"burst (all at window start)"})]})]})]}),_&&l.jsxs(l.Fragment,{children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Fills / window"}),l.jsx("input",{type:"number",min:1,value:r.rate.fills_per_window,onChange:P=>ae({fills_per_window:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Fill window (ms)"}),l.jsx("input",{type:"number",min:1,value:r.rate.fill_window_ms,onChange:P=>ae({fill_window_ms:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Fill ratio"}),l.jsx("input",{type:"number",min:0,max:1,step:.05,value:r.rate.fill_ratio,onChange:P=>ae({fill_ratio:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Fill dispatch"}),l.jsxs("select",{value:r.rate.fill_dispatch??"smooth",onChange:P=>ae({fill_dispatch:P.target.value}),children:[l.jsx("option",{value:"smooth",children:"smooth (evenly spaced)"}),l.jsx("option",{value:"burst",children:"burst (all at window start)"})]})]})]})]}),l.jsxs("div",{className:"perf-hint",children:[w&&l.jsxs(l.Fragment,{children:["≈ ",qp(r.rate.orders_per_window,r.rate.order_window_ms).toLocaleString()," orders/s"]}),w&&_&&" · ",_&&l.jsxs(l.Fragment,{children:["≈ ",qp(r.rate.fills_per_window,r.rate.fill_window_ms).toLocaleString()," fills/s"]})," ",r.rate.dispatch==="burst"?l.jsxs(l.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."]}):l.jsx(l.Fragment,{children:"Smaller windows smooth the injection."})]})]}),l.jsxs("div",{className:"perf-form-section",children:[l.jsx("div",{className:"perf-section-title",children:"Test"}),l.jsxs("div",{className:"perf-field-grid",children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Duration (s, 0 = no time limit)"}),l.jsx("input",{type:"number",min:0,value:r.test.duration,onChange:P=>ee({duration:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Max orders (0 = no count limit)"}),l.jsx("input",{type:"number",min:0,value:r.test.max_orders,onChange:P=>ee({max_orders:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Order timeout (ms)"}),l.jsx("input",{type:"number",min:1,step:1e3,value:r.test.scenario_timeout_ms,onChange:P=>ee({scenario_timeout_ms:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"On saturation"}),l.jsxs("select",{value:r.test.on_saturation,onChange:P=>ee({on_saturation:P.target.value}),children:[l.jsx("option",{value:"pause",children:"pause (preserves latency accuracy)"}),l.jsx("option",{value:"drop_oldest",children:"drop_oldest"})]})]})]}),w&&l.jsxs("label",{className:"checkbox-label",children:[l.jsx("input",{type:"checkbox",checked:r.test.record_execs,onChange:P=>ee({record_execs:P.target.checked})}),"Record per-exec detail — one CSV row per ack/fill with latency + inter-report gap (downloads as csv.gz after the run)"]}),l.jsxs("div",{className:"perf-hint",children:["An order not fully filled within the order timeout is counted ",l.jsx("strong",{children:"Lost"}),". Raise it if rate-limited fills are being flagged lost on full-fill runs."]})]}),w&&l.jsxs("div",{className:"perf-form-section",children:[l.jsx("div",{className:"perf-section-title",children:"Amends (35=G)"}),l.jsxs("label",{className:"checkbox-label",children:[l.jsx("input",{type:"checkbox",checked:S.enabled,onChange:P=>me({enabled:P.target.checked})}),"Amend working orders — price-only OrderCancelReplaceRequest fired every nth fill"]}),S.enabled&&l.jsxs(l.Fragment,{children:[l.jsxs("div",{className:"perf-field-grid",children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Every n fills"}),l.jsx("input",{type:"number",min:1,value:S.every_n_fills,onChange:P=>me({every_n_fills:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Amend ratio (orders)"}),l.jsx("input",{type:"number",min:0,max:1,step:.05,value:S.amend_ratio,onChange:P=>me({amend_ratio:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Max amends / order (0 = no cap)"}),l.jsx("input",{type:"number",min:0,value:S.max_per_order,onChange:P=>me({max_per_order:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Price offset (±)"}),l.jsx("input",{type:"number",min:1e-4,step:.01,value:S.price_offset,onChange:P=>me({price_offset:Number(P.target.value)})})]})]}),_&&l.jsxs("label",{className:"checkbox-label",children:[l.jsx("input",{type:"checkbox",checked:S.pending_replace,onChange:P=>me({pending_replace:P.target.checked})}),"Pending Replace — venue replies 150=E before 150=5 (two ExecReports per amend)"]}),l.jsx("div",{className:"perf-hint",children:"Amends fire only while the order is still working — never on the final fill, so use partial fills or fills per order > 1. The 35=G keeps the order's correlation id (376); amend rate ≈ fills/s ÷ n. Latency is measured amend-2-replaced (150=5)."}),_&&r.payload.fill.fill_type==="full"&&r.payload.fill.fills_per_order===1&&l.jsxs("div",{className:"perf-hint",children:["⚠ With a single full fill per order there is never a working order to amend — raise ",l.jsx("strong",{children:"Fills per order"})," or use ",l.jsx("strong",{children:"partial"})," fill type."]})]})]}),w&&l.jsxs("div",{className:`perf-form-section ${k?"perf-section-layered":""}`,children:[l.jsxs("div",{className:"perf-section-title",children:["Order payload",k&&l.jsx("span",{className:"perf-layer-badge",children:"values for template"})]}),k&&l.jsxs("div",{className:"perf-hint perf-layer-note",children:["Order template ",l.jsx("strong",{children:W})," defines the message shape. These fields supply the values it references via ",l.jsx("code",{children:"{symbol}"})," ",l.jsx("code",{children:"{side}"})," ",l.jsx("code",{children:"{qty}"})," ",l.jsx("code",{children:"{price}"})," tokens. Value generators (",l.jsx("code",{children:"uuid()"}),", ",l.jsx("code",{children:"random_str(n)"}),", ",l.jsx("code",{children:"random_int(a,b)"}),","," ",l.jsx("code",{children:"seq(width)"}),", ",l.jsx("code",{children:"timestamp()"}),", ",l.jsx("code",{children:"date()"}),") are evaluated fresh per order and mix with text as ",l.jsx("code",{children:"ORD-{seq(8)}"}),"; ClOrdID(11) is always made unique per order. ",l.jsx("strong",{children:"Order type"})," is taken from the template."]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Symbols (comma-separated)"}),l.jsx("input",{value:a,onChange:P=>i(P.target.value)})]}),l.jsxs("div",{className:"perf-field-grid",children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Side"}),l.jsxs("select",{value:r.payload.order.side,onChange:P=>de({side:P.target.value}),children:[l.jsx("option",{value:"alternate",children:"alternate"}),l.jsx("option",{value:"fixed_buy",children:"fixed_buy"}),l.jsx("option",{value:"fixed_sell",children:"fixed_sell"}),l.jsx("option",{value:"random",children:"random"})]})]}),l.jsxs("label",{className:`perf-field ${k?"perf-field-overridden":""}`,children:[l.jsxs("span",{children:["Order type ",k&&l.jsx("em",{className:"perf-field-tag",children:"from template"})]}),l.jsxs("select",{value:r.payload.order.ord_type,disabled:k,onChange:P=>de({ord_type:P.target.value}),children:[l.jsx("option",{value:"limit",children:"limit"}),l.jsx("option",{value:"market",children:"market"})]})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Qty min"}),l.jsx("input",{type:"number",min:1,value:r.payload.order.quantity_min,onChange:P=>de({quantity_min:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Qty max"}),l.jsx("input",{type:"number",min:1,value:r.payload.order.quantity_max,onChange:P=>de({quantity_max:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Price min"}),l.jsx("input",{type:"number",min:0,step:.01,value:r.payload.order.price_min,onChange:P=>de({price_min:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Price max"}),l.jsx("input",{type:"number",min:0,step:.01,value:r.payload.order.price_max,onChange:P=>de({price_max:Number(P.target.value)})})]})]})]}),w&&l.jsxs("div",{className:"perf-form-section",children:[l.jsx("div",{className:"perf-section-title",children:"Send options"}),l.jsxs("div",{className:"perf-field-row",children:[l.jsxs("label",{className:"checkbox-label",children:[l.jsx("input",{type:"checkbox",checked:r.payload.auto_expiry??!0,onChange:P=>R({auto_expiry:P.target.checked})}),"Auto expiry (tags 126 / 432, only if present)"]}),(r.payload.auto_expiry??!0)&&l.jsxs(l.Fragment,{children:[l.jsx("input",{type:"number",min:0,style:{width:64},value:r.payload.expiry_offset??1,onChange:P=>R({expiry_offset:Number(P.target.value)})}),l.jsxs("select",{value:r.payload.expiry_unit??"days",onChange:P=>R({expiry_unit:P.target.value}),children:[l.jsx("option",{value:"minutes",children:"minutes"}),l.jsx("option",{value:"hours",children:"hours"}),l.jsx("option",{value:"days",children:"days"})]}),l.jsx("span",{children:"into the future"})]})]}),ie.map((P,B)=>l.jsxs("div",{className:"perf-field-row",children:[l.jsx("input",{style:{width:72},placeholder:"tag",value:P.tag,onChange:it=>F(B,{tag:it.target.value.replace(/[^0-9]/g,"")})}),l.jsx("span",{children:"="}),l.jsx("input",{style:{flex:1},placeholder:"uuid() / seq(8) / ORD-{seq(8)} / literal",value:P.value,onChange:it=>F(B,{value:it.target.value})}),l.jsx("button",{className:"btn btn-sm btn-ghost btn-danger-ghost",onClick:()=>z(ie.filter((it,Er)=>Er!==B)),children:"✕"})]},B)),l.jsx("div",{className:"perf-field-row",children:l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>z([...ie,{tag:"",value:""}]),children:"+ Add tag rule"})}),l.jsxs("div",{className:"perf-hint",children:["Rules set the tag on every order/scenario message (insert or replace). Generators: ",l.jsx("code",{children:"uuid()"})," · ",l.jsx("code",{children:"random_str(n)"})," · ",l.jsx("code",{children:"random_int(a,b)"})," ·"," ",l.jsx("code",{children:"seq(width[,start])"})," · ",l.jsx("code",{children:"timestamp()"})," · ",l.jsx("code",{children:"date()"})," — mix with text as ",l.jsx("code",{children:"ORD-{seq(8)}"}),". ClOrdID(11) always stays unique per order."]})]}),_&&l.jsxs("div",{className:`perf-form-section ${$?"perf-section-layered":""}`,children:[l.jsxs("div",{className:"perf-section-title",children:["Fill payload",$&&l.jsx("span",{className:"perf-layer-badge",children:"behavior for template"})]}),$&&l.jsxs("div",{className:"perf-hint perf-layer-note",children:["Exec template ",l.jsx("strong",{children:G})," reshapes each report (custom tags). These still control fill ",l.jsx("strong",{children:"behavior"})," — how many reports, the fill ratio, and the ack — while the engine sets the standard exec tags."]}),l.jsxs("div",{className:"perf-field-grid",children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Fill type"}),l.jsxs("select",{value:r.payload.fill.fill_type,onChange:P=>Oe({fill_type:P.target.value}),children:[l.jsx("option",{value:"full",children:"full"}),l.jsx("option",{value:"partial",children:"partial"}),l.jsx("option",{value:"random",children:"random"})]})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Fills per order"}),l.jsx("input",{type:"number",min:1,value:r.payload.fill.fills_per_order,onChange:P=>Oe({fills_per_order:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field perf-field-check",children:[l.jsx("input",{type:"checkbox",checked:r.payload.fill.send_ack,onChange:P=>Oe({send_ack:P.target.checked})}),l.jsx("span",{children:"Send ack before fills (35=8|150=0|39=0)"})]}),r.payload.fill.fill_type!=="full"&&l.jsxs(l.Fragment,{children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Partial % min"}),l.jsx("input",{type:"number",min:1,max:100,value:r.payload.fill.partial_fill_pct_min,onChange:P=>Oe({partial_fill_pct_min:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Partial % max"}),l.jsx("input",{type:"number",min:1,max:100,value:r.payload.fill.partial_fill_pct_max,onChange:P=>Oe({partial_fill_pct_max:Number(P.target.value)})})]})]})]})]}),(w||_)&&l.jsxs("div",{className:"perf-form-section",children:[l.jsx("div",{className:"perf-section-title",children:"Templates (optional)"}),l.jsxs("div",{className:"perf-field-grid",children:[w&&l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Order template"}),l.jsxs("select",{value:r.payload.order_template_id??"",onChange:P=>R({order_template_id:P.target.value||null}),children:[l.jsx("option",{value:"",children:"— built-in —"}),L.map(P=>l.jsx("option",{value:P.id,children:P.name},P.id))]})]}),_&&l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Exec report template"}),l.jsxs("select",{value:r.payload.exec_template_id??"",onChange:P=>R({exec_template_id:P.target.value||null}),children:[l.jsx("option",{value:"",children:"— built-in —"}),H.map(P=>l.jsx("option",{value:P.id,children:P.name},P.id))]})]})]}),l.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."})]}),l.jsxs("div",{className:"perf-form-section",children:[l.jsxs("button",{className:"btn btn-sm btn-ghost",onClick:()=>s(P=>!P),children:[o?"▾":"▸"," Advanced"]}),o&&l.jsxs("div",{className:"perf-field-grid",style:{marginTop:8},children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Correlation tag"}),l.jsx("input",{type:"number",min:1,value:r.correlation_tag,onChange:P=>J({correlation_tag:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Exec ID tag"}),l.jsx("input",{type:"number",min:1,value:r.exec_id_tag,onChange:P=>J({exec_id_tag:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Max pending"}),l.jsx("input",{type:"number",min:1,value:r.test.max_pending,onChange:P=>ee({max_pending:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field perf-field-check",children:[l.jsx("input",{type:"checkbox",checked:r.rate.allow_burst,onChange:P=>ae({allow_burst:P.target.checked})}),l.jsx("span",{children:"Allow burst"})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Max burst multiplier"}),l.jsx("input",{type:"number",min:1,step:.5,value:r.rate.max_burst_multiplier,onChange:P=>ae({max_burst_multiplier:Number(P.target.value)})})]})]})]}),l.jsxs("div",{className:"perf-form-actions",children:[T?l.jsx("span",{className:"perf-not-ready",children:T}):I&&!(N&&A)?l.jsxs("span",{className:"perf-not-ready",children:["Selected session",w&&_?"s":""," must be LOGGED_ON before starting."]}):null,l.jsx("button",{className:"btn btn-primary",onClick:M,disabled:!C,title:I?T||(N&&A?void 0:"Session not logged on yet"):"Select the required session(s)",children:f?"Starting…":"▶ Start run"})]})]})}function Yp({status:e}){const t=e==="LOGGED_ON";return l.jsxs("span",{className:`perf-status-chip ${t?"ok":"warn"}`,children:[t?"● ":"○ ",e]})}const Xp=600,oW=5e3,sW=new Set(["completed","stopped","error"]);function lW(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,c=null,u,f=0,d=!1;async function m(){if(s||d||!e)return;let v;try{({ticket:v}=await Z.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}`;c=new WebSocket(g),c.onopen=()=>{s||(f=0,o(!0))},c.onmessage=b=>{let x;try{x=JSON.parse(b.data)}catch{return}r(x),a(w=>{var j,O,N,A,I,T,C,U,L;const _={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,aps:((N=x.client)==null?void 0:N.aps_live)??0,tps:(((A=x.client)==null?void 0:A.ops_live)??0)+(((I=x.venue)==null?void 0:I.fps_live)??0),p50:(((T=x.response_latency)==null?void 0:T.p50_us)??0)/1e3,p99:(((C=x.response_latency)==null?void 0:C.p99_us)??0)/1e3,fc50:(((U=x.fill_completion_latency)==null?void 0:U.p50_us)??0)/1e3,fc99:(((L=x.fill_completion_latency)==null?void 0:L.p99_us)??0)/1e3},S=[...w,_];return S.length>Xp?S.slice(-Xp):S}),sW.has(x.status)&&(d=!0,c==null||c.close())},c.onclose=()=>{s||(o(!1),d||p())},c.onerror=()=>{c==null||c.close()}}function p(){if(s||d)return;f+=1;const v=Math.min(oW,500*2**Math.min(f,4));u=setTimeout(m,v)}return m(),()=>{s=!0,clearTimeout(u),c==null||c.close()}},[e]),{latest:t,series:n,connected:i}}const Lw="img-export-exclude";async function cW(e,t,r){const{toPng:n,toJpeg:a}=await kv(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(Lw))}),c=document.createElement("a");c.href=s,c.download=t,c.click()}function uW(e,t){const r=new Date().toISOString().slice(0,19).replace(/[:T]/g,"-");return`${e}-${r}.${t}`}const Pl=new Set(["completed","stopped","error"]);function vr(e){return e?(e/1e3).toFixed(3):"—"}function Fw(e,t){return t<=0?"—":(e/t).toFixed(2)}function Nl({title:e,sub:t,stats:r}){return l.jsxs("div",{className:"perf-lat-card",children:[l.jsxs("div",{className:"perf-lat-title",children:[e,t&&l.jsx("span",{className:"perf-lat-sub",children:t})]}),l.jsx("div",{className:"perf-lat-grid",children:["p50_us","p95_us","p99_us","max_us"].map(n=>l.jsxs("div",{className:"perf-lat-cell",children:[l.jsx("span",{className:"perf-lat-val",children:vr(r[n])}),l.jsxs("span",{className:"perf-lat-lbl",children:[n.replace("_us","")," ms"]})]},n))})]})}function dW({runId:e}){var w,_;const{latest:t,series:r,connected:n}=lW(e),[a,i]=h.useState(null),[o,s]=h.useState(!1),c=h.useRef(null);h.useEffect(()=>{let S=!0;i(null);let j;const O=async()=>{try{const N=await Z.perf.runs.get(e);if(!S)return;i(N),Pl.has(N.status)&&clearInterval(j)}catch{}};return j=setInterval(O,2e3),O(),()=>{S=!1,clearInterval(j)}},[e]);const u=t?Pl.has(t.status):a?Pl.has(a.status):!1,f=!u;async function d(){try{await Z.perf.runs.stop(e)}catch{}}async function m(S){if(!(!c.current||o)){s(!0);try{const j=S==="png"?"png":"jpg";await cW(c.current,uW(`perf-${e.slice(0,8)}`,j),S)}catch(j){console.error("image export failed",j)}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()}),(p.amends_sent>0||p.aps_live>0)&&(x.push({label:"amends/s",value:Math.round(p.aps_live).toLocaleString()},{label:"amends",value:p.amends_sent.toLocaleString()},{label:"replaces",value:p.replaces_received.toLocaleString()}),p.amends_lost>0&&x.push({label:"amends lost",value:p.amends_lost.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:Fw(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()}),v.amends_received>0&&x.push({label:"amends recv",value:v.amends_received.toLocaleString()})),y&&x.push({label:"lost",value:y.lost_timeout.toLocaleString()},{label:"rejected",value:y.rejected.toLocaleString()}),l.jsxs("div",{className:"perf-dashboard",ref:c,children:[l.jsxs("div",{className:"perf-dash-header",children:[l.jsxs("div",{className:"perf-dash-status",children:[l.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&&l.jsxs("span",{className:"perf-dash-elapsed",children:[Math.round(t.elapsed_s),"s",t.duration_s?` / ${t.duration_s}s`:""," · ",t.mode]}),l.jsx("span",{className:`perf-conn ${n?"on":"off"}`,children:n?"● live":"○ offline"})]}),l.jsxs("div",{className:`perf-dash-actions ${Lw}`,children:[f&&l.jsx("button",{className:"btn btn-sm btn-danger",onClick:d,children:"■ Stop"}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>Z.perf.runs.export(e,"messages"),children:"Export CSV"}),u&&l.jsx("button",{className:"btn btn-sm btn-ghost",title:"Per-exec detail (requires record_execs)",onClick:()=>Z.perf.runs.export(e,"execs"),children:"Execs CSV"}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>Z.perf.runs.export(e,"both"),children:"Export ZIP"}),l.jsx("button",{className:"btn btn-sm btn-ghost",disabled:o,onClick:()=>m("png"),children:o?"…":"PNG"}),l.jsx("button",{className:"btn btn-sm btn-ghost",disabled:o,onClick:()=>m("jpeg"),children:o?"…":"JPG"})]})]}),g&&l.jsxs("div",{className:"perf-saturated-banner",children:["⚠ SATURATED — injection paused to protect latency measurement.",p&&l.jsxs(l.Fragment,{children:[" Pending ",p.pending.toLocaleString(),", dropped ",p.dropped.toLocaleString(),"."]})]}),(w=a==null?void 0:a.warnings)==null?void 0:w.map((S,j)=>l.jsxs("div",{className:"perf-warning-banner",children:["⚠ ",S]},j)),!t&&!u&&l.jsx("div",{className:"loading-msg",children:"Waiting for first snapshot…"}),x.length>0&&l.jsx("div",{className:"stat-cards",children:x.map(({label:S,value:j})=>l.jsxs("div",{className:"stat-card",children:[l.jsx("span",{className:"stat-card-value",children:j}),l.jsx("span",{className:"stat-card-label",children:S})]},S))}),t&&l.jsxs("div",{className:"perf-lat-row",children:[l.jsx(Nl,{title:"Response latency",sub:"order-2-ack",stats:t.response_latency}),l.jsx(Nl,{title:"Fill-completion latency",sub:"order-2-last-fill",stats:t.fill_completion_latency}),((_=t.amend_latency)==null?void 0:_.count)>0&&l.jsx(Nl,{title:"Amend latency",sub:"amend-2-replaced",stats:t.amend_latency})]}),r.length>1&&l.jsxs(l.Fragment,{children:[l.jsx("div",{className:"analysis-section-title",children:"Throughput"}),l.jsx("div",{className:"analysis-chart-wrap",children:l.jsx(Nn,{width:"100%",height:200,children:l.jsxs(Ti,{data:r,margin:{top:4,right:16,bottom:4,left:8},children:[l.jsx(Zr,{strokeDasharray:"3 3",stroke:"var(--border)"}),l.jsx(Kr,{dataKey:"t",tick:{fontSize:11,fill:"var(--text2)"},tickFormatter:S=>`${Math.round(S)}s`}),l.jsx(qr,{tick:{fontSize:11,fill:"var(--text2)"},allowDecimals:!1}),l.jsx(An,{contentStyle:{background:"var(--bg3)",border:"1px solid var(--border)",fontSize:12},labelStyle:{color:"var(--text)"},labelFormatter:S=>`${Math.round(Number(S))}s`}),l.jsx(ga,{wrapperStyle:{fontSize:11}}),l.jsx(yt,{type:"monotone",dataKey:"ops",name:"orders/s",stroke:"#00b4c8",dot:!1,isAnimationActive:!1}),l.jsx(yt,{type:"monotone",dataKey:"fps",name:"fills/s",stroke:"#a78bfa",dot:!1,isAnimationActive:!1}),((p==null?void 0:p.amends_sent)??0)>0&&l.jsx(yt,{type:"monotone",dataKey:"aps",name:"amends/s",stroke:"#c084fc",dot:!1,isAnimationActive:!1}),l.jsx(yt,{type:"monotone",dataKey:"tps",name:"total msgs/s",stroke:"#fbbf24",dot:!1,isAnimationActive:!1})]})})}),l.jsx("div",{className:"analysis-section-title",children:"Response latency (ms)"}),l.jsx("div",{className:"analysis-chart-wrap",children:l.jsx(Nn,{width:"100%",height:200,children:l.jsxs(Ti,{data:r,margin:{top:4,right:16,bottom:4,left:8},children:[l.jsx(Zr,{strokeDasharray:"3 3",stroke:"var(--border)"}),l.jsx(Kr,{dataKey:"t",tick:{fontSize:11,fill:"var(--text2)"},tickFormatter:S=>`${Math.round(S)}s`}),l.jsx(qr,{tick:{fontSize:11,fill:"var(--text2)"}}),l.jsx(An,{contentStyle:{background:"var(--bg3)",border:"1px solid var(--border)",fontSize:12},labelStyle:{color:"var(--text)"},labelFormatter:S=>`${Math.round(Number(S))}s`}),l.jsx(ga,{wrapperStyle:{fontSize:11}}),l.jsx(yt,{type:"monotone",dataKey:"p50",name:"p50",stroke:"#4ade80",dot:!1,isAnimationActive:!1}),l.jsx(yt,{type:"monotone",dataKey:"p99",name:"p99",stroke:"#f87171",dot:!1,isAnimationActive:!1})]})})}),l.jsx("div",{className:"analysis-section-title",children:"Fill-completion latency (ms)"}),l.jsx("div",{className:"analysis-chart-wrap",children:l.jsx(Nn,{width:"100%",height:200,children:l.jsxs(Ti,{data:r,margin:{top:4,right:16,bottom:4,left:8},children:[l.jsx(Zr,{strokeDasharray:"3 3",stroke:"var(--border)"}),l.jsx(Kr,{dataKey:"t",tick:{fontSize:11,fill:"var(--text2)"},tickFormatter:S=>`${Math.round(S)}s`}),l.jsx(qr,{tick:{fontSize:11,fill:"var(--text2)"}}),l.jsx(An,{contentStyle:{background:"var(--bg3)",border:"1px solid var(--border)",fontSize:12},labelStyle:{color:"var(--text)"},labelFormatter:S=>`${Math.round(Number(S))}s`}),l.jsx(ga,{wrapperStyle:{fontSize:11}}),l.jsx(yt,{type:"monotone",dataKey:"fc50",name:"p50",stroke:"#4ade80",dot:!1,isAnimationActive:!1}),l.jsx(yt,{type:"monotone",dataKey:"fc99",name:"p99",stroke:"#f87171",dot:!1,isAnimationActive:!1})]})})})]}),u&&(a==null?void 0:a.summary)&&l.jsx(fW,{status:a})]})}function fW({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)",Fw(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)",`${vr(t.response_latency.p50_us)} / ${vr(t.response_latency.p99_us)}`],["Response max (ms)",vr(t.response_latency.max_us)],["Fill-completion p50 / p99 (ms)",`${vr(t.fill_completion_latency.p50_us)} / ${vr(t.fill_completion_latency.p99_us)}`]];return t.amends_sent&&(r.push(["Amends sent / replaced / lost",`${t.amends_sent} / ${t.replaces_received??0} / ${t.amends_lost??0}`]),t.amend_latency&&r.push(["Amend p50 / p99 (ms)",`${vr(t.amend_latency.p50_us)} / ${vr(t.amend_latency.p99_us)}`])),l.jsxs("div",{className:"perf-summary-card",children:[l.jsxs("div",{className:"perf-section-title",children:["Run summary — ",e.status]}),l.jsx("table",{className:"perf-summary-table",children:l.jsx("tbody",{children:r.map(([n,a])=>l.jsxs("tr",{children:[l.jsx("td",{children:n}),l.jsx("td",{className:"mono",children:a})]},n))})})]})}const hW=new Set(["completed","stopped","error"]);function mW(e){if(!e)return"—";try{return new Date(e*1e3).toLocaleTimeString()}catch{return"—"}}function pW({activeRunId:e,onSelect:t,refreshKey:r}){const[n,a]=h.useState([]);h.useEffect(()=>{let o=!0;const s=()=>Z.perf.runs.list().then(u=>{o&&a(u)}).catch(()=>{});s();const c=setInterval(s,2e3);return()=>{o=!1,clearInterval(c)}},[r]);async function i(o,s){o.stopPropagation();try{await Z.perf.runs.stop(s)}catch{}}return n.length===0?l.jsx("div",{className:"perf-runlist-empty",children:"No runs yet. Configure and start one."}):l.jsxs("table",{className:"perf-runlist",children:[l.jsx("thead",{children:l.jsxs("tr",{children:[l.jsx("th",{children:"Name"}),l.jsx("th",{children:"Mode"}),l.jsx("th",{children:"Status"}),l.jsx("th",{children:"Started"}),l.jsx("th",{children:"Elapsed"}),l.jsx("th",{children:"Orders"}),l.jsx("th",{children:"Fill"}),l.jsx("th",{})]})}),l.jsx("tbody",{children:n.map(o=>l.jsxs("tr",{className:o.run_id===e?"perf-run-active":"",onClick:()=>t(o.run_id),children:[l.jsx("td",{children:o.name}),l.jsx("td",{children:o.mode}),l.jsx("td",{children:l.jsx("span",{className:`badge perf-state-${o.status}`,children:o.status})}),l.jsx("td",{children:mW(o.started_at)}),l.jsxs("td",{className:"mono",children:[Math.round(o.elapsed_s),"s"]}),l.jsx("td",{className:"mono",children:o.summary?o.summary.orders_sent.toLocaleString():"—"}),l.jsx("td",{className:"mono",children:o.summary?o.summary.fill_ratio.toFixed(2):"—"}),l.jsx("td",{className:"perf-runlist-actions",children:!hW.has(o.status)&&l.jsx("button",{className:"btn btn-sm btn-danger",onClick:s=>i(s,o.run_id),children:"Stop"})})]},o.run_id))})]})}const Cl=200,On=(e,t,r=120)=>({field:e,headerName:t,width:r,type:"numericColumn",valueFormatter:n=>n.value==null?"":Number(n.value).toLocaleString()}),Ac=(e,t)=>({field:e,headerName:t,width:150,type:"numericColumn",valueFormatter:r=>r.value==null?"":`${Math.round(Number(r.value)).toLocaleString()}`}),vW=[{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},On("qty","Qty",90),On("price","Price",100),Ac("response_latency_us","Resp µs"),Ac("fill_latency_us","Fill µs"),On("fill_qty","Fill qty",100),On("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}],gW=[{field:"scenario_name",headerName:"Scenario",flex:1,minWidth:160},Ac("latency_us","Latency µs"),On("msg_count","Msgs",90),On("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 yW({runId:e}){const t=V(g=>g.theme),[r,n]=h.useState("messages"),[a,i]=h.useState(0),[o,s]=h.useState([]),[c,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"?Z.perf.runs.messages:Z.perf.runs.scenarios)(e,{limit:Cl,offset:a*Cl}).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"?vW:gW,[r]),p=h.useMemo(()=>({sortable:!0,filter:!0,resizable:!0}),[]),v=Math.max(0,Math.ceil(c/Cl)-1),y=t==="dark"?"ag-theme-quartz-dark":"ag-theme-quartz";return l.jsxs("div",{className:"perf-results",children:[l.jsxs("div",{className:"perf-results-toolbar",children:[l.jsxs("div",{className:"perf-results-tabs",children:[l.jsx("button",{className:`btn btn-sm ${r==="messages"?"btn-primary":"btn-ghost"}`,onClick:()=>n("messages"),children:"Messages"}),l.jsx("button",{className:`btn btn-sm ${r==="scenarios"?"btn-primary":"btn-ghost"}`,onClick:()=>n("scenarios"),children:"Scenarios"})]}),l.jsxs("div",{className:"perf-results-pager",children:[l.jsxs("span",{children:[c.toLocaleString()," rows"]}),l.jsx("button",{className:"btn btn-sm btn-ghost",disabled:a<=0||f,onClick:()=>i(g=>Math.max(0,g-1)),children:"‹ Prev"}),l.jsxs("span",{children:["Page ",a+1," / ",v+1]}),l.jsx("button",{className:"btn btn-sm btn-ghost",disabled:a>=v||f,onClick:()=>i(g=>Math.min(v,g+1)),children:"Next ›"})]})]}),l.jsx("div",{className:`${y} perf-results-grid`,children:l.jsx(kc,{rowData:o,columnDefs:m,defaultColDef:p,animateRows:!1,suppressCellFocus:!0})})]})}function bW(){const{theme:e,toggleTheme:t,branding:r,setSessions:n}=V(),a=Gs(),i=r!=null&&r.prefix?`${r.prefix} FIXture`:"FIXture";Pv(),h.useEffect(()=>{Z.sessions.list().then(n).catch(()=>{})},[]);const[o,s]=h.useState("configure"),[c,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:!c},{id:"results",label:"Results",disabled:!c},{id:"history",label:"History"}];return l.jsxs("div",{className:"app perf-page",children:[l.jsxs("header",{className:"app-header",children:[l.jsx("img",{src:a,alt:i,className:"app-logo"}),l.jsx("div",{className:"perf-page-title",children:"Performance Testing"}),l.jsxs("div",{className:"header-right",children:[l.jsx(Yr,{to:"/",className:"btn btn-sm btn-ghost",children:"← Sessions"}),l.jsx("button",{className:"btn-icon theme-toggle",onClick:t,title:"Toggle theme",children:e==="dark"?"☀":"🌙"})]})]}),l.jsx("div",{className:"perf-tabbar",children:v.map(y=>l.jsx("button",{className:`perf-tab${o===y.id?" active":""}`,disabled:y.disabled,onClick:()=>s(y.id),children:y.label},y.id))}),l.jsxs("div",{className:"perf-body",children:[o==="configure"&&l.jsx("div",{className:"perf-pane perf-pane-form",children:l.jsx(iW,{onStarted:m})}),o==="live"&&c&&l.jsx("div",{className:"perf-pane",children:l.jsx(dW,{runId:c},c)}),o==="results"&&c&&l.jsx("div",{className:"perf-pane",children:l.jsx(yW,{runId:c},c)}),o==="history"&&l.jsx("div",{className:"perf-pane",children:l.jsx(pW,{activeRunId:c,onSelect:p,refreshKey:f})})]})]})}function xW(){return V(t=>t.token)?l.jsx(hv,{}):l.jsx(Ri,{to:"/login",replace:!0})}function wW(){const e=V(t=>t.user);return e?e.role!=="platform_admin"?l.jsx(Ri,{to:"/",replace:!0}):l.jsx(hv,{}):l.jsx(Ri,{to:"/login",replace:!0})}function jW(){const e=V(t=>t.setBranding);h.useEffect(()=>{fetch("/api/branding").then(t=>t.json()).then(t=>{e(t),Rw(t)}).catch(()=>{})},[])}function SW(){const e=on(),t=V(r=>r.theme);return jW(),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(()=>{})},[]),l.jsxs(Kj,{children:[l.jsx(Lt,{path:"/setup",element:l.jsx(Vz,{})}),l.jsx(Lt,{path:"/login",element:l.jsx(Xz,{})}),l.jsx(Lt,{path:"/register",element:l.jsx(Jz,{})}),l.jsxs(Lt,{element:l.jsx(xW,{}),children:[l.jsx(Lt,{path:"/",element:l.jsx(Yz,{})}),l.jsxs(Lt,{element:l.jsx(wW,{}),children:[l.jsx(Lt,{path:"/admin/users",element:l.jsx(nW,{})}),l.jsx(Lt,{path:"/perf",element:l.jsx(bW,{})})]})]}),l.jsx(Lt,{path:"*",element:l.jsx(Ri,{to:"/",replace:!0})})]})}El.createRoot(document.getElementById("root")).render(l.jsx(Vp.StrictMode,{children:l.jsx(gS,{children:l.jsx(SW,{})})}));
|
|
102
|
+
}`;function qz({onClose:e}){const[t,r]=h.useState(""),[n,a]=h.useState(""),[i,o]=h.useState(!1);h.useEffect(()=>{Z.specOverlay.get().then(c=>r(JSON.stringify(c,null,2))).catch(console.error)},[]);async function s(){a("");let c;try{c=JSON.parse(t)}catch{a("Invalid JSON");return}o(!0);try{await Z.specOverlay.set(c),e()}catch(u){a(u instanceof Error?u.message:"Save failed")}finally{o(!1)}}return l.jsx("div",{className:"modal-overlay",onClick:e,children:l.jsxs("div",{className:"modal",style:{width:660,maxHeight:"85vh",display:"flex",flexDirection:"column"},onClick:c=>c.stopPropagation(),children:[l.jsxs("div",{className:"modal-header",children:[l.jsx("span",{children:"Spec Overlay — Custom Message Types"}),l.jsx("button",{className:"btn-icon",onClick:e,children:"✕"})]}),l.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."}),l.jsx("div",{style:{flex:1,display:"flex",flexDirection:"column",padding:"8px 16px",minHeight:0},children:l.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:c=>r(c.target.value),spellCheck:!1,placeholder:Kz})}),l.jsxs("div",{style:{padding:"0 16px 8px",fontSize:11,color:"var(--text2)"},children:["Schema: ",l.jsx("code",{children:'{ "messages": [{ "msg_type", "name", "category", "fields": [{ "tag", "required" }] }] }'})]}),n&&l.jsx("div",{className:"tpl-error",style:{margin:"0 16px 8px"},children:n}),l.jsxs("div",{className:"modal-footer",children:[l.jsx("button",{className:"btn btn-ghost",onClick:e,children:"Cancel"}),l.jsx("button",{className:"btn btn-primary",onClick:s,disabled:i,children:i?"Saving…":"Save overlay"})]})]})})}const Mw="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgdmlld0JveD0iMCAwIDY4MCAxNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgCgogIDxwb2x5Z29uIHBvaW50cz0iMTg4LDI4IDIzOCwyOCAyNTgsNzggMjM4LDEyOCAxODgsMTI4IiBmaWxsPSIjMWEzYTVjIiBzdHlsZT0iZmlsbDpyZ2IoMjYsIDU4LCA5Mik7c3Ryb2tlOm5vbmU7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGNpcmNsZSBjeD0iMjA3IiBjeT0iNzgiIHI9IjEwIiBmaWxsPSIjMDBiNGM4IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMTgwLCAyMDApO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxjaXJjbGUgY3g9IjIwNyIgY3k9Ijc4IiByPSI2IiBmaWxsPSIjMWEzYTVjIiBzdHlsZT0iZmlsbDpyZ2IoMjYsIDU4LCA5Mik7c3Ryb2tlOm5vbmU7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9IjY1IiB4Mj0iMjQ4IiB5Mj0iNjUiIHN0cm9rZT0iIzAwYjRjOCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0eWxlPSJmaWxsOnJnYigwLCAwLCAwKTtzdHJva2U6cmdiKDAsIDE4MCwgMjAwKTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjRweDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9Ijc4IiB4Mj0iMjUyIiB5Mj0iNzgiIHN0cm9rZT0iIzAwYjRjOCIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9IjAuNyIgc3R5bGU9ImZpbGw6cmdiKDAsIDAsIDApO3N0cm9rZTpyZ2IoMCwgMTgwLCAyMDApO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6NHB4O3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjAuNztmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxsaW5lIHgxPSIyMjAiIHkxPSI5MSIgeDI9IjI0NCIgeTI9IjkxIiBzdHJva2U9IiMwMGI0YzgiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBvcGFjaXR5PSIwLjQ1IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMCwgMCk7c3Ryb2tlOnJnYigwLCAxODAsIDIwMCk7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDo0cHg7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MC40NTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgoKICA8dGV4dCB4PSIyNzIiIHk9IjEwNSIgZm9udC1mYW1pbHk9InZhcigtLWZvbnQtbW9ubykiIGZvbnQtc2l6ZT0iNjYiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMwMGI0YzgiIGxldHRlci1zcGFjaW5nPSItMSIgc3R5bGU9ImZpbGw6cmdiKDAsIDE4MCwgMjAwKTtzdHJva2U6bm9uZTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjE7Zm9udC1mYW1pbHk6dWktbW9ub3NwYWNlLCBtb25vc3BhY2U7Zm9udC1zaXplOjY2cHg7Zm9udC13ZWlnaHQ6NzAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iPkZJWDwvdGV4dD4KICA8dGV4dCB4PSIzODYiIHk9IjEwNSIgZm9udC1mYW1pbHk9InZhcigtLWZvbnQtc2FucykiIGZvbnQtc2l6ZT0iNTAiIGZvbnQtd2VpZ2h0PSIzMDAiIGxldHRlci1zcGFjaW5nPSItMSIgc3R5bGU9ImZpbGw6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6NTBweDtmb250LXdlaWdodDozMDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byI+dHVyZTwvdGV4dD4KPC9zdmc+",Dw="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgdmlld0JveD0iMCAwIDY4MCAxNjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CgogIDxwb2x5Z29uIHBvaW50cz0iMTg4LDI4IDIzOCwyOCAyNTgsNzggMjM4LDEyOCAxODgsMTI4IiBmaWxsPSIjMDBiNGM4IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMTgwLCAyMDApO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxjaXJjbGUgY3g9IjIwNyIgY3k9Ijc4IiByPSIxMCIgZmlsbD0iIzFhM2E1YyIgc3R5bGU9ImZpbGw6cmdiKDI2LCA1OCwgOTIpO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxjaXJjbGUgY3g9IjIwNyIgY3k9Ijc4IiByPSI2IiBmaWxsPSIjMDBiNGM4IiBzdHlsZT0iZmlsbDpyZ2IoMCwgMTgwLCAyMDApO3N0cm9rZTpub25lO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MTtmb250LWZhbWlseTomcXVvdDtBbnRocm9waWMgU2FucyZxdW90OywgLWFwcGxlLXN5c3RlbSwgQmxpbmtNYWNTeXN0ZW1Gb250LCAmcXVvdDtTZWdvZSBVSSZxdW90Oywgc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo0MDA7dGV4dC1hbmNob3I6c3RhcnQ7ZG9taW5hbnQtYmFzZWxpbmU6YXV0byIvPgogIDxsaW5lIHgxPSIyMjAiIHkxPSI2NSIgeDI9IjI0OCIgeTI9IjY1IiBzdHJva2U9IiMxYTNhNWMiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHlsZT0iZmlsbDpyZ2IoMCwgMCwgMCk7c3Ryb2tlOnJnYigyNiwgNTgsIDkyKTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjRweDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46bWl0ZXI7b3BhY2l0eToxO2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9Ijc4IiB4Mj0iMjUyIiB5Mj0iNzgiIHN0cm9rZT0iIzFhM2E1YyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9IjAuNyIgc3R5bGU9ImZpbGw6cmdiKDAsIDAsIDApO3N0cm9rZTpyZ2IoMjYsIDU4LCA5Mik7Y29sb3I6cmdiKDI1NSwgMjU1LCAyNTUpO3N0cm9rZS13aWR0aDo0cHg7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO29wYWNpdHk6MC43O2ZvbnQtZmFtaWx5OiZxdW90O0FudGhyb3BpYyBTYW5zJnF1b3Q7LCAtYXBwbGUtc3lzdGVtLCBCbGlua01hY1N5c3RlbUZvbnQsICZxdW90O1NlZ29lIFVJJnF1b3Q7LCBzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OjQwMDt0ZXh0LWFuY2hvcjpzdGFydDtkb21pbmFudC1iYXNlbGluZTphdXRvIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9IjkxIiB4Mj0iMjQ0IiB5Mj0iOTEiIHN0cm9rZT0iIzFhM2E1YyIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9IjAuNDUiIHN0eWxlPSJmaWxsOnJnYigwLCAwLCAwKTtzdHJva2U6cmdiKDI2LCA1OCwgOTIpO2NvbG9yOnJnYigyNTUsIDI1NSwgMjU1KTtzdHJva2Utd2lkdGg6NHB4O3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjAuNDU7Zm9udC1mYW1pbHk6JnF1b3Q7QW50aHJvcGljIFNhbnMmcXVvdDssIC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgJnF1b3Q7U2Vnb2UgVUkmcXVvdDssIHNhbnMtc2VyaWY7Zm9udC1zaXplOjE2cHg7Zm9udC13ZWlnaHQ6NDAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iLz4KCiAgPHRleHQgeD0iMjcyIiB5PSIxMDUiIGZvbnQtZmFtaWx5PSJ2YXIoLS1mb250LW1vbm8pIiBmb250LXNpemU9IjY2IiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWEzYTVjIiBsZXR0ZXItc3BhY2luZz0iLTEiIHN0eWxlPSJmaWxsOnJnYigyNiwgNTgsIDkyKTtzdHJva2U6bm9uZTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjE7Zm9udC1mYW1pbHk6dWktbW9ub3NwYWNlLCBtb25vc3BhY2U7Zm9udC1zaXplOjY2cHg7Zm9udC13ZWlnaHQ6NzAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iPkZJWDwvdGV4dD4KICA8dGV4dCB4PSIzODYiIHk9IjEwNSIgZm9udC1mYW1pbHk9InZhcigtLWZvbnQtc2FucykiIGZvbnQtc2l6ZT0iNTAiIGZvbnQtd2VpZ2h0PSIzMDAiIGZpbGw9IiMwMGI0YzgiIGxldHRlci1zcGFjaW5nPSItMSIgc3R5bGU9ImZpbGw6cmdiKDAsIDE4MCwgMjAwKTtzdHJva2U6bm9uZTtjb2xvcjpyZ2IoMjU1LCAyNTUsIDI1NSk7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtvcGFjaXR5OjE7Zm9udC1mYW1pbHk6JnF1b3Q7QW50aHJvcGljIFNhbnMmcXVvdDssIHNhbnMtc2VyaWY7Zm9udC1zaXplOjUwcHg7Zm9udC13ZWlnaHQ6MzAwO3RleHQtYW5jaG9yOnN0YXJ0O2RvbWluYW50LWJhc2VsaW5lOmF1dG8iPnR1cmU8L3RleHQ+Cjwvc3ZnPg==";function Gs(){const e=V(r=>r.theme),t=V(r=>r.branding);return e==="dark"?(t==null?void 0:t.logo_dark)||Mw:(t==null?void 0:t.logo_light)||Dw}function Yz(){const e=V(g=>g.setSessions),t=V(g=>g.theme),r=V(g=>g.toggleTheme),n=V(g=>g.user),a=V(g=>g.logout),i=V(g=>g.openTabs),o=V(g=>g.activeTabId),s=V(g=>g.branding),c=Gs(),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 Pv(),h.useEffect(()=>{document.documentElement.setAttribute("data-theme",t)},[t]),h.useEffect(()=>{Z.sessions.list().then(e).catch(console.error)},[]),h.useEffect(()=>{if(!n){a_();return}Z.customTags.list().then(Ov).catch(console.error)},[n]),l.jsxs("div",{className:"app",children:[l.jsxs("header",{className:"app-header",children:[l.jsx("img",{src:c,alt:u,className:"app-logo"}),l.jsxs("div",{className:"header-right",children:[n&&l.jsx("span",{className:"header-user",children:n.username}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>d(!0),children:"Scenarios"}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p(!0),children:"Custom Tags"}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>y(!0),children:"Spec Overlay"}),(n==null?void 0:n.role)==="platform_admin"&&l.jsx(Yr,{to:"/perf",className:"btn btn-sm btn-ghost",title:"Performance testing",children:"Perf"}),(n==null?void 0:n.role)==="platform_admin"&&l.jsx(Yr,{to:"/admin/users",className:"btn-icon",title:"Admin",children:"⚙"}),l.jsx("button",{className:"btn-icon theme-toggle",onClick:r,title:"Toggle theme",children:t==="dark"?"☀":"🌙"}),l.jsx("button",{className:"btn-icon",onClick:a,title:"Sign out",children:"⎋"})]})]}),l.jsxs("div",{className:"app-body",children:[l.jsx(y_,{}),l.jsx("div",{className:"tab-area",children:i.length===0?l.jsx("div",{className:"tab-area-empty",children:"Select a session to open it."}):l.jsxs(l.Fragment,{children:[l.jsx(kz,{}),l.jsx("div",{className:"tab-panels",children:i.map(g=>l.jsx("div",{className:`tab-panel${g===o?" active":""}`,children:l.jsx(Ez,{sessionId:g})},g))})]})})]}),f&&l.jsx(zz,{onClose:()=>d(!1)}),m&&l.jsx(Zz,{onClose:()=>p(!1)}),v&&l.jsx(qz,{onClose:()=>y(!1)})]})}function Xz(){const[e,t]=h.useState(""),[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),[c,u]=h.useState(!1),{login:f}=V(),d=Gs(),m=on();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 l.jsx("div",{className:"auth-page",children:l.jsxs("div",{className:"auth-card",children:[l.jsx("img",{src:d,alt:"FIXture",className:"auth-logo"}),l.jsx("h2",{children:"Sign in"}),l.jsxs("form",{onSubmit:p,className:"auth-form",children:[l.jsxs("label",{children:["Username",l.jsx("input",{type:"text",value:e,onChange:v=>t(v.target.value),autoComplete:"username",required:!0})]}),l.jsxs("label",{children:["Password",l.jsx("input",{type:"password",value:r,onChange:v=>n(v.target.value),autoComplete:"current-password",required:!0})]}),a&&l.jsx("p",{className:"auth-error",children:a}),l.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Signing in…":"Sign in"})]}),c&&l.jsxs("p",{className:"auth-hint",style:{marginTop:12},children:["No account? ",l.jsx(Yr,{to:"/register",children:"Register"})]})]})})}function Vz(){const[e,t]=h.useState(""),[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),c=Gs(),u=on();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 l.jsx("div",{className:"auth-page",children:l.jsxs("div",{className:"auth-card",children:[l.jsx("img",{src:c,alt:"FIXture",className:"auth-logo"}),l.jsx("h2",{children:"Create admin account"}),l.jsx("p",{className:"auth-hint",children:"First-time setup — create the platform admin."}),l.jsxs("form",{onSubmit:f,className:"auth-form",children:[l.jsxs("label",{children:["Username",l.jsx("input",{type:"text",value:e,onChange:d=>t(d.target.value),autoComplete:"username",required:!0})]}),l.jsxs("label",{children:["Password",l.jsx("input",{type:"password",value:r,onChange:d=>n(d.target.value),autoComplete:"new-password",minLength:8,required:!0})]}),a&&l.jsx("p",{className:"auth-error",children:a}),l.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Creating…":"Create admin"})]})]})})}function Jz(){const[e,t]=h.useState(""),[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),[c,u]=h.useState(null),{login:f,theme:d}=V(),m=on();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=c&&c.current_users>=c.max_users;return l.jsx("div",{className:"auth-page",children:l.jsxs("div",{className:"auth-card",children:[l.jsx("img",{src:d==="dark"?Mw:Dw,alt:"FIXture",className:"auth-logo"}),l.jsx("h2",{children:"Create account"}),c===null&&l.jsx("p",{className:"auth-hint",children:"Checking registration status…"}),c&&!c.enabled&&l.jsx("p",{className:"auth-error",children:"Registration is currently closed. Contact an administrator."}),c&&c.enabled&&v&&l.jsx("p",{className:"auth-error",children:"Registration limit reached. Contact an administrator."}),c&&c.enabled&&!v&&l.jsxs("form",{onSubmit:p,className:"auth-form",children:[l.jsxs("label",{children:["Username",l.jsx("input",{type:"text",value:e,onChange:y=>t(y.target.value),autoComplete:"username",required:!0})]}),l.jsxs("label",{children:["Password",l.jsx("input",{type:"password",value:r,onChange:y=>n(y.target.value),autoComplete:"new-password",minLength:8,required:!0})]}),a&&l.jsx("p",{className:"auth-error",children:a}),l.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Creating account…":"Create account"})]}),l.jsxs("p",{className:"auth-hint",style:{marginTop:12},children:["Already have an account? ",l.jsx(Yr,{to:"/login",children:"Sign in"})]})]})})}const Qz=["accent","bg","bg2","bg3","border","text","text2"];function Rw(e){let t=document.getElementById("branding-style");t||(t=document.createElement("style"),t.id="branding-style",document.head.appendChild(t));const r=Qz.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 Ol(e){return new Promise((t,r)=>{const n=new FileReader;n.onload=()=>t(n.result),n.onerror=r,n.readAsDataURL(e)})}const $w=["user","user_admin","platform_admin"];function eW(e){return new Date(e*1e3).toLocaleString()}function tW({onClose:e,onCreated:t}){const[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState("user"),[c,u]=h.useState(""),[f,d]=h.useState(!1);async function m(p){p.preventDefault(),u(""),d(!0);try{const v=await Z.admin.users.create({username:r,password:a,role:o});t(v),e()}catch(v){u(v.message)}finally{d(!1)}}return l.jsx("div",{className:"modal-overlay",onClick:e,children:l.jsxs("div",{className:"modal",onClick:p=>p.stopPropagation(),children:[l.jsxs("div",{className:"modal-header",children:[l.jsx("span",{children:"Create User"}),l.jsx("button",{className:"btn-icon",onClick:e,children:"✕"})]}),l.jsxs("form",{onSubmit:m,children:[l.jsxs("div",{className:"form-grid",children:[l.jsx("label",{children:"Username"}),l.jsx("input",{value:r,onChange:p=>n(p.target.value),required:!0}),l.jsx("label",{children:"Password"}),l.jsx("input",{type:"password",value:a,onChange:p=>i(p.target.value),minLength:8,required:!0}),l.jsx("label",{children:"Role"}),l.jsx("select",{value:o,onChange:p=>s(p.target.value),children:$w.map(p=>l.jsx("option",{value:p,children:p},p))})]}),c&&l.jsx("div",{className:"error-msg",children:c}),l.jsxs("div",{className:"modal-footer",children:[l.jsx("button",{type:"button",className:"btn btn-secondary",onClick:e,children:"Cancel"}),l.jsx("button",{type:"submit",className:"btn btn-primary",disabled:f,children:f?"Creating…":"Create"})]})]})]})})}function rW({user:e,onClose:t}){const[r,n]=h.useState(""),[a,i]=h.useState(""),[o,s]=h.useState(!1),[c,u]=h.useState(!1);async function f(d){d.preventDefault(),i(""),s(!0);try{await Z.admin.users.update(e.uid,{password:r}),u(!0)}catch(m){i(m.message)}finally{s(!1)}}return l.jsx("div",{className:"modal-overlay",onClick:t,children:l.jsxs("div",{className:"modal",onClick:d=>d.stopPropagation(),children:[l.jsxs("div",{className:"modal-header",children:[l.jsxs("span",{children:["Reset Password — ",e.username]}),l.jsx("button",{className:"btn-icon",onClick:t,children:"✕"})]}),c?l.jsxs("div",{style:{padding:16},children:[l.jsx("p",{style:{color:"var(--green)"},children:"Password updated successfully."}),l.jsx("div",{className:"modal-footer",children:l.jsx("button",{className:"btn btn-primary",onClick:t,children:"Close"})})]}):l.jsxs("form",{onSubmit:f,children:[l.jsxs("div",{className:"form-grid",children:[l.jsx("label",{children:"New Password"}),l.jsx("input",{type:"password",value:r,onChange:d=>n(d.target.value),minLength:8,required:!0})]}),a&&l.jsx("div",{className:"error-msg",children:a}),l.jsxs("div",{className:"modal-footer",children:[l.jsx("button",{type:"button",className:"btn btn-secondary",onClick:t,children:"Cancel"}),l.jsx("button",{type:"submit",className:"btn btn-primary",disabled:o,children:o?"Saving…":"Save"})]})]})]})})}function nW(){const e=V(k=>k.user),t=on(),[r,n]=h.useState([]),[a,i]=h.useState(null),[o,s]=h.useState(null),[c,u]=h.useState(null),[f,d]=h.useState(!1),[m,p]=h.useState(null),[v,y]=h.useState(!1),{setBranding:g}=V(),[b,x]=h.useState(!1),[w,_]=h.useState(null),[S,j]=h.useState("");async function O(){try{const[k,$,W,G]=await Promise.all([Z.admin.users.list(),Z.admin.settings.get(),Z.admin.housekeeping.get(),Z.admin.branding.get()]);n(k),i($),s(W),p(G)}catch(k){j(k.message)}}h.useEffect(()=>{O()},[]);async function N(k){try{const $=await Z.admin.users.update(k.uid,{is_active:!k.is_active});n(W=>W.map(G=>G.uid===k.uid?$:G))}catch($){j($.message)}}async function A(k,$){try{const W=await Z.admin.users.update(k.uid,{role:$});n(G=>G.map(J=>J.uid===k.uid?W:J))}catch(W){j(W.message)}}async function I(k){if(confirm(`Delete user "${k.username}"? This cannot be undone.`))try{await Z.admin.users.delete(k.uid),n($=>$.filter(W=>W.uid!==k.uid))}catch($){j($.message)}}async function T(){if(a)try{const k=await Z.admin.settings.update({registration_enabled:!a.enabled});i(k)}catch(k){j(k.message)}}async function C(k){const $=parseInt(k,10);if(!(isNaN($)||$<1))try{const W=await Z.admin.settings.update({max_users:$});i(W)}catch(W){j(W.message)}}async function U(){if(o)try{const k=await Z.admin.housekeeping.update({enabled:!o.enabled});s(k)}catch(k){j(k.message)}}async function L(k,$){const W=parseInt($,10);if(!(isNaN(W)||W<0))try{const G=await Z.admin.housekeeping.update({[k]:W});s(G)}catch(G){j(G.message)}}async function H(){d(!0),u(null);try{const k=await Z.admin.housekeeping.runNow();u(`Done — deleted ${k.msgs_deleted} messages, ${k.logs_deleted} log files`)}catch(k){j(k.message)}finally{d(!1)}}return l.jsxs("div",{className:"admin-page",children:[l.jsxs("div",{className:"admin-header",children:[l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[l.jsx("button",{className:"btn-icon",onClick:()=>t("/"),title:"Back",children:"←"}),l.jsx("h1",{children:"User Management"})]}),l.jsx("button",{className:"btn btn-primary btn-sm",onClick:()=>x(!0),children:"+ Create User"})]}),S&&l.jsx("div",{className:"error-msg",style:{margin:"0 0 12px"},children:S}),a&&l.jsxs("div",{className:"admin-settings-bar",children:[l.jsx("span",{className:"admin-settings-label",children:"Self-registration:"}),l.jsx("button",{className:`btn btn-sm ${a.enabled?"btn-stop":"btn-start"}`,onClick:T,children:a.enabled?"Enabled — click to disable":"Disabled — click to enable"}),l.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Max users:"}),l.jsx("input",{type:"number",className:"admin-max-users-input",defaultValue:a.max_users,min:1,onBlur:k=>C(k.target.value)}),l.jsxs("span",{className:"admin-settings-label",style:{color:"var(--text2)"},children:["(",a.current_users," / ",a.max_users," registered)"]})]}),o&&l.jsxs("div",{className:"admin-settings-bar",children:[l.jsx("span",{className:"admin-settings-label",children:"Housekeeping:"}),l.jsx("button",{className:`btn btn-sm ${o.enabled?"btn-stop":"btn-start"}`,onClick:U,children:o.enabled?"Auto — click to disable":"Manual only — click to enable"}),l.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Keep messages (days):"}),l.jsx("input",{type:"number",className:"admin-max-users-input",defaultValue:o.msg_retention_days,min:0,title:"0 = keep forever",onBlur:k=>L("msg_retention_days",k.target.value)}),l.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Keep logs (days):"}),l.jsx("input",{type:"number",className:"admin-max-users-input",defaultValue:o.log_retention_days,min:0,title:"0 = keep forever",onBlur:k=>L("log_retention_days",k.target.value)}),l.jsx("button",{className:"btn btn-sm btn-ghost",style:{marginLeft:16},onClick:H,disabled:f,children:f?"Running…":"Run Now"}),c&&l.jsx("span",{style:{marginLeft:8,color:"var(--green)",fontSize:12},children:c})]}),m&&l.jsxs("div",{className:"admin-settings-bar",style:{flexDirection:"column",alignItems:"flex-start",gap:10},children:[l.jsx("span",{className:"admin-settings-label",style:{fontWeight:700},children:"Branding"}),l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[l.jsx("span",{className:"admin-settings-label",children:"Brand prefix:"}),l.jsx("input",{className:"admin-max-users-input",style:{width:160},value:m.prefix,placeholder:"e.g. Acme (shown as Acme FIXture)",onChange:k=>p({...m,prefix:k.target.value})}),l.jsx("span",{className:"admin-settings-label",style:{marginLeft:16},children:"Accent colour:"}),l.jsx("input",{type:"color",value:m.accent||"#388bfd",onChange:k=>p({...m,accent:k.target.value}),title:"Primary accent colour"}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,accent:""}),title:"Reset to default blue",children:"Reset"})]}),l.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(([k,$,W])=>l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:4},children:[l.jsxs("span",{className:"admin-settings-label",children:[$,":"]}),l.jsx("input",{type:"color",value:m[k]||W,onChange:G=>p({...m,[k]:G.target.value}),title:$}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,[k]:""}),title:"Reset to default",children:"↺"})]},k))}),l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[l.jsx("span",{className:"admin-settings-label",children:"Logo (dark theme):"}),m.logo_dark&&l.jsx("img",{src:m.logo_dark,style:{height:28,background:"#161b22",borderRadius:4,padding:"2px 6px"},alt:"dark logo preview"}),l.jsx("input",{type:"file",accept:"image/*",onChange:async k=>{var W;const $=(W=k.target.files)==null?void 0:W[0];$&&p({...m,logo_dark:await Ol($)})}}),m.logo_dark&&l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,logo_dark:""}),children:"Clear"})]}),l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[l.jsx("span",{className:"admin-settings-label",children:"Logo (light theme):"}),m.logo_light&&l.jsx("img",{src:m.logo_light,style:{height:28,background:"#f0f2f5",borderRadius:4,padding:"2px 6px"},alt:"light logo preview"}),l.jsx("input",{type:"file",accept:"image/*",onChange:async k=>{var W;const $=(W=k.target.files)==null?void 0:W[0];$&&p({...m,logo_light:await Ol($)})}}),m.logo_light&&l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,logo_light:""}),children:"Clear"})]}),l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[l.jsx("span",{className:"admin-settings-label",children:"Favicon:"}),m.favicon&&l.jsx("img",{src:m.favicon,style:{height:20,width:20},alt:"favicon preview"}),l.jsx("input",{type:"file",accept:"image/png,image/svg+xml,image/x-icon",onChange:async k=>{var W;const $=(W=k.target.files)==null?void 0:W[0];$&&p({...m,favicon:await Ol($)})}}),m.favicon&&l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>p({...m,favicon:""}),children:"Clear"})]}),l.jsx("button",{className:"btn btn-sm btn-primary",disabled:v,onClick:async()=>{y(!0);try{const k=await Z.admin.branding.update(m);p(k),g(k),Rw(k)}catch(k){j(k.message)}finally{y(!1)}},children:v?"Saving…":"Save Branding"})]}),l.jsxs("table",{className:"admin-table",children:[l.jsx("thead",{children:l.jsxs("tr",{children:[l.jsx("th",{children:"Username"}),l.jsx("th",{children:"Role"}),l.jsx("th",{children:"Status"}),l.jsx("th",{children:"Created"}),l.jsx("th",{children:"Actions"})]})}),l.jsx("tbody",{children:r.map(k=>l.jsxs("tr",{className:k.is_active?"":"admin-row-inactive",children:[l.jsxs("td",{children:[k.username,k.uid===(e==null?void 0:e.uid)&&l.jsx("span",{className:"admin-you-badge",children:" (you)"})]}),l.jsx("td",{children:l.jsx("select",{value:k.role,onChange:$=>A(k,$.target.value),className:"admin-role-select",disabled:k.uid===(e==null?void 0:e.uid),children:$w.map($=>l.jsx("option",{value:$,children:$},$))})}),l.jsx("td",{children:l.jsx("span",{style:{color:k.is_active?"var(--green)":"var(--text2)"},children:k.is_active?"Active":"Inactive"})}),l.jsx("td",{style:{color:"var(--text2)",fontSize:11},children:eW(k.created_at)}),l.jsx("td",{children:l.jsxs("div",{className:"admin-row-actions",children:[l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>N(k),disabled:k.uid===(e==null?void 0:e.uid),title:k.is_active?"Deactivate":"Activate",children:k.is_active?"Deactivate":"Activate"}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>_(k),title:"Reset password",children:"Reset PW"}),l.jsx("button",{className:"btn btn-sm btn-danger",onClick:()=>I(k),disabled:k.uid===(e==null?void 0:e.uid),title:"Delete user",children:"✕"})]})})]},k.uid))})]}),b&&l.jsx(tW,{onClose:()=>x(!1),onCreated:k=>n($=>[...$,k])}),w&&l.jsx(rW,{user:w,onClose:()=>_(null)})]})}const Ec={enabled:!1,every_n_fills:1,amend_ratio:1,max_per_order:1,price_offset:.01,pending_replace:!1},Kp={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},amend:Ec,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:{}}},aW=["loopback","manager","client","venue"];function qp(e,t){return t?Math.round(e/t*1e3):0}function iW({onStarted:e}){var ea,D;const t=V(P=>P.sessions),[r,n]=h.useState(Kp),[a,i]=h.useState(Kp.payload.order.symbols.join(", ")),[o,s]=h.useState(!1),[c,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",_=r.mode==="loopback"||r.mode==="manager"||r.mode==="venue",S=r.amend??Ec,j=r.client_session_id?t[r.client_session_id]:void 0,O=r.venue_session_id?t[r.venue_session_id]:void 0,N=!w||(j==null?void 0:j.status)==="LOGGED_ON",A=!_||(O==null?void 0:O.status)==="LOGGED_ON",I=(!w||!!r.client_session_id)&&(!_||!!r.venue_session_id),T=(()=>{if(!r.name.trim())return"Name is required";const P=r.rate;if(P.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(P.orders_per_window<1||P.order_window_ms<1)return"Order rate must be > 0";if(a.split(",").map(it=>it.trim()).filter(Boolean).length===0)return"At least one symbol is required";const B=r.payload.order;if(B.quantity_min<1||B.quantity_max<1||B.quantity_min>B.quantity_max)return"Check quantity min/max";if(B.price_min<=0||B.price_max<=0||B.price_min>B.price_max)return"Check price min/max";if(S.enabled){if(S.every_n_fills<1)return"Amend every-n-fills must be ≥ 1";if(S.amend_ratio<0||S.amend_ratio>1)return"Amend ratio must be between 0 and 1";if(S.max_per_order<0)return"Max amends per order must be ≥ 0";if(S.price_offset<=0)return"Amend price offset must be > 0"}}if(_){if(P.fills_per_window<1||P.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(P.fill_ratio<0||P.fill_ratio>1)return"Fill ratio must be between 0 and 1"}return null})(),C=I&&N&&A&&!T&&!f;async function U(){try{p(await Z.perf.configs.list())}catch{}}h.useEffect(()=>{U()},[]),h.useEffect(()=>{Z.templates.list().then(b).catch(()=>{})},[]);const L=g.filter(P=>P.msg_type==="D"),H=g.filter(P=>P.msg_type==="8"),k=!!r.payload.order_template_id,$=!!r.payload.exec_template_id,W=((ea=L.find(P=>P.id===r.payload.order_template_id))==null?void 0:ea.name)??"selected",G=((D=H.find(P=>P.id===r.payload.exec_template_id))==null?void 0:D.name)??"selected",J=P=>n(B=>({...B,...P})),ae=P=>n(B=>({...B,rate:{...B.rate,...P}})),ee=P=>n(B=>({...B,test:{...B.test,...P}})),me=P=>n(B=>({...B,amend:{...Ec,...B.amend,...P}})),de=P=>n(B=>({...B,payload:{...B.payload,order:{...B.payload.order,...P}}})),Oe=P=>n(B=>({...B,payload:{...B.payload,fill:{...B.payload.fill,...P}}})),R=P=>n(B=>({...B,payload:{...B.payload,...P}})),[ie,ne]=h.useState([]),z=P=>{ne(P),R({gen_rules:Object.fromEntries(P.filter(B=>B.tag.trim()).map(B=>[B.tag.trim(),B.value]))})},F=(P,B)=>z(ie.map((it,Er)=>Er===P?{...it,...B}:it));function Q(){const P=a.split(",").map(B=>B.trim()).filter(Boolean);return{...r,client_session_id:w?r.client_session_id:null,venue_session_id:_?r.venue_session_id:null,payload:{...r.payload,order:{...r.payload.order,symbols:P}}}}async function M(){u(""),d(!0);try{const{run_id:P}=await Z.perf.runs.create(Q());e(P)}catch(P){u(P.message??"Failed to start run")}finally{d(!1)}}async function Ie(){u("");try{await Z.perf.configs.save(Q()),await U()}catch(P){u(P.message??"Failed to save config")}}async function se(P){if(y(P),!!P){u("");try{const B=await Z.perf.configs.get(P);n(B),i(B.payload.order.symbols.join(", ")),ne(Object.entries(B.payload.gen_rules??{}).map(([it,Er])=>({tag:it,value:Er})))}catch(B){u(B.message??"Failed to load config")}}}async function ze(){if(v&&window.confirm("Delete this saved config?"))try{await Z.perf.configs.delete(v),y(""),await U()}catch(P){u(P.message??"Failed to delete config")}}const Te=P=>{const B=t[P];return B?`${B.display_name||`${B.sender_comp_id} → ${B.target_comp_id}`} (${B.status})`:P},_t=x.filter(P=>P.session_role!=="venue"),mn=x.filter(P=>P.session_role!=="client");return l.jsxs("div",{className:"perf-form",children:[c&&l.jsx("div",{className:"error-msg",children:c}),l.jsxs("div",{className:"perf-form-section",children:[l.jsx("div",{className:"perf-section-title",children:"Saved configs"}),l.jsxs("div",{className:"perf-field-row",children:[l.jsxs("select",{value:v,onChange:P=>se(P.target.value),children:[l.jsx("option",{value:"",children:"(load a saved config…)"}),m.map(P=>l.jsxs("option",{value:P.config_id,children:[P.name," — ",P.mode]},P.config_id))]}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:Ie,children:"Save current"}),l.jsx("button",{className:"btn btn-sm btn-ghost btn-danger-ghost",onClick:ze,disabled:!v,children:"Delete"})]})]}),l.jsxs("div",{className:"perf-form-section",children:[l.jsx("div",{className:"perf-section-title",children:"Run"}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Name"}),l.jsx("input",{value:r.name,onChange:P=>J({name:P.target.value})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Mode"}),l.jsx("select",{value:r.mode,onChange:P=>J({mode:P.target.value}),children:aW.map(P=>l.jsx("option",{value:P,children:P},P))})]}),w&&l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Client session"}),l.jsxs("select",{value:r.client_session_id??"",onChange:P=>J({client_session_id:P.target.value||null}),children:[l.jsx("option",{value:"",children:"(select…)"}),_t.map(P=>l.jsx("option",{value:P.session_id,children:Te(P.session_id)},P.session_id))]}),j&&l.jsx(Yp,{status:j.status})]}),_&&l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Venue session"}),l.jsxs("select",{value:r.venue_session_id??"",onChange:P=>J({venue_session_id:P.target.value||null}),children:[l.jsx("option",{value:"",children:"(select…)"}),mn.map(P=>l.jsx("option",{value:P.session_id,children:Te(P.session_id)},P.session_id))]}),O&&l.jsx(Yp,{status:O.status})]})]}),l.jsxs("div",{className:"perf-form-section",children:[l.jsx("div",{className:"perf-section-title",children:"Rate"}),l.jsxs("div",{className:"perf-field-grid",children:[w&&l.jsxs(l.Fragment,{children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Orders / window"}),l.jsx("input",{type:"number",min:1,value:r.rate.orders_per_window,onChange:P=>ae({orders_per_window:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Order window (ms)"}),l.jsx("input",{type:"number",min:1,value:r.rate.order_window_ms,onChange:P=>ae({order_window_ms:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Order dispatch"}),l.jsxs("select",{value:r.rate.dispatch??"smooth",onChange:P=>ae({dispatch:P.target.value}),children:[l.jsx("option",{value:"smooth",children:"smooth (evenly spaced)"}),l.jsx("option",{value:"burst",children:"burst (all at window start)"})]})]})]}),_&&l.jsxs(l.Fragment,{children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Fills / window"}),l.jsx("input",{type:"number",min:1,value:r.rate.fills_per_window,onChange:P=>ae({fills_per_window:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Fill window (ms)"}),l.jsx("input",{type:"number",min:1,value:r.rate.fill_window_ms,onChange:P=>ae({fill_window_ms:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Fill ratio"}),l.jsx("input",{type:"number",min:0,max:1,step:.05,value:r.rate.fill_ratio,onChange:P=>ae({fill_ratio:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Fill dispatch"}),l.jsxs("select",{value:r.rate.fill_dispatch??"smooth",onChange:P=>ae({fill_dispatch:P.target.value}),children:[l.jsx("option",{value:"smooth",children:"smooth (evenly spaced)"}),l.jsx("option",{value:"burst",children:"burst (all at window start)"})]})]})]})]}),l.jsxs("div",{className:"perf-hint",children:[w&&l.jsxs(l.Fragment,{children:["≈ ",qp(r.rate.orders_per_window,r.rate.order_window_ms).toLocaleString()," orders/s"]}),w&&_&&" · ",_&&l.jsxs(l.Fragment,{children:["≈ ",qp(r.rate.fills_per_window,r.rate.fill_window_ms).toLocaleString()," fills/s"]})," ",r.rate.dispatch==="burst"?l.jsxs(l.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."]}):l.jsx(l.Fragment,{children:"Smaller windows smooth the injection."})]})]}),l.jsxs("div",{className:"perf-form-section",children:[l.jsx("div",{className:"perf-section-title",children:"Test"}),l.jsxs("div",{className:"perf-field-grid",children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Duration (s, 0 = no time limit)"}),l.jsx("input",{type:"number",min:0,value:r.test.duration,onChange:P=>ee({duration:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Max orders (0 = no count limit)"}),l.jsx("input",{type:"number",min:0,value:r.test.max_orders,onChange:P=>ee({max_orders:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Order timeout (ms)"}),l.jsx("input",{type:"number",min:1,step:1e3,value:r.test.scenario_timeout_ms,onChange:P=>ee({scenario_timeout_ms:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"On saturation"}),l.jsxs("select",{value:r.test.on_saturation,onChange:P=>ee({on_saturation:P.target.value}),children:[l.jsx("option",{value:"pause",children:"pause (preserves latency accuracy)"}),l.jsx("option",{value:"drop_oldest",children:"drop_oldest"})]})]})]}),w&&l.jsxs("label",{className:"checkbox-label",children:[l.jsx("input",{type:"checkbox",checked:r.test.record_execs,onChange:P=>ee({record_execs:P.target.checked})}),"Record per-exec detail — one CSV row per ack/fill with latency + inter-report gap (downloads as csv.gz after the run)"]}),l.jsxs("div",{className:"perf-hint",children:["An order not fully filled within the order timeout is counted ",l.jsx("strong",{children:"Lost"}),". Raise it if rate-limited fills are being flagged lost on full-fill runs."]})]}),w&&l.jsxs("div",{className:"perf-form-section",children:[l.jsx("div",{className:"perf-section-title",children:"Amends (35=G)"}),l.jsxs("label",{className:"checkbox-label",children:[l.jsx("input",{type:"checkbox",checked:S.enabled,onChange:P=>me({enabled:P.target.checked})}),"Amend working orders — price-only OrderCancelReplaceRequest fired every nth fill"]}),S.enabled&&l.jsxs(l.Fragment,{children:[l.jsxs("div",{className:"perf-field-grid",children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Every n fills"}),l.jsx("input",{type:"number",min:1,value:S.every_n_fills,onChange:P=>me({every_n_fills:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Amend ratio (orders)"}),l.jsx("input",{type:"number",min:0,max:1,step:.05,value:S.amend_ratio,onChange:P=>me({amend_ratio:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Max amends / order (0 = no cap)"}),l.jsx("input",{type:"number",min:0,value:S.max_per_order,onChange:P=>me({max_per_order:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Price offset (±)"}),l.jsx("input",{type:"number",min:1e-4,step:.01,value:S.price_offset,onChange:P=>me({price_offset:Number(P.target.value)})})]})]}),_&&l.jsxs("label",{className:"checkbox-label",children:[l.jsx("input",{type:"checkbox",checked:S.pending_replace,onChange:P=>me({pending_replace:P.target.checked})}),"Pending Replace — venue replies 150=E before 150=5 (two ExecReports per amend)"]}),l.jsx("div",{className:"perf-hint",children:"Amends fire only while the order is still working — never on the final fill, so use partial fills or fills per order > 1. The 35=G keeps the order's correlation id (376); amend rate ≈ fills/s ÷ n. Latency is measured amend-2-replaced (150=5)."}),_&&r.payload.fill.fill_type==="full"&&r.payload.fill.fills_per_order===1&&l.jsxs("div",{className:"perf-hint",children:["⚠ With a single full fill per order there is never a working order to amend — raise ",l.jsx("strong",{children:"Fills per order"})," or use ",l.jsx("strong",{children:"partial"})," fill type."]})]})]}),w&&l.jsxs("div",{className:`perf-form-section ${k?"perf-section-layered":""}`,children:[l.jsxs("div",{className:"perf-section-title",children:["Order payload",k&&l.jsx("span",{className:"perf-layer-badge",children:"values for template"})]}),k&&l.jsxs("div",{className:"perf-hint perf-layer-note",children:["Order template ",l.jsx("strong",{children:W})," defines the message shape. These fields supply the values it references via ",l.jsx("code",{children:"{symbol}"})," ",l.jsx("code",{children:"{side}"})," ",l.jsx("code",{children:"{qty}"})," ",l.jsx("code",{children:"{price}"})," tokens. Value generators (",l.jsx("code",{children:"uuid()"}),", ",l.jsx("code",{children:"random_str(n)"}),", ",l.jsx("code",{children:"random_int(a,b)"}),","," ",l.jsx("code",{children:"seq(width)"}),", ",l.jsx("code",{children:"timestamp()"}),", ",l.jsx("code",{children:"date()"}),") are evaluated fresh per order and mix with text as ",l.jsx("code",{children:"ORD-{seq(8)}"}),"; ClOrdID(11) is always made unique per order. ",l.jsx("strong",{children:"Order type"})," is taken from the template."]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Symbols (comma-separated)"}),l.jsx("input",{value:a,onChange:P=>i(P.target.value)})]}),l.jsxs("div",{className:"perf-field-grid",children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Side"}),l.jsxs("select",{value:r.payload.order.side,onChange:P=>de({side:P.target.value}),children:[l.jsx("option",{value:"alternate",children:"alternate"}),l.jsx("option",{value:"fixed_buy",children:"fixed_buy"}),l.jsx("option",{value:"fixed_sell",children:"fixed_sell"}),l.jsx("option",{value:"random",children:"random"})]})]}),l.jsxs("label",{className:`perf-field ${k?"perf-field-overridden":""}`,children:[l.jsxs("span",{children:["Order type ",k&&l.jsx("em",{className:"perf-field-tag",children:"from template"})]}),l.jsxs("select",{value:r.payload.order.ord_type,disabled:k,onChange:P=>de({ord_type:P.target.value}),children:[l.jsx("option",{value:"limit",children:"limit"}),l.jsx("option",{value:"market",children:"market"})]})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Qty min"}),l.jsx("input",{type:"number",min:1,value:r.payload.order.quantity_min,onChange:P=>de({quantity_min:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Qty max"}),l.jsx("input",{type:"number",min:1,value:r.payload.order.quantity_max,onChange:P=>de({quantity_max:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Price min"}),l.jsx("input",{type:"number",min:0,step:.01,value:r.payload.order.price_min,onChange:P=>de({price_min:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Price max"}),l.jsx("input",{type:"number",min:0,step:.01,value:r.payload.order.price_max,onChange:P=>de({price_max:Number(P.target.value)})})]})]})]}),w&&l.jsxs("div",{className:"perf-form-section",children:[l.jsx("div",{className:"perf-section-title",children:"Send options"}),l.jsxs("div",{className:"perf-field-row",children:[l.jsxs("label",{className:"checkbox-label",children:[l.jsx("input",{type:"checkbox",checked:r.payload.auto_expiry??!0,onChange:P=>R({auto_expiry:P.target.checked})}),"Auto expiry (tags 126 / 432, only if present)"]}),(r.payload.auto_expiry??!0)&&l.jsxs(l.Fragment,{children:[l.jsx("input",{type:"number",min:0,style:{width:64},value:r.payload.expiry_offset??1,onChange:P=>R({expiry_offset:Number(P.target.value)})}),l.jsxs("select",{value:r.payload.expiry_unit??"days",onChange:P=>R({expiry_unit:P.target.value}),children:[l.jsx("option",{value:"minutes",children:"minutes"}),l.jsx("option",{value:"hours",children:"hours"}),l.jsx("option",{value:"days",children:"days"})]}),l.jsx("span",{children:"into the future"})]})]}),ie.map((P,B)=>l.jsxs("div",{className:"perf-field-row",children:[l.jsx("input",{style:{width:72},placeholder:"tag",value:P.tag,onChange:it=>F(B,{tag:it.target.value.replace(/[^0-9]/g,"")})}),l.jsx("span",{children:"="}),l.jsx("input",{style:{flex:1},placeholder:"uuid() / seq(8) / ORD-{seq(8)} / literal",value:P.value,onChange:it=>F(B,{value:it.target.value})}),l.jsx("button",{className:"btn btn-sm btn-ghost btn-danger-ghost",onClick:()=>z(ie.filter((it,Er)=>Er!==B)),children:"✕"})]},B)),l.jsx("div",{className:"perf-field-row",children:l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>z([...ie,{tag:"",value:""}]),children:"+ Add tag rule"})}),l.jsxs("div",{className:"perf-hint",children:["Rules set the tag on every order/scenario message (insert or replace). Generators: ",l.jsx("code",{children:"uuid()"})," · ",l.jsx("code",{children:"random_str(n)"})," · ",l.jsx("code",{children:"random_int(a,b)"})," ·"," ",l.jsx("code",{children:"seq(width[,start])"})," · ",l.jsx("code",{children:"timestamp()"})," · ",l.jsx("code",{children:"date()"})," — mix with text as ",l.jsx("code",{children:"ORD-{seq(8)}"}),". ClOrdID(11) always stays unique per order."]})]}),_&&l.jsxs("div",{className:`perf-form-section ${$?"perf-section-layered":""}`,children:[l.jsxs("div",{className:"perf-section-title",children:["Fill payload",$&&l.jsx("span",{className:"perf-layer-badge",children:"behavior for template"})]}),$&&l.jsxs("div",{className:"perf-hint perf-layer-note",children:["Exec template ",l.jsx("strong",{children:G})," reshapes each report (custom tags). These still control fill ",l.jsx("strong",{children:"behavior"})," — how many reports, the fill ratio, and the ack — while the engine sets the standard exec tags."]}),l.jsxs("div",{className:"perf-field-grid",children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Fill type"}),l.jsxs("select",{value:r.payload.fill.fill_type,onChange:P=>Oe({fill_type:P.target.value}),children:[l.jsx("option",{value:"full",children:"full"}),l.jsx("option",{value:"partial",children:"partial"}),l.jsx("option",{value:"random",children:"random"})]})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Fills per order"}),l.jsx("input",{type:"number",min:1,value:r.payload.fill.fills_per_order,onChange:P=>Oe({fills_per_order:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field perf-field-check",children:[l.jsx("input",{type:"checkbox",checked:r.payload.fill.send_ack,onChange:P=>Oe({send_ack:P.target.checked})}),l.jsx("span",{children:"Send ack before fills (35=8|150=0|39=0)"})]}),r.payload.fill.fill_type!=="full"&&l.jsxs(l.Fragment,{children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Partial % min"}),l.jsx("input",{type:"number",min:1,max:100,value:r.payload.fill.partial_fill_pct_min,onChange:P=>Oe({partial_fill_pct_min:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Partial % max"}),l.jsx("input",{type:"number",min:1,max:100,value:r.payload.fill.partial_fill_pct_max,onChange:P=>Oe({partial_fill_pct_max:Number(P.target.value)})})]})]})]})]}),(w||_)&&l.jsxs("div",{className:"perf-form-section",children:[l.jsx("div",{className:"perf-section-title",children:"Templates (optional)"}),l.jsxs("div",{className:"perf-field-grid",children:[w&&l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Order template"}),l.jsxs("select",{value:r.payload.order_template_id??"",onChange:P=>R({order_template_id:P.target.value||null}),children:[l.jsx("option",{value:"",children:"— built-in —"}),L.map(P=>l.jsx("option",{value:P.id,children:P.name},P.id))]})]}),_&&l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Exec report template"}),l.jsxs("select",{value:r.payload.exec_template_id??"",onChange:P=>R({exec_template_id:P.target.value||null}),children:[l.jsx("option",{value:"",children:"— built-in —"}),H.map(P=>l.jsx("option",{value:P.id,children:P.name},P.id))]})]})]}),l.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."})]}),l.jsxs("div",{className:"perf-form-section",children:[l.jsxs("button",{className:"btn btn-sm btn-ghost",onClick:()=>s(P=>!P),children:[o?"▾":"▸"," Advanced"]}),o&&l.jsxs("div",{className:"perf-field-grid",style:{marginTop:8},children:[l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Correlation tag"}),l.jsx("input",{type:"number",min:1,value:r.correlation_tag,onChange:P=>J({correlation_tag:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Exec ID tag"}),l.jsx("input",{type:"number",min:1,value:r.exec_id_tag,onChange:P=>J({exec_id_tag:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Max pending"}),l.jsx("input",{type:"number",min:1,value:r.test.max_pending,onChange:P=>ee({max_pending:Number(P.target.value)})})]}),l.jsxs("label",{className:"perf-field perf-field-check",children:[l.jsx("input",{type:"checkbox",checked:r.rate.allow_burst,onChange:P=>ae({allow_burst:P.target.checked})}),l.jsx("span",{children:"Allow burst"})]}),l.jsxs("label",{className:"perf-field",children:[l.jsx("span",{children:"Max burst multiplier"}),l.jsx("input",{type:"number",min:1,step:.5,value:r.rate.max_burst_multiplier,onChange:P=>ae({max_burst_multiplier:Number(P.target.value)})})]})]})]}),l.jsxs("div",{className:"perf-form-actions",children:[T?l.jsx("span",{className:"perf-not-ready",children:T}):I&&!(N&&A)?l.jsxs("span",{className:"perf-not-ready",children:["Selected session",w&&_?"s":""," must be LOGGED_ON before starting."]}):null,l.jsx("button",{className:"btn btn-primary",onClick:M,disabled:!C,title:I?T||(N&&A?void 0:"Session not logged on yet"):"Select the required session(s)",children:f?"Starting…":"▶ Start run"})]})]})}function Yp({status:e}){const t=e==="LOGGED_ON";return l.jsxs("span",{className:`perf-status-chip ${t?"ok":"warn"}`,children:[t?"● ":"○ ",e]})}const Xp=600,oW=5e3,sW=new Set(["completed","stopped","error"]);function lW(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,c=null,u,f=0,d=!1;async function m(){if(s||d||!e)return;let v;try{({ticket:v}=await Z.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}`;c=new WebSocket(g),c.onopen=()=>{s||(f=0,o(!0))},c.onmessage=b=>{let x;try{x=JSON.parse(b.data)}catch{return}r(x),a(w=>{var j,O,N,A,I,T,C,U,L;const _={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,aps:((N=x.client)==null?void 0:N.aps_live)??0,tps:(((A=x.client)==null?void 0:A.ops_live)??0)+(((I=x.venue)==null?void 0:I.fps_live)??0),p50:(((T=x.response_latency)==null?void 0:T.p50_us)??0)/1e3,p99:(((C=x.response_latency)==null?void 0:C.p99_us)??0)/1e3,fc50:(((U=x.fill_completion_latency)==null?void 0:U.p50_us)??0)/1e3,fc99:(((L=x.fill_completion_latency)==null?void 0:L.p99_us)??0)/1e3},S=[...w,_];return S.length>Xp?S.slice(-Xp):S}),sW.has(x.status)&&(d=!0,c==null||c.close())},c.onclose=()=>{s||(o(!1),d||p())},c.onerror=()=>{c==null||c.close()}}function p(){if(s||d)return;f+=1;const v=Math.min(oW,500*2**Math.min(f,4));u=setTimeout(m,v)}return m(),()=>{s=!0,clearTimeout(u),c==null||c.close()}},[e]),{latest:t,series:n,connected:i}}const Lw="img-export-exclude";async function cW(e,t,r){const{toPng:n,toJpeg:a}=await kv(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(Lw))}),c=document.createElement("a");c.href=s,c.download=t,c.click()}function uW(e,t){const r=new Date().toISOString().slice(0,19).replace(/[:T]/g,"-");return`${e}-${r}.${t}`}const Pl=new Set(["completed","stopped","error"]);function vr(e){return e?(e/1e3).toFixed(3):"—"}function Fw(e,t){return t<=0?"—":(e/t).toFixed(2)}function Nl({title:e,sub:t,stats:r}){return l.jsxs("div",{className:"perf-lat-card",children:[l.jsxs("div",{className:"perf-lat-title",children:[e,t&&l.jsx("span",{className:"perf-lat-sub",children:t})]}),l.jsx("div",{className:"perf-lat-grid",children:["p50_us","p95_us","p99_us","max_us"].map(n=>l.jsxs("div",{className:"perf-lat-cell",children:[l.jsx("span",{className:"perf-lat-val",children:vr(r[n])}),l.jsxs("span",{className:"perf-lat-lbl",children:[n.replace("_us","")," ms"]})]},n))})]})}function dW({runId:e}){var w,_;const{latest:t,series:r,connected:n}=lW(e),[a,i]=h.useState(null),[o,s]=h.useState(!1),c=h.useRef(null);h.useEffect(()=>{let S=!0;i(null);let j;const O=async()=>{try{const N=await Z.perf.runs.get(e);if(!S)return;i(N),Pl.has(N.status)&&clearInterval(j)}catch{}};return j=setInterval(O,2e3),O(),()=>{S=!1,clearInterval(j)}},[e]);const u=t?Pl.has(t.status):a?Pl.has(a.status):!1,f=!u;async function d(){try{await Z.perf.runs.stop(e)}catch{}}async function m(S){if(!(!c.current||o)){s(!0);try{const j=S==="png"?"png":"jpg";await cW(c.current,uW(`perf-${e.slice(0,8)}`,j),S)}catch(j){console.error("image export failed",j)}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()}),(p.amends_sent>0||p.aps_live>0)&&(x.push({label:"amends/s",value:Math.round(p.aps_live).toLocaleString()},{label:"amends",value:p.amends_sent.toLocaleString()},{label:"replaces",value:p.replaces_received.toLocaleString()}),p.amends_lost>0&&x.push({label:"amends lost",value:p.amends_lost.toLocaleString()})),p.amend_rejects>0&&x.push({label:"amend rejects (35=9)",value:p.amend_rejects.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:Fw(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()}),v.amends_received>0&&x.push({label:"amends recv",value:v.amends_received.toLocaleString()}),v.amends_uncorrelated>0&&x.push({label:"amends no-corr",value:v.amends_uncorrelated.toLocaleString()})),y&&x.push({label:"lost",value:y.lost_timeout.toLocaleString()},{label:"rejected",value:y.rejected.toLocaleString()}),l.jsxs("div",{className:"perf-dashboard",ref:c,children:[l.jsxs("div",{className:"perf-dash-header",children:[l.jsxs("div",{className:"perf-dash-status",children:[l.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&&l.jsxs("span",{className:"perf-dash-elapsed",children:[Math.round(t.elapsed_s),"s",t.duration_s?` / ${t.duration_s}s`:""," · ",t.mode]}),l.jsx("span",{className:`perf-conn ${n?"on":"off"}`,children:n?"● live":"○ offline"})]}),l.jsxs("div",{className:`perf-dash-actions ${Lw}`,children:[f&&l.jsx("button",{className:"btn btn-sm btn-danger",onClick:d,children:"■ Stop"}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>Z.perf.runs.export(e,"messages"),children:"Export CSV"}),u&&l.jsx("button",{className:"btn btn-sm btn-ghost",title:"Per-exec detail (requires record_execs)",onClick:()=>Z.perf.runs.export(e,"execs"),children:"Execs CSV"}),l.jsx("button",{className:"btn btn-sm btn-ghost",onClick:()=>Z.perf.runs.export(e,"both"),children:"Export ZIP"}),l.jsx("button",{className:"btn btn-sm btn-ghost",disabled:o,onClick:()=>m("png"),children:o?"…":"PNG"}),l.jsx("button",{className:"btn btn-sm btn-ghost",disabled:o,onClick:()=>m("jpeg"),children:o?"…":"JPG"})]})]}),g&&l.jsxs("div",{className:"perf-saturated-banner",children:["⚠ SATURATED — injection paused to protect latency measurement.",p&&l.jsxs(l.Fragment,{children:[" Pending ",p.pending.toLocaleString(),", dropped ",p.dropped.toLocaleString(),"."]})]}),(w=a==null?void 0:a.warnings)==null?void 0:w.map((S,j)=>l.jsxs("div",{className:"perf-warning-banner",children:["⚠ ",S]},j)),!t&&!u&&l.jsx("div",{className:"loading-msg",children:"Waiting for first snapshot…"}),x.length>0&&l.jsx("div",{className:"stat-cards",children:x.map(({label:S,value:j})=>l.jsxs("div",{className:"stat-card",children:[l.jsx("span",{className:"stat-card-value",children:j}),l.jsx("span",{className:"stat-card-label",children:S})]},S))}),t&&l.jsxs("div",{className:"perf-lat-row",children:[l.jsx(Nl,{title:"Response latency",sub:"order-2-ack",stats:t.response_latency}),l.jsx(Nl,{title:"Fill-completion latency",sub:"order-2-last-fill",stats:t.fill_completion_latency}),((_=t.amend_latency)==null?void 0:_.count)>0&&l.jsx(Nl,{title:"Amend latency",sub:"amend-2-replaced",stats:t.amend_latency})]}),r.length>1&&l.jsxs(l.Fragment,{children:[l.jsx("div",{className:"analysis-section-title",children:"Throughput"}),l.jsx("div",{className:"analysis-chart-wrap",children:l.jsx(Nn,{width:"100%",height:200,children:l.jsxs(Ti,{data:r,margin:{top:4,right:16,bottom:4,left:8},children:[l.jsx(Zr,{strokeDasharray:"3 3",stroke:"var(--border)"}),l.jsx(Kr,{dataKey:"t",tick:{fontSize:11,fill:"var(--text2)"},tickFormatter:S=>`${Math.round(S)}s`}),l.jsx(qr,{tick:{fontSize:11,fill:"var(--text2)"},allowDecimals:!1}),l.jsx(An,{contentStyle:{background:"var(--bg3)",border:"1px solid var(--border)",fontSize:12},labelStyle:{color:"var(--text)"},labelFormatter:S=>`${Math.round(Number(S))}s`}),l.jsx(ga,{wrapperStyle:{fontSize:11}}),l.jsx(yt,{type:"monotone",dataKey:"ops",name:"orders/s",stroke:"#00b4c8",dot:!1,isAnimationActive:!1}),l.jsx(yt,{type:"monotone",dataKey:"fps",name:"fills/s",stroke:"#a78bfa",dot:!1,isAnimationActive:!1}),((p==null?void 0:p.amends_sent)??0)>0&&l.jsx(yt,{type:"monotone",dataKey:"aps",name:"amends/s",stroke:"#c084fc",dot:!1,isAnimationActive:!1}),l.jsx(yt,{type:"monotone",dataKey:"tps",name:"total msgs/s",stroke:"#fbbf24",dot:!1,isAnimationActive:!1})]})})}),l.jsx("div",{className:"analysis-section-title",children:"Response latency (ms)"}),l.jsx("div",{className:"analysis-chart-wrap",children:l.jsx(Nn,{width:"100%",height:200,children:l.jsxs(Ti,{data:r,margin:{top:4,right:16,bottom:4,left:8},children:[l.jsx(Zr,{strokeDasharray:"3 3",stroke:"var(--border)"}),l.jsx(Kr,{dataKey:"t",tick:{fontSize:11,fill:"var(--text2)"},tickFormatter:S=>`${Math.round(S)}s`}),l.jsx(qr,{tick:{fontSize:11,fill:"var(--text2)"}}),l.jsx(An,{contentStyle:{background:"var(--bg3)",border:"1px solid var(--border)",fontSize:12},labelStyle:{color:"var(--text)"},labelFormatter:S=>`${Math.round(Number(S))}s`}),l.jsx(ga,{wrapperStyle:{fontSize:11}}),l.jsx(yt,{type:"monotone",dataKey:"p50",name:"p50",stroke:"#4ade80",dot:!1,isAnimationActive:!1}),l.jsx(yt,{type:"monotone",dataKey:"p99",name:"p99",stroke:"#f87171",dot:!1,isAnimationActive:!1})]})})}),l.jsx("div",{className:"analysis-section-title",children:"Fill-completion latency (ms)"}),l.jsx("div",{className:"analysis-chart-wrap",children:l.jsx(Nn,{width:"100%",height:200,children:l.jsxs(Ti,{data:r,margin:{top:4,right:16,bottom:4,left:8},children:[l.jsx(Zr,{strokeDasharray:"3 3",stroke:"var(--border)"}),l.jsx(Kr,{dataKey:"t",tick:{fontSize:11,fill:"var(--text2)"},tickFormatter:S=>`${Math.round(S)}s`}),l.jsx(qr,{tick:{fontSize:11,fill:"var(--text2)"}}),l.jsx(An,{contentStyle:{background:"var(--bg3)",border:"1px solid var(--border)",fontSize:12},labelStyle:{color:"var(--text)"},labelFormatter:S=>`${Math.round(Number(S))}s`}),l.jsx(ga,{wrapperStyle:{fontSize:11}}),l.jsx(yt,{type:"monotone",dataKey:"fc50",name:"p50",stroke:"#4ade80",dot:!1,isAnimationActive:!1}),l.jsx(yt,{type:"monotone",dataKey:"fc99",name:"p99",stroke:"#f87171",dot:!1,isAnimationActive:!1})]})})})]}),u&&(a==null?void 0:a.summary)&&l.jsx(fW,{status:a})]})}function fW({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)",Fw(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)",`${vr(t.response_latency.p50_us)} / ${vr(t.response_latency.p99_us)}`],["Response max (ms)",vr(t.response_latency.max_us)],["Fill-completion p50 / p99 (ms)",`${vr(t.fill_completion_latency.p50_us)} / ${vr(t.fill_completion_latency.p99_us)}`]];return t.amends_sent&&(r.push(["Amends sent / replaced / rejected / lost",`${t.amends_sent} / ${t.replaces_received??0} / ${t.amend_rejects??0} / ${t.amends_lost??0}`],["Amends recv (venue) / no-corr",`${t.amends_received??0} / ${t.amends_uncorrelated??0}`]),t.amend_latency&&r.push(["Amend p50 / p99 (ms)",`${vr(t.amend_latency.p50_us)} / ${vr(t.amend_latency.p99_us)}`])),l.jsxs("div",{className:"perf-summary-card",children:[l.jsxs("div",{className:"perf-section-title",children:["Run summary — ",e.status]}),l.jsx("table",{className:"perf-summary-table",children:l.jsx("tbody",{children:r.map(([n,a])=>l.jsxs("tr",{children:[l.jsx("td",{children:n}),l.jsx("td",{className:"mono",children:a})]},n))})})]})}const hW=new Set(["completed","stopped","error"]);function mW(e){if(!e)return"—";try{return new Date(e*1e3).toLocaleTimeString()}catch{return"—"}}function pW({activeRunId:e,onSelect:t,refreshKey:r}){const[n,a]=h.useState([]);h.useEffect(()=>{let o=!0;const s=()=>Z.perf.runs.list().then(u=>{o&&a(u)}).catch(()=>{});s();const c=setInterval(s,2e3);return()=>{o=!1,clearInterval(c)}},[r]);async function i(o,s){o.stopPropagation();try{await Z.perf.runs.stop(s)}catch{}}return n.length===0?l.jsx("div",{className:"perf-runlist-empty",children:"No runs yet. Configure and start one."}):l.jsxs("table",{className:"perf-runlist",children:[l.jsx("thead",{children:l.jsxs("tr",{children:[l.jsx("th",{children:"Name"}),l.jsx("th",{children:"Mode"}),l.jsx("th",{children:"Status"}),l.jsx("th",{children:"Started"}),l.jsx("th",{children:"Elapsed"}),l.jsx("th",{children:"Orders"}),l.jsx("th",{children:"Fill"}),l.jsx("th",{})]})}),l.jsx("tbody",{children:n.map(o=>l.jsxs("tr",{className:o.run_id===e?"perf-run-active":"",onClick:()=>t(o.run_id),children:[l.jsx("td",{children:o.name}),l.jsx("td",{children:o.mode}),l.jsx("td",{children:l.jsx("span",{className:`badge perf-state-${o.status}`,children:o.status})}),l.jsx("td",{children:mW(o.started_at)}),l.jsxs("td",{className:"mono",children:[Math.round(o.elapsed_s),"s"]}),l.jsx("td",{className:"mono",children:o.summary?o.summary.orders_sent.toLocaleString():"—"}),l.jsx("td",{className:"mono",children:o.summary?o.summary.fill_ratio.toFixed(2):"—"}),l.jsx("td",{className:"perf-runlist-actions",children:!hW.has(o.status)&&l.jsx("button",{className:"btn btn-sm btn-danger",onClick:s=>i(s,o.run_id),children:"Stop"})})]},o.run_id))})]})}const Cl=200,On=(e,t,r=120)=>({field:e,headerName:t,width:r,type:"numericColumn",valueFormatter:n=>n.value==null?"":Number(n.value).toLocaleString()}),Ac=(e,t)=>({field:e,headerName:t,width:150,type:"numericColumn",valueFormatter:r=>r.value==null?"":`${Math.round(Number(r.value)).toLocaleString()}`}),vW=[{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},On("qty","Qty",90),On("price","Price",100),Ac("response_latency_us","Resp µs"),Ac("fill_latency_us","Fill µs"),On("fill_qty","Fill qty",100),On("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}],gW=[{field:"scenario_name",headerName:"Scenario",flex:1,minWidth:160},Ac("latency_us","Latency µs"),On("msg_count","Msgs",90),On("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 yW({runId:e}){const t=V(g=>g.theme),[r,n]=h.useState("messages"),[a,i]=h.useState(0),[o,s]=h.useState([]),[c,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"?Z.perf.runs.messages:Z.perf.runs.scenarios)(e,{limit:Cl,offset:a*Cl}).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"?vW:gW,[r]),p=h.useMemo(()=>({sortable:!0,filter:!0,resizable:!0}),[]),v=Math.max(0,Math.ceil(c/Cl)-1),y=t==="dark"?"ag-theme-quartz-dark":"ag-theme-quartz";return l.jsxs("div",{className:"perf-results",children:[l.jsxs("div",{className:"perf-results-toolbar",children:[l.jsxs("div",{className:"perf-results-tabs",children:[l.jsx("button",{className:`btn btn-sm ${r==="messages"?"btn-primary":"btn-ghost"}`,onClick:()=>n("messages"),children:"Messages"}),l.jsx("button",{className:`btn btn-sm ${r==="scenarios"?"btn-primary":"btn-ghost"}`,onClick:()=>n("scenarios"),children:"Scenarios"})]}),l.jsxs("div",{className:"perf-results-pager",children:[l.jsxs("span",{children:[c.toLocaleString()," rows"]}),l.jsx("button",{className:"btn btn-sm btn-ghost",disabled:a<=0||f,onClick:()=>i(g=>Math.max(0,g-1)),children:"‹ Prev"}),l.jsxs("span",{children:["Page ",a+1," / ",v+1]}),l.jsx("button",{className:"btn btn-sm btn-ghost",disabled:a>=v||f,onClick:()=>i(g=>Math.min(v,g+1)),children:"Next ›"})]})]}),l.jsx("div",{className:`${y} perf-results-grid`,children:l.jsx(kc,{rowData:o,columnDefs:m,defaultColDef:p,animateRows:!1,suppressCellFocus:!0})})]})}function bW(){const{theme:e,toggleTheme:t,branding:r,setSessions:n}=V(),a=Gs(),i=r!=null&&r.prefix?`${r.prefix} FIXture`:"FIXture";Pv(),h.useEffect(()=>{Z.sessions.list().then(n).catch(()=>{})},[]);const[o,s]=h.useState("configure"),[c,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:!c},{id:"results",label:"Results",disabled:!c},{id:"history",label:"History"}];return l.jsxs("div",{className:"app perf-page",children:[l.jsxs("header",{className:"app-header",children:[l.jsx("img",{src:a,alt:i,className:"app-logo"}),l.jsx("div",{className:"perf-page-title",children:"Performance Testing"}),l.jsxs("div",{className:"header-right",children:[l.jsx(Yr,{to:"/",className:"btn btn-sm btn-ghost",children:"← Sessions"}),l.jsx("button",{className:"btn-icon theme-toggle",onClick:t,title:"Toggle theme",children:e==="dark"?"☀":"🌙"})]})]}),l.jsx("div",{className:"perf-tabbar",children:v.map(y=>l.jsx("button",{className:`perf-tab${o===y.id?" active":""}`,disabled:y.disabled,onClick:()=>s(y.id),children:y.label},y.id))}),l.jsxs("div",{className:"perf-body",children:[o==="configure"&&l.jsx("div",{className:"perf-pane perf-pane-form",children:l.jsx(iW,{onStarted:m})}),o==="live"&&c&&l.jsx("div",{className:"perf-pane",children:l.jsx(dW,{runId:c},c)}),o==="results"&&c&&l.jsx("div",{className:"perf-pane",children:l.jsx(yW,{runId:c},c)}),o==="history"&&l.jsx("div",{className:"perf-pane",children:l.jsx(pW,{activeRunId:c,onSelect:p,refreshKey:f})})]})]})}function xW(){return V(t=>t.token)?l.jsx(hv,{}):l.jsx(Ri,{to:"/login",replace:!0})}function wW(){const e=V(t=>t.user);return e?e.role!=="platform_admin"?l.jsx(Ri,{to:"/",replace:!0}):l.jsx(hv,{}):l.jsx(Ri,{to:"/login",replace:!0})}function jW(){const e=V(t=>t.setBranding);h.useEffect(()=>{fetch("/api/branding").then(t=>t.json()).then(t=>{e(t),Rw(t)}).catch(()=>{})},[])}function SW(){const e=on(),t=V(r=>r.theme);return jW(),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(()=>{})},[]),l.jsxs(Kj,{children:[l.jsx(Lt,{path:"/setup",element:l.jsx(Vz,{})}),l.jsx(Lt,{path:"/login",element:l.jsx(Xz,{})}),l.jsx(Lt,{path:"/register",element:l.jsx(Jz,{})}),l.jsxs(Lt,{element:l.jsx(xW,{}),children:[l.jsx(Lt,{path:"/",element:l.jsx(Yz,{})}),l.jsxs(Lt,{element:l.jsx(wW,{}),children:[l.jsx(Lt,{path:"/admin/users",element:l.jsx(nW,{})}),l.jsx(Lt,{path:"/perf",element:l.jsx(bW,{})})]})]}),l.jsx(Lt,{path:"*",element:l.jsx(Ri,{to:"/",replace:!0})})]})}El.createRoot(document.getElementById("root")).render(l.jsx(Vp.StrictMode,{children:l.jsx(gS,{children:l.jsx(SW,{})})}));
|
|
@@ -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-BFPDQdQb.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>
|
|
@@ -63,9 +63,9 @@ fixture/fix_specs/FIX44.xml
|
|
|
63
63
|
fixture/static/favicon.svg
|
|
64
64
|
fixture/static/index.html
|
|
65
65
|
fixture/static/assets/ag-grid-_QKprVdm.js
|
|
66
|
+
fixture/static/assets/index-BFPDQdQb.js
|
|
66
67
|
fixture/static/assets/index-BwQf-cei.css
|
|
67
68
|
fixture/static/assets/index-CyNOPa0n.js
|
|
68
|
-
fixture/static/assets/index-DrmyYeG0.js
|
|
69
69
|
fixture/static/assets/react-vendor-2eF0YfZT.js
|
|
70
70
|
fixture/ui/__init__.py
|
|
71
71
|
fixtureqa.egg-info/PKG-INFO
|
|
@@ -113,6 +113,35 @@ def test_perf_configs_crud(authed):
|
|
|
113
113
|
assert client.get(f"/api/perf/configs/{cid}").status_code == 404
|
|
114
114
|
|
|
115
115
|
|
|
116
|
+
def test_perf_config_save_same_name_overwrites(authed):
|
|
117
|
+
client, _ = authed
|
|
118
|
+
body = _run_body("c", "v")
|
|
119
|
+
body["name"] = "upsert-me"
|
|
120
|
+
cid1 = client.post("/api/perf/configs", json=body).json()["config_id"]
|
|
121
|
+
body["rate"]["orders_per_window"] = 99 # same name, new content
|
|
122
|
+
cid2 = client.post("/api/perf/configs", json=body).json()["config_id"]
|
|
123
|
+
assert cid2 == cid1 # overwrote in place
|
|
124
|
+
names = [c["name"] for c in client.get("/api/perf/configs").json()]
|
|
125
|
+
assert names.count("upsert-me") == 1 # no duplicate dropdown rows
|
|
126
|
+
got = client.get(f"/api/perf/configs/{cid1}").json()
|
|
127
|
+
assert got["rate"]["orders_per_window"] == 99 # latest content won
|
|
128
|
+
client.delete(f"/api/perf/configs/{cid1}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_perf_store_collapses_legacy_duplicates(tmp_path):
|
|
132
|
+
# Duplicates saved before the upsert fix collapse into the first entry the
|
|
133
|
+
# next time that name is saved.
|
|
134
|
+
from fixture.core.perf_store import PerfStore
|
|
135
|
+
store = PerfStore(str(tmp_path))
|
|
136
|
+
a = store.create_item("", {"name": "dup", "config": {"name": "dup", "v": 1}})
|
|
137
|
+
store.create_item("", {"name": "dup", "config": {"name": "dup", "v": 2}})
|
|
138
|
+
assert sum(c["name"] == "dup" for c in store.list()) == 2
|
|
139
|
+
cid = store.save({"name": "dup", "v": 3})
|
|
140
|
+
assert cid == a["config_id"] # first entry keeps its id
|
|
141
|
+
assert sum(c["name"] == "dup" for c in store.list()) == 1
|
|
142
|
+
assert store.get(cid)["v"] == 3
|
|
143
|
+
|
|
144
|
+
|
|
116
145
|
def test_perf_run_rejects_unstarted_session(authed):
|
|
117
146
|
client, _ = authed
|
|
118
147
|
port = _free_port()
|
|
@@ -699,6 +699,27 @@ async def test_venue_answers_inbound_amend():
|
|
|
699
699
|
assert run.stats.amends_received == 1
|
|
700
700
|
|
|
701
701
|
|
|
702
|
+
async def test_stop_during_drain_wins_immediately():
|
|
703
|
+
# Starved fill leg → the post-duration drain would grind for minutes
|
|
704
|
+
# (cap ≈ 2× backlog/fill_rate). Stop pressed mid-drain must end the run
|
|
705
|
+
# promptly, finalizing the backlog as lost, not wait out the drain.
|
|
706
|
+
cfg = _cfg(
|
|
707
|
+
rate=RateConfig(orders_per_window=50, order_window_ms=100, # ~500/s in
|
|
708
|
+
fills_per_window=1, fill_window_ms=1000), # 1 fill/s out
|
|
709
|
+
test=RunTestConfig(duration=1),
|
|
710
|
+
)
|
|
711
|
+
run = _run(cfg)
|
|
712
|
+
run.start()
|
|
713
|
+
await asyncio.sleep(1.3) # duration fired → inside drain
|
|
714
|
+
assert run.status == "running" # still draining the backlog
|
|
715
|
+
t0 = time.perf_counter()
|
|
716
|
+
await asyncio.wait_for(run.stop(), timeout=10)
|
|
717
|
+
assert time.perf_counter() - t0 < 5 # did not ride out the drain
|
|
718
|
+
assert run.status == "stopped"
|
|
719
|
+
assert not run._pending # backlog finalized
|
|
720
|
+
assert run.stats.orders_sent > run.stats.completions # it really was starved
|
|
721
|
+
|
|
722
|
+
|
|
702
723
|
async def test_venue_amend_without_pending_replace():
|
|
703
724
|
run = _run(_cfg(amend=AmendConfig(enabled=True)))
|
|
704
725
|
await run._handle_amend({35: "G", 11: "NEW-2", 41: "OLD-2", 376: "VC2",
|
|
@@ -706,3 +727,57 @@ async def test_venue_amend_without_pending_replace():
|
|
|
706
727
|
venue_msgs = [f for sid, f in run._sm.sent if sid == "v"]
|
|
707
728
|
assert [f.get(150) for f in venue_msgs] == ["5"]
|
|
708
729
|
assert run.stats.amends_received == 1
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
async def test_amend_reject_counts_and_releases_order():
|
|
733
|
+
# A 35=9 OrderCancelReject ends the in-flight amend: counted as a reject
|
|
734
|
+
# (not lost), chain tip NOT promoted, and the order still finalizes.
|
|
735
|
+
cfg = _cfg(amend=AmendConfig(enabled=True, every_n_fills=1, max_per_order=1))
|
|
736
|
+
run = _run(cfg)
|
|
737
|
+
run._register_pending(_amend_meta("A3"))
|
|
738
|
+
entry = run._pending["A3"]
|
|
739
|
+
run._on_exec_report({376: "A3", 25116: "F1", 14: "50", 38: "100",
|
|
740
|
+
150: "1", 39: "1"}, time.perf_counter_ns())
|
|
741
|
+
await asyncio.sleep(0.01)
|
|
742
|
+
assert run.stats.amends_sent == 1 and entry["amend_inflight"]
|
|
743
|
+
|
|
744
|
+
run._on_cancel_reject({376: "A3", 11: entry["pending_clordid"], 41: "PCL-ORIG"})
|
|
745
|
+
assert run.stats.amend_rejects == 1
|
|
746
|
+
assert not entry["amend_inflight"]
|
|
747
|
+
assert entry["clordid"] == "PCL-ORIG" # chain tip unchanged
|
|
748
|
+
assert any("OrderCancelReject" in w for w in run.warnings)
|
|
749
|
+
|
|
750
|
+
run._on_exec_report({376: "A3", 25116: "F2", 14: "100", 38: "100",
|
|
751
|
+
150: "2", 39: "2"}, time.perf_counter_ns())
|
|
752
|
+
assert run.stats.completions == 1
|
|
753
|
+
assert "A3" not in run._pending # finalized normally
|
|
754
|
+
assert run.stats.amends_lost == 0 # rejected ≠ lost
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
async def test_amend_reject_after_completion_finalizes():
|
|
758
|
+
# Reject arriving after the final fill (entry deferred for the amend) must
|
|
759
|
+
# still release and finalize the order as filled.
|
|
760
|
+
cfg = _cfg(amend=AmendConfig(enabled=True, every_n_fills=1, max_per_order=1))
|
|
761
|
+
run = _run(cfg)
|
|
762
|
+
run._register_pending(_amend_meta("A4"))
|
|
763
|
+
run._on_exec_report({376: "A4", 25116: "F1", 14: "50", 38: "100",
|
|
764
|
+
150: "1", 39: "1"}, time.perf_counter_ns())
|
|
765
|
+
await asyncio.sleep(0.01)
|
|
766
|
+
run._on_exec_report({376: "A4", 25116: "F2", 14: "100", 38: "100",
|
|
767
|
+
150: "2", 39: "2"}, time.perf_counter_ns())
|
|
768
|
+
assert "A4" in run._pending # held for the amend
|
|
769
|
+
run._on_cancel_reject({376: "A4"})
|
|
770
|
+
assert run.stats.amend_rejects == 1
|
|
771
|
+
assert "A4" not in run._pending
|
|
772
|
+
assert [r["status"] for r in run._writer.msgs
|
|
773
|
+
if r["corr_id"] == "A4"] == ["filled"]
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
async def test_venue_amend_without_corr_counts_uncorrelated():
|
|
777
|
+
run = _run(_cfg(amend=AmendConfig(enabled=True)))
|
|
778
|
+
await run._handle_amend({35: "G", 11: "NEW-3", 41: "OLD-3",
|
|
779
|
+
38: "100", 44: "10.05"}) # no 376
|
|
780
|
+
assert run.stats.amends_received == 1
|
|
781
|
+
assert run.stats.amends_uncorrelated == 1
|
|
782
|
+
assert not [f for sid, f in run._sm.sent if sid == "v"] # no reply possible
|
|
783
|
+
assert any("without correlation tag" in w for w in run.warnings)
|
|
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
|