fixtureqa 0.4.5__tar.gz → 0.4.7__tar.gz

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