KarlQuant commited on
Commit
ec8e87d
·
verified ·
1 Parent(s): c15bc8b

Upload 3 files

Browse files
Files changed (3) hide show
  1. hub_dashboard.html +292 -10
  2. hub_dashboard_service.py +43 -3
  3. ranker_logging.py +1 -1
hub_dashboard.html CHANGED
@@ -1436,6 +1436,60 @@ td.rc {
1436
  color: var(--cyan) !important;
1437
  }
1438
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1439
  </style>
1440
 
1441
  <script>
@@ -1653,22 +1707,68 @@ td.rc {
1653
  <div id="detail-panel"></div>
1654
  </div>
1655
 
 
1656
  <div id="bottom-grid">
1657
- <div class="panel" id="feed-panel">
1658
  <div class="panel-hd">
1659
  <div class="panel-hd-left">
1660
- <div class="panel-hd-pip" style="background:#a855f7;box-shadow:0 0 10px rgba(168,85,247,0.5)"></div>
1661
- <span class="panel-hd-label">Live Operation Feed</span>
 
 
 
 
 
 
 
1662
  </div>
1663
- <span class="panel-hd-meta" id="feed-meta">syncing...</span>
1664
  </div>
1665
- <div class="feed-cols hd">
1666
- <span>Target / T-Minus</span>
1667
- <span>Vector</span>
1668
- <span style="text-align:right">Delta</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
1669
  </div>
1670
- <div id="feed-body">
1671
- <div class="feed-cols row"><div class="f-info"><span class="f-asset" style="color:var(--t4)">Standing by...</span></div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1672
  </div>
1673
  </div>
1674
  </div>
@@ -3650,6 +3750,188 @@ setInterval(function(){
3650
  if (aTab && aTab.classList.contains('active')) renderAssetsTab();
3651
  }, 2000);
3652
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3653
  </script>
3654
  </body>
3655
  </html>
 
1436
  color: var(--cyan) !important;
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(--t3); }
1485
+ .tlog-info { color: var(--t2); }
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(--t4); margin-right: 6px; }
1491
+ .tlog-badge { font-weight: 800; margin-right: 4px; }
1492
+
1493
  </style>
1494
 
1495
  <script>
 
1707
  <div id="detail-panel"></div>
1708
  </div>
1709
 
1710
+ <!-- TRAINING PANEL -->
1711
  <div id="bottom-grid">
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:'SF Mono','Fira Code','Cascadia Code',monospace;
1730
+ font-size:0.72rem;
1731
+ line-height:1.65;
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.01em;
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>
 
3750
  if (aTab && aTab.classList.contains('active')) renderAssetsTab();
3751
  }, 2000);
3752
 
