// Igneum miner log intake. POST /api/log with header x-igneum-key and a JSON body // {label, machine, run_id, lines}. Stores one row in Neon table miner_logs. // Zero dependencies: Neon's HTTP SQL endpoint over Node's built-in fetch. // Read back on the Mac with `node tools/logs.mjs`. const MAX_LINES = 256 * 1024; function neon() { const url = process.env.DATABASE_URL; if (!url) throw new Error('DATABASE_URL is not set'); const host = new URL(url).hostname.replace('-pooler', ''); return async (query, params = []) => { const r = await fetch(`https://${host}/sql`, { method: 'POST', headers: { 'Neon-Connection-String': url, 'Content-Type': 'application/json' }, body: JSON.stringify({ query, params }), }); const j = await r.json(); if (!r.ok) throw new Error(j.message || JSON.stringify(j)); return j; }; } async function readBody(req) { if (req.body !== undefined && req.body !== null) { if (typeof req.body === 'string') return JSON.parse(req.body); if (Buffer.isBuffer(req.body)) return JSON.parse(req.body.toString('utf8')); return req.body; } const chunks = []; for await (const c of req) chunks.push(c); return JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'); } const str = (v, max) => (v === undefined || v === null ? '' : String(v)).slice(0, max); export default async function handler(req, res) { res.setHeader('Cache-Control', 'no-store'); if (req.method !== 'POST') { res.setHeader('Allow', 'POST'); return res.status(405).json({ ok: false, error: 'method not allowed' }); } const key = process.env.LOG_INTAKE_KEY; const given = req.headers['x-igneum-key']; if (!key || typeof given !== 'string' || given !== key) { return res.status(401).json({ ok: false, error: 'bad key' }); } let body; try { body = await readBody(req); } catch { return res.status(400).json({ ok: false, error: 'bad json' }); } let lines = str(body.lines, Infinity); const buf = Buffer.from(lines, 'utf8'); if (buf.length > MAX_LINES) lines = buf.subarray(buf.length - MAX_LINES).toString('utf8'); const bytes = Buffer.byteLength(lines, 'utf8'); try { const sql = neon(); const r = await sql( 'INSERT INTO miner_logs (label, machine, run_id, lines, bytes) VALUES ($1, $2, $3, $4, $5) RETURNING id', [str(body.label, 200), str(body.machine, 200), str(body.run_id, 200), lines, bytes], ); return res.status(200).json({ ok: true, id: Number(r.rows[0].id), bytes }); } catch (e) { return res.status(500).json({ ok: false, error: String(e.message || e) }); } }