feat: binary bars, sparkline, full-metrics binary ticker

- CPU/MEM/DISK bars replaced with 20-cell binary bars (1=filled, 0=empty)
- Service cards: latency bar removed, mini sparkline with gradient area added
- Footer ticker: all metrics as binary tape (CPU/MEM/DISK/LOAD/UP/SESSION/TIME/IP/HOST)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
posimai 2026-03-31 22:03:13 +09:00
parent df1a41560b
commit 0113a5d777
1 changed files with 72 additions and 33 deletions

View File

@ -83,11 +83,11 @@
.metric-header-row { display:flex;justify-content:space-between;align-items:baseline; } .metric-header-row { display:flex;justify-content:space-between;align-items:baseline; }
.metric-label { font-size:12px;color:var(--text2); } .metric-label { font-size:12px;color:var(--text2); }
.metric-val { font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:500; } .metric-val { font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:500; }
.metric-bin { font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:0.18em;color:rgba(34,211,238,0.35);margin-top:2px;transition:color 0.4s; } .bin-bar { font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:0.04em;display:flex;gap:0px;margin-top:2px; }
.bar-track { height:4px;border-radius:2px;background:rgba(255,255,255,0.05);overflow:hidden; } .bin-bar .b1 { color:var(--accent);transition:color 0.5s; }
.bar-fill { height:100%;border-radius:2px;background:var(--accent);transition:width 0.8s cubic-bezier(0.4,0,0.2,1),background 0.4s; } .bin-bar .b0 { color:rgba(255,255,255,0.1); }
.bar-fill.warn { background:var(--warn); } .bin-bar.warn .b1 { color:var(--warn); }
.bar-fill.crit { background:var(--crit); } .bin-bar.crit .b1 { color:var(--crit); }
.load-row { display:flex;gap:6px;flex-shrink:0; } .load-row { display:flex;gap:6px;flex-shrink:0; }
.load-chip { flex:1;background:var(--surface2);border-radius:7px;padding:7px;text-align:center; } .load-chip { flex:1;background:var(--surface2);border-radius:7px;padding:7px;text-align:center; }
@ -144,8 +144,8 @@
.service-uptime.full { color:var(--ok); } .service-uptime.full { color:var(--ok); }
.service-uptime.partial { color:#FB923C; } .service-uptime.partial { color:#FB923C; }
.service-uptime.down { color:var(--crit); } .service-uptime.down { color:var(--crit); }
.latency-bar-wrap { height:3px;border-radius:2px;background:rgba(255,255,255,0.07);overflow:hidden;margin-top:4px; } .svc-spark-wrap { height:28px;margin:2px 0; }
.latency-bar { height:100%;border-radius:2px;transition:width 0.6s ease; } .svc-spark { width:100%;height:100%;overflow:visible; }
/* stream */ /* stream */
#stream-feed { flex:1;overflow:hidden;display:flex;flex-direction:column;gap:0; } #stream-feed { flex:1;overflow:hidden;display:flex;flex-direction:column;gap:0; }
@ -162,7 +162,7 @@
.s-bar-fill.warn { background:var(--warn); } .s-bar-fill.warn { background:var(--warn); }
.s-bar-fill.crit { background:var(--crit); } .s-bar-fill.crit { background:var(--crit); }
#stream-ticker { font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--text3);padding-top:7px;overflow:hidden;white-space:nowrap;border-top:1px solid var(--border2);flex-shrink:0; } #stream-ticker { font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--text3);padding-top:7px;overflow:hidden;white-space:nowrap;border-top:1px solid var(--border2);flex-shrink:0; }
#stream-ticker-inner { display:inline-block;animation:ticker 30s linear infinite; } #stream-ticker-inner { display:inline-block;animation:ticker 60s linear infinite; }
@keyframes ticker { from{transform:translateX(0)} to{transform:translateX(-50%)} } @keyframes ticker { from{transform:translateX(0)} to{transform:translateX(-50%)} }
#bottom { display:flex;align-items:center;justify-content:space-between;padding-top:12px;border-top:1px solid var(--border); } #bottom { display:flex;align-items:center;justify-content:space-between;padding-top:12px;border-top:1px solid var(--border); }
@ -200,18 +200,15 @@
<div class="panel-title"><i data-lucide="cpu"></i>Ubuntu PC</div> <div class="panel-title"><i data-lucide="cpu"></i>Ubuntu PC</div>
<div class="metric-item"> <div class="metric-item">
<div class="metric-header-row"><span class="metric-label">CPU</span><span class="metric-val" id="cpu-val"></span></div> <div class="metric-header-row"><span class="metric-label">CPU</span><span class="metric-val" id="cpu-val"></span></div>
<div class="bar-track"><div class="bar-fill" id="cpu-bar" style="width:0%"></div></div> <div class="bin-bar" id="cpu-bar"></div>
<div class="metric-bin" id="cpu-bin">--------</div>
</div> </div>
<div class="metric-item"> <div class="metric-item">
<div class="metric-header-row"><span class="metric-label">Memory</span><span class="metric-val" id="mem-val"></span></div> <div class="metric-header-row"><span class="metric-label">Memory</span><span class="metric-val" id="mem-val"></span></div>
<div class="bar-track"><div class="bar-fill" id="mem-bar" style="width:0%"></div></div> <div class="bin-bar" id="mem-bar"></div>
<div class="metric-bin" id="mem-bin">--------</div>
</div> </div>
<div class="metric-item"> <div class="metric-item">
<div class="metric-header-row"><span class="metric-label">Disk (/)</span><span class="metric-val" id="disk-val"></span></div> <div class="metric-header-row"><span class="metric-label">Disk (/)</span><span class="metric-val" id="disk-val"></span></div>
<div class="bar-track"><div class="bar-fill" id="disk-bar" style="width:0%"></div></div> <div class="bin-bar" id="disk-bar"></div>
<div class="metric-bin" id="disk-bin">--------</div>
</div> </div>
<div> <div>
<div class="metric-label" style="margin-bottom:6px">Load Avg <span id="cpu-count-label" style="color:var(--text3);font-size:9px"></span></div> <div class="metric-label" style="margin-bottom:6px">Load Avg <span id="cpu-count-label" style="color:var(--text3);font-size:9px"></span></div>
@ -328,7 +325,8 @@ const SERVICES = [
]; ];
const hist = {cpu:[], load:[]}; const hist = {cpu:[], load:[]};
const svcHist = {}; const svcHist = {};
SERVICES.forEach(s => svcHist[s.id] = []); const svcLatHist = {};
SERVICES.forEach(s => { svcHist[s.id] = []; svcLatHist[s.id] = []; });
let streamData = null; let streamData = null;
function p(n){ return String(n).padStart(2,'0'); } function p(n){ return String(n).padStart(2,'0'); }
@ -396,15 +394,12 @@ async function fetchHealth(){
window._cpuCount=cpuCount; window._cpuCount=cpuCount;
document.getElementById('cpu-val').textContent=`${cpuPct}%`; document.getElementById('cpu-val').textContent=`${cpuPct}%`;
const cb=document.getElementById('cpu-bar');cb.style.width=`${cpuPct}%`;cb.className='bar-fill'+(cpuPct>80?' crit':cpuPct>60?' warn':''); renderBinBar('cpu-bar',cpuPct,60,80);
document.getElementById('cpu-bin').textContent=cpuPct.toString(2).padStart(8,'0');
document.getElementById('mem-val').textContent=`${data.mem_used_mb}/${data.mem_total_mb}MB (${memPct}%)`; document.getElementById('mem-val').textContent=`${data.mem_used_mb}/${data.mem_total_mb}MB (${memPct}%)`;
const mb=document.getElementById('mem-bar');mb.style.width=`${memPct}%`;mb.className='bar-fill'+(memPct>85?' crit':memPct>65?' warn':''); renderBinBar('mem-bar',memPct,65,85);
document.getElementById('mem-bin').textContent=memPct.toString(2).padStart(8,'0');
if(data.disk){ if(data.disk){
document.getElementById('disk-val').textContent=`${data.disk.used_gb}/${data.disk.total_gb}GB (${diskPct}%)`; document.getElementById('disk-val').textContent=`${data.disk.used_gb}/${data.disk.total_gb}GB (${diskPct}%)`;
const db=document.getElementById('disk-bar');db.style.width=`${diskPct}%`;db.className='bar-fill'+(diskPct>90?' crit':diskPct>75?' warn':''); renderBinBar('disk-bar',diskPct,75,90);
document.getElementById('disk-bin').textContent=diskPct.toString(2).padStart(8,'0');
} }
document.getElementById('cpu-count-label').textContent=`(core:${cpuCount})`; document.getElementById('cpu-count-label').textContent=`(core:${cpuCount})`;
['load-1','load-5','load-15'].forEach((id,i)=>{ ['load-1','load-5','load-15'].forEach((id,i)=>{
@ -453,17 +448,50 @@ function buildServiceCards(){
SERVICES.forEach(svc=>{ SERVICES.forEach(svc=>{
const card=document.createElement('div'); card.className='service-card'; card.id=`svc-${svc.id}`; const card=document.createElement('div'); card.className='service-card'; card.id=`svc-${svc.id}`;
const dots=Array(5).fill(0).map((_,i)=>`<div class="svc-dot" id="dot-${svc.id}-${i}"></div>`).join(''); const dots=Array(5).fill(0).map((_,i)=>`<div class="svc-dot" id="dot-${svc.id}-${i}"></div>`).join('');
card.innerHTML=`<div class="service-card-top"><span class="service-name">${svc.name}</span><span class="service-badge checking" id="badge-${svc.id}">...</span></div><span class="service-desc">${svc.desc}</span><div class="latency-bar-wrap"><div class="latency-bar" id="lbar-${svc.id}" style="width:0%;background:var(--ok)"></div></div><div class="service-footer"><div class="service-dots">${dots}</div><span class="service-uptime" id="upt-${svc.id}"></span><span class="service-latency" id="lat-${svc.id}"></span></div>`; card.innerHTML=`<div class="service-card-top"><span class="service-name">${svc.name}</span><span class="service-badge checking" id="badge-${svc.id}">...</span></div><span class="service-desc">${svc.desc}</span><div class="svc-spark-wrap"><svg class="svc-spark" id="spark-${svc.id}" viewBox="0 0 100 24" preserveAspectRatio="none"></svg></div><div class="service-footer"><div class="service-dots">${dots}</div><span class="service-uptime" id="upt-${svc.id}"></span><span class="service-latency" id="lat-${svc.id}"></span></div>`;
grid.appendChild(card); grid.appendChild(card);
}); });
} }
function updateLatencyBar(id,ms){ function renderBinBar(id,pct,warnTh,critTh){
const bar=document.getElementById(`lbar-${id}`); if(!bar)return; const el=document.getElementById(id); if(!el)return;
if(ms===null){bar.style.width='100%';bar.style.background='var(--crit)';return;} const cells=20,filled=Math.round(pct/100*cells);
const w=ms<=50?100:ms<=200?80:ms<=500?55:35; const cls=pct>critTh?'crit':pct>warnTh?'warn':'';
const color=ms<=200?'var(--ok)':ms<=500?'#FB923C':'var(--crit)'; el.className='bin-bar'+(cls?' '+cls:'');
bar.style.width=w+'%'; bar.style.background=color; el.innerHTML=Array.from({length:cells},(_,i)=>
`<span class="${i<filled?'b1':'b0'}">${i<filled?'1':'0'}</span>`
).join('');
}
function updateSparkline(id, ms, ok){
const h = svcLatHist[id];
h.push(ok ? (ms||0) : null);
if(h.length > 12) h.shift();
const svg = document.getElementById(`spark-${id}`);
if(!svg || h.length < 2) return;
const valid = h.filter(v => v !== null);
if(valid.length < 2){ svg.innerHTML=''; return; }
const maxV = Math.max(...valid, 1);
const W=100, H=24, pad=2;
const pts = h.map((v,i) => {
const x = (i/(h.length-1))*W;
const y = v===null ? H-pad : H-pad - ((v/maxV)*(H-pad*2));
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
const color = !ok ? 'rgba(248,113,113,0.7)' : maxV > 500 ? 'rgba(251,146,60,0.7)' : 'rgba(34,211,238,0.6)';
const area = h.map((v,i) => {
const x=(i/(h.length-1))*W;
const y=v===null?H-pad:H-pad-((v/maxV)*(H-pad*2));
return `${x.toFixed(1)},${y.toFixed(1)}`;
});
area.push(`${W},${H}`, `0,${H}`);
svg.innerHTML = `
<defs><linearGradient id="sg-${id}" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="${color}" stop-opacity="0.25"/>
<stop offset="100%" stop-color="${color}" stop-opacity="0.03"/>
</linearGradient></defs>
<polygon points="${area.join(' ')}" fill="url(#sg-${id})"/>
<polyline points="${pts}" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`;
} }
function pushSvcHistory(id,ok){ function pushSvcHistory(id,ok){
@ -498,7 +526,7 @@ async function checkService(svc){
badge.className='service-badge '+(ok?'ok':'crit'); badge.className='service-badge '+(ok?'ok':'crit');
badge.textContent=ok?'OK':'DOWN'; badge.textContent=ok?'OK':'DOWN';
latEl.textContent=ms?`${ms}ms`:''; latEl.textContent=ms?`${ms}ms`:'';
updateLatencyBar(svc.id,ok?ms:null); updateSparkline(svc.id,ms,!!ok);
pushSvcHistory(svc.id,!!ok); pushSvcHistory(svc.id,!!ok);
}else{ }else{
await fetch(svc.url,{method:'HEAD',mode:'no-cors',signal:ctrl.signal}); await fetch(svc.url,{method:'HEAD',mode:'no-cors',signal:ctrl.signal});
@ -506,7 +534,7 @@ async function checkService(svc){
const ms=Date.now()-t0; const ms=Date.now()-t0;
badge.className='service-badge ok'; badge.textContent='OK'; badge.className='service-badge ok'; badge.textContent='OK';
latEl.textContent=`${ms}ms`; latEl.textContent=`${ms}ms`;
updateLatencyBar(svc.id,ms); updateSparkline(svc.id,ms,true);
pushSvcHistory(svc.id,true); pushSvcHistory(svc.id,true);
} }
}catch(e){ }catch(e){
@ -555,10 +583,21 @@ function updateStream(data){
const r=rows[Math.floor(Date.now()/1000)%rows.length]; const r=rows[Math.floor(Date.now()/1000)%rows.length];
pushStreamRow(r.label,r.value,r.bin,r.pct,r.level); pushStreamRow(r.label,r.value,r.bin,r.pct,r.level);
const ip='100.77.11.43'; const ip='100.77.11.43';
const ipBin=ip.split('.').map(o=>toBin(parseInt(o),8)).join(' '); const b8 = n => n.toString(2).padStart(8,'0');
const base=`${ipBin} // ${data.hostname||'ubuntu-pc'} // ${data.node_version||''} // `; const segments = [
`CPU:${b8(cpuPct??0)}`,
`MEM:${b8(memPct??0)}`,
`DISK:${b8(diskPct??0)}`,
`LOAD:${b8(Math.min(255,Math.round((loadAvg[0]||0)*64)))}`,
`UP:${(data.uptime_s||0).toString(2)}`,
`SESSION:${b8(data.active_sessions||0)}`,
`TIME:${(Math.floor(Date.now()/1000)).toString(2).slice(-20)}`,
`IP:${ip.split('.').map(o=>b8(parseInt(o))).join('.')}`,
`HOST:${(data.hostname||'ubuntu-pc').split('').map(c=>b8(c.charCodeAt(0))).join(' ')}`,
];
const tape = segments.join(' // ') + ' // ';
const el=document.getElementById('stream-ticker-inner'); const el=document.getElementById('stream-ticker-inner');
if(el)el.textContent=base+base; if(el){ el.textContent=tape+tape; el.style.animation='none'; void el.offsetWidth; el.style.animation=''; }
} }
setInterval(()=>{if(streamData)updateStream(streamData);},4000); setInterval(()=>{if(streamData)updateStream(streamData);},4000);