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 @@
-
- Claude や Gemini のチャットに貼り付けると、AI があなたの環境を即座に把握します
-
@@ -980,11 +1042,25 @@
+
+
+
+
接続タイプ
+
push / calls
+
trigger
+
hosts
+
runs-on
+
dns
+
connects
+
+
@@ -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);