2026-04-02 00:36:08 +00:00
|
|
|
|
<!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; }
|
|
|
|
|
|
|
2026-04-02 02:20:21 +00:00
|
|
|
|
#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; }
|
2026-04-02 00:36:08 +00:00
|
|
|
|
#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 */
|
2026-04-02 02:20:21 +00:00
|
|
|
|
.service-grid { display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:8px;overflow-y:auto;flex:1; }
|
2026-04-02 00:36:08 +00:00
|
|
|
|
.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 4–6 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`;
|
|
|
|
|
|
|
2026-04-02 01:20:53 +00:00
|
|
|
|
cols.forEach((col, i) => {
|
2026-04-02 00:36:08 +00:00
|
|
|
|
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 + ' ' + 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>
|