diff --git a/docs/master-architecture.md b/docs/master-architecture.md index a23b87c9..46388611 100644 --- a/docs/master-architecture.md +++ b/docs/master-architecture.md @@ -37,6 +37,9 @@ ║ diff / clean / timer / digest / think / site ║ ║ events / maps / tech-events / analytics / roadmap ║ ║ ║ +║ 【セルフホスト】posimai-dev(Ubuntu PC / Tailscale) ║ +║ https://ubuntu-pc-pc-mkm21cz79ys4.tail72e846.ts.net:3333║ +║ ║ ║ 【計画中】*.posimai.soar-enrich.com ワイルドカード DNS ║ ║ → Passkey の rpID 問題を解決・Eiji に依頼予定 ║ ╚══════════════════════════════════════════════════════════╝ diff --git a/posimai-dev/index.html b/posimai-dev/index.html index 8be60cad..fa996ddb 100644 --- a/posimai-dev/index.html +++ b/posimai-dev/index.html @@ -36,9 +36,11 @@ :root { --accent: #A78BFA; --accent-dim: rgba(167, 139, 250, 0.15); + --dev-bg: #0C1221; } [data-theme="light"] { --accent: #7C3AED; + --dev-bg: #0C1221; } * { box-sizing: border-box; margin: 0; padding: 0; } @@ -48,7 +50,7 @@ flex-direction: column; height: 100dvh; overflow: hidden; - background: var(--bg); + background: var(--dev-bg); } .header { @@ -107,6 +109,10 @@ overflow: hidden; padding: 12px; min-height: 0; + background: + radial-gradient(ellipse at 15% 60%, rgba(34, 211, 238, 0.07) 0%, transparent 55%), + radial-gradient(ellipse at 85% 25%, rgba(167, 139, 250, 0.07) 0%, transparent 55%), + var(--dev-bg); } #terminal-container .xterm { @@ -184,8 +190,9 @@ fontSize: 14, lineHeight: 1.4, cursorBlink: true, + allowTransparency: true, theme: { - background: '#0D0D0D', + background: 'rgba(12, 18, 33, 0.0)', foreground: '#F3F4F6', cursor: '#A78BFA', selectionBackground: 'rgba(167, 139, 250, 0.3)', diff --git a/posimai-dev/posimai-dev.service b/posimai-dev/posimai-dev.service new file mode 100644 index 00000000..f54b62f4 --- /dev/null +++ b/posimai-dev/posimai-dev.service @@ -0,0 +1,15 @@ +[Unit] +Description=posimai-dev portal +After=network.target tailscaled.service + +[Service] +Type=simple +User=ubuntu-pc +WorkingDirectory=/home/ubuntu-pc/posimai-project/posimai-dev +ExecStart=/home/ubuntu-pc/.npm-global/bin/node server.js +Restart=on-failure +RestartSec=5 +Environment=PATH=/home/ubuntu-pc/.npm-global/bin:/usr/local/bin:/usr/bin:/bin + +[Install] +WantedBy=multi-user.target diff --git a/server.js b/server.js index d902a6d8..7a868c98 100644 --- a/server.js +++ b/server.js @@ -2141,6 +2141,154 @@ ${excerpt} } }); + // ── Atlas: GitHub scan proxy ─────────────────────────────────── + r.get('/atlas/github-scan', (req, res) => { + const token = req.query.token; + const org = req.query.org || ''; + if (!token) return res.status(400).json({ error: 'token required' }); + + const https = require('https'); + + function ghRequest(path, cb) { + const options = { + hostname: 'api.github.com', + path, + method: 'GET', + family: 4, + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + 'User-Agent': 'Posimai-Atlas/1.0', + 'X-GitHub-Api-Version': '2022-11-28', + }, + timeout: 12000, + }; + const r2 = https.request(options, (resp) => { + let body = ''; + resp.on('data', chunk => { body += chunk; }); + resp.on('end', () => cb(null, resp.statusCode, body)); + }); + r2.on('timeout', () => { r2.destroy(); cb(new Error('Timeout')); }); + r2.on('error', cb); + r2.end(); + } + + const orgPath = org ? `/orgs/${encodeURIComponent(org)}/repos?per_page=100&sort=updated` : null; + const userPath = `/user/repos?per_page=100&sort=updated&affiliation=owner`; + + function handleResult(status, body) { + if (status !== 200) return res.status(status).json({ error: body }); + try { res.json(JSON.parse(body)); } + catch (e) { res.status(500).json({ error: 'Invalid JSON' }); } + } + + if (orgPath) { + ghRequest(orgPath, (err, status, body) => { + if (err) return res.status(500).json({ error: err.message }); + // If org not accessible, fall back to user repos + if (status === 404 || status === 403) { + ghRequest(userPath, (err2, status2, body2) => { + if (err2) return res.status(500).json({ error: err2.message }); + // Signal to client that we fell back + if (status2 === 200) { + try { + const data = JSON.parse(body2); + return res.json({ repos: data, fallback: true }); + } catch (e) { return res.status(500).json({ error: 'Invalid JSON' }); } + } + handleResult(status2, body2); + }); + } else { + handleResult(status, body); + } + }); + } else { + ghRequest(userPath, (err, status, body) => { + if (err) return res.status(500).json({ error: err.message }); + handleResult(status, body); + }); + } + }); + + // ── Atlas: Vercel scan proxy ─────────────────────────────────── + r.get('/atlas/vercel-scan', (req, res) => { + const token = req.query.token; + if (!token) return res.status(400).json({ error: 'token required' }); + + const https = require('https'); + const options = { + hostname: 'api.vercel.com', + path: '/v9/projects?limit=100', + method: 'GET', + family: 4, + headers: { + Authorization: `Bearer ${token}`, + 'User-Agent': 'Posimai-Atlas/1.0', + }, + timeout: 12000, + }; + + const req2 = https.request(options, (r2) => { + let body = ''; + r2.on('data', chunk => { body += chunk; }); + r2.on('end', () => { + if (r2.statusCode !== 200) return res.status(r2.statusCode).json({ error: body }); + try { res.json(JSON.parse(body)); } + catch (e) { res.status(500).json({ error: 'Invalid JSON' }); } + }); + }); + req2.on('timeout', () => { req2.destroy(); res.status(500).json({ error: 'Timeout' }); }); + req2.on('error', (e) => { res.status(500).json({ error: e.message, code: e.code }); }); + req2.end(); + }); + + // ── Atlas: Tailscale scan proxy ──────────────────────────────── + r.get('/atlas/tailscale-scan', (req, res) => { + const token = req.query.token; + if (!token) return res.status(400).json({ error: 'token required' }); + + const https = require('https'); + const options = { + hostname: 'api.tailscale.com', + path: '/api/v2/tailnet/-/devices', + method: 'GET', + family: 4, // force IPv4; container IPv6 to tailscale times out + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + 'User-Agent': 'Posimai-Atlas/1.0', + }, + timeout: 12000, + }; + + const req2 = https.request(options, (r2) => { + let body = ''; + r2.on('data', chunk => { body += chunk; }); + r2.on('end', () => { + if (r2.statusCode !== 200) { + return res.status(r2.statusCode).json({ error: body }); + } + try { + res.json(JSON.parse(body)); + } catch (e) { + res.status(500).json({ error: 'Invalid JSON from Tailscale' }); + } + }); + }); + + req2.on('timeout', () => { + req2.destroy(); + res.status(500).json({ error: 'Request timed out' }); + }); + + req2.on('error', (e) => { + console.error('[atlas/tailscale-scan] error:', e.code, e.message); + res.status(500).json({ error: e.message, code: e.code }); + }); + + req2.end(); + }); + return r; }