feat: initial runner dashboard

Node 20 / no-framework HTTP service.
Routes: /healthz (no auth), /api/runners (proxy to GitHub), / (dashboard UI).
Dark theme, Basic Auth, 15s poll, stale-data strip, token-missing empty state.

Agent-Session: runner-dashboard-deploy-2026-06-19
This commit is contained in:
peptiderepo
2026-06-19 09:02:59 +00:00
commit 34858a3088
5 changed files with 615 additions and 0 deletions

183
server.js Normal file
View File

@@ -0,0 +1,183 @@
/**
* Runner Dashboard — HTTP server
*
* What: Serves the CI runner status dashboard for peptide-repo org.
* Who triggers: Node process (started by Docker CMD or local `node server.js`).
* Deps: Node 20 built-ins only (http, https, crypto, fs, path).
*
* Routes:
* GET /healthz → 200 "ok" (no auth, no GitHub call — for Coolify health check)
* GET /api/runners → proxied GitHub runners JSON (auth required)
* GET / → dashboard HTML (auth required)
*
* Env:
* PORT default 3000
* DASH_USER / DASH_PASS HTTP Basic Auth credentials
* GITHUB_RUNNERS_READ_TOKEN fine-grained PAT for org runners API
*/
'use strict';
const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');
const PORT = parseInt(process.env.PORT || '3000', 10);
const DASH_USER = process.env.DASH_USER || '';
const DASH_PASS = process.env.DASH_PASS || '';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Parse and validate HTTP Basic Auth header.
* @param {http.IncomingMessage} req
* @returns {boolean}
*/
function isAuthorised(req) {
if (!DASH_USER || !DASH_PASS) return false;
const header = req.headers['authorization'] || '';
if (!header.startsWith('Basic ')) return false;
const [user, pass] = Buffer.from(header.slice(6), 'base64').toString().split(':');
return user === DASH_USER && pass === DASH_PASS;
}
/**
* Send a 401 Unauthorized response demanding Basic Auth.
* @param {http.ServerResponse} res
*/
function requireAuth(res) {
res.writeHead(401, {
'WWW-Authenticate': 'Basic realm="Runner Dashboard"',
'Content-Type': 'text/plain',
});
res.end('Unauthorized');
}
/**
* Fetch the GitHub org runners list server-side.
* Reads token from process.env at call time (never cached at module level).
* @returns {Promise<{status:number, body:string}>}
*/
function fetchRunners() {
return new Promise((resolve) => {
const token = process.env.GITHUB_RUNNERS_READ_TOKEN;
if (!token) {
resolve({ status: 200, body: JSON.stringify({ error: 'token_missing' }) });
return;
}
const options = {
hostname: 'api.github.com',
path: '/orgs/peptide-repo/actions/runners',
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github+json',
'User-Agent': 'runner-dashboard/1.0',
'X-GitHub-Api-Version': '2022-11-28',
},
};
const req = https.request(options, (ghRes) => {
let raw = '';
ghRes.on('data', (chunk) => { raw += chunk; });
ghRes.on('end', () => {
if (ghRes.statusCode !== 200) {
const body = JSON.stringify({ error: 'api_error', status: ghRes.statusCode });
resolve({ status: 200, body });
return;
}
try {
const parsed = JSON.parse(raw);
resolve({ status: 200, body: JSON.stringify(parsed.runners || parsed) });
} catch {
resolve({ status: 200, body: JSON.stringify({ error: 'api_error', status: ghRes.statusCode }) });
}
});
});
req.on('error', () => {
resolve({ status: 200, body: JSON.stringify({ error: 'api_error', status: 0 }) });
});
req.setTimeout(10000, () => {
req.destroy();
resolve({ status: 200, body: JSON.stringify({ error: 'api_error', status: 0 }) });
});
req.end();
});
}
// ---------------------------------------------------------------------------
// Request handler
// ---------------------------------------------------------------------------
/**
* Main HTTP request handler.
* @param {http.IncomingMessage} req
* @param {http.ServerResponse} res
*/
async function handler(req, res) {
const url = (req.url || '/').split('?')[0];
// Health check — no auth, no GitHub call
if (url === '/healthz') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
return;
}
// All other routes require Basic Auth
if (!isAuthorised(req)) {
requireAuth(res);
return;
}
if (url === '/api/runners') {
const { status, body } = await fetchRunners();
res.writeHead(status, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(body);
return;
}
if (url === '/') {
const htmlPath = path.join(__dirname, 'index.html');
try {
const html = fs.readFileSync(htmlPath, 'utf8');
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
} catch {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('index.html not found');
}
return;
}
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not found');
}
// ---------------------------------------------------------------------------
// Start server
// ---------------------------------------------------------------------------
const server = http.createServer((req, res) => {
handler(req, res).catch((err) => {
console.error('Unhandled error:', err);
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal server error');
}
});
});
server.listen(PORT, '0.0.0.0', () => {
console.log(`Runner dashboard listening on port ${PORT}`);
});