posimai-root/posimai-dev/station-b.html

745 lines
42 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="dark">
<title>posimai-station B</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #070D18;
--surface: rgba(10,18,35,0.72);
--surface2: rgba(18,28,50,0.82);
--border: rgba(255,255,255,0.07);
--border2: rgba(255,255,255,0.04);
--text: #F1F5F9;
--text2: #94A3B8;
--text3: #64748B;
--accent: #22D3EE;
--violet: #A78BFA;
--ok: #4ADE80;
--warn: #FB923C;
--crit: #F87171;
}
html, body { height: 100%; background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; overflow: hidden; user-select: none; }
/* ── Binary curtain canvas ── */
#binary-canvas { position: fixed; inset: 0; z-index: 0; pointer-events: none; }
/* ── Layout ── */
.panel {
background: var(--surface);
backdrop-filter: blur(28px) saturate(180%);
-webkit-backdrop-filter: blur(28px) saturate(180%);
border: 1px solid var(--border);
border-radius: 16px; padding: 18px;
display: flex; flex-direction: column; gap: 14px;
overflow: hidden; position: relative;
}
.panel::before {
content:''; position:absolute; inset:0; border-radius:16px;
background: linear-gradient(135deg, rgba(34,211,238,0.04) 0%, transparent 60%);
pointer-events: none;
}
.panel-title { font-size:11px;font-weight:600;color:var(--text3);letter-spacing:0.12em;text-transform:uppercase;display:flex;align-items:center;gap:6px;flex-shrink:0; }
.panel-title svg { width:12px;height:12px; }
#alert-bar { display:none;position:relative;z-index:2;align-items:center;gap:10px;padding:7px 14px;border-radius:10px;font-size:12px;font-weight:500;margin-bottom:10px; }
#alert-bar.visible { display:flex; }
#alert-bar.warn { background:rgba(251,146,60,0.10);border:1px solid rgba(251,146,60,0.28);color:var(--warn); }
#alert-bar.crit { background:rgba(248,113,113,0.10);border:1px solid rgba(248,113,113,0.3);color:var(--crit);animation:alert-pulse 2s ease-in-out infinite; }
@keyframes alert-pulse { 0%,100%{border-color:rgba(248,113,113,0.3)} 50%{border-color:rgba(248,113,113,0.65)} }
#app { position:relative;z-index:1;height:100vh;display:grid;grid-template-rows:auto auto 1fr auto;padding:20px;gap:0; }
#top { display:flex;align-items:flex-end;justify-content:space-between;padding:10px 16px 14px;flex-shrink:0;background:rgba(8,14,26,0.72);backdrop-filter:blur(16px);border-radius:14px;margin-bottom:4px; }
#hostname-area { display:flex;align-items:center;gap:8px; }
#status-dot { width:8px;height:8px;border-radius:50%;background:var(--text3); }
#status-dot.ok { background:var(--ok); box-shadow:0 0 8px var(--ok); }
#status-dot.warn { background:var(--warn); box-shadow:0 0 7px var(--warn);animation:pdot 1.5s ease-in-out infinite; }
#status-dot.crit { background:var(--crit); box-shadow:0 0 7px var(--crit);animation:pdot 1s ease-in-out infinite; }
@keyframes pdot { 0%,100%{opacity:1} 50%{opacity:0.35} }
#hostname { font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--text2); }
#clock-area { text-align:center; }
#clock { font-family:'JetBrains Mono',monospace;font-size:clamp(48px,5.5vw,84px);font-weight:400;letter-spacing:-0.03em;line-height:1;color:var(--text); }
#date { font-size:12px;color:var(--text3);margin-top:4px;text-align:center; }
#last-checked { font-size:11px;color:var(--text3); }
#middle { display:grid;grid-template-columns:270px 280px 1fr 196px;gap:12px;min-height:0; }
/* metric panel */
.metric-item { display:flex;flex-direction:column;gap:5px; }
.metric-header-row { display:flex;align-items:center;justify-content:space-between; }
.metric-label { font-size:10px;color:var(--text3);font-weight:500;letter-spacing:0.06em;text-transform:uppercase; }
.metric-val { font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:500;color:var(--text2); }
.bin-bar { display:flex;gap:2px;font-family:'JetBrains Mono',monospace;font-size:9px;line-height:1; }
.bin-bar .b1 { color:var(--accent); }
.bin-bar .b0 { color:rgba(100,180,220,0.32); }
.bin-bar.warn .b1 { color:var(--warn); }
.bin-bar.crit .b1 { color:var(--crit); }
.load-row { display:flex;gap:6px; }
.load-chip { flex:1;background:var(--surface2);border:1px solid var(--border2);border-radius:8px;padding:6px 8px;display:flex;flex-direction:column;gap:3px;align-items:center; }
.load-chip-label { font-size:9px;color:var(--text3);font-weight:600;letter-spacing:0.06em;text-transform:uppercase; }
.load-chip-val { font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:500;color:var(--text); }
.load-chip-val.warn { color:var(--warn); }
.load-chip-val.crit { color:var(--crit); }
.stat-grid { display:grid;grid-template-columns:1fr 1fr;gap:6px; }
.stat-card { background:var(--surface2);border:1px solid var(--border2);border-radius:10px;padding:8px 10px; }
.stat-label { font-size:9px;color:var(--text3);font-weight:600;text-transform:uppercase;letter-spacing:0.06em; }
.stat-val { font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:500;margin-top:3px; }
.open-btn { display:flex;align-items:center;justify-content:center;gap:6px;padding:8px;background:rgba(34,211,238,0.07);border:1px solid rgba(34,211,238,0.18);border-radius:9px;color:var(--accent);text-decoration:none;font-size:12px;font-weight:500;transition:background 0.2s,border-color 0.2s;margin-top:auto;flex-shrink:0; }
.open-btn:hover { background:rgba(34,211,238,0.13);border-color:rgba(34,211,238,0.35); }
.open-btn svg { width:13px;height:13px; }
/* rings panel */
.rings-panel { align-items:center; }
.ring-track { fill:none;stroke:rgba(255,255,255,0.05);stroke-width:8; }
.ring-fill { fill:none;stroke-width:8;stroke-linecap:round;transition:stroke-dashoffset 1s cubic-bezier(0.4,0,0.2,1),stroke 0.4s; }
.ring-center { font-family:'JetBrains Mono',monospace;font-size:14px;font-weight:500;fill:var(--text);text-anchor:middle; }
.ring-sublabel { font-size:9px;fill:var(--text3);text-anchor:middle;font-weight:600;letter-spacing:0.1em;text-transform:uppercase; }
.ring-group { display:flex;flex-direction:column;align-items:center;gap:4px; }
.ring-label-text { font-size:9px;color:var(--text3);font-weight:600;letter-spacing:0.08em;text-transform:uppercase; }
.small-rings { display:flex;gap:16px;justify-content:center; }
.spark-section { width:100%;display:flex;flex-direction:column;gap:6px;flex:1;min-height:0; }
.spark-header { display:flex;align-items:center;justify-content:space-between; }
.spark-legend { display:flex;gap:8px; }
.spark-legend-item { display:flex;align-items:center;gap:3px;font-size:9px;color:var(--text3); }
.spark-legend-dot { width:6px;height:6px;border-radius:50%; }
.spark-wrap { flex:1;min-height:40px; }
.spark-svg { width:100%;height:100%;overflow:visible; }
/* services */
.service-grid { display:grid;grid-template-columns:repeat(3,1fr);gap:8px;overflow-y:auto;flex:1; }
.service-card { background:var(--surface2);border:1px solid var(--border2);border-radius:10px;padding:9px 11px;display:flex;flex-direction:column;gap:5px; }
.service-card-top { display:flex;align-items:center;justify-content:space-between; }
.service-name { font-size:12px;font-weight:600; }
.service-desc { font-size:10px;color:var(--text3); }
.service-badge { font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:600;padding:2px 6px;border-radius:5px; }
.service-badge.checking { background:rgba(100,116,139,0.12);color:var(--text3); }
.service-badge.ok { background:rgba(74,222,128,0.14); color:var(--ok); }
.service-badge.crit { background:rgba(248,113,113,0.14);color:var(--crit); }
.svc-spark-wrap { height:24px; }
.svc-spark { width:100%;height:100%;overflow:visible; }
.service-footer { display:flex;align-items:center;justify-content:space-between;margin-top:2px; }
.service-dots { display:flex;gap:3px; }
.svc-dot { width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,0.06); }
.svc-dot.ok { background:var(--ok); }
.svc-dot.crit { background:var(--crit); }
.service-uptime { font-family:'JetBrains Mono',monospace;font-size:9px;font-weight:600; }
.service-uptime.full { color:var(--ok); }
.service-uptime.partial { color:var(--warn); }
.service-uptime.down { color:var(--crit); }
.service-latency { font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--text3); }
/* stream */
#stream-feed { flex:1;overflow:hidden;display:flex;flex-direction:column;gap:0; }
.stream-row { display:flex;flex-direction:column;gap:4px;padding:8px 0;border-bottom:1px solid var(--border2);animation:stream-in 0.45s cubic-bezier(0.4,0,0.2,1);flex-shrink:0; }
.stream-row:last-child { border-bottom:none; }
@keyframes stream-in { from{opacity:0;transform:translateY(-5px)} to{opacity:1;transform:translateY(0)} }
.stream-key { font-size:9px;font-weight:600;letter-spacing:0.1em;color:var(--text3);text-transform:uppercase; }
.stream-binary { font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:0.05em;line-height:1.5;word-break:break-all; }
.bit-1 { color:var(--accent);opacity:0.9; }
.bit-0 { color:rgba(100,180,220,0.32); }
.stream-human { font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:500;display:flex;align-items:center;gap:5px; }
.s-bar { flex:1;height:2px;border-radius:1px;background:rgba(255,255,255,0.05);overflow:hidden; }
.s-bar-fill { height:100%;border-radius:1px;background:var(--accent);transition:width 0.6s ease; }
.s-bar-fill.warn { background:var(--warn); }
.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-inner { display:inline-block;animation:ticker 60s linear infinite; }
@keyframes ticker { from{transform:translateX(0)} to{transform:translateX(-50%)} }
/* bottom */
#bottom { display:flex;flex-direction:column;gap:8px;padding-top:10px; }
#bin-footer { width:100%;overflow:hidden;white-space:nowrap;font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:0.12em;line-height:1;border-top:1px solid var(--border2);padding-top:7px; }
#bin-footer-inner { display:inline-block;animation:bfticker 40s linear infinite; }
@keyframes bfticker { from{transform:translateX(0)} to{transform:translateX(-50%)} }
.bf1 { color:var(--accent);opacity:0.75; }
.bf0 { color:rgba(100,180,220,0.28); }
#bottom-bar { 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; }
.bottom-link { display:flex;align-items:center;gap:4px;padding:4px 9px;background:var(--surface);backdrop-filter:blur(12px);border:1px solid var(--border);border-radius:7px;color:var(--text2);text-decoration:none;font-size:11px;transition:color 0.2s,border-color 0.2s; }
.bottom-link:hover { color:var(--accent);border-color:rgba(34,211,238,0.3); }
.bottom-link svg { width:11px;height:11px; }
#refresh-countdown { font-size:11px;color:var(--text3); }
</style>
</head>
<body>
<!-- Binary curtain aurora canvas -->
<canvas id="binary-canvas"></canvas>
<div id="app">
<div id="top">
<div id="hostname-area">
<div id="status-dot" class="off"></div>
<span id="hostname"></span>
</div>
<div id="clock-area">
<div id="clock">00:00:00</div>
<div id="date"></div>
</div>
<div id="last-checked"></div>
</div>
<div id="alert-bar"><i data-lucide="alert-triangle"></i><span id="alert-messages"></span></div>
<div id="middle">
<!-- Col 1: text metrics -->
<div class="panel">
<div class="panel-title"><i data-lucide="cpu"></i>Ubuntu PC</div>
<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="bin-bar" id="cpu-bar"></div>
</div>
<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="bin-bar" id="mem-bar"></div>
</div>
<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="bin-bar" id="disk-bar"></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="load-row">
<div class="load-chip"><div class="load-chip-label">1m</div><div class="load-chip-val" id="load-1"></div></div>
<div class="load-chip"><div class="load-chip-label">5m</div><div class="load-chip-val" id="load-5"></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 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">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">Platform</div><div class="stat-val" id="platform-val"></div></div>
</div>
<a class="open-btn" href="/" target="_blank" rel="noopener"><i data-lucide="terminal"></i>posimai-dev を開く</a>
</div>
<!-- Col 2: rings + sparkline -->
<div class="panel rings-panel">
<div class="panel-title"><i data-lucide="activity"></i>Vitals</div>
<div class="ring-group">
<svg viewBox="0 0 120 120" style="width:120px;height:120px;overflow:visible">
<defs><filter id="glow-c"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>
<circle class="ring-track" cx="60" cy="60" r="48"/>
<circle class="ring-fill" cx="60" cy="60" r="48" id="ring-cpu-fill"
stroke="#22D3EE" filter="url(#glow-c)"
stroke-dasharray="301.6" stroke-dashoffset="301.6"
style="transform:rotate(-90deg);transform-origin:60px 60px"/>
<text class="ring-center" id="ring-cpu-val" x="60" y="56"></text>
<text class="ring-sublabel" x="60" y="70">CPU</text>
</svg>
</div>
<div class="small-rings">
<div class="ring-group">
<svg viewBox="0 0 80 80" style="width:78px;height:78px;overflow:visible">
<defs><filter id="glow-m"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>
<circle class="ring-track" cx="40" cy="40" r="30"/>
<circle class="ring-fill" cx="40" cy="40" r="30" id="ring-mem-fill"
stroke="#A78BFA" filter="url(#glow-m)"
stroke-dasharray="188.5" stroke-dashoffset="188.5"
style="transform:rotate(-90deg);transform-origin:40px 40px"/>
<text class="ring-center" id="ring-mem-val" x="40" y="37" style="font-size:11px"></text>
<text class="ring-sublabel" x="40" y="48" style="font-size:8px">MEM</text>
</svg>
<span class="ring-label-text">Memory</span>
</div>
<div class="ring-group">
<svg viewBox="0 0 80 80" style="width:78px;height:78px;overflow:visible">
<defs><filter id="glow-d"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>
<circle class="ring-track" cx="40" cy="40" r="30"/>
<circle class="ring-fill" cx="40" cy="40" r="30" id="ring-disk-fill"
stroke="#4ADE80" filter="url(#glow-d)"
stroke-dasharray="188.5" stroke-dashoffset="188.5"
style="transform:rotate(-90deg);transform-origin:40px 40px"/>
<text class="ring-center" id="ring-disk-val" x="40" y="37" style="font-size:11px"></text>
<text class="ring-sublabel" x="40" y="48" style="font-size:8px">DISK</text>
</svg>
<span class="ring-label-text">Disk</span>
</div>
</div>
<div class="spark-section">
<div class="spark-header">
<span style="font-size:9px;color:var(--text3);letter-spacing:0.08em;text-transform:uppercase">History</span>
<div class="spark-legend">
<div class="spark-legend-item"><div class="spark-legend-dot" style="background:#22D3EE"></div>CPU</div>
<div class="spark-legend-item"><div class="spark-legend-dot" style="background:#A78BFA"></div>Load</div>
</div>
</div>
<div class="spark-wrap" id="spark-wrap">
<svg class="spark-svg" id="spark-svg" preserveAspectRatio="none">
<path id="spark-cpu-area" fill="#22D3EE" opacity="0.10" d=""/>
<polyline id="spark-cpu-line" fill="none" stroke="#22D3EE" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" points=""/>
<polyline id="spark-load-line" fill="none" stroke="#A78BFA" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="3 2" points=""/>
</svg>
</div>
</div>
</div>
<!-- Col 3: services -->
<div class="panel">
<div class="panel-title"><i data-lucide="radio"></i>Services</div>
<div class="service-grid" id="service-grid"></div>
</div>
<!-- Col 4: binary stream -->
<div class="panel">
<div class="panel-title"><i data-lucide="binary"></i>Stream</div>
<div id="stream-feed"></div>
<div id="stream-ticker"><span id="stream-ticker-inner"></span></div>
</div>
</div>
<div id="bottom">
<div id="bin-footer"><span id="bin-footer-inner"></span></div>
<div id="bottom-bar">
<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>
</div>
<div id="refresh-countdown">次の更新まで <span id="countdown">30</span>s</div>
</div>
</div>
</div>
<script>
'use strict';
// ── Binary curtain aurora ────────────────────────────────────────────────────
(function initBinaryCurtain(){
const canvas = document.getElementById('binary-canvas');
const ctx = canvas.getContext('2d');
// Column state
const FONT_SIZE = 14;
const COLS_PER_BAND = 1; // one char column per pixel column slot
let cols = [];
// Aurora color bands: each band has a hue center, width, and phase
// We generate 46 soft color regions that shift slowly
const BANDS = [
{ hue: 185, sat: 90, x: 0.15, speed: 0.00018, phase: 0 }, // cyan
{ hue: 265, sat: 80, x: 0.38, speed: 0.00013, phase: 1.5 }, // violet
{ hue: 185, sat: 85, x: 0.62, speed: 0.00020, phase: 3.0 }, // cyan-2
{ hue: 150, sat: 70, x: 0.80, speed: 0.00015, phase: 4.2 }, // green
];
function resize(){
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const numCols = Math.ceil(canvas.width / FONT_SIZE);
// preserve existing cols, extend if needed
if(cols.length < numCols){
for(let i = cols.length; i < numCols; i++){
cols.push({
y: Math.random() * canvas.height, // current head y position
speed: 1.2 + Math.random() * 3.5, // fall speed px/frame
len: 8 + Math.floor(Math.random() * 20), // trail length in chars
chars: [], // char values
nextChange: 0, // when to mutate a char
opacity: 0.3 + Math.random() * 0.5,
});
}
} else {
cols.length = numCols;
}
}
resize();
window.addEventListener('resize', resize);
let t = 0;
function getBandColor(x, t){
// find dominant band at this x fraction
const xf = x / canvas.width;
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; }
});
// brightness falloff from band center
const bx = best.x + Math.sin(t * best.speed + best.phase) * 0.12;
const dist = Math.abs(xf - bx);
const alpha = Math.max(0, 1 - dist / 0.22);
return { hue: best.hue, sat: best.sat, alpha };
}
function draw(){
t++;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = `${FONT_SIZE}px 'JetBrains Mono', monospace`;
cols.forEach((col, i) => {
const x = i * FONT_SIZE;
const band = getBandColor(x, t);
// draw trail
for(let j = 0; j < col.len; j++){
const cy = col.y - j * FONT_SIZE;
if(cy < -FONT_SIZE || cy > canvas.height + FONT_SIZE) continue;
// ensure char exists
if(!col.chars[j] || (t % 8 === 0 && Math.random() < 0.05)){
col.chars[j] = Math.random() < 0.5 ? '1' : '0';
}
const ch = col.chars[j];
// fade trail: head is brightest
const trailAlpha = (1 - j / col.len) * col.opacity;
const finalAlpha = trailAlpha * (band.alpha * 0.7 + 0.15);
if(j === 0){
// head char — bright white/cyan
ctx.fillStyle = `hsla(${band.hue},${band.sat}%,92%,${Math.min(1, finalAlpha * 2.2)})`;
} else if(ch === '1'){
ctx.fillStyle = `hsla(${band.hue},${band.sat}%,65%,${finalAlpha})`;
} else {
// '0' — slightly different hue for cyber feel
ctx.fillStyle = `hsla(${(band.hue + 30) % 360},${Math.round(band.sat * 0.6)}%,45%,${finalAlpha * 0.55})`;
}
ctx.fillText(ch, x, cy);
}
// advance column
col.y += col.speed;
if(col.y - col.len * FONT_SIZE > canvas.height){
col.y = -FONT_SIZE * 2;
col.speed = 1.2 + Math.random() * 3.5;
col.len = 8 + Math.floor(Math.random() * 20);
col.chars = [];
col.opacity = 0.3 + Math.random() * 0.5;
}
});
requestAnimationFrame(draw);
}
draw();
})();
// ── full-width binary footer tape ──────────────────────────────────────────
(function initBinFooter(){
const el = document.getElementById('bin-footer-inner');
if(!el) return;
function makeTape(len){
let s = '';
for(let i = 0; i < len; i++){
const b = Math.random() < 0.5 ? '1' : '0';
const sp = (i > 0 && i % 8 === 0) ? ' ' : '';
s += sp + (b === '1' ? `<span class="bf1">1</span>` : `<span class="bf0">0</span>`);
}
return s;
}
const tape = makeTape(600);
el.innerHTML = tape + '&nbsp;&nbsp;&nbsp;&nbsp;' + tape;
})();
// ── Shared logic (same as station.html) ────────────────────────────────────
const HEALTH_URL = '/api/health';
const REFRESH_SEC = 30;
const HISTORY_MAX = 20;
const SERVICES = [
{id:'posimai-dev',name:'posimai-dev', desc:'ブラウザターミナル + Claude Code',url:HEALTH_URL, isHealth:true},
{id:'posimai-api',name:'Posimai API',desc:'Node.js / Express — VPS 本番', url:'https://api.soar-enrich.com', isHealth:false},
{id:'gitea', name:'Gitea', desc:'ローカル Git バックアップ', url:'/api/check?url=http://100.76.7.3:3000', isHealth:false, proxy:true},
{id:'syncthing', name:'Syncthing', desc:'ファイル同期 GUI', url:'/api/check?url=http://100.77.11.43:8384', isHealth:false, proxy:true},
{id:'vercel', name:'Vercel', desc:'PWA ホスティング (27本)', url:'https://vercel.com', isHealth:false},
{id:'github', name:'GitHub', desc:'ソースコード管理', url:'https://github.com/posimai', isHealth:false},
];
const hist = {cpu:[], load:[]};
const svcHist = {};
const svcLatHist = {};
SERVICES.forEach(s => { svcHist[s.id] = []; svcLatHist[s.id] = []; });
let _metrics = { cpuPct:0, memPct:0, diskPct:0, loadAvg:[0,0,0], uptimeS:0, sessions:0, hostname:'ubuntu-pc', nodeVer:'' };
let streamData = null;
function p(n){ return String(n).padStart(2,'0'); }
function updateClock(){
const now=new Date();
document.getElementById('clock').textContent=`${p(now.getHours())}:${p(now.getMinutes())}:${p(now.getSeconds())}`;
const days=['日','月','火','水','木','金','土'];
document.getElementById('date').textContent=`${now.getFullYear()}.${p(now.getMonth()+1)}.${p(now.getDate())} (${days[now.getDay()]})`;
}
setInterval(updateClock,1000); updateClock();
function formatUptime(s){
const d=Math.floor(s/86400),h=Math.floor((s%86400)/3600),m=Math.floor((s%3600)/60);
if(d>0)return`${d}d ${h}h`; if(h>0)return`${h}h ${m}m`; return`${m}m`;
}
function setAlerts(alerts){
const bar=document.getElementById('alert-bar'),msgs=document.getElementById('alert-messages');
if(!alerts.length){bar.className='';return;}
bar.className=alerts.some(a=>a.level==='crit')?'crit visible':'warn visible';
msgs.textContent=alerts.map(a=>a.msg).join(' / ');
if(window.lucide)lucide.createIcons({nodes:[bar]});
}
function updateRing(fillId,valId,pct,circ,base,warn,crit){
const fill=document.getElementById(fillId),valEl=document.getElementById(valId);
if(!fill||!valEl)return;
fill.style.strokeDashoffset=circ*(1-Math.min(pct,100)/100);
const color=pct>80?(crit||'#F87171'):pct>60?(warn||'#FB923C'):(base||'#22D3EE');
fill.style.stroke=color;
valEl.textContent=pct>0?`${pct}%`:'—';
valEl.style.fill=pct>80?'#F87171':pct>60?'#FB923C':'var(--text)';
}
function renderSparklines(){
const wrap=document.getElementById('spark-wrap');
if(!wrap||hist.cpu.length<2)return;
const W=wrap.clientWidth||160,H=wrap.clientHeight||55,pad=4;
function pts(arr,max){
return arr.map((v,i)=>{
const x=pad+(i/(HISTORY_MAX-1))*(W-pad*2);
const y=H-pad-(v/max)*(H-pad*2);
return`${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
}
const cpuMax=100,loadMax=Math.max((window._cpuCount||4)*1.5,...hist.load,1);
document.getElementById('spark-cpu-line').setAttribute('points',pts(hist.cpu,cpuMax));
document.getElementById('spark-load-line').setAttribute('points',pts(hist.load,loadMax));
if(hist.cpu.length>=2){
const arr=hist.cpu.map((v,i)=>[pad+(i/(HISTORY_MAX-1))*(W-pad*2),H-pad-(v/cpuMax)*(H-pad*2)]);
const d=`M${arr[0][0].toFixed(1)},${H-pad} L${arr.map(p=>p.map(n=>n.toFixed(1)).join(',')).join(' L')} L${arr[arr.length-1][0].toFixed(1)},${H-pad} Z`;
document.getElementById('spark-cpu-area').setAttribute('d',d);
}
}
async function fetchHealth(){
try{
const res=await fetch(HEALTH_URL); if(!res.ok)throw new Error();
const data=await res.json();
const cpuPct=data.cpu_pct||0;
const memPct=data.mem_total_mb?Math.round((data.mem_used_mb/data.mem_total_mb)*100):0;
const diskPct=data.disk?.use_pct??0;
const cpuCount=data.cpu_count||1;
const loadAvg=data.load_avg||[0,0,0];
window._cpuCount=cpuCount;
document.getElementById('cpu-val').textContent=`${cpuPct}%`;
renderBinBar('cpu-bar',cpuPct,60,80);
document.getElementById('mem-val').textContent=`${data.mem_used_mb}/${data.mem_total_mb}MB (${memPct}%)`;
renderBinBar('mem-bar',memPct,65,85);
if(data.disk){
document.getElementById('disk-val').textContent=`${data.disk.used_gb}/${data.disk.total_gb}GB (${diskPct}%)`;
renderBinBar('disk-bar',diskPct,75,90);
}
document.getElementById('cpu-count-label').textContent=`(core:${cpuCount})`;
['load-1','load-5','load-15'].forEach((id,i)=>{
const el=document.getElementById(id),val=loadAvg[i]||0;
el.textContent=val.toFixed(2);
el.className='load-chip-val'+(val>cpuCount*1.5?' crit':val>cpuCount?' warn':'');
});
document.getElementById('uptime-val').textContent=formatUptime(data.uptime_s||0);
document.getElementById('sessions-val').textContent=String(data.active_sessions||0);
document.getElementById('node-val').textContent=(data.node_version||'—').replace('v','');
document.getElementById('platform-val').textContent=data.platform||'—';
document.getElementById('hostname').textContent=data.hostname||'ubuntu-pc';
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-disk-fill','ring-disk-val',diskPct,188.5,'#4ADE80','#FB923C','#F87171');
hist.cpu.push(cpuPct); if(hist.cpu.length>HISTORY_MAX)hist.cpu.shift();
hist.load.push(loadAvg[0]||0); if(hist.load.length>HISTORY_MAX)hist.load.shift();
renderSparklines();
const alerts=[];
if(cpuPct>80)alerts.push({level:'crit',msg:`CPU ${cpuPct}% — 高負荷`});
else if(cpuPct>60)alerts.push({level:'warn',msg:`CPU ${cpuPct}%`});
if(memPct>85)alerts.push({level:'crit',msg:`メモリ ${memPct}% — OOM注意`});
else if(memPct>65)alerts.push({level:'warn',msg:`メモリ ${memPct}%`});
if(diskPct>90)alerts.push({level:'crit',msg:`ディスク ${diskPct}% — 残りわずか`});
else if(diskPct>75)alerts.push({level:'warn',msg:`ディスク ${diskPct}%`});
if(loadAvg[0]>cpuCount*1.5)alerts.push({level:'crit',msg:`Load ${loadAvg[0].toFixed(2)} — コア数超過`});
else if(loadAvg[0]>cpuCount)alerts.push({level:'warn',msg:`Load ${loadAvg[0].toFixed(2)}`});
setAlerts(alerts);
const hasCrit=alerts.some(a=>a.level==='crit'),hasWarn=alerts.some(a=>a.level==='warn');
document.getElementById('status-dot').className=hasCrit?'crit':hasWarn?'warn':'ok';
updateStream(data);
return true;
}catch(e){
document.getElementById('status-dot').className='off';
setAlerts([{level:'crit',msg:'Ubuntu PC に接続できません'}]);
return false;
}
}
function buildServiceCards(){
const grid=document.getElementById('service-grid'); grid.innerHTML='';
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)=>`<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="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);
});
}
function renderBinBar(id,pct,warnTh,critTh){
const el=document.getElementById(id); if(!el)return;
const cells=20,filled=Math.round(pct/100*cells);
const cls=pct>critTh?'crit':pct>warnTh?'warn':'';
el.className='bin-bar'+(cls?' '+cls:'');
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){
svcHist[id].push(ok); if(svcHist[id].length>5)svcHist[id].shift();
const h=svcHist[id];
for(let i=0;i<5;i++){
const dot=document.getElementById(`dot-${id}-${i}`); if(!dot)continue;
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=`UP:${pct}%`;
uptEl.className='service-uptime '+(pct===100?'full':pct>=60?'partial':'down');
}
}
async function checkService(svc){
const badge=document.getElementById(`badge-${svc.id}`),latEl=document.getElementById(`lat-${svc.id}`);
if(!badge)return;
const t0=Date.now();
try{
const ctrl=new AbortController(),timer=setTimeout(()=>ctrl.abort(),7000);
if(svc.proxy){
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=ms?`${ms}ms`:'';
updateSparkline(svc.id,ms,!!ok);
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=`${ms}ms`;
updateSparkline(svc.id,ms,true);
pushSvcHistory(svc.id,true);
}
}catch(e){
badge.className='service-badge crit'; badge.textContent='DOWN'; latEl.textContent='';
pushSvcHistory(svc.id,false);
}
}
function checkAllServices(devOk){
SERVICES.forEach(svc=>{
if(svc.isHealth){
const badge=document.getElementById(`badge-${svc.id}`);
if(badge){badge.className=devOk?'service-badge ok':'service-badge crit';badge.textContent=devOk?'OK':'DOWN';}
pushSvcHistory(svc.id,devOk);
}else{
checkService(svc);
}
});
}
function toBin(n,bits){return(n>>>0).toString(2).padStart(bits,'0').slice(-bits);}
function renderBits(b){return b.split('').map(c=>`<span class="bit-${c}">${c}</span>`).join('');}
function pushStreamRow(label,value,bin,pct,level){
const feed=document.getElementById('stream-feed');
const row=document.createElement('div'); row.className='stream-row';
row.innerHTML=`<div class="stream-key">${label}</div><div class="stream-binary">${renderBits(bin)}</div><div class="stream-human"><span>${value}</span>${pct!==null?`<div class="s-bar"><div class="s-bar-fill${level?' '+level:''}" style="width:${pct}%"></div></div>`:''}</div>`;
feed.insertBefore(row,feed.firstChild);
while(feed.children.length>8)feed.removeChild(feed.lastChild);
}
function updateStream(data){
streamData=data;
const cpuPct=data.cpu_pct||0,memPct=data.mem_total_mb?Math.round((data.mem_used_mb/data.mem_total_mb)*100):0;
const diskPct=data.disk?.use_pct??null,load1=(data.load_avg&&data.load_avg[0])||0;
const ts=Math.floor(Date.now()/1000);
const rows=[
{label:'UNIX TIME',value:String(ts), bin:toBin(ts&0xFFFF,16), pct:null, level:null},
{label:'CPU USAGE',value:`${cpuPct}%`, bin:toBin(cpuPct,8), pct:cpuPct, level:cpuPct>80?'crit':cpuPct>60?'warn':null},
{label:'MEMORY', value:`${memPct}%`, bin:toBin(memPct,8), pct:memPct, level:memPct>85?'crit':memPct>65?'warn':null},
{label:'LOAD AVG', value:load1.toFixed(2), bin:toBin(Math.round(load1*100)&0xFF,8),pct:null, level:null},
...(diskPct!==null?[{label:'DISK /',value:`${diskPct}%`,bin:toBin(diskPct,8),pct:diskPct,level:diskPct>90?'crit':diskPct>75?'warn':null}]:[]),
{label:'SESSIONS', value:String(data.active_sessions||0),bin:toBin(data.active_sessions||0,8),pct:null,level:null},
{label:'UPTIME', value:formatUptime(data.uptime_s||0), bin:toBin(Math.floor((data.uptime_s||0)/60)&0xFFFF,16),pct:null,level:null},
];
const r=rows[Math.floor(Date.now()/1000)%rows.length];
pushStreamRow(r.label,r.value,r.bin,r.pct,r.level);
const ip='100.77.11.43';
const b8 = n => n.toString(2).padStart(8,'0');
const segments = [
`CPU:${b8(cpuPct??0)}`,`MEM:${b8(memPct??0)}`,`DISK:${b8(diskPct??0)}`,
`LOAD:${b8(Math.min(255,Math.round(((data.load_avg&&data.load_avg[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');
if(el){ el.textContent=tape+tape; el.style.animation='none'; void el.offsetWidth; el.style.animation=''; }
}
setInterval(()=>{if(streamData)updateStream(streamData);},4000);
let countdownVal=REFRESH_SEC,refreshTimer=null;
function startCountdown(){
countdownVal=REFRESH_SEC;
document.getElementById('countdown').textContent=countdownVal;
if(refreshTimer)clearInterval(refreshTimer);
refreshTimer=setInterval(()=>{
countdownVal--;
document.getElementById('countdown').textContent=countdownVal;
if(countdownVal<=0){clearInterval(refreshTimer);runRefresh();}
},1000);
}
async function runRefresh(){
const devOk=await fetchHealth();
checkAllServices(devOk);
const now=new Date().toLocaleTimeString('ja-JP',{hour:'2-digit',minute:'2-digit',second:'2-digit'});
document.getElementById('last-checked').textContent=`最終更新: ${now}`;
startCountdown();
}
buildServiceCards();
if(window.lucide)lucide.createIcons();
runRefresh();
window.addEventListener('resize',renderSparklines);
</script>
</body>
</html>