feat: health check / edge legend / keyboard shortcuts / remove AI hint

This commit is contained in:
posimai 2026-03-29 17:14:09 +09:00
parent 0252ee9315
commit db605440af
1 changed files with 152 additions and 4 deletions

View File

@ -725,6 +725,65 @@
color: #F87171; 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 label tooltip ────────────────────────────── */
#edge-tooltip { #edge-tooltip {
position: fixed; position: fixed;
@ -836,7 +895,13 @@
<div class="detail-body"> <div class="detail-body">
<div class="detail-desc" id="dp-desc"></div> <div class="detail-desc" id="dp-desc"></div>
<div class="detail-url" id="dp-url"></div> <div class="detail-url" id="dp-url"></div>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<div id="dp-status"></div> <div id="dp-status"></div>
<button id="dp-health-btn" style="display:none">
<span class="health-dot" id="dp-health-dot"></span>
<span id="dp-health-label">接続確認</span>
</button>
</div>
<div id="dp-connections"> <div id="dp-connections">
<div class="detail-section-label">接続</div> <div class="detail-section-label">接続</div>
<div class="detail-connections" id="dp-conn-list"></div> <div class="detail-connections" id="dp-conn-list"></div>
@ -961,9 +1026,6 @@
</button> </button>
</div> </div>
</div> </div>
<div style="padding:8px 16px;font-size:11px;color:var(--text3);border-bottom:1px solid var(--border);line-height:1.6">
Claude や Gemini のチャットに貼り付けると、AI があなたの環境を即座に把握します
</div>
<pre id="ai-context-text"></pre> <pre id="ai-context-text"></pre>
</div> </div>
</div> </div>
@ -980,11 +1042,25 @@
<button class="tb-btn" id="btn-add-node" aria-label="ノードを追加" title="ノードを追加"> <button class="tb-btn" id="btn-add-node" aria-label="ノードを追加" title="ノードを追加">
<i data-lucide="plus" style="width:17px;height:17px;stroke-width:1.75"></i> <i data-lucide="plus" style="width:17px;height:17px;stroke-width:1.75"></i>
</button> </button>
<button class="tb-btn" id="btn-legend" aria-label="凡例" title="接続タイプ凡例">
<i data-lucide="info" style="width:15px;height:15px;stroke-width:1.75"></i>
</button>
<button class="tb-btn" id="btn-fit" aria-label="画面に合わせる" title="フィット"> <button class="tb-btn" id="btn-fit" aria-label="画面に合わせる" title="フィット">
<i data-lucide="maximize-2" style="width:15px;height:15px;stroke-width:1.75"></i> <i data-lucide="maximize-2" style="width:15px;height:15px;stroke-width:1.75"></i>
</button> </button>
</div> </div>
<!-- Edge type legend -->
<div id="edge-legend" role="complementary" aria-label="接続タイプ凡例">
<div style="font-size:10px;font-weight:600;color:var(--text3);letter-spacing:.05em;margin-bottom:2px">接続タイプ</div>
<div class="legend-row"><span class="legend-line" style="background:rgba(34,211,238,0.5)"></span>push / calls</div>
<div class="legend-row"><span class="legend-line" style="background:rgba(192,132,252,0.5)"></span>trigger</div>
<div class="legend-row"><span class="legend-line" style="background:rgba(74,222,128,0.4)"></span>hosts</div>
<div class="legend-row"><span class="legend-line" style="background:rgba(251,146,60,0.4)"></span>runs-on</div>
<div class="legend-row"><span class="legend-line" style="background:rgba(129,140,248,0.5)"></span>dns</div>
<div class="legend-row"><span class="legend-line" style="background:rgba(255,255,255,0.15)"></span>connects</div>
</div>
<!-- Edge label tooltip --> <!-- Edge label tooltip -->
<div id="edge-tooltip"></div> <div id="edge-tooltip"></div>
@ -1324,6 +1400,20 @@ function showDetail(id, simNodes, simEdges) {
const s = node.status || 'unknown'; const s = node.status || 'unknown';
statusEl.innerHTML = `<span class="status-badge ${s}"><span class="status-dot"></span>${s}</span>`; statusEl.innerHTML = `<span class="status-badge ${s}"><span class="status-dot"></span>${s}</span>`;
// 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 // Connections
const connList = document.getElementById('dp-conn-list'); const connList = document.getElementById('dp-conn-list');
const related = atlasData.edges.filter(e => e.from === id || e.to === id); const related = atlasData.edges.filter(e => e.from === id || e.to === id);
@ -1655,6 +1745,28 @@ function bindEvents() {
document.getElementById('fileInput').click(); 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', () => { window.addEventListener('resize', () => {
if (simulation) { if (simulation) {
const svg = d3.select('#graph-svg'); const svg = d3.select('#graph-svg');
@ -1672,6 +1784,42 @@ if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js'); 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 =
`<span class="status-badge ${node.status}"><span class="status-dot"></span>${node.status}</span>`;
}
}
// ── URL Sharing ──────────────────────────────────────────────── // ── URL Sharing ────────────────────────────────────────────────
function generateShareURL() { function generateShareURL() {
const json = JSON.stringify(atlasData); const json = JSON.stringify(atlasData);