diff --git a/posimai-dev/station.html b/posimai-dev/station.html
index 059ab7aa..d1f8fad4 100644
--- a/posimai-dev/station.html
+++ b/posimai-dev/station.html
@@ -139,6 +139,12 @@
.svc-dot { width:5px;height:5px;border-radius:50%;background:var(--text3);opacity:0.25; }
.svc-dot.ok { background:var(--ok); opacity:0.85; }
.svc-dot.crit { background:var(--crit); opacity:0.85; }
+ .service-uptime { font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:600; }
+ .service-uptime.full { color:var(--ok); }
+ .service-uptime.partial { color:#FB923C; }
+ .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; }
+ .latency-bar { height:100%;border-radius:2px;transition:width 0.6s ease; }
/* stream */
#stream-feed { flex:1;overflow:hidden;display:flex;flex-direction:column;gap:0; }
@@ -298,7 +304,7 @@
次の更新まで 30s
@@ -440,11 +446,19 @@ function buildServiceCards(){
SERVICES.forEach(svc=>{
const card=document.createElement('div'); card.className='service-card'; card.id=`svc-${svc.id}`;
const dots=Array(5).fill(0).map((_,i)=>``).join('');
- card.innerHTML=`${svc.name}...
${svc.desc}`;
+ card.innerHTML=`${svc.name}...
${svc.desc}`;
grid.appendChild(card);
});
}
+function updateLatencyBar(id,ms){
+ const bar=document.getElementById(`lbar-${id}`); if(!bar)return;
+ if(ms===null){bar.style.width='100%';bar.style.background='var(--crit)';return;}
+ const w=ms<=50?100:ms<=200?80:ms<=500?55:35;
+ const color=ms<=200?'var(--ok)':ms<=500?'#FB923C':'var(--crit)';
+ bar.style.width=w+'%'; bar.style.background=color;
+}
+
function pushSvcHistory(id,ok){
svcHist[id].push(ok); if(svcHist[id].length>5)svcHist[id].shift();
const h=svcHist[id];
@@ -453,6 +467,13 @@ function pushSvcHistory(id,ok){
const idx=i-(5-h.length); if(idx<0){dot.className='svc-dot';continue;}
dot.className='svc-dot '+(h[idx]?'ok':'crit');
}
+ // 稼働率表示
+ const uptEl=document.getElementById(`upt-${id}`);
+ if(uptEl&&h.length>0){
+ const pct=Math.round(h.filter(Boolean).length/h.length*100);
+ uptEl.textContent=`${pct}%`;
+ uptEl.className='service-uptime '+(pct===100?'full':pct>=60?'partial':'down');
+ }
}
async function checkService(svc){
@@ -462,20 +483,23 @@ async function checkService(svc){
try{
const ctrl=new AbortController(),timer=setTimeout(()=>ctrl.abort(),7000);
if(svc.proxy){
- // サーバー経由プロキシチェック(mixed-content 回避)
const r=await fetch(svc.url,{signal:ctrl.signal});
clearTimeout(timer);
const data=await r.json();
const ok=data.ok||(data.status&&data.status<500);
+ const ms=data.latency_ms||0;
badge.className='service-badge '+(ok?'ok':'crit');
badge.textContent=ok?'OK':'DOWN';
- latEl.textContent=data.latency_ms?`${data.latency_ms}ms`:'';
+ latEl.textContent=ms?`${ms}ms`:'';
+ updateLatencyBar(svc.id,ok?ms:null);
pushSvcHistory(svc.id,!!ok);
}else{
await fetch(svc.url,{method:'HEAD',mode:'no-cors',signal:ctrl.signal});
clearTimeout(timer);
+ const ms=Date.now()-t0;
badge.className='service-badge ok'; badge.textContent='OK';
- latEl.textContent=`${Date.now()-t0}ms`;
+ latEl.textContent=`${ms}ms`;
+ updateLatencyBar(svc.id,ms);
pushSvcHistory(svc.id,true);
}
}catch(e){