diff --git a/index.html b/index.html
index 843bd4d..4e79690 100644
--- a/index.html
+++ b/index.html
@@ -933,6 +933,38 @@
+
+
GitHub
+
+
+ Personal Access Token でリポジトリ・連携サービスを検出します(read:user, repo スコープ)
+
+
+
+
+
+
+
+
+
Vercel
+
+
+ API トークンでデプロイ中のプロジェクトを自動検出します
+
+
+
+
+
+
@@ -2008,6 +2040,118 @@ async function runTailscaleScan() {
}
}
+// ── GitHub scan ────────────────────────────────────────────────
+async function runGithubScan() {
+ const token = document.getElementById('github-token-input').value.trim();
+ const org = document.getElementById('github-org-input').value.trim();
+ const statusEl = document.getElementById('github-scan-status');
+ const btn = document.getElementById('btnGithubScan');
+ if (!token) { showToast('GitHub トークンを入力してください'); return; }
+
+ statusEl.className = 'scan-status visible';
+ statusEl.textContent = 'スキャン中...';
+ btn.disabled = true;
+
+ try {
+ const apiBase = 'https://api.soar-enrich.com/brain/api';
+ const url = `${apiBase}/atlas/github-scan?token=${encodeURIComponent(token)}${org ? '&org=' + encodeURIComponent(org) : ''}`;
+ const res = await fetch(url);
+ if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || `HTTP ${res.status}`); }
+ const repos = await res.json();
+
+ let added = 0;
+ // Add GitHub org/user as cloud node if not present
+ const githubId = 'github';
+ repos.forEach(repo => {
+ const nodeId = `gh-${repo.name}`;
+ if (atlasData.nodes.find(n => n.id === nodeId)) return;
+ atlasData.nodes.push({
+ id: nodeId,
+ label: repo.name,
+ type: 'app',
+ description: repo.description || `GitHub リポジトリ: ${repo.full_name}`,
+ url: repo.html_url,
+ status: repo.archived ? 'inactive' : 'active',
+ parent: githubId,
+ });
+ // edge: github → vercel if homepage looks like vercel
+ if (repo.homepage && repo.homepage.includes('vercel.app')) {
+ const vercelId = 'vercel';
+ if (!atlasData.edges.find(e => e.from === githubId && e.to === vercelId)) {
+ // already exists globally, skip per-repo
+ }
+ atlasData.edges.push({ from: nodeId, to: vercelId, type: 'hosts', label: 'deploy' });
+ }
+ added++;
+ });
+
+ saveData();
+ buildFilterBar();
+ initGraph();
+ setTimeout(fitGraph, 500);
+ statusEl.className = 'scan-status visible ok';
+ statusEl.textContent = `${repos.length} リポジトリ検出 — ${added} 件追加`;
+ showToast(`GitHub: ${added} リポジトリを追加しました`);
+ } catch (e) {
+ statusEl.className = 'scan-status visible err';
+ statusEl.textContent = `エラー: ${e.message}`;
+ } finally {
+ btn.disabled = false;
+ }
+}
+
+// ── Vercel scan ─────────────────────────────────────────────────
+async function runVercelScan() {
+ const token = document.getElementById('vercel-token-input').value.trim();
+ const statusEl = document.getElementById('vercel-scan-status');
+ const btn = document.getElementById('btnVercelScan');
+ if (!token) { showToast('Vercel トークンを入力してください'); return; }
+
+ statusEl.className = 'scan-status visible';
+ statusEl.textContent = 'スキャン中...';
+ btn.disabled = true;
+
+ try {
+ const apiBase = 'https://api.soar-enrich.com/brain/api';
+ const res = await fetch(`${apiBase}/atlas/vercel-scan?token=${encodeURIComponent(token)}`);
+ if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || `HTTP ${res.status}`); }
+ const data = await res.json();
+ const projects = data.projects || [];
+
+ let added = 0;
+ projects.forEach(proj => {
+ const nodeId = `vercel-${proj.name}`;
+ if (atlasData.nodes.find(n => n.id === nodeId)) return;
+ const prodUrl = proj.targets?.production?.alias?.[0]
+ ? `https://${proj.targets.production.alias[0]}`
+ : null;
+ atlasData.nodes.push({
+ id: nodeId,
+ label: proj.name,
+ type: 'app',
+ description: `Vercel プロジェクト: ${proj.framework || '静的'}`,
+ url: prodUrl,
+ status: 'active',
+ parent: 'vercel',
+ });
+ added++;
+ });
+
+ saveData();
+ buildFilterBar();
+ initGraph();
+ setTimeout(fitGraph, 500);
+ statusEl.className = 'scan-status visible ok';
+ statusEl.textContent = `${projects.length} プロジェクト検出 — ${added} 件追加`;
+ showToast(`Vercel: ${added} プロジェクトを追加しました`);
+ } catch (e) {
+ statusEl.className = 'scan-status visible err';
+ statusEl.textContent = `エラー: ${e.message}`;
+ } finally {
+ btn.disabled = false;
+ }
+}
+
// ── Event bindings ─────────────────────────────────────────────
function bindEvents() {
document.getElementById('btn-fit').addEventListener('click', fitGraph);
@@ -2080,6 +2224,9 @@ function bindEvents() {
applyMonitorSetting(parseInt(e.target.value, 10));
});
+ document.getElementById('btnGithubScan').addEventListener('click', runGithubScan);
+ document.getElementById('btnVercelScan').addEventListener('click', runVercelScan);
+
// File input (wizard + settings import)
document.getElementById('fileInput').addEventListener('change', e => {
const isWizard = !document.getElementById('wizard-overlay').hasAttribute('hidden');