KarlQuant commited on
Commit
086ea89
·
verified ·
1 Parent(s): 7de9f3a

Upload hub_dashboard.html

Browse files
Files changed (1) hide show
  1. hub_dashboard.html +159 -157
hub_dashboard.html CHANGED
@@ -1437,85 +1437,58 @@ td.rc {
1437
  }
1438
 
1439
  /* ════════════════════════════════════════════════════════════════════════════
1440
- TRAINING PANEL POLISHED v3
1441
  ════════════════════════════════════════════════════════════════════════════ */
1442
-
1443
- /* KPI strip */
1444
  .training-kpi-cell {
1445
  display: flex;
1446
  flex-direction: column;
1447
- align-items: flex-start;
1448
  justify-content: center;
1449
- padding: 22px 28px;
1450
- gap: 5px;
 
1451
  position: relative;
1452
- transition: background 0.25s ease;
 
 
 
1453
  }
1454
- .training-kpi-cell::before {
1455
- content: '';
1456
- position: absolute;
1457
- left: 0; top: 20%; bottom: 20%;
1458
- width: 2px;
1459
- border-radius: 2px;
1460
- opacity: 0.5;
1461
- }
1462
- .training-kpi-cell:nth-child(1)::before { background: var(--red); }
1463
- .training-kpi-cell:nth-child(2)::before { background: var(--amber); }
1464
- .training-kpi-cell:nth-child(3)::before { background: var(--cyan); }
1465
- .training-kpi-cell:nth-child(4)::before { background: var(--green); }
1466
- .training-kpi-cell:hover { background: rgba(255,255,255,0.025); }
1467
- .training-kpi-cell:hover::before { opacity: 1; }
1468
-
1469
  .training-kpi-label {
1470
- font-size: 0.6rem;
1471
- font-weight: 700;
1472
- letter-spacing: 0.18em;
1473
  text-transform: uppercase;
1474
  color: var(--t3);
1475
  }
1476
  .training-kpi-value {
1477
- font-size: 1.9rem;
1478
  font-weight: 800;
1479
- letter-spacing: -0.03em;
1480
  line-height: 1;
1481
  font-variant-numeric: tabular-nums;
1482
- transition: color 0.4s ease;
1483
  }
1484
  .training-kpi-sub {
1485
- font-size: 0.6rem;
1486
  color: var(--t3);
1487
  font-weight: 500;
1488
- letter-spacing: 0.05em;
1489
- min-height: 13px;
1490
  }
1491
-
1492
- /* Terminal */
1493
- #training-log-terminal::-webkit-scrollbar { width: 3px; }
1494
- #training-log-terminal::-webkit-scrollbar-thumb { background: rgba(0,212,255,0.15); border-radius: 4px; }
1495
  #training-log-terminal::-webkit-scrollbar-track { background: transparent; }
1496
 
1497
  /* Log line colorings */
1498
- .tlog-debug { color: rgba(90,130,160,0.75); }
1499
- .tlog-info { color: var(--t2); }
1500
  .tlog-signal { color: var(--cyan); }
1501
  .tlog-trade { color: var(--amber); }
1502
  .tlog-error { color: var(--red); }
1503
  .tlog-warn { color: var(--amber); }
