feat: GitHub + Vercel auto-scan, fix XServer/Cloudflare data
This commit is contained in:
parent
81c09e9819
commit
d39c0d58fd
147
index.html
147
index.html
|
|
@ -933,6 +933,38 @@
|
||||||
<div id="tailscale-scan-status" class="scan-status"></div>
|
<div id="tailscale-scan-status" class="scan-status"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="settings-group-label">GitHub</div>
|
||||||
|
<div class="settings-item" style="flex-direction:column;align-items:flex-start;gap:8px">
|
||||||
|
<div style="font-size:11px;color:var(--text3);line-height:1.5">
|
||||||
|
Personal Access Token でリポジトリ・連携サービスを検出します(read:user, repo スコープ)
|
||||||
|
</div>
|
||||||
|
<input class="form-input" id="github-token-input" type="password"
|
||||||
|
placeholder="ghp_..." style="font-size:12px">
|
||||||
|
<input class="form-input" id="github-org-input" type="text"
|
||||||
|
placeholder="org 名(例: posimai)" style="font-size:12px">
|
||||||
|
<button class="btn btn-secondary" id="btnGithubScan" style="width:100%;font-size:12px">
|
||||||
|
<i data-lucide="github" style="width:13px;height:13px;stroke-width:1.75"></i>
|
||||||
|
GitHub スキャン
|
||||||
|
</button>
|
||||||
|
<div id="github-scan-status" class="scan-status"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="settings-group-label">Vercel</div>
|
||||||
|
<div class="settings-item" style="flex-direction:column;align-items:flex-start;gap:8px">
|
||||||
|
<div style="font-size:11px;color:var(--text3);line-height:1.5">
|
||||||
|
API トークンでデプロイ中のプロジェクトを自動検出します
|
||||||
|
</div>
|
||||||
|
<input class="form-input" id="vercel-token-input" type="password"
|
||||||
|
placeholder="vercel token..." style="font-size:12px">
|
||||||
|
<button class="btn btn-secondary" id="btnVercelScan" style="width:100%;font-size:12px">
|
||||||
|
<i data-lucide="triangle" style="width:13px;height:13px;stroke-width:1.75"></i>
|
||||||
|
Vercel スキャン
|
||||||
|
</button>
|
||||||
|
<div id="vercel-scan-status" class="scan-status"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<div class="overlay" id="overlay" aria-hidden="true"></div>
|
<div class="overlay" id="overlay" aria-hidden="true"></div>
|
||||||
|
|
@ -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 ─────────────────────────────────────────────
|
// ── Event bindings ─────────────────────────────────────────────
|
||||||
function bindEvents() {
|
function bindEvents() {
|
||||||
document.getElementById('btn-fit').addEventListener('click', fitGraph);
|
document.getElementById('btn-fit').addEventListener('click', fitGraph);
|
||||||
|
|
@ -2080,6 +2224,9 @@ function bindEvents() {
|
||||||
applyMonitorSetting(parseInt(e.target.value, 10));
|
applyMonitorSetting(parseInt(e.target.value, 10));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('btnGithubScan').addEventListener('click', runGithubScan);
|
||||||
|
document.getElementById('btnVercelScan').addEventListener('click', runVercelScan);
|
||||||
|
|
||||||
// File input (wizard + settings import)
|
// File input (wizard + settings import)
|
||||||
document.getElementById('fileInput').addEventListener('change', e => {
|
document.getElementById('fileInput').addEventListener('change', e => {
|
||||||
const isWizard = !document.getElementById('wizard-overlay').hasAttribute('hidden');
|
const isWizard = !document.getElementById('wizard-overlay').hasAttribute('hidden');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue