diff --git a/atlas.json b/atlas.json
index 1e97626..922c2a8 100644
--- a/atlas.json
+++ b/atlas.json
@@ -17,7 +17,9 @@
"id": "ubuntu-pc",
"label": "Ubuntu PC",
"type": "device",
- "description": "Linux 開発・検証機。Tailscale 経由でアクセス",
+ "description": "常時起動の開発サーバー。posimai-dev(ブラウザターミナル)稼働中。Claude Code インストール済み。Tailscale HTTPS でどこからでもアクセス可能。",
+ "url": "https://100.77.11.43:3333",
+ "health_url": "https://100.77.11.43:3333/api/health",
"status": "active"
},
{
@@ -136,6 +138,8 @@
{ "from": "posimai-apps", "to": "posimai-api", "type": "calls", "label": "REST API" },
{ "from": "posimai-apps", "to": "supabase", "type": "calls", "label": "realtime" },
{ "from": "posimai-apps", "to": "stripe", "type": "calls", "label": "payment" },
- { "from": "windows-pc", "to": "vps-xserver", "type": "connects", "label": "SSH" }
+ { "from": "windows-pc", "to": "vps-xserver", "type": "connects", "label": "SSH" },
+ { "from": "windows-pc", "to": "ubuntu-pc", "type": "connects", "label": "SSH / posimai-dev" },
+ { "from": "android-phone", "to": "ubuntu-pc", "type": "connects", "label": "Tailscale HTTPS" }
]
}
diff --git a/index.html b/index.html
index 950586c..18aab3f 100644
--- a/index.html
+++ b/index.html
@@ -812,6 +812,61 @@
opacity: 0.65;
}
+ /* ── Metrics panel ────────────────────────────────── */
+ #dp-metrics {
+ display: none;
+ flex-direction: column;
+ gap: 10px;
+ }
+ #dp-metrics.visible { display: flex; }
+ .metric-row {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+ .metric-header {
+ display: flex;
+ justify-content: space-between;
+ font-size: 11px;
+ color: var(--text3);
+ }
+ .metric-value { font-weight: 500; color: var(--text2); }
+ .metric-bar-track {
+ height: 4px;
+ background: rgba(255,255,255,0.07);
+ border-radius: 2px;
+ overflow: hidden;
+ }
+ [data-theme="light"] .metric-bar-track { background: rgba(0,0,0,0.07); }
+ .metric-bar-fill {
+ height: 100%;
+ border-radius: 2px;
+ background: var(--accent);
+ transition: width 0.5s ease;
+ }
+ .metric-bar-fill.warn { background: #FB923C; }
+ .metric-bar-fill.crit { background: #F87171; }
+ .metric-stat-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 6px;
+ }
+ .metric-stat {
+ background: var(--surface2, rgba(255,255,255,0.04));
+ border-radius: 8px;
+ padding: 8px 10px;
+ font-size: 11px;
+ }
+ .metric-stat-label { color: var(--text3); margin-bottom: 2px; }
+ .metric-stat-val { font-size: 14px; font-weight: 600; color: var(--text); letter-spacing: -0.02em; }
+ .metric-open-btn {
+ display: flex; align-items: center; gap: 5px;
+ font-size: 11px; color: var(--accent); text-decoration: none;
+ padding: 5px 0; border: none; background: none; cursor: pointer;
+ transition: opacity 0.15s;
+ }
+ .metric-open-btn:hover { opacity: 0.75; }
+
/* ── Tailscale scan ───────────────────────────────── */
.scan-status {
font-size: 11px;
@@ -1030,6 +1085,39 @@
接続確認
+
+
+
接続
@@ -2293,6 +2381,15 @@ if ('serviceWorker' in navigator) {
}
// ── Health check ───────────────────────────────────────────────
+function formatUptime(seconds) {
+ const d = Math.floor(seconds / 86400);
+ const h = Math.floor((seconds % 86400) / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+ if (d > 0) return `${d}d ${h}h`;
+ if (h > 0) return `${h}h ${m}m`;
+ return `${m}m`;
+}
+
async function checkNodeHealth(nodeId, url) {
const btn = document.getElementById('dp-health-btn');
const dot = document.getElementById('dp-health-dot');
@@ -2301,16 +2398,69 @@ async function checkNodeHealth(nodeId, url) {
label.textContent = '確認中...';
dot.className = 'health-dot';
+ // メトリクスパネルを非表示にリセット
+ const metricsEl = document.getElementById('dp-metrics');
+ metricsEl.classList.remove('visible');
+
+ const node = atlasData.nodes.find(n => n.id === nodeId);
+ const healthUrl = (node && node.health_url) ? node.health_url : null;
+
let result = 'offline';
- try {
- const ctrl = new AbortController();
- const timer = setTimeout(() => ctrl.abort(), 6000);
- const res = await fetch(url, { method: 'HEAD', mode: 'no-cors', signal: ctrl.signal });
- clearTimeout(timer);
- // no-cors returns opaque (type='opaque', status=0) — server was reached
- result = res.type === 'opaque' ? 'limited' : (res.ok ? 'online' : 'offline');
- } catch (e) {
- result = 'offline';
+
+ // /api/health があればメトリクス取得を試みる
+ if (healthUrl) {
+ try {
+ const ctrl = new AbortController();
+ const timer = setTimeout(() => ctrl.abort(), 6000);
+ const res = await fetch(healthUrl, { signal: ctrl.signal });
+ clearTimeout(timer);
+ if (res.ok) {
+ const data = await res.json();
+ result = data.ok ? 'online' : 'limited';
+
+ // メトリクス描画
+ const cpuPct = data.cpu_pct || 0;
+ const memPct = Math.round((data.mem_used_mb / data.mem_total_mb) * 100) || 0;
+ document.getElementById('metric-cpu-val').textContent = `${cpuPct}%`;
+ const cpuBar = document.getElementById('metric-cpu-bar');
+ cpuBar.style.width = `${cpuPct}%`;
+ cpuBar.className = 'metric-bar-fill' + (cpuPct > 80 ? ' crit' : cpuPct > 60 ? ' warn' : '');
+
+ document.getElementById('metric-mem-val').textContent =
+ `${data.mem_used_mb} / ${data.mem_total_mb} MB (${memPct}%)`;
+ const memBar = document.getElementById('metric-mem-bar');
+ memBar.style.width = `${memPct}%`;
+ memBar.className = 'metric-bar-fill' + (memPct > 85 ? ' crit' : memPct > 65 ? ' warn' : '');
+
+ document.getElementById('metric-uptime').textContent =
+ formatUptime(data.uptime_s || 0);
+ document.getElementById('metric-sessions').textContent =
+ `${data.active_sessions || 0} 本`;
+
+ if (node.url) {
+ const openLink = document.getElementById('metric-open-link');
+ openLink.href = node.url;
+ }
+
+ metricsEl.classList.add('visible');
+ if (window.lucide) lucide.createIcons({ nodes: [metricsEl] });
+ } else {
+ result = 'offline';
+ }
+ } catch (e) {
+ result = 'offline';
+ }
+ } else {
+ // /api/health なし → 従来の HEAD リクエスト
+ try {
+ const ctrl = new AbortController();
+ const timer = setTimeout(() => ctrl.abort(), 6000);
+ const res = await fetch(url, { method: 'HEAD', mode: 'no-cors', signal: ctrl.signal });
+ clearTimeout(timer);
+ result = res.type === 'opaque' ? 'limited' : (res.ok ? 'online' : 'offline');
+ } catch (e) {
+ result = 'offline';
+ }
}
const labelMap = { online: 'online', limited: '到達可能', offline: '到達不可' };
@@ -2318,8 +2468,6 @@ async function checkNodeHealth(nodeId, url) {
label.textContent = labelMap[result];
btn.classList.remove('checking');
- // Update node status in data
- const node = atlasData.nodes.find(n => n.id === nodeId);
if (node) {
node.status = result === 'offline' ? 'inactive' : 'active';
saveData();