3753
+ // ════════════════════════════════════════════════════════════════════════════
3754
+ // TRAINING PANEL — live log stream + KPI metrics
3755
+ // Polls /api/ranker/logs/recent?category=training (DEBUG) + all recent logs
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:1px 0">` +
3795
+ `<span class="tlog-ts" style="flex-shrink:0;width:68px">${ts}</span>` +
3796
+ `<span style="color:${catCol};font-weight:700;flex-shrink:0;width:76px">${catLabel}</span>` +
3797
+ `<span>${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);
3875
+ lossEl.textContent = v.toFixed(4);
3876
+ if (lossTrendEl && _prevLoss != null) {
3877
+ const delta = v - _prevLoss;
3878
+ lossTrendEl.textContent = (delta < 0 ? '▼ ' : '▲ ') + Math.abs(delta).toFixed(4);
3879
+ lossTrendEl.style.color = delta < 0 ? 'var(--green)' : 'var(--red)';
3880
+ }
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);
3888
+ lrEl.textContent = lr < 0.001 ? lr.toExponential(3) : lr.toFixed(6);
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');
3901
+ if (assetsEl) {
3902
+ const n = d.asset_count ?? d.assets ?? null;
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');
3916
+ if (!res.ok) throw new Error(res.status);
3917
+ const json = await res.json();
3918
+ const entries = json.logs || [];
3919
+ updateTerminal(entries);
3920
+ updateTrainingKPIs(entries);
3921
+ } catch(err) {
3922
+ // Silently tolerate connection errors — dashboard may run offline
3923
+ const term = document.getElementById('training-log-terminal');
3924
+ if (term && !_termLines.length) {
3925
+ term.innerHTML = '<div class="tlog-error">Error loading logs — ' + escHtml(err.message) + '</div>';
3926
+ }
3927
+ }
3928
+ }
3929
+
3930
+ // Init: first poll immediately, then repeat
3931
+ pollTrainingLogs();
3932
+ setInterval(pollTrainingLogs, POLL_MS);
3933
+ })();
3934
+
3935
  </script>
3936
  </body>
3937
  </html>
hub_dashboard_service.py CHANGED
@@ -620,6 +620,14 @@ def api_logs_recent():
620
  # Reverse so newest is first
621
  raw_lines.reverse()
622
 
 
 
 
 
 
 
 
 
623
  for line in raw_lines[:limit]:
624
  if not line.strip():
625
  continue
@@ -628,14 +636,46 @@ def api_logs_recent():
628
  continue
629
 
630
  ts = ts_match.group(1)
631
- level_match = re.search(r'\|\s*(INFO|DEBUG|WARNING|ERROR)\s*\|', line)
632
- level = level_match.group(1) if level_match else "INFO"
 
 
 
 
633
 
634
  if category:
635
  if category.upper() not in line.upper():
636
  continue
637
 
638
- logs.append({"timestamp": ts, "level": level, "message": line.strip()})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
 
640
  return jsonify({"logs": logs, "count": len(logs)})
641
 
 
620
  # Reverse so newest is first
621
  raw_lines.reverse()
622
 
623
+ # Pre-compiled patterns for structured field extraction
624
+ _CAT_RE = re.compile(r'\|\s*(INFO|DEBUG|WARNING|ERROR|CRITICAL)\s*\|\s*([A-Z_]+)\s*\|')
625
+ _ASSET_RE = re.compile(r'\|\s*([A-Z][A-Z0-9_]+)\s*\|', re.IGNORECASE)
626
+ _TRAINING_RE = re.compile(
627
+ r'step=(\d+)\s*\|\s*loss=([\d.]+)\s*\|\s*lr=([\d.eE+\-]+)\s*\|\s*assets=(\d+)'
628
+ )
629
+ _JSON_RE = re.compile(r'(\{.*\})\s*$')
630
+
631
  for line in raw_lines[:limit]:
632
  if not line.strip():
633
  continue
 
636
  continue
637
 
638
  ts = ts_match.group(1)
639
+
640
+ # Extract level AND category from the pipe-delimited format:
641
+ # "[ts] | LEVEL | CATEGORY | ..."
642
+ cat_m = _CAT_RE.search(line)
643
+ level = cat_m.group(1) if cat_m else "INFO"
644
+ category_field = cat_m.group(2).strip() if cat_m else ""
645
 
646
  if category:
647
  if category.upper() not in line.upper():
648
  continue
649
 
650
+ # Build base entry
651
+ entry = {
652
+ "timestamp": ts,
653
+ "level": level,
654
+ "category": category_field,
655
+ "message": line.strip(),
656
+ "data": None,
657
+ }
658
+
659
+ # Parse structured data for TRAINING entries
660
+ if category_field == "TRAINING":
661
+ tm = _TRAINING_RE.search(line)
662
+ if tm:
663
+ entry["data"] = {
664
+ "step": int(tm.group(1)),
665
+ "loss": float(tm.group(2)),
666
+ "lr": float(tm.group(3)),
667
+ "asset_count": int(tm.group(4)),
668
+ }
669
+ else:
670
+ # Fallback: try the trailing JSON blob
671
+ jm = _JSON_RE.search(line)
672
+ if jm:
673
+ try:
674
+ entry["data"] = json.loads(jm.group(1))
675
+ except ValueError:
676
+ pass
677
+
678
+ logs.append(entry)
679
 
680
  return jsonify({"logs": logs, "count": len(logs)})
681
 
ranker_logging.py CHANGED
@@ -277,7 +277,7 @@ class RankerLogger:
277
  """Get most recent n log entries, optionally filtered by category."""
278
  with self._lock:
279
  if category:
280
- entries = list(self._by_category.get(category, []))
281
  else:
282
  entries = list(self._buffer)
283
  return [e.to_dict() for e in entries[-n:]]
 
277
  """Get most recent n log entries, optionally filtered by category."""
278
  with self._lock:
279
  if category:
280
+ entries = list(self._by_category.get(category.upper(), []))
281
  else:
282
  entries = list(self._buffer)
283
  return [e.to_dict() for e in entries[-n:]]