import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' function healthCheckPlugin() { return { name: 'health-check-proxy', configureServer(server) { server.middlewares.use('/api/health', async (req, res) => { const params = new URL(req.url, 'http://localhost').searchParams; const url = params.get('url'); const mode = params.get('mode'); // 'tcp' for raw TCP port check if (!url) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Missing url param' })); return; } const start = Date.now(); const parsedUrl = new URL(url); try { if (mode === 'tcp') { // TCP socket connect check for non-HTTP services (e.g. Wyoming) const { default: net } = await import('net'); await new Promise((resolve, reject) => { const socket = net.createConnection( { host: parsedUrl.hostname, port: parseInt(parsedUrl.port), timeout: 5000 }, () => { socket.destroy(); resolve(); } ); socket.on('error', reject); socket.on('timeout', () => { socket.destroy(); reject(new Error('timeout')); }); }); } else { // HTTP/HTTPS health check const { default: https } = await import('https'); const { default: http } = await import('http'); const client = parsedUrl.protocol === 'https:' ? https : http; await new Promise((resolve, reject) => { const reqObj = client.get(url, { rejectUnauthorized: false, timeout: 5000 }, (resp) => { resp.resume(); resolve(); }); reqObj.on('error', reject); reqObj.on('timeout', () => { reqObj.destroy(); reject(new Error('timeout')); }); }); } res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'online', responseTime: Date.now() - start })); } catch { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'offline', responseTime: null })); } }); // Service restart — runs launchctl or docker restart server.middlewares.use('/api/service/restart', async (req, res) => { if (req.method === 'OPTIONS') { res.writeHead(204, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST', 'Access-Control-Allow-Headers': 'Content-Type' }); res.end(); return; } if (req.method !== 'POST') { res.writeHead(405); res.end(); return; } try { const chunks = []; for await (const chunk of req) chunks.push(chunk); const { type, id } = JSON.parse(Buffer.concat(chunks).toString()); if (!type || !id) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: 'Missing type or id' })); return; } // Whitelist valid service IDs to prevent command injection const ALLOWED_LAUNCHD = [ 'gui/501/com.homeai.ollama', 'gui/501/com.homeai.openclaw', 'gui/501/com.homeai.openclaw-bridge', 'gui/501/com.homeai.wyoming-stt', 'gui/501/com.homeai.wyoming-tts', 'gui/501/com.homeai.wyoming-satellite', 'gui/501/com.homeai.character-dashboard', ]; const ALLOWED_DOCKER = [ 'homeai-open-webui', 'homeai-uptime-kuma', 'homeai-n8n', 'homeai-code-server', ]; let cmd; if (type === 'launchd' && ALLOWED_LAUNCHD.includes(id)) { cmd = ['launchctl', 'kickstart', '-k', id]; } else if (type === 'docker' && ALLOWED_DOCKER.includes(id)) { cmd = ['docker', 'restart', id]; } else { res.writeHead(403, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: 'Service not in allowed list' })); return; } const { execFile } = await import('child_process'); const { promisify } = await import('util'); const execFileAsync = promisify(execFile); const { stdout, stderr } = await execFileAsync(cmd[0], cmd.slice(1), { timeout: 30000 }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, stdout: stdout.trim(), stderr: stderr.trim() })); } catch (err) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: err.message })); } }); // TTS preview proxy — forwards POST to OpenClaw bridge, returns audio server.middlewares.use('/api/tts', async (req, res) => { if (req.method !== 'POST') { res.writeHead(405); res.end(); return; } try { const { default: http } = await import('http'); const chunks = []; for await (const chunk of req) chunks.push(chunk); const body = Buffer.concat(chunks); await new Promise((resolve, reject) => { const proxyReq = http.request( 'http://localhost:8081/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': body.length }, timeout: 30000 }, (proxyRes) => { res.writeHead(proxyRes.statusCode, { 'Content-Type': proxyRes.headers['content-type'] || 'audio/wav', }); proxyRes.pipe(res); proxyRes.on('end', resolve); } ); proxyReq.on('error', reject); proxyReq.on('timeout', () => { proxyReq.destroy(); reject(new Error('timeout')); }); proxyReq.write(body); proxyReq.end(); }); } catch { res.writeHead(502, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'TTS bridge unreachable' })); } }); }, }; } // https://vite.dev/config/ export default defineConfig({ plugins: [ healthCheckPlugin(), tailwindcss(), react(), ], server: { host: '0.0.0.0', }, })