diff --git a/index.html b/index.html index 6cba136..ed55036 100644 --- a/index.html +++ b/index.html @@ -725,6 +725,65 @@ color: #F87171; } + /* ── Health check button ───────────────────────────── */ + #dp-health-btn { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + padding: 5px 10px; + border-radius: 8px; + border: 1px solid var(--border); + background: transparent; + color: var(--text3); + cursor: pointer; + transition: all 0.15s; + width: fit-content; + } + #dp-health-btn:hover { border-color: var(--accent); color: var(--accent); } + #dp-health-btn.checking { opacity: 0.6; pointer-events: none; } + .health-dot { + width: 6px; height: 6px; + border-radius: 50%; + background: var(--text3); + flex-shrink: 0; + } + .health-dot.online { background: #4ADE80; box-shadow: 0 0 6px #4ADE8088; } + .health-dot.offline { background: #F87171; } + .health-dot.limited { background: #FB923C; } + + /* ── Edge legend ────────────────────────────────────── */ + #edge-legend { + position: fixed; + bottom: max(76px, calc(env(safe-area-inset-bottom) + 76px)); + right: 16px; + z-index: 18; + background: rgba(12,18,33,0.88); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--border); + border-radius: 10px; + padding: 10px 12px; + display: none; + flex-direction: column; + gap: 5px; + min-width: 140px; + } + [data-theme="light"] #edge-legend { background: rgba(239,246,255,0.92); } + #edge-legend.open { display: flex; } + .legend-row { + display: flex; + align-items: center; + gap: 8px; + font-size: 11px; + color: var(--text2); + } + .legend-line { + width: 22px; height: 2px; + border-radius: 1px; + flex-shrink: 0; + } + /* ── Edge label tooltip ────────────────────────────── */ #edge-tooltip { position: fixed; @@ -836,7 +895,13 @@
-
+
+
+ +
@@ -961,9 +1026,6 @@
-
- Claude や Gemini のチャットに貼り付けると、AI があなたの環境を即座に把握します -

     
 
@@ -980,11 +1042,25 @@
     
+    
     
 
 
+
+
+
 
 
@@ -1324,6 +1400,20 @@ function showDetail(id, simNodes, simEdges) { const s = node.status || 'unknown'; statusEl.innerHTML = `${s}`; + // Health check button + const healthBtn = document.getElementById('dp-health-btn'); + const healthDot = document.getElementById('dp-health-dot'); + const healthLabel = document.getElementById('dp-health-label'); + if (node.url && !isReadOnly) { + healthBtn.style.display = 'flex'; + healthBtn.classList.remove('checking'); + healthDot.className = 'health-dot'; + healthLabel.textContent = '接続確認'; + healthBtn.onclick = () => checkNodeHealth(node.id, node.url); + } else { + healthBtn.style.display = 'none'; + } + // Connections const connList = document.getElementById('dp-conn-list'); const related = atlasData.edges.filter(e => e.from === id || e.to === id); @@ -1655,6 +1745,28 @@ function bindEvents() { document.getElementById('fileInput').click(); }); + // Legend toggle + document.getElementById('btn-legend').addEventListener('click', () => { + document.getElementById('edge-legend').classList.toggle('open'); + }); + document.addEventListener('click', e => { + if (!e.target.closest('#edge-legend') && !e.target.closest('#btn-legend')) { + document.getElementById('edge-legend').classList.remove('open'); + } + }); + + // Keyboard shortcuts + document.addEventListener('keydown', e => { + if (e.target.matches('input, textarea, select')) return; + if (e.key === 'Escape') { + if (document.getElementById('modal-overlay').classList.contains('open')) { closeModal('modal-overlay'); return; } + if (document.getElementById('edge-modal-overlay').classList.contains('open')) { closeModal('edge-modal-overlay'); return; } + if (document.getElementById('ai-modal-overlay').classList.contains('open')) { closeModal('ai-modal-overlay'); return; } + closeDetail(); + } + if (e.key === 'f' || e.key === 'F') fitGraph(); + }); + window.addEventListener('resize', () => { if (simulation) { const svg = d3.select('#graph-svg'); @@ -1672,6 +1784,42 @@ if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js'); } +// ── Health check ─────────────────────────────────────────────── +async function checkNodeHealth(nodeId, url) { + const btn = document.getElementById('dp-health-btn'); + const dot = document.getElementById('dp-health-dot'); + const label = document.getElementById('dp-health-label'); + btn.classList.add('checking'); + label.textContent = '確認中...'; + dot.className = 'health-dot'; + + 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'; + } + + const labelMap = { online: 'online', limited: '到達可能', offline: '到達不可' }; + dot.className = `health-dot ${result}`; + 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(); + document.getElementById('dp-status').innerHTML = + `${node.status}`; + } +} + // ── URL Sharing ──────────────────────────────────────────────── function generateShareURL() { const json = JSON.stringify(atlasData);