feat(station): binary stream panel with real metrics encoded as bits
New right-column panel: each metric (CPU, memory, disk, load, uptime, sessions, unix timestamp) shown as actual binary bits + human value + mini bar. Rotates through rows every 4s. IP scrolls as binary ticker. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
af8707644f
commit
ca765544ce
|
|
@ -194,7 +194,7 @@
|
|||
/* ── Middle ─────────────────────────────────────────────── */
|
||||
#middle {
|
||||
display: grid;
|
||||
grid-template-columns: 340px 1fr;
|
||||
grid-template-columns: 340px 1fr 220px;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
|
@ -384,6 +384,103 @@
|
|||
.bottom-link:hover { color: var(--accent); border-color: rgba(34,211,238,0.3); }
|
||||
.bottom-link svg { width: 12px; height: 12px; }
|
||||
#refresh-countdown { font-size: 12px; color: var(--text3); }
|
||||
|
||||
/* ── Binary stream panel ─────────────────────────────────── */
|
||||
#stream-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#stream-feed {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stream-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
animation: stream-in 0.5s cubic-bezier(0.4,0,0.2,1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stream-row:last-child { border-bottom: none; }
|
||||
@keyframes stream-in {
|
||||
from { opacity: 0; transform: translateY(-6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.stream-key {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text3);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stream-binary {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--accent);
|
||||
letter-spacing: 0.08em;
|
||||
opacity: 0.7;
|
||||
word-break: break-all;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.stream-binary .bit-1 { color: var(--accent); opacity: 1; }
|
||||
.stream-binary .bit-0 { color: var(--text3); opacity: 0.4; }
|
||||
|
||||
.stream-human {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.stream-bar {
|
||||
flex: 1;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
background: rgba(255,255,255,0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
.stream-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
background: var(--accent);
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
.stream-bar-fill.warn { background: var(--warn); }
|
||||
.stream-bar-fill.crit { background: var(--crit); }
|
||||
|
||||
/* Ticker at bottom of stream panel */
|
||||
#stream-ticker {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
color: var(--text3);
|
||||
padding-top: 8px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
border-top: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#stream-ticker-inner {
|
||||
display: inline-block;
|
||||
animation: ticker-scroll 28s linear infinite;
|
||||
}
|
||||
@keyframes ticker-scroll {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -495,6 +592,15 @@
|
|||
<div class="service-grid" id="service-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- Binary stream -->
|
||||
<div class="panel" id="stream-panel">
|
||||
<div class="panel-title"><i data-lucide="binary"></i>Stream</div>
|
||||
<div id="stream-feed"></div>
|
||||
<div id="stream-ticker">
|
||||
<span id="stream-ticker-inner"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Bottom -->
|
||||
|
|
@ -645,6 +751,8 @@ async function fetchHealth() {
|
|||
document.getElementById('status-dot').className =
|
||||
hasCrit ? 'crit' : hasWarn ? 'warn' : 'ok';
|
||||
|
||||
updateStream(data);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
document.getElementById('status-dot').className = 'off';
|
||||
|
|
@ -732,6 +840,98 @@ async function runRefresh() {
|
|||
startCountdown();
|
||||
}
|
||||
|
||||
// ── Binary stream ─────────────────────────────────────────────────
|
||||
function toBin(n, bits) {
|
||||
return (n >>> 0).toString(2).padStart(bits, '0').slice(-bits);
|
||||
}
|
||||
|
||||
function renderBinSpans(binStr) {
|
||||
return binStr.split('').map(b =>
|
||||
`<span class="bit-${b}">${b}</span>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function formatBinDisplay(binStr) {
|
||||
// group into nibbles: 0000 1010 0011 ...
|
||||
return binStr.match(/.{1,4}/g).join(' ');
|
||||
}
|
||||
|
||||
let streamData = null;
|
||||
|
||||
function pushStreamRow(key, label, value, binStr, pct, level) {
|
||||
const feed = document.getElementById('stream-feed');
|
||||
const row = document.createElement('div');
|
||||
row.className = 'stream-row';
|
||||
const binGrouped = formatBinDisplay(binStr);
|
||||
row.innerHTML = `
|
||||
<div class="stream-key">${label}</div>
|
||||
<div class="stream-binary">${renderBinSpans(binGrouped.replace(/ /g,''))} <span style="opacity:0.3">//</span> ${binGrouped}</div>
|
||||
<div class="stream-human">
|
||||
<span>${value}</span>
|
||||
${pct !== null ? `<div class="stream-bar"><div class="stream-bar-fill${level ? ' '+level : ''}" style="width:${pct}%"></div></div>` : ''}
|
||||
</div>`;
|
||||
// Prepend so newest is at top
|
||||
feed.insertBefore(row, feed.firstChild);
|
||||
// Keep max 8 rows
|
||||
while (feed.children.length > 8) feed.removeChild(feed.lastChild);
|
||||
}
|
||||
|
||||
function updateStream(data) {
|
||||
if (!data) return;
|
||||
streamData = data;
|
||||
|
||||
const cpuPct = data.cpu_pct || 0;
|
||||
const memPct = data.mem_total_mb
|
||||
? Math.round((data.mem_used_mb / data.mem_total_mb) * 100) : 0;
|
||||
const diskPct = data.disk?.use_pct ?? null;
|
||||
const load1 = (data.load_avg && data.load_avg[0]) || 0;
|
||||
const uptime = data.uptime_s || 0;
|
||||
const sessions = data.active_sessions || 0;
|
||||
|
||||
// Unix timestamp (lower 16 bits)
|
||||
const ts = Math.floor(Date.now() / 1000);
|
||||
|
||||
const rows = [
|
||||
{ key:'ts', label:'UNIX TIME', value: String(ts),
|
||||
bin: toBin(ts & 0xFFFF, 16), pct: null, level: null },
|
||||
{ key:'cpu', label:'CPU USAGE', value: `${cpuPct}%`,
|
||||
bin: toBin(cpuPct, 8), pct: cpuPct,
|
||||
level: cpuPct > 80 ? 'crit' : cpuPct > 60 ? 'warn' : null },
|
||||
{ key:'mem', label:'MEMORY', value: `${memPct}%`,
|
||||
bin: toBin(memPct, 8), pct: memPct,
|
||||
level: memPct > 85 ? 'crit' : memPct > 65 ? 'warn' : null },
|
||||
{ key:'load', label:'LOAD AVG', value: load1.toFixed(2),
|
||||
bin: toBin(Math.round(load1 * 100) & 0xFF, 8), pct: null, level: null },
|
||||
...(diskPct !== null ? [{ key:'disk', label:'DISK /', value: `${diskPct}%`,
|
||||
bin: toBin(diskPct, 8), pct: diskPct,
|
||||
level: diskPct > 90 ? 'crit' : diskPct > 75 ? 'warn' : null }] : []),
|
||||
{ key:'ses', label:'SESSIONS', value: String(sessions),
|
||||
bin: toBin(sessions, 8), pct: null, level: null },
|
||||
{ key:'upt', label:'UPTIME', value: formatUptime(uptime),
|
||||
bin: toBin(Math.floor(uptime / 60) & 0xFFFF, 16), pct: null, level: null },
|
||||
];
|
||||
|
||||
// Add one new row each call (rotate through)
|
||||
const idx = Math.floor(Date.now() / 1000) % rows.length;
|
||||
const r = rows[idx];
|
||||
pushStreamRow(r.key, r.label, r.value, r.bin, r.pct, r.level);
|
||||
|
||||
// Ticker: IP + hostname + node version in binary
|
||||
updateTicker(data);
|
||||
}
|
||||
|
||||
function updateTicker(data) {
|
||||
const ip = '100.77.11.43';
|
||||
const ipBin = ip.split('.').map(o => toBin(parseInt(o), 8)).join(' ');
|
||||
const base = `${ipBin} // ${data.hostname || 'ubuntu-pc'} // ${data.node_version || ''} // platform:${data.platform || ''} // `;
|
||||
const doubled = base + base; // loop seamlessly
|
||||
const el = document.getElementById('stream-ticker-inner');
|
||||
if (el) el.textContent = doubled;
|
||||
}
|
||||
|
||||
// Tick stream every 4 seconds even between full refreshes
|
||||
setInterval(() => { if (streamData) updateStream(streamData); }, 4000);
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────
|
||||
buildServiceCards();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
|
|
|||
Loading…
Reference in New Issue