diff --git a/posimai-dev/server.js b/posimai-dev/server.js index 1973bf7e..744cbbe6 100644 --- a/posimai-dev/server.js +++ b/posimai-dev/server.js @@ -53,6 +53,49 @@ app.get('/api/sessions/:id', requireLocal, (req, res) => { 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) ────────────────────── // Atlas など外部から参照される。CORS ヘッダーを付与して Vercel 上の Atlas からも取得可能にする function getCpuSample() { @@ -107,6 +150,8 @@ app.get('/api/health', (req, res) => { mem_used_mb: Math.round((total - mem) / 1024 / 1024), mem_total_mb: Math.round(total / 1024 / 1024), disk, + net: getNetDelta(), + cpu_temp_c: getCpuTemp(), active_sessions: wss.clients ? wss.clients.size : 0, node_version: process.version, platform: os.platform(), @@ -115,6 +160,26 @@ app.get('/api/health', (req, res) => { }, 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=...) ────────── // ブラウザの mixed-content 制限を回避するためサーバー側から HTTP チェック app.get('/api/check', async (req, res) => { diff --git a/posimai-dev/station-b.html b/posimai-dev/station-b.html index 8a32386a..6c587ad8 100644 --- a/posimai-dev/station-b.html +++ b/posimai-dev/station-b.html @@ -158,7 +158,13 @@ #stream-ticker-inner { display:inline-block;animation:ticker 60s linear infinite; } @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 span { color:var(--accent); } .bottom-links { display:flex;gap:7px; } @@ -207,11 +213,15 @@