From ca765544ce63d3f3e1355c6be7de4276aabbd167 Mon Sep 17 00:00:00 2001 From: posimai Date: Tue, 31 Mar 2026 10:09:04 +0900 Subject: [PATCH] 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 --- posimai-dev/station.html | 202 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 201 insertions(+), 1 deletion(-) diff --git a/posimai-dev/station.html b/posimai-dev/station.html index 925f51fe..8f32c9d9 100644 --- a/posimai-dev/station.html +++ b/posimai-dev/station.html @@ -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%); } + } @@ -495,6 +592,15 @@
+ +
+
Stream
+
+
+ +
+
+ @@ -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 => + `${b}` + ).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 = ` +
${label}
+
${renderBinSpans(binGrouped.replace(/ /g,''))}  //  ${binGrouped}
+
+ ${value} + ${pct !== null ? `
` : ''} +
`; + // 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();