fixcore-engine 0.1.1__tar.gz → 0.1.3__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.
- {fixcore_engine-0.1.1/fixcore_engine.egg-info → fixcore_engine-0.1.3}/PKG-INFO +1 -1
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/gui.py +26 -11
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/gui_ui/app.js +158 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/gui_ui/index.html +18 -2
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/gui_ui/style.css +87 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/transport/initiator.py +14 -1
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3/fixcore_engine.egg-info}/PKG-INFO +1 -1
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/pyproject.toml +1 -1
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/LICENSE +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/README.md +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/__init__.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/application.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/gui_ui/fixcore_logo.svg +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/log/__init__.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/log/base.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/log/factory.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/log/file_log.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/log/screen.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/message/__init__.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/message/cracker.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/message/data_dictionary.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/message/exceptions.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/message/field.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/message/message.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/session/__init__.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/session/session.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/session/session_id.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/session/session_settings.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/session/state.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/store/__init__.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/store/base.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/store/factory.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/store/file_store.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/store/memory.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/transport/__init__.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/transport/acceptor.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore/transport/framer.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore_engine.egg-info/SOURCES.txt +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore_engine.egg-info/dependency_links.txt +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore_engine.egg-info/entry_points.txt +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore_engine.egg-info/requires.txt +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/fixcore_engine.egg-info/top_level.txt +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/setup.cfg +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/tests/test_cracker.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/tests/test_data_dictionary.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/tests/test_file_log.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/tests/test_file_store.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/tests/test_framer.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/tests/test_integration.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/tests/test_message.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/tests/test_session.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/tests/test_session_id.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/tests/test_store.py +0 -0
- {fixcore_engine-0.1.1 → fixcore_engine-0.1.3}/tests/test_transport.py +0 -0
|
@@ -224,6 +224,8 @@ async def _dispatch(method: str, body: dict[str, Any]) -> Any:
|
|
|
224
224
|
)
|
|
225
225
|
if method == "update_session":
|
|
226
226
|
return await _update_session(body.get("old_key", ""), body)
|
|
227
|
+
if method == "stop":
|
|
228
|
+
return await _stop(body.get("sid_key", ""))
|
|
227
229
|
return {"ok": False, "error": f"Unknown method: {method!r}"}
|
|
228
230
|
|
|
229
231
|
|
|
@@ -377,24 +379,37 @@ async def _logon(sid_key: str) -> dict[str, Any]:
|
|
|
377
379
|
|
|
378
380
|
|
|
379
381
|
async def _logout(sid_key: str) -> dict[str, Any]:
|
|
382
|
+
"""Graceful FIX logout — sends Logout message and disables auto-reconnect."""
|
|
383
|
+
entry = _sessions.get(sid_key)
|
|
384
|
+
if entry is None:
|
|
385
|
+
return {"ok": False, "error": "Unknown session"}
|
|
386
|
+
transport = entry.get("transport")
|
|
387
|
+
if transport is None:
|
|
388
|
+
return {"ok": False, "error": "No transport"}
|
|
389
|
+
try:
|
|
390
|
+
if isinstance(transport, SocketInitiator):
|
|
391
|
+
await transport.logout()
|
|
392
|
+
else:
|
|
393
|
+
session = next(iter(transport.sessions.values()), None)
|
|
394
|
+
if session and session.is_logged_on:
|
|
395
|
+
await session.send_logout()
|
|
396
|
+
entry["started"] = False
|
|
397
|
+
except Exception as exc:
|
|
398
|
+
return {"ok": False, "error": str(exc)}
|
|
399
|
+
return {"ok": True}
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
async def _stop(sid_key: str) -> dict[str, Any]:
|
|
403
|
+
"""Hard stop — kills the transport immediately without sending FIX Logout."""
|
|
380
404
|
entry = _sessions.get(sid_key)
|
|
381
405
|
if entry is None:
|
|
382
406
|
return {"ok": False, "error": "Unknown session"}
|
|
383
407
|
transport = entry.get("transport")
|
|
384
408
|
if transport is None:
|
|
385
409
|
return {"ok": False, "error": "No transport"}
|
|
386
|
-
session = next(iter(transport.sessions.values()), None)
|
|
387
|
-
if session is None:
|
|
388
|
-
return {"ok": False, "error": "No session"}
|
|
389
|
-
if not session.is_logged_on:
|
|
390
|
-
try:
|
|
391
|
-
await transport.stop()
|
|
392
|
-
entry["started"] = False
|
|
393
|
-
except Exception as exc:
|
|
394
|
-
return {"ok": False, "error": str(exc)}
|
|
395
|
-
return {"ok": True}
|
|
396
410
|
try:
|
|
397
|
-
await
|
|
411
|
+
await transport.stop()
|
|
412
|
+
entry["started"] = False
|
|
398
413
|
except Exception as exc:
|
|
399
414
|
return {"ok": False, "error": str(exc)}
|
|
400
415
|
return {"ok": True}
|
|
@@ -76,12 +76,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
76
76
|
connectWS();
|
|
77
77
|
apiCall('get_version').then(v => { $id('version').textContent = v; });
|
|
78
78
|
startPolling();
|
|
79
|
+
loadLog();
|
|
80
|
+
rebuildLog();
|
|
79
81
|
updateAutoClordidVisibility();
|
|
80
82
|
updateClordidPreview();
|
|
81
83
|
$id('chk-auto-clordid').addEventListener('change', () => {
|
|
82
84
|
updateClordidPreview();
|
|
83
85
|
});
|
|
84
86
|
$id('inp-clordid-prefix').addEventListener('input', updateClordidPreview);
|
|
87
|
+
|
|
88
|
+
$id('btn-close-detail').addEventListener('click', closeMsgDetail);
|
|
89
|
+
$id('log-entries').addEventListener('click', e => {
|
|
90
|
+
const row = e.target.closest('.log-entry');
|
|
91
|
+
if (!row || !row._logEntry) return;
|
|
92
|
+
if (row === selectedLogRow) { closeMsgDetail(); return; }
|
|
93
|
+
showMsgDetail(row, row._logEntry);
|
|
94
|
+
});
|
|
85
95
|
});
|
|
86
96
|
|
|
87
97
|
// ── Polling (seq nums + state) ────────────────────────────────────────────
|
|
@@ -153,6 +163,7 @@ function dotClass(state) {
|
|
|
153
163
|
function selectSession(key) {
|
|
154
164
|
activeSid = key;
|
|
155
165
|
refreshSessions();
|
|
166
|
+
rebuildLog();
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
function renderSessionDetail(s) {
|
|
@@ -293,6 +304,14 @@ $id('btn-logout').addEventListener('click', () => {
|
|
|
293
304
|
});
|
|
294
305
|
});
|
|
295
306
|
|
|
307
|
+
$id('btn-stop').addEventListener('click', () => {
|
|
308
|
+
if (!activeSid) return;
|
|
309
|
+
apiCall('stop', { sid_key: activeSid }).then(res => {
|
|
310
|
+
if (!res.ok) showToast('Stop: ' + res.error, 'error');
|
|
311
|
+
else { showToast('Session stopped'); refreshSessions(); }
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
296
315
|
$id('btn-edit').addEventListener('click', () => {
|
|
297
316
|
if (!activeSid) return;
|
|
298
317
|
openEditModal(activeSid);
|
|
@@ -343,6 +362,24 @@ function generateClordId() {
|
|
|
343
362
|
return prefix + String(clordIdSeq).padStart(6, '0');
|
|
344
363
|
}
|
|
345
364
|
|
|
365
|
+
// ── Send panel collapse ───────────────────────────────────────────────────
|
|
366
|
+
const SEND_COLLAPSED_KEY = 'fixcore_send_collapsed';
|
|
367
|
+
|
|
368
|
+
function setSendCollapsed(collapsed) {
|
|
369
|
+
$id('panel-send').classList.toggle('panel-send-collapsed', collapsed);
|
|
370
|
+
try { localStorage.setItem(SEND_COLLAPSED_KEY, collapsed ? '1' : '0'); } catch (_) {}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
$id('btn-toggle-send').addEventListener('click', () => {
|
|
374
|
+
const collapsed = !$id('panel-send').classList.contains('panel-send-collapsed');
|
|
375
|
+
setSendCollapsed(collapsed);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Restore collapse state on load
|
|
379
|
+
try {
|
|
380
|
+
if (localStorage.getItem(SEND_COLLAPSED_KEY) === '1') setSendCollapsed(true);
|
|
381
|
+
} catch (_) {}
|
|
382
|
+
|
|
346
383
|
// ── Message builder ───────────────────────────────────────────────────────
|
|
347
384
|
const MSG_TYPE_DEFAULTS = {
|
|
348
385
|
'D': [{tag:'11',value:'ORD0001'},{tag:'55',value:'AAPL'},{tag:'54',value:'1'},
|
|
@@ -521,11 +558,115 @@ $id('paste-fix-input').addEventListener('keydown', e => {
|
|
|
521
558
|
}
|
|
522
559
|
});
|
|
523
560
|
|
|
561
|
+
// ── FIX tag reference ─────────────────────────────────────────────────────
|
|
562
|
+
const FIX_TAG_NAMES = {
|
|
563
|
+
1:'Account', 6:'AvgPx', 7:'BeginSeqNo', 8:'BeginString', 9:'BodyLength',
|
|
564
|
+
10:'CheckSum', 11:'ClOrdID', 14:'CumQty', 15:'Currency', 16:'EndSeqNo',
|
|
565
|
+
31:'LastPx', 32:'LastQty',
|
|
566
|
+
17:'ExecID', 18:'ExecInst', 20:'ExecTransType', 21:'HandlInst',
|
|
567
|
+
34:'MsgSeqNum', 35:'MsgType', 36:'NewSeqNo', 37:'OrderID', 38:'OrderQty',
|
|
568
|
+
39:'OrdStatus', 40:'OrdType', 41:'OrigClOrdID', 43:'PossDupFlag',
|
|
569
|
+
44:'Price', 45:'RefSeqNum', 49:'SenderCompID', 50:'SenderSubID',
|
|
570
|
+
52:'SendingTime', 54:'Side', 55:'Symbol', 56:'TargetCompID',
|
|
571
|
+
57:'TargetSubID', 58:'Text', 60:'TransactTime', 97:'PossResend',
|
|
572
|
+
98:'EncryptMethod', 108:'HeartBtInt', 110:'MinQty', 111:'MaxFloor',
|
|
573
|
+
112:'TestReqID', 114:'LocateReqd', 122:'OrigSendingTime', 123:'GapFillFlag',
|
|
574
|
+
126:'ExpireTime', 131:'QuoteReqID', 140:'PrevClosePx', 146:'NoRelatedSym',
|
|
575
|
+
150:'ExecType', 151:'LeavesQty', 167:'SecurityType', 200:'MaturityMonthYear',
|
|
576
|
+
202:'StrikePrice', 207:'SecurityExchange', 262:'MDReqID',
|
|
577
|
+
263:'SubscriptionRequestType', 264:'MarketDepth', 267:'NoMDEntryTypes',
|
|
578
|
+
268:'NoMDEntries', 269:'MDEntryType', 270:'MDEntryPx', 271:'MDEntrySize',
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const FIX_ENUM_VALUES = {
|
|
582
|
+
35: {'0':'Heartbeat','1':'TestRequest','2':'ResendRequest','3':'Reject',
|
|
583
|
+
'4':'SequenceReset','5':'Logout','8':'ExecutionReport',
|
|
584
|
+
'9':'OrderCancelReject','A':'Logon','D':'NewOrderSingle',
|
|
585
|
+
'F':'OrderCancelRequest','G':'OrderCancelReplaceRequest',
|
|
586
|
+
'H':'OrderStatusRequest','V':'MarketDataRequest',
|
|
587
|
+
'W':'MarketDataSnapshot','X':'MarketDataIncremental','Y':'MarketDataRequestReject'},
|
|
588
|
+
39: {'0':'New','1':'PartiallyFilled','2':'Filled','3':'DoneForDay',
|
|
589
|
+
'4':'Canceled','5':'Replaced','6':'PendingCancel','7':'Stopped',
|
|
590
|
+
'8':'Rejected','9':'Suspended','A':'PendingNew','C':'Expired'},
|
|
591
|
+
40: {'1':'Market','2':'Limit','3':'Stop','4':'StopLimit','5':'MarketOnClose'},
|
|
592
|
+
54: {'1':'Buy','2':'Sell','3':'BuyMinus','4':'SellPlus','5':'SellShort','6':'SellShortExempt'},
|
|
593
|
+
98: {'0':'None','1':'Value','2':'DES','3':'PKCS','4':'PGP','5':'PCSDSS','6':'PEM'},
|
|
594
|
+
150:{'0':'New','1':'PartialFill','2':'Fill','3':'DoneForDay','4':'Canceled',
|
|
595
|
+
'5':'Replace','6':'PendingCancel','7':'Stopped','8':'Rejected',
|
|
596
|
+
'D':'Restated','E':'Trade','F':'TradeCorrect','G':'TradeCancel','H':'OrderStatus'},
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
function fixTagName(tag) { return FIX_TAG_NAMES[parseInt(tag)] || ''; }
|
|
600
|
+
function fixEnumVal(tag, val) {
|
|
601
|
+
const map = FIX_ENUM_VALUES[parseInt(tag)];
|
|
602
|
+
return map ? (map[val] || val) : val;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ── Message detail panel ──────────────────────────────────────────────────
|
|
606
|
+
let selectedLogRow = null;
|
|
607
|
+
|
|
608
|
+
function showMsgDetail(row, entry) {
|
|
609
|
+
if (selectedLogRow) selectedLogRow.classList.remove('selected');
|
|
610
|
+
selectedLogRow = row;
|
|
611
|
+
row.classList.add('selected');
|
|
612
|
+
|
|
613
|
+
const content = $id('msg-detail-content');
|
|
614
|
+
content.innerHTML = '';
|
|
615
|
+
|
|
616
|
+
// Meta info
|
|
617
|
+
const meta = document.createElement('div');
|
|
618
|
+
meta.className = 'msg-detail-meta';
|
|
619
|
+
meta.innerHTML = `<strong>${dirArrow(entry.dir)} ${entry.dir.toUpperCase()}</strong>
|
|
620
|
+
${escHtml(entry.time)}
|
|
621
|
+
<span title="${escHtml(entry.sid)}">${escHtml(shortSid(entry.sid))}</span>`;
|
|
622
|
+
content.appendChild(meta);
|
|
623
|
+
|
|
624
|
+
// Parse fields
|
|
625
|
+
entry.raw.split('|').filter(Boolean).forEach(part => {
|
|
626
|
+
const eq = part.indexOf('=');
|
|
627
|
+
if (eq < 1) return;
|
|
628
|
+
const tag = part.slice(0, eq).trim();
|
|
629
|
+
const val = part.slice(eq + 1);
|
|
630
|
+
const name = fixTagName(tag);
|
|
631
|
+
const display = fixEnumVal(tag, val);
|
|
632
|
+
|
|
633
|
+
const row = document.createElement('div');
|
|
634
|
+
row.className = 'msg-detail-row';
|
|
635
|
+
row.innerHTML =
|
|
636
|
+
`<span class="detail-tag">${escHtml(tag)}</span>` +
|
|
637
|
+
`<span class="detail-name" title="${escHtml(name)}">${escHtml(name)}</span>` +
|
|
638
|
+
`<span class="detail-val" title="${escHtml(val)}">${escHtml(display)}</span>`;
|
|
639
|
+
content.appendChild(row);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
$id('msg-detail-panel').classList.remove('hidden');
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function closeMsgDetail() {
|
|
646
|
+
if (selectedLogRow) { selectedLogRow.classList.remove('selected'); selectedLogRow = null; }
|
|
647
|
+
$id('msg-detail-panel').classList.add('hidden');
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
|
|
524
651
|
// ── Message log ───────────────────────────────────────────────────────────
|
|
652
|
+
const LOG_STORAGE_KEY = 'fixcore_log';
|
|
653
|
+
|
|
654
|
+
function saveLog() {
|
|
655
|
+
try { localStorage.setItem(LOG_STORAGE_KEY, JSON.stringify(logEntries)); } catch (_) {}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function loadLog() {
|
|
659
|
+
try {
|
|
660
|
+
const raw = localStorage.getItem(LOG_STORAGE_KEY);
|
|
661
|
+
if (raw) logEntries = JSON.parse(raw);
|
|
662
|
+
} catch (_) {}
|
|
663
|
+
}
|
|
664
|
+
|
|
525
665
|
function addLogEntry(dir, sid, raw) {
|
|
526
666
|
const entry = { dir, sid, raw, time: nowStr() };
|
|
527
667
|
logEntries.push(entry);
|
|
528
668
|
if (logEntries.length > 2000) logEntries.shift();
|
|
669
|
+
saveLog();
|
|
529
670
|
appendLogRow(entry);
|
|
530
671
|
}
|
|
531
672
|
|
|
@@ -537,6 +678,7 @@ function appendLogRow(entry) {
|
|
|
537
678
|
const row = document.createElement('div');
|
|
538
679
|
row.className = `log-entry ${entry.dir}`;
|
|
539
680
|
row.dataset.dir = entry.dir;
|
|
681
|
+
row._logEntry = entry;
|
|
540
682
|
row.innerHTML = `
|
|
541
683
|
<span class="log-time">${escHtml(entry.time)}</span>
|
|
542
684
|
<span class="log-dir">${dirArrow(entry.dir)}</span>
|
|
@@ -562,8 +704,21 @@ function shortSid(sid) {
|
|
|
562
704
|
: sid.slice(0, 12);
|
|
563
705
|
}
|
|
564
706
|
|
|
707
|
+
const SESSION_MSG_TYPES = new Set(['0','1','2','3','4','5','A']);
|
|
708
|
+
|
|
709
|
+
function getMsgType(raw) {
|
|
710
|
+
const m = raw.match(/\b35=([^|]+)/);
|
|
711
|
+
return m ? m[1].trim() : null;
|
|
712
|
+
}
|
|
713
|
+
|
|
565
714
|
function matchesFilter(entry) {
|
|
715
|
+
if (activeSid && entry.sid !== activeSid) return false;
|
|
566
716
|
if (logFilter === 'all') return true;
|
|
717
|
+
if (logFilter === 'app') {
|
|
718
|
+
if (entry.dir === 'evt') return false;
|
|
719
|
+
const mt = getMsgType(entry.raw);
|
|
720
|
+
return mt !== null && !SESSION_MSG_TYPES.has(mt);
|
|
721
|
+
}
|
|
567
722
|
if (logFilter === 'in') return entry.dir === 'in';
|
|
568
723
|
if (logFilter === 'out') return entry.dir === 'out';
|
|
569
724
|
if (logFilter === 'event') return entry.dir === 'evt';
|
|
@@ -588,7 +743,9 @@ $id('panel-log').querySelectorAll('.filter-btn').forEach(btn => {
|
|
|
588
743
|
|
|
589
744
|
$id('btn-clear-log').addEventListener('click', () => {
|
|
590
745
|
logEntries = [];
|
|
746
|
+
localStorage.removeItem(LOG_STORAGE_KEY);
|
|
591
747
|
$id('log-entries').innerHTML = '';
|
|
748
|
+
closeMsgDetail();
|
|
592
749
|
});
|
|
593
750
|
|
|
594
751
|
// ── Keyboard shortcuts ────────────────────────────────────────────────────
|
|
@@ -597,6 +754,7 @@ document.addEventListener('keydown', e => {
|
|
|
597
754
|
$id('modal-overlay').classList.add('hidden');
|
|
598
755
|
$id('paste-modal-overlay').classList.add('hidden');
|
|
599
756
|
editMode = false; editOldKey = null;
|
|
757
|
+
closeMsgDetail();
|
|
600
758
|
}
|
|
601
759
|
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
|
602
760
|
e.preventDefault();
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
<div class="detail-actions">
|
|
44
44
|
<button class="btn btn-primary" id="btn-logon">Logon</button>
|
|
45
45
|
<button class="btn btn-warn" id="btn-logout">Logout</button>
|
|
46
|
+
<button class="btn btn-secondary" id="btn-stop">Stop</button>
|
|
46
47
|
<button class="btn btn-secondary" id="btn-reset">Reset SeqNums</button>
|
|
47
48
|
<button class="btn btn-secondary" id="btn-edit">Edit</button>
|
|
48
49
|
<button class="btn btn-danger" id="btn-remove">Remove</button>
|
|
@@ -52,7 +53,11 @@
|
|
|
52
53
|
|
|
53
54
|
<!-- Send message panel -->
|
|
54
55
|
<section class="panel" id="panel-send">
|
|
55
|
-
<div class="panel-title"
|
|
56
|
+
<div class="panel-title panel-title-collapsible" id="btn-toggle-send">
|
|
57
|
+
<span>Send Message</span>
|
|
58
|
+
<span class="collapse-chevron" id="send-chevron">▲</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div id="panel-send-body">
|
|
56
61
|
<div class="send-row">
|
|
57
62
|
<label class="send-label">MsgType</label>
|
|
58
63
|
<select id="msg-type-select" class="select-msg-type">
|
|
@@ -87,6 +92,7 @@
|
|
|
87
92
|
<button class="btn btn-secondary" id="btn-add-field">+ Add Field</button>
|
|
88
93
|
<button class="btn btn-primary" id="btn-send">Send Message</button>
|
|
89
94
|
</div>
|
|
95
|
+
</div><!-- /panel-send-body -->
|
|
90
96
|
</section>
|
|
91
97
|
|
|
92
98
|
<!-- Message log -->
|
|
@@ -95,13 +101,23 @@
|
|
|
95
101
|
<span class="panel-title">Message Log</span>
|
|
96
102
|
<div class="log-filters">
|
|
97
103
|
<button class="filter-btn active" data-filter="all">All</button>
|
|
104
|
+
<button class="filter-btn" data-filter="app">App</button>
|
|
98
105
|
<button class="filter-btn" data-filter="out">▶ Out</button>
|
|
99
106
|
<button class="filter-btn" data-filter="in">◀ In</button>
|
|
100
107
|
<button class="filter-btn" data-filter="event">Events</button>
|
|
101
108
|
</div>
|
|
102
109
|
<button class="btn btn-secondary btn-sm" id="btn-clear-log">Clear</button>
|
|
103
110
|
</div>
|
|
104
|
-
<div
|
|
111
|
+
<div class="log-body">
|
|
112
|
+
<div id="log-entries" class="log-entries"></div>
|
|
113
|
+
<aside id="msg-detail-panel" class="msg-detail-panel hidden">
|
|
114
|
+
<div class="msg-detail-header">
|
|
115
|
+
<span class="msg-detail-title">Message Detail</span>
|
|
116
|
+
<button id="btn-close-detail" class="btn-close-detail">×</button>
|
|
117
|
+
</div>
|
|
118
|
+
<div id="msg-detail-content" class="msg-detail-content"></div>
|
|
119
|
+
</aside>
|
|
120
|
+
</div>
|
|
105
121
|
</section>
|
|
106
122
|
</main>
|
|
107
123
|
</div>
|
|
@@ -138,6 +138,19 @@ body {
|
|
|
138
138
|
color: var(--text-dim);
|
|
139
139
|
margin-bottom: 10px;
|
|
140
140
|
}
|
|
141
|
+
.panel-title-collapsible {
|
|
142
|
+
display: flex;
|
|
143
|
+
justify-content: space-between;
|
|
144
|
+
align-items: center;
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
user-select: none;
|
|
147
|
+
margin-bottom: 10px;
|
|
148
|
+
}
|
|
149
|
+
.panel-title-collapsible:hover { color: var(--text); }
|
|
150
|
+
.collapse-chevron { font-size: 10px; transition: transform .2s; }
|
|
151
|
+
.panel-send-collapsed .collapse-chevron { transform: rotate(180deg); }
|
|
152
|
+
.panel-send-collapsed #panel-send-body { display: none; }
|
|
153
|
+
.panel-send-collapsed { padding-bottom: 14px; }
|
|
141
154
|
.placeholder {
|
|
142
155
|
color: var(--text-dim);
|
|
143
156
|
font-style: italic;
|
|
@@ -302,6 +315,12 @@ body {
|
|
|
302
315
|
border-color: var(--accent);
|
|
303
316
|
color: #fff;
|
|
304
317
|
}
|
|
318
|
+
.log-body {
|
|
319
|
+
display: flex;
|
|
320
|
+
flex: 1;
|
|
321
|
+
gap: 0;
|
|
322
|
+
overflow: hidden;
|
|
323
|
+
}
|
|
305
324
|
.log-entries {
|
|
306
325
|
flex: 1;
|
|
307
326
|
overflow-y: auto;
|
|
@@ -311,6 +330,7 @@ body {
|
|
|
311
330
|
border: 1px solid var(--border);
|
|
312
331
|
border-radius: var(--radius);
|
|
313
332
|
padding: 6px 0;
|
|
333
|
+
min-width: 0;
|
|
314
334
|
}
|
|
315
335
|
.log-entry {
|
|
316
336
|
display: flex;
|
|
@@ -318,8 +338,10 @@ body {
|
|
|
318
338
|
padding: 2px 10px;
|
|
319
339
|
border-bottom: 1px solid transparent;
|
|
320
340
|
line-height: 1.5;
|
|
341
|
+
cursor: pointer;
|
|
321
342
|
}
|
|
322
343
|
.log-entry:hover { background: var(--bg2); }
|
|
344
|
+
.log-entry.selected { background: var(--bg3); outline: 1px solid var(--accent); }
|
|
323
345
|
.log-entry.in .log-dir { color: var(--in-color); }
|
|
324
346
|
.log-entry.out .log-dir { color: var(--out-color); }
|
|
325
347
|
.log-entry.evt .log-dir { color: var(--event-color); }
|
|
@@ -329,6 +351,71 @@ body {
|
|
|
329
351
|
.log-sid { color: var(--text-dim); flex-shrink: 0; max-width: 120px; overflow: hidden; text-overflow: ellipsis; }
|
|
330
352
|
.log-msg { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
331
353
|
|
|
354
|
+
/* ── Message detail panel ─────────────────────────────────────────────── */
|
|
355
|
+
.msg-detail-panel {
|
|
356
|
+
width: 320px;
|
|
357
|
+
flex-shrink: 0;
|
|
358
|
+
border-left: 1px solid var(--border);
|
|
359
|
+
display: flex;
|
|
360
|
+
flex-direction: column;
|
|
361
|
+
overflow: hidden;
|
|
362
|
+
background: var(--bg);
|
|
363
|
+
margin-left: 8px;
|
|
364
|
+
border-radius: var(--radius);
|
|
365
|
+
border: 1px solid var(--border);
|
|
366
|
+
}
|
|
367
|
+
.msg-detail-header {
|
|
368
|
+
display: flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
justify-content: space-between;
|
|
371
|
+
padding: 6px 10px;
|
|
372
|
+
border-bottom: 1px solid var(--border);
|
|
373
|
+
flex-shrink: 0;
|
|
374
|
+
background: var(--bg2);
|
|
375
|
+
}
|
|
376
|
+
.msg-detail-title {
|
|
377
|
+
font-size: 11px;
|
|
378
|
+
font-weight: 600;
|
|
379
|
+
text-transform: uppercase;
|
|
380
|
+
letter-spacing: .08em;
|
|
381
|
+
color: var(--text-dim);
|
|
382
|
+
}
|
|
383
|
+
.btn-close-detail {
|
|
384
|
+
background: none;
|
|
385
|
+
border: none;
|
|
386
|
+
color: var(--text-dim);
|
|
387
|
+
cursor: pointer;
|
|
388
|
+
font-size: 16px;
|
|
389
|
+
padding: 0 2px;
|
|
390
|
+
line-height: 1;
|
|
391
|
+
}
|
|
392
|
+
.btn-close-detail:hover { color: var(--text); }
|
|
393
|
+
.msg-detail-content {
|
|
394
|
+
flex: 1;
|
|
395
|
+
overflow-y: auto;
|
|
396
|
+
padding: 4px 0;
|
|
397
|
+
}
|
|
398
|
+
.msg-detail-meta {
|
|
399
|
+
font-size: 11px;
|
|
400
|
+
color: var(--text-dim);
|
|
401
|
+
padding: 4px 10px 6px;
|
|
402
|
+
border-bottom: 1px solid var(--border);
|
|
403
|
+
margin-bottom: 2px;
|
|
404
|
+
}
|
|
405
|
+
.msg-detail-meta strong { color: var(--text); }
|
|
406
|
+
.msg-detail-row {
|
|
407
|
+
display: grid;
|
|
408
|
+
grid-template-columns: 42px minmax(0,1fr) minmax(0,1.4fr);
|
|
409
|
+
gap: 0 6px;
|
|
410
|
+
padding: 2px 10px;
|
|
411
|
+
font-size: 11px;
|
|
412
|
+
line-height: 1.6;
|
|
413
|
+
}
|
|
414
|
+
.msg-detail-row:hover { background: var(--bg2); }
|
|
415
|
+
.detail-tag { color: var(--text-dim); font-variant-numeric: tabular-nums; }
|
|
416
|
+
.detail-name { color: var(--accent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
417
|
+
.detail-val { color: var(--text); word-break: break-all; }
|
|
418
|
+
|
|
332
419
|
/* ── Buttons ─────────────────────────────────────────────────────────── */
|
|
333
420
|
.btn {
|
|
334
421
|
font-family: var(--font);
|
|
@@ -42,6 +42,7 @@ class SocketInitiator:
|
|
|
42
42
|
self._sessions: dict[SessionID, Session] = {}
|
|
43
43
|
self._tasks: list[asyncio.Task[None]] = []
|
|
44
44
|
self._stop_event = asyncio.Event()
|
|
45
|
+
self._no_reconnect = asyncio.Event()
|
|
45
46
|
|
|
46
47
|
for sid in settings.session_ids:
|
|
47
48
|
conn_type = settings.get_or(sid, "ConnectionType", "initiator").lower()
|
|
@@ -59,6 +60,7 @@ class SocketInitiator:
|
|
|
59
60
|
async def start(self) -> None:
|
|
60
61
|
"""Launch a connect-loop task for each configured initiator session."""
|
|
61
62
|
self._stop_event.clear()
|
|
63
|
+
self._no_reconnect.clear()
|
|
62
64
|
for sid, session in self._sessions.items():
|
|
63
65
|
task = asyncio.create_task(
|
|
64
66
|
self._connect_loop(sid, session),
|
|
@@ -73,6 +75,17 @@ class SocketInitiator:
|
|
|
73
75
|
await asyncio.gather(*self._tasks, return_exceptions=True)
|
|
74
76
|
self._tasks.clear()
|
|
75
77
|
|
|
78
|
+
async def logout(self) -> None:
|
|
79
|
+
"""Send FIX Logout for all active sessions and disable auto-reconnect.
|
|
80
|
+
|
|
81
|
+
The connect loop will exit cleanly after the connection drops rather
|
|
82
|
+
than sleeping and retrying. Call :meth:`start` to reconnect later.
|
|
83
|
+
"""
|
|
84
|
+
self._no_reconnect.set()
|
|
85
|
+
for session in self._sessions.values():
|
|
86
|
+
if session.is_logged_on:
|
|
87
|
+
await session.send_logout()
|
|
88
|
+
|
|
76
89
|
async def __aenter__(self) -> "SocketInitiator":
|
|
77
90
|
await self.start()
|
|
78
91
|
return self
|
|
@@ -132,7 +145,7 @@ class SocketInitiator:
|
|
|
132
145
|
pass
|
|
133
146
|
framer.reset()
|
|
134
147
|
|
|
135
|
-
if not self._stop_event.is_set():
|
|
148
|
+
if not self._stop_event.is_set() and not self._no_reconnect.is_set():
|
|
136
149
|
await self._interruptible_sleep(reconnect)
|
|
137
150
|
|
|
138
151
|
async def _interruptible_sleep(self, seconds: float) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|