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

825 lines
48 KiB
HTML
Raw Normal View History

<!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="icon" href="/favicon.svg" type="image/svg+xml">
<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" integrity="sha384-tTkFttkBclaU1cloKwOi9xk3pbao3VZxTjLNBt8iFABWDBQibbAbWpVmO28zMuxq" crossorigin="anonymous"></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; }
#binary-canvas { position:fixed;inset:0;z-index:0;pointer-events:none; }
.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:320px 1fr 460px 180px;gap:10px;min-height:0; }
.machines-panel { gap:8px;overflow-y:auto;scrollbar-width:none; }
.machines-panel::-webkit-scrollbar { display:none; }
.machines-section { font-size:9px;font-weight:600;color:var(--text3);letter-spacing:0.14em;text-transform:uppercase;margin-bottom:-2px; }
.machines-divider { border-top:1px solid var(--border);margin:2px 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; }
.bin-bar { font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:0.04em;display:flex;gap:0px;margin-top:2px; }
.bin-bar .b1 { color:var(--accent);transition:color 0.5s; }
.bin-bar .b0 { color:rgba(255,255,255,0.1); }
.bin-bar.warn .b1 { color:var(--warn); }
.bin-bar.crit .b1 { color: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;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(2,1fr);grid-auto-rows: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; }
.service-uptime { font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:600; }
.service-uptime.full { color:var(--ok); }
.service-uptime.partial { color:#FB923C; }
.service-uptime.down { color:var(--crit); }
.svc-spark-wrap { height:28px;margin:2px 0; }
.svc-spark { width:100%;height:100%;overflow:visible; }
/* 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 { display:flex;flex-direction:column;gap:6px;padding-top:10px;border-top:1px solid var(--border); }
#ecosystem-bar { display:flex;align-items:center;gap:14px;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text3);overflow:hidden; }
.eco-item { display:flex;align-items:center;gap:5px;flex-shrink:0; }
.eco-dot { width:5px;height:5px;border-radius:50%;background:var(--text3); }
.eco-dot.ok { background:var(--ok); }
.eco-dot.crit { background:var(--crit); }
#bottom-row { display:flex;align-items:center;justify-content:space-between; }
.bottom-brand { font-size:12px;color:var(--text3);font-weight:500;letter-spacing:0.04em; }
.bottom-brand span { color:var(--accent); }
.bottom-links { display:flex;gap:7px; }
.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>
<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: MACHINES (Ubuntu PC + VPS stacked) -->
<div class="panel machines-panel">
<div class="panel-title"><i data-lucide="server"></i>Machines</div>
<!-- Ubuntu PC section -->
<div class="machines-section">Ubuntu PC</div>
<div class="metric-item">
<div class="metric-header-row"><span class="metric-label">CPU Usage</span><span class="metric-val" id="cpu-val"></span></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>
<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>
<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="metric-item" id="net-row" style="display:none">
<div class="metric-header-row"><span class="metric-label">Network</span><span class="metric-val" id="net-val"></span></div>
</div>
<div class="stat-grid" style="grid-template-columns:1fr 1fr 1fr">
<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" id="temp-card" style="display:none"><div class="stat-label">CPU Temp</div><div class="stat-val" id="temp-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>
<!-- Divider -->
<div class="machines-divider"></div>
<!-- VPS section -->
<div class="machines-section">VPS</div>
<div class="metric-item">
<div class="metric-header-row"><span class="metric-label">Load Avg <span style="font-size:9px;color:var(--text3)">(1m/5m)</span></span><span class="metric-val" id="vps-load"></span></div>
</div>
<div class="metric-item">
<div class="metric-header-row"><span class="metric-label">Memory</span><span class="metric-val" id="vps-mem"></span></div>
</div>
<div class="metric-item">
<div class="metric-header-row"><span class="metric-label">Disk (/)</span><span class="metric-val" id="vps-disk"></span></div>
</div>
<div class="stat-grid" style="grid-template-columns:1fr 1fr;gap:6px">
<div class="stat-card"><div class="stat-label">Uptime</div><div class="stat-val" id="vps-uptime"></div></div>
<div class="stat-card"><div class="stat-label">Users</div><div class="stat-val" id="vps-users"></div></div>
<div class="stat-card"><div class="stat-label">Gemini</div><div class="stat-val" id="vps-gemini"></div></div>
<div class="stat-card"><div class="stat-label">Node</div><div class="stat-val" id="vps-node"></div></div>
</div>
</div>
<!-- Col 3: 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:152px;height:152px;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:98px;height:98px;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:98px;height:98px;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="ecosystem-bar">
<div class="eco-item"><div class="eco-dot" id="eco-gitea-dot"></div><span id="eco-commit">commit: —</span></div>
<div class="eco-item" style="color:var(--border);font-size:8px">|</div>
<div class="eco-item"><div class="eco-dot" id="eco-vercel-dot"></div><span id="eco-vercel">deploy: —</span></div>
<div class="eco-item" style="color:var(--border);font-size:8px">|</div>
<div class="eco-item" id="eco-temp-item" style="display:none"><span id="eco-temp-label">temp: —</span></div>
</div>
<div id="bottom-row">
<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>
<a class="bottom-link" href="https://posimai.soar-enrich.com" 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>
</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:'/api/vps-health', isHealth:false, proxy:true},
{id:'gitea', name:'Gitea', desc:'ローカル Git バックアップ (NAS)', url:'/api/check?url=http://100.76.7.3:3000', isHealth:false, proxy:true},
2026-04-05 13:44:25 +00:00
{id:'uptime-kuma',name:'Uptime Kuma', desc:'死活監視 — NAS Docker', url:'/api/check?url=http://100.76.7.3:3002', isHealth:false, proxy:true},
{id:'vercel', name:'Vercel', desc:'PWA ホスティング (27本)', url:'/api/check?url=https://vercel.com', isHealth:false, proxy:true},
{id:'github', name:'GitHub', desc:'ソースコード管理', url:'/api/check?url=https://github.com', isHealth:false, proxy:true},
];
const hist = {cpu:[], load:[]};
const svcHist = {};
const svcLatHist = {};
SERVICES.forEach(s => { svcHist[s.id] = []; svcLatHist[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}%`;
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();
if(data.status!=='ok')throw new Error('invalid health response');
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}%`;
document.getElementById('mem-val').textContent=`${data.mem_used_mb}/${data.mem_total_mb}MB (${memPct}%)`;
if(data.disk){
document.getElementById('disk-val').textContent=`${data.disk.used_gb}/${data.disk.total_gb}GB (${diskPct}%)`;
}
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';
// Network I/O
if(data.net){
const netRow=document.getElementById('net-row');
if(netRow) netRow.style.display='';
const rx=data.net.rx_kbps,tx=data.net.tx_kbps;
const fmt=v=>v>=1024?`${(v/1024).toFixed(1)}MB/s`:`${v}KB/s`;
document.getElementById('net-val').textContent=`↓${fmt(rx)} ↑${fmt(tx)}`;
}
// CPU Temp
if(data.cpu_temp_c!=null){
const tc=document.getElementById('temp-card');
const ti=document.getElementById('eco-temp-item');
if(tc) tc.style.display='';
if(ti) ti.style.display='';
const t=data.cpu_temp_c;
const tcol=t>=80?'var(--crit)':t>=70?'var(--warn)':'var(--ok)';
document.getElementById('temp-val').textContent=`${t}°C`;
document.getElementById('temp-val').style.color=tcol;
document.getElementById('eco-temp-label').textContent=`temp: ${t}°C`;
document.getElementById('eco-temp-label').style.color=tcol;
}
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');
window._systemCrit = hasCrit; // canvas aurora hue shift
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 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 < 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);
fetchVpsHealth();
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);
// ── Gitea 最新コミット ─────────────────────────────────────────
async function fetchGiteaCommit(){
try{
const r=await fetch('/api/gitea-commit',{signal:AbortSignal.timeout(4000)});
const d=await r.json();
if(d.error||!d.sha) return;
const dot=document.getElementById('eco-gitea-dot');
const el=document.getElementById('eco-commit');
if(dot) dot.className='eco-dot ok';
if(el){
const ago=timeAgo(d.date);
el.textContent=`${d.sha} ${d.message} (${ago})`;
}
}catch(_){}
}
function timeAgo(iso){
const diff=Math.floor((Date.now()-new Date(iso))/60000);
if(diff<1)return 'just now';
if(diff<60)return `${diff}m ago`;
if(diff<1440)return `${Math.floor(diff/60)}h ago`;
return `${Math.floor(diff/1440)}d ago`;
}
fetchGiteaCommit();
setInterval(fetchGiteaCommit, 120000);
// ── Vercel 最新デプロイ ─────────────────────────────────────────
async function fetchVercelDeploy(){
try{
const r=await fetch('/api/vercel-deploys',{signal:AbortSignal.timeout(8000)});
const d=await r.json();
if(d.error) return;
const dot=document.getElementById('eco-vercel-dot');
const el=document.getElementById('eco-vercel');
const stateMap={READY:'var(--ok)',ERROR:'var(--crit)',BUILDING:'var(--warn)',CANCELED:'var(--text3)'};
const color=stateMap[d.state]||'var(--text3)';
if(dot) dot.style.background=color;
const ago=timeAgo(d.created);
const label=(d.name||'').replace(/^posimai-/,'');
if(el) el.textContent=`deploy: ${label} ${d.state?.toLowerCase()} ${ago}`;
}catch(_){}
}
fetchVercelDeploy();
setInterval(fetchVercelDeploy, 300000);
// ── VPS メトリクス ─────────────────────────────────────────────
async function fetchVpsHealth(){
try{
const r=await fetch('/api/vps-health',{signal:AbortSignal.timeout(8000)});
const d=await r.json();
const memPct=d.mem_total_mb?Math.round(d.mem_used_mb/d.mem_total_mb*100):0;
const diskPct=d.disk?.use_pct??0;
document.getElementById('vps-load').textContent=d.load_avg?`${d.load_avg[0]} / ${d.load_avg[1]}`:'—';
document.getElementById('vps-mem').textContent=d.mem_total_mb?`${d.mem_used_mb}/${d.mem_total_mb}MB (${memPct}%)`:'—';
if(d.disk){
document.getElementById('vps-disk').textContent=`${d.disk.used_gb}/${d.disk.total_gb}GB (${diskPct}%)`;
}
document.getElementById('vps-uptime').textContent=formatUptime(d.uptime_s||0);
document.getElementById('vps-users').textContent=String(d.users??'—');
const gemEl=document.getElementById('vps-gemini');
gemEl.textContent=d.gemini?'ON':'OFF';
gemEl.style.color=d.gemini?'var(--ok)':'var(--crit)';
document.getElementById('vps-node').textContent=(d.node_version||'—').replace('v','');
}catch(e){
['vps-load','vps-mem','vps-disk','vps-uptime','vps-users','vps-gemini','vps-node']
.forEach(id=>{ const el=document.getElementById(id); if(el) el.textContent='ERR'; });
}
}
// ── キーボードショートカット ────────────────────────────────────
document.addEventListener('keydown', e => {
if(e.key==='r'||e.key==='R'){ runRefresh(); }
if(e.key==='b'||e.key==='B'){ window.location.href='/station'; }
if(e.key==='f'||e.key==='F'){
if(!document.fullscreenElement) document.documentElement.requestFullscreen().catch(()=>{});
else document.exitFullscreen();
}
});
// ── 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;
// CRIT hue shift: 0=normal, 1=full red. Transitions gradually.
let _critProgress = 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);
// Lerp hue toward red (hue=10) when CRIT — subtle, takes ~3s to fully shift
const hue = Math.round(best.hue + (10 - best.hue) * _critProgress);
const sat = Math.round(best.sat + (100 - best.sat) * _critProgress * 0.5);
return { hue, sat, alpha };
}
let rafId = null;
function draw(){
t++;
const critTarget = window._systemCrit ? 1 : 0;
_critProgress += (critTarget - _critProgress) * 0.008;
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; }
});
rafId = requestAnimationFrame(draw);
}
function startCanvas(){ if(!rafId) rafId = requestAnimationFrame(draw); }
function stopCanvas(){ if(rafId){ cancelAnimationFrame(rafId); rafId=null; } }
document.addEventListener('visibilitychange', () => { document.hidden ? stopCanvas() : startCanvas(); });
startCanvas();
})();
</script>
</body>
</html>