561 lines
35 KiB
HTML
561 lines
35 KiB
HTML
<!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</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@300;400;500;600;700&family=JetBrains+Mono:wght@300;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: #0C1221;
|
||
--surface: rgba(15,22,40,0.70);
|
||
--surface2: rgba(26,34,53,0.80);
|
||
--border: rgba(255,255,255,0.08);
|
||
--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; }
|
||
.aurora { position: fixed; inset: 0; pointer-events: none; overflow: hidden; z-index: 0; }
|
||
.aurora-blob { position: absolute; border-radius: 50%; will-change: transform; }
|
||
.aurora-blob-1 { width:900px;height:600px;background:radial-gradient(ellipse,rgba(34,211,238,0.12) 0%,transparent 68%);top:-150px;right:-150px;filter:blur(100px);animation:adrift-1 22s ease-in-out infinite alternate; }
|
||
.aurora-blob-2 { width:700px;height:500px;background:radial-gradient(ellipse,rgba(167,139,250,0.09) 0%,transparent 68%);bottom:-120px;left:-100px;filter:blur(90px);animation:adrift-2 28s ease-in-out infinite alternate; }
|
||
.aurora-blob-3 { width:500px;height:400px;background:radial-gradient(ellipse,rgba(34,211,238,0.06) 0%,transparent 68%);top:40%;right:20%;filter:blur(80px);animation:adrift-3 18s ease-in-out infinite alternate; }
|
||
@keyframes adrift-1 { from{transform:translate(0,0) scale(1)} to{transform:translate(-80px,60px) scale(1.1)} }
|
||
@keyframes adrift-2 { from{transform:translate(0,0) scale(1)} to{transform:translate(60px,-40px) scale(1.08)} }
|
||
@keyframes adrift-3 { from{transform:translate(0,0) scale(1)} to{transform:translate(-40px,50px) scale(0.95)} }
|
||
|
||
.panel {
|
||
background: var(--surface);
|
||
backdrop-filter: blur(24px) saturate(160%);
|
||
-webkit-backdrop-filter: blur(24px) saturate(160%);
|
||
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(145deg,rgba(255,255,255,0.035) 0%,transparent 55%);
|
||
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:13px;height:13px; }
|
||
|
||
#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)} }
|
||
#alert-bar svg { width:13px;height:13px;flex-shrink:0; }
|
||
|
||
#app { position:relative;z-index:1;height:100vh;display:grid;grid-template-rows:auto auto 1fr auto;padding:20px;gap:0; }
|
||
|
||
#top { display:grid;grid-template-columns:1fr auto 1fr;align-items:center;padding-bottom:14px; }
|
||
#hostname-area { display:flex;align-items:center;gap:8px; }
|
||
#hostname { font-size:12px;font-weight:500;color:var(--text3);letter-spacing:0.08em;text-transform:uppercase; }
|
||
#status-dot { width:7px;height:7px;border-radius:50%;background:var(--text3);flex-shrink:0;transition:background 0.4s; }
|
||
#status-dot.ok { background:var(--ok); box-shadow:0 0 7px var(--ok); animation:pdot 2.4s ease-in-out infinite; }
|
||
#status-dot.warn { background:var(--warn); box-shadow:0 0 7px var(--warn); }
|
||
#status-dot.crit { background:var(--crit); box-shadow:0 0 7px var(--crit);animation:pdot 1s ease-in-out infinite; }
|
||
#status-dot.off { background:var(--text3);box-shadow:none; }
|
||
@keyframes pdot { 0%,100%{opacity:1} 50%{opacity:0.35} }
|
||
#clock-area { text-align:center; }
|
||
#clock { font-family:'JetBrains Mono',monospace;font-size:clamp(48px,5.5vw,84px);font-weight:300;letter-spacing:-0.03em;line-height:1; }
|
||
#date { font-size:12px;color:var(--text3);margin-top:3px;letter-spacing:0.06em; }
|
||
#last-checked { text-align:right;font-size:11px;color:var(--text3); }
|
||
|
||
#middle { display:grid;grid-template-columns:270px 280px 1fr 196px;gap:12px;min-height:0; }
|
||
|
||
.metric-item { display:flex;flex-direction:column;gap:5px;flex-shrink:0; }
|
||
.metric-header-row { display:flex;justify-content:space-between;align-items:baseline; }
|
||
.metric-label { font-size:12px;color:var(--text2); }
|
||
.metric-val { font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:500; }
|
||
.bar-track { height:4px;border-radius:2px;background:rgba(255,255,255,0.05);overflow:hidden; }
|
||
.bar-fill { height:100%;border-radius:2px;background:var(--accent);transition:width 0.8s cubic-bezier(0.4,0,0.2,1),background 0.4s; }
|
||
.bar-fill.warn { background:var(--warn); }
|
||
.bar-fill.crit { background:var(--crit); }
|
||
|
||
.load-row { display:flex;gap:6px;flex-shrink:0; }
|
||
.load-chip { flex:1;background:var(--surface2);border-radius:7px;padding:7px;text-align:center; }
|
||
.load-chip-label { font-size:10px;color:var(--text3);margin-bottom:2px; }
|
||
.load-chip-val { font-family:'JetBrains Mono',monospace;font-size:14px;font-weight:500; }
|
||
.load-chip-val.warn { color:var(--warn); }
|
||
.load-chip-val.crit { color:var(--crit); }
|
||
|
||
.stat-grid { display:grid;grid-template-columns:1fr 1fr;gap:7px;flex-shrink:0; }
|
||
.stat-card { background:var(--surface2);border-radius:9px;padding:9px 11px; }
|
||
.stat-label { font-size:11px;color:var(--text3);margin-bottom:2px; }
|
||
.stat-val { font-family:'JetBrains Mono',monospace;font-size:15px;font-weight:500;letter-spacing:-0.02em; }
|
||
|
||
.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 { display:flex;flex-direction:column;gap:12px; }
|
||
.ring-group { display:flex;flex-direction:column;align-items:center;gap:3px; }
|
||
.ring-track { fill:none;stroke:rgba(255,255,255,0.06);stroke-width:8;stroke-linecap:round; }
|
||
.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:15px;font-weight:500;fill:var(--text);dominant-baseline:middle;text-anchor:middle; }
|
||
.ring-sublabel { font-size:9px;fill:var(--text3);dominant-baseline:middle;text-anchor:middle; }
|
||
.ring-label-text { font-size:9px;color:var(--text3);letter-spacing:0.06em;text-transform:uppercase; }
|
||
.small-rings { display:flex;gap:8px;justify-content:center; }
|
||
|
||
/* sparkline */
|
||
.spark-section { flex:1;display:flex;flex-direction:column;gap:6px;min-height:0; }
|
||
.spark-header { display:flex;justify-content:space-between;align-items:center; }
|
||
.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;background:var(--surface2);border-radius:8px;padding:8px;min-height:55px;position:relative; }
|
||
.spark-svg { width:100%;height:100%;overflow:visible; }
|
||
|
||
/* services */
|
||
.service-grid { display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:8px;overflow-y:auto;flex:1; }
|
||
.service-card { background:var(--surface2);border:1px solid var(--border2);border-radius:11px;padding:12px;display:flex;flex-direction:column;gap:6px; }
|
||
.service-card-top { display:flex;align-items:center;justify-content:space-between; }
|
||
.service-name { font-size:13px;font-weight:500; }
|
||
.service-badge { font-size:10px;font-weight:600;letter-spacing:0.06em;padding:2px 7px;border-radius:20px; }
|
||
.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); }
|
||
.service-badge.checking { background:rgba(34,211,238,0.09); color:var(--accent); }
|
||
.service-desc { font-size:11px;color:var(--text3);line-height:1.4; }
|
||
.service-footer { display:flex;align-items:center;justify-content:space-between;margin-top:2px; }
|
||
.service-latency { font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text3); }
|
||
.service-dots { display:flex;gap:3px; }
|
||
.svc-dot { width:5px;height:5px;border-radius:50%;background:var(--text3);opacity:0.25; }
|
||
.svc-dot.ok { background:var(--ok); opacity:0.85; }
|
||
.svc-dot.crit { background:var(--crit); opacity:0.85; }
|
||
|
||
/* 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:var(--text3);opacity:0.35; }
|
||
.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 30s 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-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>
|
||
<div class="aurora">
|
||
<div class="aurora-blob aurora-blob-1"></div>
|
||
<div class="aurora-blob aurora-blob-2"></div>
|
||
<div class="aurora-blob aurora-blob-3"></div>
|
||
</div>
|
||
<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="bar-track"><div class="bar-fill" id="cpu-bar" style="width:0%"></div></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="bar-track"><div class="bar-fill" id="mem-bar" style="width:0%"></div></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="bar-track"><div class="bar-fill" id="disk-bar" style="width:0%"></div></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 class="bottom-brand">posimai<span>-station</span></div>
|
||
<div class="bottom-links">
|
||
<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-dashboard.vercel.app" 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>
|
||
<script>
|
||
'use strict';
|
||
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 = {};
|
||
SERVICES.forEach(s => svcHist[s.id] = []);
|
||
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}%`;
|
||
const cb=document.getElementById('cpu-bar');cb.style.width=`${cpuPct}%`;cb.className='bar-fill'+(cpuPct>80?' crit':cpuPct>60?' warn':'');
|
||
document.getElementById('mem-val').textContent=`${data.mem_used_mb}/${data.mem_total_mb}MB (${memPct}%)`;
|
||
const mb=document.getElementById('mem-bar');mb.style.width=`${memPct}%`;mb.className='bar-fill'+(memPct>85?' crit':memPct>65?' warn':'');
|
||
if(data.disk){
|
||
document.getElementById('disk-val').textContent=`${data.disk.used_gb}/${data.disk.total_gb}GB (${diskPct}%)`;
|
||
const db=document.getElementById('disk-bar');db.style.width=`${diskPct}%`;db.className='bar-fill'+(diskPct>90?' crit':diskPct>75?' warn':'');
|
||
}
|
||
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="service-footer"><div class="service-dots">${dots}</div><span class="service-latency" id="lat-${svc.id}"></span></div>`;
|
||
grid.appendChild(card);
|
||
});
|
||
}
|
||
|
||
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');
|
||
}
|
||
}
|
||
|
||
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){
|
||
// サーバー経由プロキシチェック(mixed-content 回避)
|
||
const r=await fetch(svc.url,{signal:ctrl.signal});
|
||
clearTimeout(timer);
|
||
const data=await r.json();
|
||
const ok=data.ok||(data.status&&data.status<500);
|
||
badge.className='service-badge '+(ok?'ok':'crit');
|
||
badge.textContent=ok?'OK':'DOWN';
|
||
latEl.textContent=data.latency_ms?`${data.latency_ms}ms`:'';
|
||
pushSvcHistory(svc.id,!!ok);
|
||
}else{
|
||
await fetch(svc.url,{method:'HEAD',mode:'no-cors',signal:ctrl.signal});
|
||
clearTimeout(timer);
|
||
badge.className='service-badge ok'; badge.textContent='OK';
|
||
latEl.textContent=`${Date.now()-t0}ms`;
|
||
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 ipBin=ip.split('.').map(o=>toBin(parseInt(o),8)).join(' ');
|
||
const base=`${ipBin} // ${data.hostname||'ubuntu-pc'} // ${data.node_version||''} // `;
|
||
const el=document.getElementById('stream-ticker-inner');
|
||
if(el)el.textContent=base+base;
|
||
}
|
||
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>
|