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 @@
+
+
+
@@ -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();