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.
Files changed (99) hide show
  1. {fixtureqa-0.4.8/fixtureqa.egg-info → fixtureqa-0.4.10}/PKG-INFO +1 -1
  2. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/perf_engine.py +39 -3
  3. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/perf_models.py +2 -0
  4. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/perf_stats.py +4 -0
  5. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/perf_store.py +19 -2
  6. fixtureqa-0.4.8/fixture/static/assets/index-DrmyYeG0.js → fixtureqa-0.4.10/fixture/static/assets/index-BFPDQdQb.js +1 -1
  7. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/static/index.html +1 -1
  8. {fixtureqa-0.4.8 → fixtureqa-0.4.10/fixtureqa.egg-info}/PKG-INFO +1 -1
  9. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixtureqa.egg-info/SOURCES.txt +1 -1
  10. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/pyproject.toml +1 -1
  11. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_perf_api.py +29 -0
  12. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_perf_engine.py +75 -0
  13. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/LICENSE +0 -0
  14. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/README.md +0 -0
  15. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/__init__.py +0 -0
  16. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/__main__.py +0 -0
  17. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/__init__.py +0 -0
  18. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/app.py +0 -0
  19. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/connection_manager.py +0 -0
  20. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/deps.py +0 -0
  21. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/__init__.py +0 -0
  22. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/admin.py +0 -0
  23. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/auth.py +0 -0
  24. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/branding.py +0 -0
  25. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/custom_tags.py +0 -0
  26. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/fix_spec.py +0 -0
  27. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/messages.py +0 -0
  28. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/perf.py +0 -0
  29. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/scenarios.py +0 -0
  30. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/sessions.py +0 -0
  31. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/setup.py +0 -0
  32. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/spec_overlay.py +0 -0
  33. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/templates.py +0 -0
  34. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/routers/ws.py +0 -0
  35. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/api/schemas.py +0 -0
  36. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/config/__init__.py +0 -0
  37. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/__init__.py +0 -0
  38. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/atomic_io.py +0 -0
  39. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/auth.py +0 -0
  40. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/config_store.py +0 -0
  41. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/custom_tag_store.py +0 -0
  42. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/db_migrations.py +0 -0
  43. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/events.py +0 -0
  44. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/exec_csv_writer.py +0 -0
  45. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/fix_application.py +0 -0
  46. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/fix_builder.py +0 -0
  47. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/fix_parser.py +0 -0
  48. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/fix_spec_parser.py +0 -0
  49. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/fix_tags.py +0 -0
  50. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/fix_time.py +0 -0
  51. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/housekeeping.py +0 -0
  52. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/inbound.py +0 -0
  53. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/json_store.py +0 -0
  54. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/message_log.py +0 -0
  55. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/message_store.py +0 -0
  56. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/models.py +0 -0
  57. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/perf_payload.py +0 -0
  58. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/perf_writer.py +0 -0
  59. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/scenario_runner.py +0 -0
  60. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/scenario_store.py +0 -0
  61. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/session.py +0 -0
  62. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/session_manager.py +0 -0
  63. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/spec_overlay_store.py +0 -0
  64. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/template_store.py +0 -0
  65. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/user_store.py +0 -0
  66. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/core/venue_responses.py +0 -0
  67. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/fix_specs/FIX42.xml +0 -0
  68. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/fix_specs/FIX44.xml +0 -0
  69. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/server.py +0 -0
  70. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  71. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/static/assets/index-BwQf-cei.css +0 -0
  72. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  73. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  74. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/static/favicon.svg +0 -0
  75. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixture/ui/__init__.py +0 -0
  76. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixtureqa.egg-info/dependency_links.txt +0 -0
  77. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixtureqa.egg-info/entry_points.txt +0 -0
  78. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixtureqa.egg-info/requires.txt +0 -0
  79. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/fixtureqa.egg-info/top_level.txt +0 -0
  80. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/setup.cfg +0 -0
  81. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_atomic_io.py +0 -0
  82. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_auth.py +0 -0
  83. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_config_store.py +0 -0
  84. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_connection_manager.py +0 -0
  85. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_db_migrations.py +0 -0
  86. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_fix_builder.py +0 -0
  87. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_health.py +0 -0
  88. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_inbound.py +0 -0
  89. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_inbound_validation.py +0 -0
  90. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_message_store.py +0 -0
  91. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_perf_models.py +0 -0
  92. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_perf_payload.py +0 -0
  93. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_perf_rehydrate.py +0 -0
  94. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_scenarios.py +0 -0
  95. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_session_lifecycle.py +0 -0
  96. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_session_manager_concurrency.py +0 -0
  97. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_sessions.py +0 -0
  98. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_templates.py +0 -0
  99. {fixtureqa-0.4.8 → fixtureqa-0.4.10}/tests/test_ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.4.8
3
+ Version: 0.4.10
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -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
- if not self._pending or now >= cap:
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
- item = self.create_item("", {"name": config.get("name", ""), "config": config})
32
- return item["config_id"]
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-DrmyYeG0.js"></script>
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>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.4.8
3
+ Version: 0.4.10
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fixtureqa"
7
- version = "0.4.8"
7
+ version = "0.4.10"
8
8
  description = "FIXture — FIX Protocol Testing Tool"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -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