Spaces:
Running
Running
Upload hub_dashboard.html
Browse files- hub_dashboard.html +159 -157
hub_dashboard.html
CHANGED
|
@@ -1437,85 +1437,58 @@ td.rc {
|
|
| 1437 |
}
|
| 1438 |
|
| 1439 |
/* ════════════════════════════════════════════════════════════════════════════
|
| 1440 |
-
TRAINING PANEL
|
| 1441 |
════════════════════════════════════════════════════════════════════════════ */
|
| 1442 |
-
|
| 1443 |
-
/* KPI strip */
|
| 1444 |
.training-kpi-cell {
|
| 1445 |
display: flex;
|
| 1446 |
flex-direction: column;
|
| 1447 |
-
align-items:
|
| 1448 |
justify-content: center;
|
| 1449 |
-
padding:
|
| 1450 |
-
gap:
|
|
|
|
| 1451 |
position: relative;
|
| 1452 |
-
transition: background 0.
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 1471 |
-
font-weight:
|
| 1472 |
-
letter-spacing: 0.
|
| 1473 |
text-transform: uppercase;
|
| 1474 |
color: var(--t3);
|
| 1475 |
}
|
| 1476 |
.training-kpi-value {
|
| 1477 |
-
font-size: 1.
|
| 1478 |
font-weight: 800;
|
| 1479 |
-
letter-spacing: -0.
|
| 1480 |
line-height: 1;
|
| 1481 |
font-variant-numeric: tabular-nums;
|
| 1482 |
-
transition:
|
| 1483 |
}
|
| 1484 |
.training-kpi-sub {
|
| 1485 |
-
font-size: 0.
|
| 1486 |
color: var(--t3);
|
| 1487 |
font-weight: 500;
|
| 1488 |
-
letter-spacing: 0.
|
| 1489 |
-
min-height:
|
| 1490 |
}
|
| 1491 |
-
|
| 1492 |
-
|
| 1493 |
-
#training-log-terminal::-webkit-scrollbar {
|
| 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:
|
| 1499 |
-
.tlog-info { color: var(--
|
| 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-
|
| 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:
|
| 1743 |
<span class="panel-hd-label">Training Logs</span>
|
| 1744 |
</div>
|
| 1745 |
-
<div style="display:flex;align-items:center;gap:
|
| 1746 |
<span class="panel-hd-meta" id="training-meta">awaiting training stream…</span>
|
| 1747 |
-
<div style="display:flex;align-items:center;gap:6px;padding:
|
| 1748 |
-
<div class="pring live" id="training-dot"><div class="core"
|
| 1749 |
-
<span style="font-size:0.6rem;font-weight:800;letter-spacing:0.
|
| 1750 |
</div>
|
| 1751 |
</div>
|
| 1752 |
</div>
|
| 1753 |
|
| 1754 |
-
<!--
|
| 1755 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 1760 |
-
<div class="training-kpi-sub" id="trn-loss-trend">
|
| 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
|
| 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
|
| 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
|
| 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
|
| 3787 |
-
const POLL_MS
|
| 3788 |
-
|
| 3789 |
-
let
|
| 3790 |
-
let
|
| 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,'&').replace(/</g,'<').replace(/>/g,'>');
|
| 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 |
-
//
|
| 3811 |
let msg = entry.message || '';
|
| 3812 |
-
|
| 3813 |
-
msg = msg.replace(/\s*\
|
| 3814 |
-
|
| 3815 |
-
//
|
| 3816 |
-
|
| 3817 |
-
|
| 3818 |
-
|
| 3819 |
-
|
| 3820 |
-
|
| 3821 |
-
|
| 3822 |
-
|
| 3823 |
-
|
| 3824 |
-
|
| 3825 |
-
|
| 3826 |
-
|
| 3827 |
-
|
| 3828 |
-
|
| 3829 |
-
|
| 3830 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3831 |
`</div>`;
|
| 3832 |
}
|
| 3833 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3834 |
function updateTerminal(entries) {
|
| 3835 |
const term = document.getElementById('training-log-terminal');
|
| 3836 |
if (!term) return;
|
| 3837 |
|
| 3838 |
-
//
|
| 3839 |
-
const newEntries =
|
| 3840 |
-
|
| 3841 |
-
|
| 3842 |
-
_seenKeys.add(k);
|
| 3843 |
-
return true;
|
| 3844 |
-
});
|
| 3845 |
|
| 3846 |
if (!newEntries.length) return;
|
| 3847 |
|
| 3848 |
-
|
| 3849 |
-
if (_seenKeys.size > 2000) {
|
| 3850 |
-
const arr = [..._seenKeys];
|
| 3851 |
-
_seenKeys = new Set(arr.slice(arr.length - 1000));
|
| 3852 |
-
}
|
| 3853 |
|
| 3854 |
-
|
| 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 <
|
| 3860 |
term.innerHTML = _termLines.join('');
|
| 3861 |
if (atBottom) term.scrollTop = term.scrollHeight;
|
| 3862 |
}
|
| 3863 |
|
| 3864 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3865 |
|
| 3866 |
function _extractTrainingData(entry) {
|
| 3867 |
-
|
| 3868 |
-
if (entry.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3869 |
const m = _TRAINING_MSG_RE.exec(entry.message || '');
|
| 3870 |
-
if (m)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3871 |
return null;
|
| 3872 |
}
|
| 3873 |
|
| 3874 |
function updateTrainingKPIs(entries) {
|
|
|
|
| 3875 |
const trainEntries = entries
|
| 3876 |
-
.filter(e =>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
//
|
| 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
|
| 3907 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
| 3919 |
}
|
| 3920 |
|
| 3921 |
async function pollTrainingLogs() {
|
| 3922 |
try {
|
| 3923 |
const res = await fetch('/api/ranker/logs/recent?limit=80&category=TRAINING');
|
| 3924 |
-
|
| 3925 |
-
|
| 3926 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3927 |
}
|
| 3928 |
if (!res.ok) {
|
|
|
|
|
|
|
| 3929 |
const term = document.getElementById('training-log-terminal');
|
| 3930 |
-
if (term && !_termLines.length)
|
| 3931 |
-
term.innerHTML =
|
|
|
|
| 3932 |
return;
|
| 3933 |
}
|
| 3934 |
-
const entries =
|
| 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 =
|
|
|
|
| 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,'&').replace(/</g,'<').replace(/>/g,'>');
|
| 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 |
})();
|