1504
- .tlog-training { color: #c084fc; }
1505
-
1506
- /* Log row hover */
1507
- .tlog-row {
1508
- display: flex;
1509
- gap: 0;
1510
- padding: 2px 0;
1511
- border-radius: 3px;
1512
- transition: background 0.15s ease;
1513
- align-items: baseline;
1514
- }
1515
- .tlog-row:hover { background: rgba(255,255,255,0.03); }
1516
- .tlog-ts { color: rgba(58,104,136,0.7); flex-shrink:0; width:60px; font-size:0.68rem; padding-right:8px; }
1517
- .tlog-cat { flex-shrink:0; width:82px; font-weight:700; font-size:0.7rem; padding-right:8px; }
1518
- .tlog-msg { color: rgba(180,210,230,0.75); font-size:0.71rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1519
 
1520
  </style>
1521
 
@@ -1739,64 +1712,64 @@ td.rc {
1739
  <div class="panel" id="training-panel">
1740
  <div class="panel-hd">
1741
  <div class="panel-hd-left">
1742
- <div class="panel-hd-pip" style="background:linear-gradient(180deg,#c084fc,var(--cyan));box-shadow:0 0 12px rgba(192,132,252,0.5)"></div>
1743
  <span class="panel-hd-label">Training Logs</span>
1744
  </div>
1745
- <div style="display:flex;align-items:center;gap:14px">
1746
  <span class="panel-hd-meta" id="training-meta">awaiting training stream…</span>
1747
- <div style="display:flex;align-items:center;gap:6px;padding:5px 12px;border-radius:var(--pill-r);border:1px solid rgba(192,132,252,0.25);background:rgba(192,132,252,0.07)">
1748
- <div class="pring live" id="training-dot"><div class="core" style="background:#c084fc;box-shadow:0 0 8px #c084fc"></div></div>
1749
- <span style="font-size:0.6rem;font-weight:800;letter-spacing:0.14em;color:#c084fc">LIVE</span>
1750
  </div>
1751
  </div>
1752
  </div>
1753
 
1754
- <!-- KPI strip sits ABOVE the terminal so metrics are immediately visible -->
1755
- <div style="display:grid;grid-template-columns:repeat(4,1fr);border-bottom:1px solid rgba(255,255,255,0.05);background:rgba(0,0,0,0.2)">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1756
 
1757
  <div class="training-kpi-cell" style="border-right:1px solid rgba(255,255,255,0.04)">
1758
  <div class="training-kpi-label">Loss</div>
1759
- <div class="training-kpi-value" id="trn-loss" style="color:var(--red);text-shadow:0 0 18px rgba(255,68,102,0.4)">—</div>
1760
- <div class="training-kpi-sub" id="trn-loss-trend">&nbsp;</div>
1761
  </div>
1762
 
1763
  <div class="training-kpi-cell" style="border-right:1px solid rgba(255,255,255,0.04)">
1764
  <div class="training-kpi-label">Learning Rate</div>
1765
- <div class="training-kpi-value" id="trn-lr" style="color:var(--amber);text-shadow:0 0 18px rgba(255,170,0,0.35)">—</div>
1766
  <div class="training-kpi-sub" id="trn-lr-sub">scheduler active</div>
1767
  </div>
1768
 
1769
  <div class="training-kpi-cell" style="border-right:1px solid rgba(255,255,255,0.04)">
1770
  <div class="training-kpi-label">Step</div>
1771
- <div class="training-kpi-value" id="trn-step" style="color:var(--cyan);text-shadow:0 0 18px rgba(0,212,255,0.35)">—</div>
1772
  <div class="training-kpi-sub" id="trn-step-sub">gradient updates</div>
1773
  </div>
1774
 
1775
  <div class="training-kpi-cell">
1776
  <div class="training-kpi-label">Assets</div>
1777
- <div class="training-kpi-value" id="trn-assets" style="color:var(--green);text-shadow:0 0 18px rgba(0,255,136,0.35)">—</div>
1778
  <div class="training-kpi-sub" id="trn-assets-sub">in cluster</div>
1779
  </div>
1780
 
1781
  </div>
1782
-
1783
- <!-- Terminal log stream -->
1784
- <div id="training-log-terminal" style="
1785
- font-family:'SF Mono','Fira Code','Cascadia Code',monospace;
1786
- font-size:0.73rem;
1787
- line-height:1.7;
1788
- color:var(--t2);
1789
- background:rgba(0,2,8,0.65);
1790
- padding:14px 20px;
1791
- height:260px;
1792
- overflow-y:auto;
1793
- letter-spacing:0.01em;
1794
- scrollbar-width:thin;
1795
- scrollbar-color:rgba(192,132,252,0.15) transparent;
1796
- ">
1797
- <div style="color:var(--t3);font-style:italic;font-size:0.7rem">Awaiting training data stream…</div>
1798
- </div>
1799
-
1800
  </div>
1801
  </div>
1802
  </div>
@@ -3783,106 +3756,119 @@ setInterval(function(){
3783
  // Falls back gracefully if endpoint is unavailable
3784
  // ════════════════════════════════════════════════════════════════════════════
3785
  (function() {
3786
- const MAX_LINES = 100;
3787
- const POLL_MS = 2500;
3788
-
3789
- let _termLines = [];
3790
- let _seenKeys = new Set(); // dedup by "timestamp|step" key — fixes duplicate entries
3791
- let _prevLoss = null;
3792
-
3793
- function _entryKey(entry) {
3794
- // Use timestamp + step (if present) as a unique key to prevent duplicate display
3795
- const ts = entry.timestamp || '';
3796
- const msg = entry.message || '';
3797
- const step = (msg.match(/step=(\d+)/) || [])[1] || '';
3798
- return ts + '|' + step + '|' + msg.slice(0, 40);
3799
- }
3800
-
3801
- function escHtml(s) {
3802
- return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
3803
- }
3804
 
 
3805
  function formatLogLine(entry) {
3806
- const ts = (entry.timestamp || '').slice(11, 19);
3807
  const cat = (entry.category || '').toUpperCase();
3808
  const lvl = (entry.level || '').toUpperCase();
3809
 
3810
- // Clean the raw message line
3811
  let msg = entry.message || '';
3812
- msg = msg.replace(/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]\s*\|\s*\S+\s*\|\s*\S+\s*(\|\s*\S+\s*)?\|\s*/, '');
3813
- msg = msg.replace(/\s*\{.*\}\s*$/, '').trim();
3814
-
3815
- // Category color and class
3816
- const catCol = cat === 'TRAINING' ? '#c084fc' :
3817
- cat === 'SIGNAL' ? 'var(--cyan)' :
3818
- cat === 'TRADE' ? 'var(--amber)' :
3819
- cat === 'RANKING' ? 'var(--green)' : 'var(--t3)';
3820
-
3821
- const rowClass = cat === 'TRAINING' ? 'tlog-row tlog-training' :
3822
- cat === 'SIGNAL' ? 'tlog-row tlog-signal' :
3823
- cat === 'TRADE' ? 'tlog-row tlog-trade' :
3824
- lvl === 'ERROR' || lvl === 'CRITICAL' ? 'tlog-row tlog-error' :
3825
- lvl === 'WARNING' ? 'tlog-row tlog-warn' : 'tlog-row tlog-info';
3826
-
3827
- return `<div class="${rowClass}">` +
3828
- `<span class="tlog-ts">${ts}</span>` +
3829
- `<span class="tlog-cat" style="color:${catCol}">${cat || lvl}</span>` +
3830
- `<span class="tlog-msg">${escHtml(msg)}</span>` +
 
 
 
 
 
 
3831
  `</div>`;
3832
  }
3833
 
 
 
 
 
3834
  function updateTerminal(entries) {
3835
  const term = document.getElementById('training-log-terminal');
3836
  if (!term) return;
3837
 
3838
- // Only add entries we haven't seen before (fix duplicates)
3839
- const newEntries = entries.filter(e => {
3840
- const k = _entryKey(e);
3841
- if (_seenKeys.has(k)) return false;
3842
- _seenKeys.add(k);
3843
- return true;
3844
- });
3845
 
3846
  if (!newEntries.length) return;
3847
 
3848
- // Keep seen set bounded
3849
- if (_seenKeys.size > 2000) {
3850
- const arr = [..._seenKeys];
3851
- _seenKeys = new Set(arr.slice(arr.length - 1000));
3852
- }
3853
 
3854
- // newEntries come newest-first from the API — reverse so oldest goes in first
3855
- const ordered = [...newEntries].reverse();
3856
- ordered.forEach(e => _termLines.push(formatLogLine(e)));
3857
  if (_termLines.length > MAX_LINES) _termLines = _termLines.slice(-MAX_LINES);
3858
 
3859
- const atBottom = term.scrollHeight - term.clientHeight - term.scrollTop < 50;
3860
  term.innerHTML = _termLines.join('');
3861
  if (atBottom) term.scrollTop = term.scrollHeight;
3862
  }
3863
 
3864
- const _TRAINING_MSG_RE = /step=(\d+).*?loss=([+-]?[\d.]+).*?lr=([\d.eE+\-]+).*?assets=(\d+)/;
 
 
 
 
3865
 
3866
  function _extractTrainingData(entry) {
3867
- if (entry.data && typeof entry.data === 'object' && entry.data.step != null) return entry.data;
3868
- if (entry.metadata && entry.metadata.step != null) return entry.metadata;
 
 
 
 
 
 
 
3869
  const m = _TRAINING_MSG_RE.exec(entry.message || '');
3870
- if (m) return { step: parseInt(m[1],10), loss: parseFloat(m[2]), lr: parseFloat(m[3]), asset_count: parseInt(m[4],10) };
 
 
 
 
 
 
 
3871
  return null;
3872
  }
3873
 
3874
  function updateTrainingKPIs(entries) {
 
3875
  const trainEntries = entries
3876
- .filter(e => (e.category||'').toUpperCase() === 'TRAINING' || (e.message||'').includes('| TRAINING '))
 
 
 
 
3877
  .map(e => ({ entry: e, data: _extractTrainingData(e) }))
3878
  .filter(x => x.data !== null)
3879
- .sort((a,b) => (a.entry.timestamp||'') < (b.entry.timestamp||'') ? -1 : 1);
3880
 
3881
  if (!trainEntries.length) return;
 
3882
  const { entry: latest, data: d } = trainEntries[trainEntries.length - 1];
3883
 
3884
  // Loss
3885
- const lossEl = document.getElementById('trn-loss');
3886
  const lossTrendEl = document.getElementById('trn-loss-trend');
3887
  if (lossEl && d.loss != null) {
3888
  const v = parseFloat(d.loss);
@@ -3895,7 +3881,7 @@ setInterval(function(){
3895
  _prevLoss = v;
3896
  }
3897
 
3898
- // LR
3899
  const lrEl = document.getElementById('trn-lr');
3900
  if (lrEl && d.lr != null) {
3901
  const lr = parseFloat(d.lr);
@@ -3903,8 +3889,12 @@ setInterval(function(){
3903
  }
3904
 
3905
  // Step
3906
- const stepEl = document.getElementById('trn-step');
3907
- if (stepEl && d.step != null) stepEl.textContent = parseInt(d.step).toLocaleString();
 
 
 
 
3908
 
3909
  // Assets
3910
  const assetsEl = document.getElementById('trn-assets');
@@ -3913,34 +3903,46 @@ setInterval(function(){
3913
  if (n != null) assetsEl.textContent = n;
3914
  }
3915
 
3916
- // Header timestamp
3917
  const metaEl = document.getElementById('training-meta');
3918
- if (metaEl && latest.timestamp) metaEl.textContent = 'last update ' + (latest.timestamp||'').slice(11,19);
 
 
3919
  }
3920
 
3921
  async function pollTrainingLogs() {
3922
  try {
3923
  const res = await fetch('/api/ranker/logs/recent?limit=80&category=TRAINING');
3924
- let data;
3925
- try { data = await res.json(); } catch(e) {
3926
- throw new Error('HTTP ' + res.status + ' — non-JSON response');
 
 
 
 
3927
  }
3928
  if (!res.ok) {
 
 
3929
  const term = document.getElementById('training-log-terminal');
3930
- if (term && !_termLines.length)
3931
- term.innerHTML = `<div class="tlog-error" style="font-size:11px">⚠ ${escHtml(data.error||'HTTP '+res.status)}</div>`;
 
3932
  return;
3933
  }
3934
- const entries = data.logs || [];
3935
  updateTerminal(entries);
3936
  updateTrainingKPIs(entries);
3937
  } catch(err) {
 
3938
  const term = document.getElementById('training-log-terminal');
3939
- if (term && !_termLines.length)
3940
- term.innerHTML = `<div class="tlog-error" style="font-size:11px">⚠ ${escHtml(err.message)}</div>`;
 
3941
  }
3942
  }
3943
 
 
3944
  pollTrainingLogs();
3945
  setInterval(pollTrainingLogs, POLL_MS);
3946
  })();
 
1437
  }
1438
 
1439
  /* ════════════════════════════════════════════════════════════════════════════
1440
+ TRAINING PANEL KPI CELLS
1441
  ════════════════════════════════════════════════════════════════════════════ */
 
 
1442
  .training-kpi-cell {
1443
  display: flex;
1444
  flex-direction: column;
1445
+ align-items: center;
1446
  justify-content: center;
1447
+ padding: 20px var(--sp-lg);
1448
+ gap: 6px;
1449
+ text-align: center;
1450
  position: relative;
1451
+ transition: background 0.3s ease;
1452
+ }
1453
+ .training-kpi-cell:hover {
1454
+ background: rgba(255,255,255,0.02);
1455
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1456
  .training-kpi-label {
1457
+ font-size: 0.62rem;
1458
+ font-weight: 800;
1459
+ letter-spacing: 0.2em;
1460
  text-transform: uppercase;
1461
  color: var(--t3);
1462
  }
1463
  .training-kpi-value {
1464
+ font-size: 1.65rem;
1465
  font-weight: 800;
1466
+ letter-spacing: -0.02em;
1467
  line-height: 1;
1468
  font-variant-numeric: tabular-nums;
1469
+ transition: all 0.4s ease;
1470
  }
1471
  .training-kpi-sub {
1472
+ font-size: 0.62rem;
1473
  color: var(--t3);
1474
  font-weight: 500;
1475
+ letter-spacing: 0.04em;
1476
+ min-height: 14px;
1477
  }
1478
+ /* Terminal scrollbar styling */
1479
+ #training-log-terminal::-webkit-scrollbar { width: 4px; }
1480
+ #training-log-terminal::-webkit-scrollbar-thumb { background: rgba(0,212,255,0.2); border-radius: 4px; }
 
1481
  #training-log-terminal::-webkit-scrollbar-track { background: transparent; }
1482
 
1483
  /* Log line colorings */
1484
+ .tlog-debug { color: var(--t2); }
1485
+ .tlog-info { color: var(--t1); }
1486
  .tlog-signal { color: var(--cyan); }
1487
  .tlog-trade { color: var(--amber); }
1488
  .tlog-error { color: var(--red); }
1489
  .tlog-warn { color: var(--amber); }
1490
+ .tlog-ts { color: var(--t3); margin-right: 6px; font-variant-numeric: tabular-nums; }
1491
+ .tlog-badge { font-weight: 700; margin-right: 4px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
1492
 
1493
  </style>
1494
 
 
1712
  <div class="panel" id="training-panel">
1713
  <div class="panel-hd">
1714
  <div class="panel-hd-left">
1715
+ <div class="panel-hd-pip" style="background:var(--cyan);box-shadow:0 0 10px var(--cyan-glow)"></div>
1716
  <span class="panel-hd-label">Training Logs</span>
1717
  </div>
1718
+ <div style="display:flex;align-items:center;gap:12px">
1719
  <span class="panel-hd-meta" id="training-meta">awaiting training stream…</span>
1720
+ <div style="display:flex;align-items:center;gap:6px;padding:4px 10px;border-radius:var(--pill-r);border:1px solid rgba(0,212,255,0.2);background:rgba(0,212,255,0.06)">
1721
+ <div class="pring live" id="training-dot"><div class="core"></div></div>
1722
+ <span style="font-size:0.6rem;font-weight:800;letter-spacing:0.12em;color:var(--cyan)">LIVE</span>
1723
  </div>
1724
  </div>
1725
  </div>
1726
 
1727
+ <!-- Terminal log stream -->
1728
+ <div id="training-log-terminal" style="
1729
+ font-family:var(--font);
1730
+ font-size:0.72rem;
1731
+ line-height:1.7;
1732
+ color:var(--t2);
1733
+ background:rgba(0,0,0,0.5);
1734
+ border-bottom:1px solid rgba(255,255,255,0.04);
1735
+ padding:16px 20px;
1736
+ height:200px;
1737
+ overflow-y:auto;
1738
+ letter-spacing:0.02em;
1739
+ scrollbar-width:thin;
1740
+ scrollbar-color:rgba(0,212,255,0.2) transparent;
1741
+ ">
1742
+ <div style="color:var(--t3);font-style:italic">Awaiting training data stream…</div>
1743
+ </div>
1744
+
1745
+ <!-- KPI strip: Loss · Learning Rate · Step · Assets -->
1746
+ <div style="display:grid;grid-template-columns:repeat(4,1fr);border-top:1px solid rgba(255,255,255,0.04)">
1747
 
1748
  <div class="training-kpi-cell" style="border-right:1px solid rgba(255,255,255,0.04)">
1749
  <div class="training-kpi-label">Loss</div>
1750
+ <div class="training-kpi-value" id="trn-loss" style="color:var(--red);text-shadow:0 0 12px rgba(255,68,102,0.45)">—</div>
1751
+ <div class="training-kpi-sub" id="trn-loss-trend"></div>
1752
  </div>
1753
 
1754
  <div class="training-kpi-cell" style="border-right:1px solid rgba(255,255,255,0.04)">
1755
  <div class="training-kpi-label">Learning Rate</div>
1756
+ <div class="training-kpi-value" id="trn-lr" style="color:var(--amber);text-shadow:0 0 12px rgba(255,170,0,0.4)">—</div>
1757
  <div class="training-kpi-sub" id="trn-lr-sub">scheduler active</div>
1758
  </div>
1759
 
1760
  <div class="training-kpi-cell" style="border-right:1px solid rgba(255,255,255,0.04)">
1761
  <div class="training-kpi-label">Step</div>
1762
+ <div class="training-kpi-value" id="trn-step" style="color:var(--cyan);text-shadow:0 0 12px rgba(0,212,255,0.4)">—</div>
1763
  <div class="training-kpi-sub" id="trn-step-sub">gradient updates</div>
1764
  </div>
1765
 
1766
  <div class="training-kpi-cell">
1767
  <div class="training-kpi-label">Assets</div>
1768
+ <div class="training-kpi-value" id="trn-assets" style="color:var(--green);text-shadow:0 0 12px rgba(0,255,136,0.4)">—</div>
1769
  <div class="training-kpi-sub" id="trn-assets-sub">in cluster</div>
1770
  </div>
1771
 
1772
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1773
  </div>
1774
  </div>
1775
  </div>
 
3756
  // Falls back gracefully if endpoint is unavailable
3757
  // ════════════════════════════════════════════════════════════════════════════
3758
  (function() {
3759
+ const MAX_LINES = 120; // keep last N lines in terminal
3760
+ const POLL_MS = 2000;
3761
+ let _termLines = [];
3762
+ let _lastLogTs = null;
3763
+ let _prevLoss = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
3764
 
3765
+ // Format a log entry into a coloured terminal line
3766
  function formatLogLine(entry) {
3767
+ const ts = (entry.timestamp || '').slice(11, 19); // HH:MM:SS (no ms for raw lines)
3768
  const cat = (entry.category || '').toUpperCase();
3769
  const lvl = (entry.level || '').toUpperCase();
3770
 
3771
+ // Prefer structured message; strip raw-line boilerplate if the API returns the full line
3772
  let msg = entry.message || '';
3773
+ // Strip leading "[YYYY-MM-DD HH:MM:SS] | LEVEL | CATEGORY | " prefix (raw line format)
3774
+ msg = msg.replace(/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]\s*\|\s*\w+\s*\|\s*\w+\s*\|\s*/, '');
3775
+ // Strip trailing JSON blob — already structured in entry.data
3776
+ msg = msg.replace(/\s*\{.*\}\s*$/, '');
3777
+ msg = msg.trim();
3778
+
3779
+ let lineClass = 'tlog-info';
3780
+ if (lvl === 'DEBUG') lineClass = 'tlog-debug';
3781
+ else if (lvl === 'ERROR' || lvl === 'CRITICAL') lineClass = 'tlog-error';
3782
+ else if (lvl === 'WARNING') lineClass = 'tlog-warn';
3783
+ else if (cat === 'SIGNAL') lineClass = 'tlog-signal';
3784
+ else if (cat === 'TRADE') lineClass = 'tlog-trade';
3785
+ else if (cat === 'TRAINING') lineClass = 'tlog-debug';
3786
+
3787
+ const catCol = cat === 'SIGNAL' ? 'var(--cyan)' :
3788
+ cat === 'TRADE' ? 'var(--amber)' :
3789
+ cat === 'TRAINING' ? '#a855f7' :
3790
+ cat === 'RANKING' ? 'var(--green)' : 'var(--t3)';
3791
+
3792
+ const catLabel = cat || lvl;
3793
+
3794
+ return `<div class="${lineClass}" style="display:flex;gap:10px;padding:2px 0">` +
3795
+ `<span class="tlog-ts" style="flex-shrink:0;width:62px;font-size:0.68rem;opacity:0.7">${ts}</span>` +
3796
+ `<span style="color:${catCol};font-weight:700;font-size:0.65rem;letter-spacing:0.08em;text-transform:uppercase;flex-shrink:0;width:72px">${catLabel}</span>` +
3797
+ `<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(msg)}</span>` +
3798
  `</div>`;
3799
  }
3800
 
3801
+ function escHtml(s) {
3802
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
3803
+ }
3804
+
3805
  function updateTerminal(entries) {
3806
  const term = document.getElementById('training-log-terminal');
3807
  if (!term) return;
3808
 
3809
+ // Append only NEW entries (filter by timestamp > _lastLogTs)
3810
+ const newEntries = _lastLogTs
3811
+ ? entries.filter(e => (e.timestamp || '') > _lastLogTs)
3812
+ : entries;
 
 
 
3813
 
3814
  if (!newEntries.length) return;
3815
 
3816
+ _lastLogTs = newEntries[newEntries.length - 1].timestamp || _lastLogTs;
 
 
 
 
3817
 
3818
+ newEntries.forEach(e => _termLines.push(formatLogLine(e)));
 
 
3819
  if (_termLines.length > MAX_LINES) _termLines = _termLines.slice(-MAX_LINES);
3820
 
3821
+ const atBottom = term.scrollHeight - term.clientHeight - term.scrollTop < 40;
3822
  term.innerHTML = _termLines.join('');
3823
  if (atBottom) term.scrollTop = term.scrollHeight;
3824
  }
3825
 
3826
+ // Regex to extract training fields directly from the raw message string.
3827
+ // Matches the format produced by ranker_logging.py training_update():
3828
+ // "step=14 | loss=0.0278 | lr=0.000300 | assets=4"
3829
+ // Also matches the full pipe-delimited log line (the API returns the raw line as "message").
3830
+ const _TRAINING_MSG_RE = /step=(\d+).*?loss=([\d.]+).*?lr=([\d.eE+\-]+).*?assets=(\d+)/;
3831
 
3832
  function _extractTrainingData(entry) {
3833
+ // Priority 1: structured "data" field from the enriched API endpoint
3834
+ if (entry.data && typeof entry.data === 'object' && entry.data.step != null) {
3835
+ return entry.data;
3836
+ }
3837
+ // Priority 2: JSON blob embedded in "metadata" (RankerLogger.to_dict())
3838
+ if (entry.metadata && entry.metadata.step != null) {
3839
+ return entry.metadata;
3840
+ }
3841
+ // Priority 3: Regex over the raw message / log line string
3842
  const m = _TRAINING_MSG_RE.exec(entry.message || '');
3843
+ if (m) {
3844
+ return {
3845
+ step: parseInt(m[1], 10),
3846
+ loss: parseFloat(m[2]),
3847
+ lr: parseFloat(m[3]),
3848
+ asset_count: parseInt(m[4], 10),
3849
+ };
3850
+ }
3851
  return null;
3852
  }
3853
 
3854
  function updateTrainingKPIs(entries) {
3855
+ // Accept entries whose category field OR raw message text indicate TRAINING
3856
  const trainEntries = entries
3857
+ .filter(e => {
3858
+ const cat = (e.category || '').toUpperCase();
3859
+ const msg = (e.message || '').toUpperCase();
3860
+ return cat === 'TRAINING' || msg.includes('| TRAINING ');
3861
+ })
3862
  .map(e => ({ entry: e, data: _extractTrainingData(e) }))
3863
  .filter(x => x.data !== null)
3864
+ .sort((a, b) => (a.entry.timestamp || '') < (b.entry.timestamp || '') ? -1 : 1);
3865
 
3866
  if (!trainEntries.length) return;
3867
+
3868
  const { entry: latest, data: d } = trainEntries[trainEntries.length - 1];
3869
 
3870
  // Loss
3871
+ const lossEl = document.getElementById('trn-loss');
3872
  const lossTrendEl = document.getElementById('trn-loss-trend');
3873
  if (lossEl && d.loss != null) {
3874
  const v = parseFloat(d.loss);
 
3881
  _prevLoss = v;
3882
  }
3883
 
3884
+ // Learning rate — display as fixed 6dp (matches log format) or sci notation if tiny
3885
  const lrEl = document.getElementById('trn-lr');
3886
  if (lrEl && d.lr != null) {
3887
  const lr = parseFloat(d.lr);
 
3889
  }
3890
 
3891
  // Step
3892
+ const stepEl = document.getElementById('trn-step');
3893
+ const stepSubEl = document.getElementById('trn-step-sub');
3894
+ if (stepEl && d.step != null) {
3895
+ stepEl.textContent = parseInt(d.step).toLocaleString();
3896
+ if (stepSubEl) stepSubEl.textContent = 'gradient updates';
3897
+ }
3898
 
3899
  // Assets
3900
  const assetsEl = document.getElementById('trn-assets');
 
3903
  if (n != null) assetsEl.textContent = n;
3904
  }
3905
 
3906
+ // Header meta timestamp
3907
  const metaEl = document.getElementById('training-meta');
3908
+ if (metaEl && latest.timestamp) {
3909
+ metaEl.textContent = 'last update ' + (latest.timestamp || '').slice(11, 19);
3910
+ }
3911
  }
3912
 
3913
  async function pollTrainingLogs() {
3914
  try {
3915
  const res = await fetch('/api/ranker/logs/recent?limit=80&category=TRAINING');
3916
+ // v2.3: always parse JSON even on non-2xx; service now always returns JSON
3917
+ let json;
3918
+ try {
3919
+ json = await res.json();
3920
+ } catch (parseErr) {
3921
+ // Response was not JSON (e.g. HTML error page from a proxy)
3922
+ throw new Error('HTTP ' + res.status + ' — non-JSON response from /api/ranker/logs/recent');
3923
  }
3924
  if (!res.ok) {
3925
+ // Service returned an error object — show it but don't abort
3926
+ const errMsg = json.error || ('HTTP ' + res.status);
3927
  const term = document.getElementById('training-log-terminal');
3928
+ if (term && !_termLines.length) {
3929
+ term.innerHTML = '<div class="tlog-error" style="font-size:11px">⚠ ' + escHtml(errMsg) + '</div>';
3930
+ }
3931
  return;
3932
  }
3933
+ const entries = json.logs || [];
3934
  updateTerminal(entries);
3935
  updateTrainingKPIs(entries);
3936
  } catch(err) {
3937
+ // Silently tolerate transient connection errors — show detail on first failure
3938
  const term = document.getElementById('training-log-terminal');
3939
+ if (term && !_termLines.length) {
3940
+ term.innerHTML = '<div class="tlog-error" style="font-size:11px">⚠ ' + escHtml(err.message) + '</div>';
3941
+ }
3942
  }
3943
  }
3944
 
3945
+ // Init: first poll immediately, then repeat
3946
  pollTrainingLogs();
3947
  setInterval(pollTrainingLogs, POLL_MS);
3948
  })();