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:
parent
df1a41560b
commit
0113a5d777
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue