import 'dotenv/config'; import express from 'express'; import { createServer } from 'node:http'; import { execFile } from 'node:child_process'; import { mkdtemp, writeFile, rm } from 'node:fs/promises'; import { join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { tmpdir } from 'node:os'; import authRouter from './auth.js'; import projectsRouter from './projects.js'; import teacherRouter from './teacher.js'; import { bootstrap, purgeExpiredSessions } from './db.js'; import { attachWsServer } from './ws.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const app = express(); const PORT = process.env.PORT || 3001; app.use(express.json({ limit: '2mb' })); // ─── Auth ──────────────────────────────────────────────── app.use('/api/auth', authRouter); app.use('/api/projects', projectsRouter); app.use('/api/teacher', teacherRouter); // ─── Helpers (Arduino CLI) ─────────────────────────────── const CLI = process.env.ARDUINO_CLI || 'arduino-cli'; function run(args, timeout = 120_000) { return new Promise((resolve, reject) => { execFile(CLI, args, { timeout }, (err, stdout, stderr) => { if (err) { const msg = stderr?.trim() || stdout?.trim() || err.message; reject(new Error(msg)); } else { resolve({ stdout, stderr }); } }); }); } async function writeSketchDir(code) { const dir = await mkdtemp(join(tmpdir(), 'arduino-sketch-')); const sketchDir = join(dir, 'sketch'); const { mkdir } = await import('node:fs/promises'); await mkdir(sketchDir, { recursive: true }); await writeFile(join(sketchDir, 'sketch.ino'), code, 'utf-8'); return { dir, sketchDir }; } async function cleanupDir(dir) { try { await rm(dir, { recursive: true, force: true }); } catch { // best-effort cleanup } } // ─── Routes (Arduino CLI) ──────────────────────────────── app.get('/api/arduino/status', async (_req, res) => { try { const { stdout } = await run(['version', '--format', 'json']); const info = JSON.parse(stdout); res.json({ ok: true, version: info.VersionString || info.version || stdout.trim() }); } catch (err) { res.status(503).json({ ok: false, error: `arduino-cli not available: ${err.message}` }); } }); app.get('/api/arduino/boards', async (_req, res) => { try { const { stdout } = await run(['board', 'list', '--format', 'json']); const raw = JSON.parse(stdout); const ports = Array.isArray(raw) ? raw : (raw.detected_ports || []); const boards = ports .filter(p => p.port) .map(entry => { const port = entry.port?.address || entry.port?.label || entry.address || ''; const matchingBoards = entry.matching_boards || entry.boards || []; const first = matchingBoards[0] || {}; return { port, name: first.name || entry.port?.protocol_label || 'Unknown', fqbn: first.fqbn || '', }; }) .filter(b => b.port); res.json(boards); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/arduino/compile', async (req, res) => { const { code } = req.body; if (!code) return res.status(400).json({ error: 'No code provided' }); let dir; try { ({ dir } = await writeSketchDir(code)); const sketchDir = join(dir, 'sketch'); const { stdout, stderr } = await run([ 'compile', '--fqbn', req.body.fqbn || 'arduino:avr:uno', sketchDir, ]); res.json({ ok: true, output: stdout + stderr }); } catch (err) { res.status(400).json({ ok: false, error: err.message }); } finally { if (dir) cleanupDir(dir); } }); app.post('/api/arduino/upload', async (req, res) => { const { code, port, fqbn } = req.body; if (!code) return res.status(400).json({ error: 'No code provided' }); if (!port) return res.status(400).json({ error: 'No port specified' }); let dir; try { ({ dir } = await writeSketchDir(code)); const sketchDir = join(dir, 'sketch'); const { stdout, stderr } = await run([ 'compile', '--upload', '--fqbn', fqbn || 'arduino:avr:uno', '--port', port, sketchDir, ]); res.json({ ok: true, output: stdout + stderr }); } catch (err) { res.status(400).json({ ok: false, error: err.message }); } finally { if (dir) cleanupDir(dir); } }); // ─── Static (built frontend) ───────────────────────────── // Serve the Vite build output. In dev, the Vite server handles assets and // proxies /api + /ws to this server instead (see vite.config.js). const DIST_DIR = resolve(__dirname, '..', 'dist'); app.use(express.static(DIST_DIR)); // ─── Start ─────────────────────────────────────────────── const server = createServer(app); attachWsServer(server); (async () => { try { await bootstrap(); purgeExpiredSessions().catch(() => { /* ignore */ }); setInterval(() => purgeExpiredSessions().catch(() => {}), 60 * 60 * 1000); } catch (err) { console.error('Database bootstrap failed:', err.message); console.error('Auth + realtime features will be unavailable. Check MYSQL_* env vars.'); } server.listen(PORT, () => { console.log(`esp32block server listening on http://localhost:${PORT}`); console.log(` static: ${DIST_DIR}`); console.log(` arduino CLI: ${CLI}`); }); })();