diff --git a/index.html b/index.html index 5306b37..ec1f6e4 100644 --- a/index.html +++ b/index.html @@ -3,177 +3,250 @@ -Dashboard +Watchboard +
-
-
+
+
STATUS
+
+
+
Pulse-Ox
diff --git a/server.js b/server.js index e7a1fe6..0ea89e3 100644 --- a/server.js +++ b/server.js @@ -17,6 +17,12 @@ function saveAlerts(alerts) { fs.writeFileSync(ALERTS_FILE, JSON.stringify(alerts, null, 2)); } +const http = require('http'); + +// Camera proxy config +const HA_URL = 'http://192.168.1.252:8123'; +const HA_TOKEN = process.env.HA_TOKEN || ''; + // SSE clients const sseClients = new Set(); @@ -52,6 +58,56 @@ app.post('/api/alerts', (req, res) => { res.status(201).json(alert); }); +// Camera proxy - snapshot from HA +app.get('/api/cam/pulse-ox', (req, res) => { + const haReq = http.get(`${HA_URL}/api/camera_proxy/camera.pulse_ox_live_view`, { + headers: { 'Authorization': `Bearer ${HA_TOKEN}` } + }, (haRes) => { + res.set('Content-Type', haRes.headers['content-type'] || 'image/jpeg'); + res.set('Cache-Control', 'no-store'); + haRes.pipe(res); + }); + haReq.on('error', () => res.status(502).end()); + haReq.setTimeout(5000, () => { haReq.destroy(); res.status(504).end(); }); +}); + +// Camera proxy - MJPEG stream from HA +app.get('/api/cam/pulse-ox/stream', (req, res) => { + const haReq = http.get(`${HA_URL}/api/camera_proxy_stream/camera.pulse_ox_live_view`, { + headers: { 'Authorization': `Bearer ${HA_TOKEN}` } + }, (haRes) => { + res.set('Content-Type', haRes.headers['content-type'] || 'multipart/x-mixed-replace; boundary=frame'); + res.set('Cache-Control', 'no-store'); + haRes.pipe(res); + req.on('close', () => haRes.destroy()); + }); + haReq.on('error', () => res.status(502).end()); +}); + +// Toggle done +app.patch('/api/alerts/:id/done', (req, res) => { + const alerts = loadAlerts(); + const alert = alerts.find(a => a.id === req.params.id); + if (!alert) return res.status(404).json({ error: 'not found' }); + alert.done = !!req.body.done; + saveAlerts(alerts); + res.json(alert); +}); + +// Delete alert +app.delete('/api/alerts/:id', (req, res) => { + const alerts = loadAlerts(); + const idx = alerts.findIndex(a => a.id === req.params.id); + if (idx === -1) return res.status(404).json({ error: 'not found' }); + alerts.splice(idx, 1); + saveAlerts(alerts); + // Notify SSE clients of removal + for (const client of sseClients) { + client.write(`data: ${JSON.stringify({ type: 'remove', id: req.params.id })}\n\n`); + } + res.json({ status: 'deleted' }); +}); + // SSE stream app.get('/api/alerts/stream', (req, res) => { res.writeHead(200, { @@ -64,6 +120,19 @@ app.get('/api/alerts/stream', (req, res) => { req.on('close', () => sseClients.delete(res)); }); +// Auto-purge done items older than 2 hours +setInterval(() => { + const alerts = loadAlerts(); + const cutoff = Date.now() - 2 * 3600000; + const filtered = alerts.filter(a => !(a.done && new Date(a.timestamp).getTime() < cutoff)); + if (filtered.length < alerts.length) { + saveAlerts(filtered); + for (const client of sseClients) { + client.write(`data: ${JSON.stringify({ type: 'refresh' })}\n\n`); + } + } +}, 300000); // Check every 5 min + app.listen(PORT, '0.0.0.0', () => { console.log(`Alert dashboard running on http://0.0.0.0:${PORT}`); });