feat: Phase 1 cockpit — net I/O, CPU temp, Gitea commit, keyboard shortcuts, CRIT aurora shift
server.js: add net delta (rx/tx KB/s), CPU temp, /api/gitea-commit proxy. station-b: net/temp in Ubuntu PC panel, ecosystem bar with latest Gitea commit, CRIT aurora hue shift (gradual 3s transition to red, then back), keyboard shortcuts R=refresh B=Design-A F=fullscreen. station-a: same additions except canvas CRIT effect.
This commit is contained in:
parent
f726b4b9af
commit
465c943e0a
|
|
@ -53,6 +53,49 @@ app.get('/api/sessions/:id', requireLocal, (req, res) => {
|
||||||
res.type('text/plain').send(fs.readFileSync(file, 'utf8'));
|
res.type('text/plain').send(fs.readFileSync(file, 'utf8'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── ネットワーク I/O デルタ計算 ────────────────────────────────
|
||||||
|
let _netPrev = null, _netPrevTime = null;
|
||||||
|
function getNetDelta() {
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync('/proc/net/dev', 'utf8');
|
||||||
|
const now = Date.now();
|
||||||
|
let rx = 0, tx = 0;
|
||||||
|
for (const line of raw.trim().split('\n').slice(2)) {
|
||||||
|
const parts = line.trim().split(/\s+/);
|
||||||
|
const iface = parts[0].replace(':', '');
|
||||||
|
if (iface === 'lo') continue;
|
||||||
|
rx += parseInt(parts[1]) || 0;
|
||||||
|
tx += parseInt(parts[9]) || 0;
|
||||||
|
}
|
||||||
|
let result = null;
|
||||||
|
if (_netPrev && _netPrevTime) {
|
||||||
|
const dt = (now - _netPrevTime) / 1000;
|
||||||
|
result = {
|
||||||
|
rx_kbps: Math.max(0, Math.round((rx - _netPrev.rx) / dt / 1024)),
|
||||||
|
tx_kbps: Math.max(0, Math.round((tx - _netPrev.tx) / dt / 1024)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_netPrev = { rx, tx }; _netPrevTime = now;
|
||||||
|
return result;
|
||||||
|
} catch (_) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CPU 温度 (/sys/class/thermal/) ─────────────────────────────
|
||||||
|
function getCpuTemp() {
|
||||||
|
try {
|
||||||
|
const zones = fs.readdirSync('/sys/class/thermal/').filter(z => z.startsWith('thermal_zone'));
|
||||||
|
for (const zone of zones) {
|
||||||
|
try {
|
||||||
|
const type = fs.readFileSync(`/sys/class/thermal/${zone}/type`, 'utf8').trim();
|
||||||
|
if (['x86_pkg_temp','cpu-thermal','acpitz'].includes(type) || type.startsWith('cpu')) {
|
||||||
|
return Math.round(parseInt(fs.readFileSync(`/sys/class/thermal/${zone}/temp`, 'utf8')) / 1000);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
return Math.round(parseInt(fs.readFileSync('/sys/class/thermal/thermal_zone0/temp', 'utf8')) / 1000);
|
||||||
|
} catch (_) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
// ── ヘルス & メトリクス API (/api/health) ──────────────────────
|
// ── ヘルス & メトリクス API (/api/health) ──────────────────────
|
||||||
// Atlas など外部から参照される。CORS ヘッダーを付与して Vercel 上の Atlas からも取得可能にする
|
// Atlas など外部から参照される。CORS ヘッダーを付与して Vercel 上の Atlas からも取得可能にする
|
||||||
function getCpuSample() {
|
function getCpuSample() {
|
||||||
|
|
@ -107,6 +150,8 @@ app.get('/api/health', (req, res) => {
|
||||||
mem_used_mb: Math.round((total - mem) / 1024 / 1024),
|
mem_used_mb: Math.round((total - mem) / 1024 / 1024),
|
||||||
mem_total_mb: Math.round(total / 1024 / 1024),
|
mem_total_mb: Math.round(total / 1024 / 1024),
|
||||||
disk,
|
disk,
|
||||||
|
net: getNetDelta(),
|
||||||
|
cpu_temp_c: getCpuTemp(),
|
||||||
active_sessions: wss.clients ? wss.clients.size : 0,
|
active_sessions: wss.clients ? wss.clients.size : 0,
|
||||||
node_version: process.version,
|
node_version: process.version,
|
||||||
platform: os.platform(),
|
platform: os.platform(),
|
||||||
|
|
@ -115,6 +160,26 @@ app.get('/api/health', (req, res) => {
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Gitea 最新コミット (/api/gitea-commit) ─────────────────────
|
||||||
|
app.get('/api/gitea-commit', async (req, res) => {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
try {
|
||||||
|
const token = process.env.GITEA_TOKEN || '';
|
||||||
|
const headers = token ? { Authorization: `token ${token}` } : {};
|
||||||
|
const r = await fetch('http://100.76.7.3:3000/api/v1/repos/mai/posimai-root/commits?limit=1', {
|
||||||
|
headers, signal: AbortSignal.timeout(3000),
|
||||||
|
});
|
||||||
|
const data = await r.json();
|
||||||
|
const c = data[0];
|
||||||
|
res.json({
|
||||||
|
sha: c.sha.slice(0, 7),
|
||||||
|
message: c.commit.message.split('\n')[0].slice(0, 60),
|
||||||
|
author: c.commit.author.name,
|
||||||
|
date: c.commit.author.date,
|
||||||
|
});
|
||||||
|
} catch (e) { res.json({ error: e.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
// ── サービス死活チェックプロキシ (/api/check?url=...) ──────────
|
// ── サービス死活チェックプロキシ (/api/check?url=...) ──────────
|
||||||
// ブラウザの mixed-content 制限を回避するためサーバー側から HTTP チェック
|
// ブラウザの mixed-content 制限を回避するためサーバー側から HTTP チェック
|
||||||
app.get('/api/check', async (req, res) => {
|
app.get('/api/check', async (req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,13 @@
|
||||||
#stream-ticker-inner { display:inline-block;animation:ticker 60s 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;flex-direction:column;gap:6px;padding-top:10px;border-top:1px solid var(--border); }
|
||||||
|
#ecosystem-bar { display:flex;align-items:center;gap:14px;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text3);overflow:hidden; }
|
||||||
|
.eco-item { display:flex;align-items:center;gap:5px;flex-shrink:0; }
|
||||||
|
.eco-dot { width:5px;height:5px;border-radius:50%;background:var(--text3); }
|
||||||
|
.eco-dot.ok { background:var(--ok); }
|
||||||
|
.eco-dot.crit { background:var(--crit); }
|
||||||
|
#bottom-row { display:flex;align-items:center;justify-content:space-between; }
|
||||||
.bottom-brand { font-size:12px;color:var(--text3);font-weight:500;letter-spacing:0.04em; }
|
.bottom-brand { font-size:12px;color:var(--text3);font-weight:500;letter-spacing:0.04em; }
|
||||||
.bottom-brand span { color:var(--accent); }
|
.bottom-brand span { color:var(--accent); }
|
||||||
.bottom-links { display:flex;gap:7px; }
|
.bottom-links { display:flex;gap:7px; }
|
||||||
|
|
@ -207,11 +213,15 @@
|
||||||
<div class="load-chip"><div class="load-chip-label">15m</div><div class="load-chip-val" id="load-15">—</div></div>
|
<div class="load-chip"><div class="load-chip-label">15m</div><div class="load-chip-val" id="load-15">—</div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="metric-item" id="net-row" style="display:none">
|
||||||
|
<div class="metric-header-row"><span class="metric-label">Network</span><span class="metric-val" id="net-val">—</span></div>
|
||||||
|
</div>
|
||||||
<div class="stat-grid">
|
<div class="stat-grid">
|
||||||
<div class="stat-card"><div class="stat-label">Uptime</div><div class="stat-val" id="uptime-val">—</div></div>
|
<div class="stat-card"><div class="stat-label">Uptime</div><div class="stat-val" id="uptime-val">—</div></div>
|
||||||
<div class="stat-card"><div class="stat-label">Sessions</div><div class="stat-val" id="sessions-val">—</div></div>
|
<div class="stat-card"><div class="stat-label">Sessions</div><div class="stat-val" id="sessions-val">—</div></div>
|
||||||
<div class="stat-card"><div class="stat-label">Node.js</div><div class="stat-val" id="node-val">—</div></div>
|
<div class="stat-card"><div class="stat-label">Node.js</div><div class="stat-val" id="node-val">—</div></div>
|
||||||
<div class="stat-card"><div class="stat-label">Platform</div><div class="stat-val" id="platform-val">—</div></div>
|
<div class="stat-card"><div class="stat-label">Platform</div><div class="stat-val" id="platform-val">—</div></div>
|
||||||
|
<div class="stat-card" id="temp-card" style="display:none"><div class="stat-label">CPU Temp</div><div class="stat-val" id="temp-val">—</div></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="open-btn" href="/" target="_blank" rel="noopener"><i data-lucide="terminal"></i>posimai-dev を開く</a>
|
<a class="open-btn" href="/" target="_blank" rel="noopener"><i data-lucide="terminal"></i>posimai-dev を開く</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -290,14 +300,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="bottom">
|
<div id="bottom">
|
||||||
<div class="bottom-brand">posimai<span>-station</span> <span style="font-size:10px;color:var(--violet);margin-left:4px">B</span></div>
|
<div id="ecosystem-bar">
|
||||||
<div class="bottom-links">
|
<div class="eco-item"><div class="eco-dot" id="eco-gitea-dot"></div><span id="eco-commit">commit: —</span></div>
|
||||||
<a class="bottom-link" href="/station" rel="noopener"><i data-lucide="monitor"></i>Design A</a>
|
<div class="eco-item" style="color:var(--border);font-size:8px">|</div>
|
||||||
<a class="bottom-link" href="/" target="_blank" rel="noopener"><i data-lucide="terminal"></i>dev</a>
|
<div class="eco-item" id="eco-temp-item" style="display:none"><span id="eco-temp-label">temp: —</span></div>
|
||||||
<a class="bottom-link" href="https://posimai-atlas.vercel.app" target="_blank" rel="noopener"><i data-lucide="network"></i>atlas</a>
|
</div>
|
||||||
<a class="bottom-link" href="https://posimai.soar-enrich.com" target="_blank" rel="noopener"><i data-lucide="layout-dashboard"></i>dashboard</a>
|
<div id="bottom-row">
|
||||||
|
<div class="bottom-brand">posimai<span>-station</span> <span style="font-size:10px;color:var(--violet);margin-left:4px">B</span></div>
|
||||||
|
<div class="bottom-links">
|
||||||
|
<a class="bottom-link" href="/station" rel="noopener"><i data-lucide="monitor"></i>Design A</a>
|
||||||
|
<a class="bottom-link" href="/" target="_blank" rel="noopener"><i data-lucide="terminal"></i>dev</a>
|
||||||
|
<a class="bottom-link" href="https://posimai-atlas.vercel.app" target="_blank" rel="noopener"><i data-lucide="network"></i>atlas</a>
|
||||||
|
<a class="bottom-link" href="https://posimai.soar-enrich.com" target="_blank" rel="noopener"><i data-lucide="layout-dashboard"></i>dashboard</a>
|
||||||
|
</div>
|
||||||
|
<div id="refresh-countdown">次の更新まで <span id="countdown">30</span>s</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="refresh-countdown">次の更新まで <span id="countdown">30</span>s</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -405,6 +422,28 @@ async function fetchHealth(){
|
||||||
document.getElementById('platform-val').textContent=data.platform||'—';
|
document.getElementById('platform-val').textContent=data.platform||'—';
|
||||||
document.getElementById('hostname').textContent=data.hostname||'ubuntu-pc';
|
document.getElementById('hostname').textContent=data.hostname||'ubuntu-pc';
|
||||||
|
|
||||||
|
// Network I/O
|
||||||
|
if(data.net){
|
||||||
|
const netRow=document.getElementById('net-row');
|
||||||
|
if(netRow) netRow.style.display='';
|
||||||
|
const rx=data.net.rx_kbps,tx=data.net.tx_kbps;
|
||||||
|
const fmt=v=>v>=1024?`${(v/1024).toFixed(1)}MB/s`:`${v}KB/s`;
|
||||||
|
document.getElementById('net-val').textContent=`↓${fmt(rx)} ↑${fmt(tx)}`;
|
||||||
|
}
|
||||||
|
// CPU Temp
|
||||||
|
if(data.cpu_temp_c!=null){
|
||||||
|
const tc=document.getElementById('temp-card');
|
||||||
|
const ti=document.getElementById('eco-temp-item');
|
||||||
|
if(tc) tc.style.display='';
|
||||||
|
if(ti) ti.style.display='';
|
||||||
|
const t=data.cpu_temp_c;
|
||||||
|
const tcol=t>=80?'var(--crit)':t>=70?'var(--warn)':'var(--ok)';
|
||||||
|
document.getElementById('temp-val').textContent=`${t}°C`;
|
||||||
|
document.getElementById('temp-val').style.color=tcol;
|
||||||
|
document.getElementById('eco-temp-label').textContent=`temp: ${t}°C`;
|
||||||
|
document.getElementById('eco-temp-label').style.color=tcol;
|
||||||
|
}
|
||||||
|
|
||||||
updateRing('ring-cpu-fill','ring-cpu-val', cpuPct, 301.6,'#22D3EE','#FB923C','#F87171');
|
updateRing('ring-cpu-fill','ring-cpu-val', cpuPct, 301.6,'#22D3EE','#FB923C','#F87171');
|
||||||
updateRing('ring-mem-fill','ring-mem-val', memPct, 188.5,'#A78BFA','#FB923C','#F87171');
|
updateRing('ring-mem-fill','ring-mem-val', memPct, 188.5,'#A78BFA','#FB923C','#F87171');
|
||||||
updateRing('ring-disk-fill','ring-disk-val',diskPct,188.5,'#4ADE80','#FB923C','#F87171');
|
updateRing('ring-disk-fill','ring-disk-val',diskPct,188.5,'#4ADE80','#FB923C','#F87171');
|
||||||
|
|
@ -424,6 +463,7 @@ async function fetchHealth(){
|
||||||
else if(loadAvg[0]>cpuCount)alerts.push({level:'warn',msg:`Load ${loadAvg[0].toFixed(2)}`});
|
else if(loadAvg[0]>cpuCount)alerts.push({level:'warn',msg:`Load ${loadAvg[0].toFixed(2)}`});
|
||||||
setAlerts(alerts);
|
setAlerts(alerts);
|
||||||
const hasCrit=alerts.some(a=>a.level==='crit'),hasWarn=alerts.some(a=>a.level==='warn');
|
const hasCrit=alerts.some(a=>a.level==='crit'),hasWarn=alerts.some(a=>a.level==='warn');
|
||||||
|
window._systemCrit = hasCrit; // canvas aurora hue shift
|
||||||
document.getElementById('status-dot').className=hasCrit?'crit':hasWarn?'warn':'ok';
|
document.getElementById('status-dot').className=hasCrit?'crit':hasWarn?'warn':'ok';
|
||||||
|
|
||||||
updateStream(data);
|
updateStream(data);
|
||||||
|
|
@ -618,6 +658,41 @@ if(window.lucide)lucide.createIcons();
|
||||||
runRefresh();
|
runRefresh();
|
||||||
window.addEventListener('resize',renderSparklines);
|
window.addEventListener('resize',renderSparklines);
|
||||||
|
|
||||||
|
// ── Gitea 最新コミット ─────────────────────────────────────────
|
||||||
|
async function fetchGiteaCommit(){
|
||||||
|
try{
|
||||||
|
const r=await fetch('/api/gitea-commit',{signal:AbortSignal.timeout(4000)});
|
||||||
|
const d=await r.json();
|
||||||
|
if(d.error||!d.sha) return;
|
||||||
|
const dot=document.getElementById('eco-gitea-dot');
|
||||||
|
const el=document.getElementById('eco-commit');
|
||||||
|
if(dot) dot.className='eco-dot ok';
|
||||||
|
if(el){
|
||||||
|
const ago=timeAgo(d.date);
|
||||||
|
el.textContent=`${d.sha} ${d.message} (${ago})`;
|
||||||
|
}
|
||||||
|
}catch(_){}
|
||||||
|
}
|
||||||
|
function timeAgo(iso){
|
||||||
|
const diff=Math.floor((Date.now()-new Date(iso))/60000);
|
||||||
|
if(diff<1)return 'just now';
|
||||||
|
if(diff<60)return `${diff}m ago`;
|
||||||
|
if(diff<1440)return `${Math.floor(diff/60)}h ago`;
|
||||||
|
return `${Math.floor(diff/1440)}d ago`;
|
||||||
|
}
|
||||||
|
fetchGiteaCommit();
|
||||||
|
setInterval(fetchGiteaCommit, 120000); // 2分ごと
|
||||||
|
|
||||||
|
// ── キーボードショートカット ────────────────────────────────────
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if(e.key==='r'||e.key==='R'){ runRefresh(); }
|
||||||
|
if(e.key==='b'||e.key==='B'){ window.location.href='/station'; }
|
||||||
|
if(e.key==='f'||e.key==='F'){
|
||||||
|
if(!document.fullscreenElement) document.documentElement.requestFullscreen().catch(()=>{});
|
||||||
|
else document.exitFullscreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ── Binary curtain aurora canvas ─────────────────────────────────────────────
|
// ── Binary curtain aurora canvas ─────────────────────────────────────────────
|
||||||
(function initBinaryCurtain(){
|
(function initBinaryCurtain(){
|
||||||
const canvas = document.getElementById('binary-canvas');
|
const canvas = document.getElementById('binary-canvas');
|
||||||
|
|
@ -643,16 +718,24 @@ window.addEventListener('resize',renderSparklines);
|
||||||
resize();
|
resize();
|
||||||
window.addEventListener('resize', resize);
|
window.addEventListener('resize', resize);
|
||||||
let t = 0;
|
let t = 0;
|
||||||
|
// CRIT hue shift: 0=normal, 1=full red. Transitions gradually.
|
||||||
|
let _critProgress = 0;
|
||||||
function getBandColor(x, t){
|
function getBandColor(x, t){
|
||||||
const xf = x / canvas.width;
|
const xf = x / canvas.width;
|
||||||
let best = BANDS[0], bestDist = Infinity;
|
let best = BANDS[0], bestDist = Infinity;
|
||||||
BANDS.forEach(b => { const bx=b.x+Math.sin(t*b.speed+b.phase)*0.12; const dist=Math.abs(xf-bx); if(dist<bestDist){bestDist=dist;best=b;} });
|
BANDS.forEach(b => { const bx=b.x+Math.sin(t*b.speed+b.phase)*0.12; const dist=Math.abs(xf-bx); if(dist<bestDist){bestDist=dist;best=b;} });
|
||||||
const bx = best.x + Math.sin(t*best.speed+best.phase)*0.12;
|
const bx = best.x + Math.sin(t*best.speed+best.phase)*0.12;
|
||||||
const alpha = Math.max(0, 1 - Math.abs(xf-bx)/0.22);
|
const alpha = Math.max(0, 1 - Math.abs(xf-bx)/0.22);
|
||||||
return { hue: best.hue, sat: best.sat, alpha };
|
// Lerp hue toward red (hue=10) when CRIT — subtle, takes ~3s to fully shift
|
||||||
|
const hue = Math.round(best.hue + (10 - best.hue) * _critProgress);
|
||||||
|
const sat = Math.round(best.sat + (100 - best.sat) * _critProgress * 0.5);
|
||||||
|
return { hue, sat, alpha };
|
||||||
}
|
}
|
||||||
function draw(){
|
function draw(){
|
||||||
t++;
|
t++;
|
||||||
|
// Gradually shift critProgress toward target (0 or 1)
|
||||||
|
const critTarget = window._systemCrit ? 1 : 0;
|
||||||
|
_critProgress += (critTarget - _critProgress) * 0.008; // ~3s transition
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
ctx.font = `${FONT_SIZE}px 'JetBrains Mono', monospace`;
|
ctx.font = `${FONT_SIZE}px 'JetBrains Mono', monospace`;
|
||||||
cols.forEach((col, i) => {
|
cols.forEach((col, i) => {
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,12 @@
|
||||||
#stream-ticker-inner { display:inline-block;animation:ticker 60s 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;flex-direction:column;gap:6px;padding-top:10px;border-top:1px solid var(--border); }
|
||||||
|
#ecosystem-bar { display:flex;align-items:center;gap:14px;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text3);overflow:hidden; }
|
||||||
|
.eco-item { display:flex;align-items:center;gap:5px;flex-shrink:0; }
|
||||||
|
.eco-dot { width:5px;height:5px;border-radius:50%;background:var(--text3); }
|
||||||
|
.eco-dot.ok { background:var(--ok); }
|
||||||
|
#bottom-row { display:flex;align-items:center;justify-content:space-between; }
|
||||||
.bottom-brand { font-size:12px;color:var(--text3);font-weight:500;letter-spacing:0.04em; }
|
.bottom-brand { font-size:12px;color:var(--text3);font-weight:500;letter-spacing:0.04em; }
|
||||||
.bottom-brand span { color:var(--accent); }
|
.bottom-brand span { color:var(--accent); }
|
||||||
.bottom-links { display:flex;gap:7px; }
|
.bottom-links { display:flex;gap:7px; }
|
||||||
|
|
@ -218,11 +223,15 @@
|
||||||
<div class="load-chip"><div class="load-chip-label">15m</div><div class="load-chip-val" id="load-15">—</div></div>
|
<div class="load-chip"><div class="load-chip-label">15m</div><div class="load-chip-val" id="load-15">—</div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="metric-item" id="net-row" style="display:none">
|
||||||
|
<div class="metric-header-row"><span class="metric-label">Network</span><span class="metric-val" id="net-val">—</span></div>
|
||||||
|
</div>
|
||||||
<div class="stat-grid">
|
<div class="stat-grid">
|
||||||
<div class="stat-card"><div class="stat-label">Uptime</div><div class="stat-val" id="uptime-val">—</div></div>
|
<div class="stat-card"><div class="stat-label">Uptime</div><div class="stat-val" id="uptime-val">—</div></div>
|
||||||
<div class="stat-card"><div class="stat-label">Sessions</div><div class="stat-val" id="sessions-val">—</div></div>
|
<div class="stat-card"><div class="stat-label">Sessions</div><div class="stat-val" id="sessions-val">—</div></div>
|
||||||
<div class="stat-card"><div class="stat-label">Node.js</div><div class="stat-val" id="node-val">—</div></div>
|
<div class="stat-card"><div class="stat-label">Node.js</div><div class="stat-val" id="node-val">—</div></div>
|
||||||
<div class="stat-card"><div class="stat-label">Platform</div><div class="stat-val" id="platform-val">—</div></div>
|
<div class="stat-card"><div class="stat-label">Platform</div><div class="stat-val" id="platform-val">—</div></div>
|
||||||
|
<div class="stat-card" id="temp-card" style="display:none"><div class="stat-label">CPU Temp</div><div class="stat-val" id="temp-val">—</div></div>
|
||||||
</div>
|
</div>
|
||||||
<a class="open-btn" href="/" target="_blank" rel="noopener"><i data-lucide="terminal"></i>posimai-dev を開く</a>
|
<a class="open-btn" href="/" target="_blank" rel="noopener"><i data-lucide="terminal"></i>posimai-dev を開く</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -301,6 +310,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="bottom">
|
<div id="bottom">
|
||||||
|
<div id="ecosystem-bar">
|
||||||
|
<div class="eco-item"><div class="eco-dot" id="eco-gitea-dot"></div><span id="eco-commit">commit: —</span></div>
|
||||||
|
</div>
|
||||||
|
<div id="bottom-row">
|
||||||
<div class="bottom-brand">posimai<span>-station</span></div>
|
<div class="bottom-brand">posimai<span>-station</span></div>
|
||||||
<div class="bottom-links">
|
<div class="bottom-links">
|
||||||
<a class="bottom-link" href="/station-b" rel="noopener"><i data-lucide="monitor"></i>Design B</a>
|
<a class="bottom-link" href="/station-b" rel="noopener"><i data-lucide="monitor"></i>Design B</a>
|
||||||
|
|
@ -309,6 +322,7 @@
|
||||||
<a class="bottom-link" href="https://posimai.soar-enrich.com" target="_blank" rel="noopener"><i data-lucide="layout-dashboard"></i>dashboard</a>
|
<a class="bottom-link" href="https://posimai.soar-enrich.com" target="_blank" rel="noopener"><i data-lucide="layout-dashboard"></i>dashboard</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="refresh-countdown">次の更新まで <span id="countdown">30</span>s</div>
|
<div id="refresh-countdown">次の更新まで <span id="countdown">30</span>s</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -416,6 +430,21 @@ async function fetchHealth(){
|
||||||
document.getElementById('platform-val').textContent=data.platform||'—';
|
document.getElementById('platform-val').textContent=data.platform||'—';
|
||||||
document.getElementById('hostname').textContent=data.hostname||'ubuntu-pc';
|
document.getElementById('hostname').textContent=data.hostname||'ubuntu-pc';
|
||||||
|
|
||||||
|
if(data.net){
|
||||||
|
const netRow=document.getElementById('net-row');
|
||||||
|
if(netRow) netRow.style.display='';
|
||||||
|
const rx=data.net.rx_kbps,tx=data.net.tx_kbps;
|
||||||
|
const fmt=v=>v>=1024?`${(v/1024).toFixed(1)}MB/s`:`${v}KB/s`;
|
||||||
|
document.getElementById('net-val').textContent=`↓${fmt(rx)} ↑${fmt(tx)}`;
|
||||||
|
}
|
||||||
|
if(data.cpu_temp_c!=null){
|
||||||
|
const tc=document.getElementById('temp-card');
|
||||||
|
if(tc) tc.style.display='';
|
||||||
|
const t=data.cpu_temp_c;
|
||||||
|
document.getElementById('temp-val').textContent=`${t}°C`;
|
||||||
|
document.getElementById('temp-val').style.color=t>=80?'var(--crit)':t>=70?'var(--warn)':'var(--ok)';
|
||||||
|
}
|
||||||
|
|
||||||
updateRing('ring-cpu-fill','ring-cpu-val', cpuPct, 301.6,'#22D3EE','#FB923C','#F87171');
|
updateRing('ring-cpu-fill','ring-cpu-val', cpuPct, 301.6,'#22D3EE','#FB923C','#F87171');
|
||||||
updateRing('ring-mem-fill','ring-mem-val', memPct, 188.5,'#A78BFA','#FB923C','#F87171');
|
updateRing('ring-mem-fill','ring-mem-val', memPct, 188.5,'#A78BFA','#FB923C','#F87171');
|
||||||
updateRing('ring-disk-fill','ring-disk-val',diskPct,188.5,'#4ADE80','#FB923C','#F87171');
|
updateRing('ring-disk-fill','ring-disk-val',diskPct,188.5,'#4ADE80','#FB923C','#F87171');
|
||||||
|
|
@ -628,6 +657,33 @@ buildServiceCards();
|
||||||
if(window.lucide)lucide.createIcons();
|
if(window.lucide)lucide.createIcons();
|
||||||
runRefresh();
|
runRefresh();
|
||||||
window.addEventListener('resize',renderSparklines);
|
window.addEventListener('resize',renderSparklines);
|
||||||
|
|
||||||
|
async function fetchGiteaCommit(){
|
||||||
|
try{
|
||||||
|
const r=await fetch('/api/gitea-commit',{signal:AbortSignal.timeout(4000)});
|
||||||
|
const d=await r.json();
|
||||||
|
if(d.error||!d.sha) return;
|
||||||
|
const dot=document.getElementById('eco-gitea-dot');
|
||||||
|
const el=document.getElementById('eco-commit');
|
||||||
|
if(dot) dot.className='eco-dot ok';
|
||||||
|
if(el){
|
||||||
|
const diff=Math.floor((Date.now()-new Date(d.date))/60000);
|
||||||
|
const ago=diff<1?'just now':diff<60?`${diff}m ago`:diff<1440?`${Math.floor(diff/60)}h ago`:`${Math.floor(diff/1440)}d ago`;
|
||||||
|
el.textContent=`${d.sha} ${d.message} (${ago})`;
|
||||||
|
}
|
||||||
|
}catch(_){}
|
||||||
|
}
|
||||||
|
fetchGiteaCommit();
|
||||||
|
setInterval(fetchGiteaCommit, 120000);
|
||||||
|
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if(e.key==='r'||e.key==='R'){ runRefresh(); }
|
||||||
|
if(e.key==='b'||e.key==='B'){ window.location.href='/station-b'; }
|
||||||
|
if(e.key==='f'||e.key==='F'){
|
||||||
|
if(!document.fullscreenElement) document.documentElement.requestFullscreen().catch(()=>{});
|
||||||
|
else document.exitFullscreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue