fix: rebuild station-b from station.html base, only canvas background differs

This commit is contained in:
posimai 2026-04-02 14:14:50 +09:00
parent 70c983f1e7
commit f726b4b9af
1 changed files with 147 additions and 183 deletions

View File

@ -8,15 +8,15 @@
<title>posimai-station B</title> <title>posimai-station B</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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"> <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> <script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
<style> <style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { :root {
--bg: #070D18; --bg: #0C1221;
--surface: rgba(10,18,35,0.72); --surface: rgba(15,22,40,0.70);
--surface2: rgba(18,28,50,0.82); --surface2: rgba(26,34,53,0.80);
--border: rgba(255,255,255,0.07); --border: rgba(255,255,255,0.08);
--border2: rgba(255,255,255,0.04); --border2: rgba(255,255,255,0.04);
--text: #F1F5F9; --text: #F1F5F9;
--text2: #94A3B8; --text2: #94A3B8;
@ -28,15 +28,12 @@
--crit: #F87171; --crit: #F87171;
} }
html, body { height: 100%; background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; overflow: hidden; user-select: none; } 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; } #binary-canvas { position:fixed;inset:0;z-index:0;pointer-events:none; }
/* ── Layout ── */
.panel { .panel {
background: var(--surface); background: var(--surface);
backdrop-filter: blur(28px) saturate(180%); backdrop-filter: blur(24px) saturate(160%);
-webkit-backdrop-filter: blur(28px) saturate(180%); -webkit-backdrop-filter: blur(24px) saturate(160%);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 16px; padding: 18px; border-radius: 16px; padding: 18px;
display: flex; flex-direction: column; gap: 14px; display: flex; flex-direction: column; gap: 14px;
@ -44,98 +41,104 @@
} }
.panel::before { .panel::before {
content:''; position:absolute; inset:0; border-radius:16px; content:''; position:absolute; inset:0; border-radius:16px;
background: linear-gradient(135deg, rgba(34,211,238,0.04) 0%, transparent 60%); background: linear-gradient(145deg,rgba(255,255,255,0.035) 0%,transparent 55%);
pointer-events:none; 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 { 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; } .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 { 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.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.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; } #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)} } @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; } #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; } #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-area { display:flex;align-items:center;gap:8px; }
#status-dot { width:8px;height:8px;border-radius:50%;background:var(--text3); } #hostname { font-size:12px;font-weight:500;color:var(--text3);letter-spacing:0.08em;text-transform:uppercase; }
#status-dot.ok { background:var(--ok); box-shadow:0 0 8px var(--ok); } #status-dot { width:7px;height:7px;border-radius:50%;background:var(--text3);flex-shrink:0;transition:background 0.4s; }
#status-dot.warn { background:var(--warn); box-shadow:0 0 7px var(--warn);animation:pdot 1.5s ease-in-out infinite; } #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.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} } @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-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); } #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:4px;text-align:center; } #date { font-size:12px;color:var(--text3);margin-top:3px;letter-spacing:0.06em; }
#last-checked { font-size:11px;color:var(--text3); } #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; } #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;flex-shrink:0; }
.metric-item { display:flex;flex-direction:column;gap:5px; } .metric-header-row { display:flex;justify-content:space-between;align-items:baseline; }
.metric-header-row { display:flex;align-items:center;justify-content:space-between; } .metric-label { font-size:12px;color:var(--text2); }
.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:12px;font-weight:500; }
.metric-val { font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:500;color:var(--text2); } .bin-bar { font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:0.04em;display:flex;gap:0px;margin-top:2px; }
.bin-bar { display:flex;gap:2px;font-family:'JetBrains Mono',monospace;font-size:9px;line-height:1; } .bin-bar .b1 { color:var(--accent);transition:color 0.5s; }
.bin-bar .b1 { color:var(--accent); } .bin-bar .b0 { color:rgba(255,255,255,0.1); }
.bin-bar .b0 { color:rgba(100,180,220,0.32); }
.bin-bar.warn .b1 { color:var(--warn); } .bin-bar.warn .b1 { color:var(--warn); }
.bin-bar.crit .b1 { color:var(--crit); } .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-row { display:flex;gap:6px;flex-shrink:0; }
.load-chip-label { font-size:9px;color:var(--text3);font-weight:600;letter-spacing:0.06em;text-transform:uppercase; } .load-chip { flex:1;background:var(--surface2);border-radius:7px;padding:7px;text-align:center; }
.load-chip-val { font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:500;color:var(--text); } .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.warn { color:var(--warn); }
.load-chip-val.crit { color:var(--crit); } .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-grid { display:grid;grid-template-columns:1fr 1fr;gap:7px;flex-shrink:0; }
.stat-label { font-size:9px;color:var(--text3);font-weight:600;text-transform:uppercase;letter-spacing:0.06em; } .stat-card { background:var(--surface2);border-radius:9px;padding:9px 11px; }
.stat-val { font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:500;margin-top:3px; } .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 { 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:hover { background:rgba(34,211,238,0.13);border-color:rgba(34,211,238,0.35); }
.open-btn svg { width:13px;height:13px; } .open-btn svg { width:13px;height:13px; }
/* rings panel */ /* rings panel */
.rings-panel { align-items:center; } .rings-panel { display:flex;flex-direction:column;gap:12px; }
.ring-track { fill:none;stroke:rgba(255,255,255,0.05);stroke-width:8; } .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-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-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);text-anchor:middle;font-weight:600;letter-spacing:0.1em;text-transform:uppercase; } .ring-sublabel { font-size:9px;fill:var(--text3);dominant-baseline:middle;text-anchor:middle; }
.ring-group { display:flex;flex-direction:column;align-items:center;gap:4px; } .ring-label-text { font-size:9px;color:var(--text3);letter-spacing:0.06em;text-transform:uppercase; }
.ring-label-text { font-size:9px;color:var(--text3);font-weight:600;letter-spacing:0.08em;text-transform:uppercase; } .small-rings { display:flex;gap:8px;justify-content:center; }
.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; } /* sparkline */
.spark-header { display:flex;align-items:center;justify-content:space-between; } .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 { display:flex;gap:8px; }
.spark-legend-item { display:flex;align-items:center;gap:3px;font-size:9px;color:var(--text3); } .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-legend-dot { width:6px;height:6px;border-radius:50%; }
.spark-wrap { flex:1;min-height:40px; } .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; } .spark-svg { width:100%;height:100%;overflow:visible; }
/* services */ /* services */
.service-grid { display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:8px;overflow-y:auto;flex:1; } .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:10px;padding:9px 11px;display:flex;flex-direction:column;gap:5px; } .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-card-top { display:flex;align-items:center;justify-content:space-between; }
.service-name { font-size:12px;font-weight:600; } .service-name { font-size:13px;font-weight:500; }
.service-desc { font-size:10px;color:var(--text3); } .service-badge { font-size:10px;font-weight:600;letter-spacing:0.06em;padding:2px 7px;border-radius:20px; }
.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.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.crit { background:rgba(248,113,113,0.14);color:var(--crit); }
.svc-spark-wrap { height:24px; } .service-badge.checking { background:rgba(34,211,238,0.09); color:var(--accent); }
.svc-spark { width:100%;height:100%;overflow:visible; } .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-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; } .service-dots { display:flex;gap:3px; }
.svc-dot { width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,0.06); } .svc-dot { width:5px;height:5px;border-radius:50%;background:var(--text3);opacity:0.25; }
.svc-dot.ok { background:var(--ok); } .svc-dot.ok { background:var(--ok); opacity:0.85; }
.svc-dot.crit { background:var(--crit); } .svc-dot.crit { background:var(--crit); opacity:0.85; }
.service-uptime { font-family:'JetBrains Mono',monospace;font-size:9px;font-weight:600; } .service-uptime { font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:600; }
.service-uptime.full { color:var(--ok); } .service-uptime.full { color:var(--ok); }
.service-uptime.partial { color:var(--warn); } .service-uptime.partial { color:#FB923C; }
.service-uptime.down { color:var(--crit); } .service-uptime.down { color:var(--crit); }
.service-latency { font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--text3); } .svc-spark-wrap { height:28px;margin:2px 0; }
.svc-spark { width:100%;height:100%;overflow:visible; }
/* stream */ /* stream */
#stream-feed { flex:1;overflow:hidden;display:flex;flex-direction:column;gap:0; } #stream-feed { flex:1;overflow:hidden;display:flex;flex-direction:column;gap:0; }
@ -155,7 +158,6 @@
#stream-ticker-inner { display:inline-block;animation:ticker 60s linear infinite; } #stream-ticker-inner { display:inline-block;animation:ticker 60s linear infinite; }
@keyframes ticker { from{transform:translateX(0)} to{transform:translateX(-50%)} } @keyframes ticker { from{transform:translateX(0)} to{transform:translateX(-50%)} }
/* bottom */
#bottom { display:flex;align-items:center;justify-content:space-between;padding-top:12px;border-top:1px solid var(--border); } #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 { font-size:12px;color:var(--text3);font-weight:500;letter-spacing:0.04em; }
.bottom-brand span { color:var(--accent); } .bottom-brand span { color:var(--accent); }
@ -167,9 +169,7 @@
</style> </style>
</head> </head>
<body> <body>
<!-- Binary curtain aurora canvas -->
<canvas id="binary-canvas"></canvas> <canvas id="binary-canvas"></canvas>
<div id="app"> <div id="app">
<div id="top"> <div id="top">
<div id="hostname-area"> <div id="hostname-area">
@ -220,7 +220,9 @@
<div class="panel-title"><i data-lucide="activity"></i>Vitals</div> <div class="panel-title"><i data-lucide="activity"></i>Vitals</div>
<div class="ring-group"> <div class="ring-group">
<svg viewBox="0 0 120 120" style="width:120px;height:120px;overflow:visible"> <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> <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-track" cx="60" cy="60" r="48"/>
<circle class="ring-fill" cx="60" cy="60" r="48" id="ring-cpu-fill" <circle class="ring-fill" cx="60" cy="60" r="48" id="ring-cpu-fill"
stroke="#22D3EE" filter="url(#glow-c)" stroke="#22D3EE" filter="url(#glow-c)"
@ -293,127 +295,13 @@
<a class="bottom-link" href="/station" rel="noopener"><i data-lucide="monitor"></i>Design A</a> <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="/" 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-atlas.vercel.app" target="_blank" rel="noopener"><i data-lucide="network"></i>atlas</a>
<a class="bottom-link" href="https://posimai.soar-enrich.com" target="_blank" rel="noopener"><i data-lucide="layout-dashboard"></i>dashboard</a>
</div> </div>
<div id="refresh-countdown">次の更新まで <span id="countdown">30</span>s</div> <div id="refresh-countdown">次の更新まで <span id="countdown">30</span>s</div>
</div> </div>
</div> </div>
<script> <script>
'use strict'; '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();
})();
// ── Shared logic (same as station.html) ────────────────────────────────────
const HEALTH_URL = '/api/health'; const HEALTH_URL = '/api/health';
const REFRESH_SEC = 30; const REFRESH_SEC = 30;
const HISTORY_MAX = 20; const HISTORY_MAX = 20;
@ -429,6 +317,7 @@ const hist = {cpu:[], load:[]};
const svcHist = {}; const svcHist = {};
const svcLatHist = {}; const svcLatHist = {};
SERVICES.forEach(s => { svcHist[s.id] = []; svcLatHist[s.id] = []; }); SERVICES.forEach(s => { svcHist[s.id] = []; svcLatHist[s.id] = []; });
// メトリクスのグローバルキャッシュupdateStream の setInterval から参照)
let _metrics = { cpuPct:0, memPct:0, diskPct:0, loadAvg:[0,0,0], uptimeS:0, sessions:0, hostname:'ubuntu-pc', nodeVer:'' }; let _metrics = { cpuPct:0, memPct:0, diskPct:0, loadAvg:[0,0,0], uptimeS:0, sessions:0, hostname:'ubuntu-pc', nodeVer:'' };
let streamData = null; let streamData = null;
@ -495,6 +384,7 @@ async function fetchHealth(){
const cpuCount=data.cpu_count||1; const cpuCount=data.cpu_count||1;
const loadAvg=data.load_avg||[0,0,0]; const loadAvg=data.load_avg||[0,0,0];
window._cpuCount=cpuCount; window._cpuCount=cpuCount;
document.getElementById('cpu-val').textContent=`${cpuPct}%`; document.getElementById('cpu-val').textContent=`${cpuPct}%`;
renderBinBar('cpu-bar',cpuPct,60,80); renderBinBar('cpu-bar',cpuPct,60,80);
document.getElementById('mem-val').textContent=`${data.mem_used_mb}/${data.mem_total_mb}MB (${memPct}%)`; document.getElementById('mem-val').textContent=`${data.mem_used_mb}/${data.mem_total_mb}MB (${memPct}%)`;
@ -514,12 +404,15 @@ async function fetchHealth(){
document.getElementById('node-val').textContent=(data.node_version||'—').replace('v',''); document.getElementById('node-val').textContent=(data.node_version||'—').replace('v','');
document.getElementById('platform-val').textContent=data.platform||'—'; document.getElementById('platform-val').textContent=data.platform||'—';
document.getElementById('hostname').textContent=data.hostname||'ubuntu-pc'; document.getElementById('hostname').textContent=data.hostname||'ubuntu-pc';
updateRing('ring-cpu-fill','ring-cpu-val', cpuPct, 301.6,'#22D3EE','#FB923C','#F87171'); updateRing('ring-cpu-fill','ring-cpu-val', cpuPct, 301.6,'#22D3EE','#FB923C','#F87171');
updateRing('ring-mem-fill','ring-mem-val', memPct, 188.5,'#A78BFA','#FB923C','#F87171'); updateRing('ring-mem-fill','ring-mem-val', memPct, 188.5,'#A78BFA','#FB923C','#F87171');
updateRing('ring-disk-fill','ring-disk-val',diskPct,188.5,'#4ADE80','#FB923C','#F87171'); updateRing('ring-disk-fill','ring-disk-val',diskPct,188.5,'#4ADE80','#FB923C','#F87171');
hist.cpu.push(cpuPct); if(hist.cpu.length>HISTORY_MAX)hist.cpu.shift(); 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(); hist.load.push(loadAvg[0]||0); if(hist.load.length>HISTORY_MAX)hist.load.shift();
renderSparklines(); renderSparklines();
const alerts=[]; const alerts=[];
if(cpuPct>80)alerts.push({level:'crit',msg:`CPU ${cpuPct}% — 高負荷`}); if(cpuPct>80)alerts.push({level:'crit',msg:`CPU ${cpuPct}% — 高負荷`});
else if(cpuPct>60)alerts.push({level:'warn',msg:`CPU ${cpuPct}%`}); else if(cpuPct>60)alerts.push({level:'warn',msg:`CPU ${cpuPct}%`});
@ -532,6 +425,7 @@ async function fetchHealth(){
setAlerts(alerts); setAlerts(alerts);
const hasCrit=alerts.some(a=>a.level==='crit'),hasWarn=alerts.some(a=>a.level==='warn'); const hasCrit=alerts.some(a=>a.level==='crit'),hasWarn=alerts.some(a=>a.level==='warn');
document.getElementById('status-dot').className=hasCrit?'crit':hasWarn?'warn':'ok'; document.getElementById('status-dot').className=hasCrit?'crit':hasWarn?'warn':'ok';
updateStream(data); updateStream(data);
return true; return true;
}catch(e){ }catch(e){
@ -583,7 +477,13 @@ function updateSparkline(id, ms, ok){
return `${x.toFixed(1)},${y.toFixed(1)}`; return `${x.toFixed(1)},${y.toFixed(1)}`;
}); });
area.push(`${W},${H}`, `0,${H}`); 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"/>`; 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){ function pushSvcHistory(id,ok){
@ -594,6 +494,7 @@ function pushSvcHistory(id,ok){
const idx=i-(5-h.length); if(idx<0){dot.className='svc-dot';continue;} const idx=i-(5-h.length); if(idx<0){dot.className='svc-dot';continue;}
dot.className='svc-dot '+(h[idx]?'ok':'crit'); dot.className='svc-dot '+(h[idx]?'ok':'crit');
} }
// 稼働率表示
const uptEl=document.getElementById(`upt-${id}`); const uptEl=document.getElementById(`upt-${id}`);
if(uptEl&&h.length>0){ if(uptEl&&h.length>0){
const pct=Math.round(h.filter(Boolean).length/h.length*100); const pct=Math.round(h.filter(Boolean).length/h.length*100);
@ -676,9 +577,12 @@ function updateStream(data){
const ip='100.77.11.43'; const ip='100.77.11.43';
const b8 = n => n.toString(2).padStart(8,'0'); const b8 = n => n.toString(2).padStart(8,'0');
const segments = [ const segments = [
`CPU:${b8(cpuPct??0)}`,`MEM:${b8(memPct??0)}`,`DISK:${b8(diskPct??0)}`, `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)))}`, `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)}`, `UP:${(data.uptime_s||0).toString(2)}`,
`SESSION:${b8(data.active_sessions||0)}`,
`TIME:${(Math.floor(Date.now()/1000)).toString(2).slice(-20)}`, `TIME:${(Math.floor(Date.now()/1000)).toString(2).slice(-20)}`,
`IP:${ip.split('.').map(o=>b8(parseInt(o))).join('.')}`, `IP:${ip.split('.').map(o=>b8(parseInt(o))).join('.')}`,
`HOST:${(data.hostname||'ubuntu-pc').split('').map(c=>b8(c.charCodeAt(0))).join(' ')}`, `HOST:${(data.hostname||'ubuntu-pc').split('').map(c=>b8(c.charCodeAt(0))).join(' ')}`,
@ -713,6 +617,66 @@ buildServiceCards();
if(window.lucide)lucide.createIcons(); if(window.lucide)lucide.createIcons();
runRefresh(); runRefresh();
window.addEventListener('resize',renderSparklines); window.addEventListener('resize',renderSparklines);
// ── Binary curtain aurora canvas ─────────────────────────────────────────────
(function initBinaryCurtain(){
const canvas = document.getElementById('binary-canvas');
const ctx = canvas.getContext('2d');
const FONT_SIZE = 14;
let cols = [];
const BANDS = [
{ hue: 185, sat: 90, x: 0.15, speed: 0.00018, phase: 0 },
{ hue: 265, sat: 80, x: 0.38, speed: 0.00013, phase: 1.5 },
{ hue: 185, sat: 85, x: 0.62, speed: 0.00020, phase: 3.0 },
{ hue: 150, sat: 70, x: 0.80, speed: 0.00015, phase: 4.2 },
];
function resize(){
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const numCols = Math.ceil(canvas.width / FONT_SIZE);
if(cols.length < numCols){
for(let i = cols.length; i < numCols; i++){
cols.push({ y: Math.random()*canvas.height, speed:1.2+Math.random()*3.5, len:8+Math.floor(Math.random()*20), chars:[], opacity:0.3+Math.random()*0.5 });
}
} else { cols.length = numCols; }
}
resize();
window.addEventListener('resize', resize);
let t = 0;
function getBandColor(x, t){
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;} });
const bx = best.x + Math.sin(t*best.speed+best.phase)*0.12;
const alpha = Math.max(0, 1 - Math.abs(xf-bx)/0.22);
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);
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;
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];
const trailAlpha = (1 - j/col.len) * col.opacity;
const finalAlpha = trailAlpha * (band.alpha*0.7+0.15);
if(j===0){ 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 { ctx.fillStyle=`hsla(${(band.hue+30)%360},${Math.round(band.sat*0.6)}%,45%,${finalAlpha*0.55})`; }
ctx.fillText(ch, x, cy);
}
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();
})();
</script> </script>
</body> </body>
</html> </html>