require('dotenv').config(); const express = require('express'); const bodyParser = require('body-parser'); const path = require('path'); const knex = require('knex'); const axios = require('axios'); // Initialize MariaDB connection via Knex const db = knex({ client: process.env.DB_CLIENT, connection: { host: process.env.DB_HOST, port: process.env.DB_PORT, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME } }); const app = express(); const PORT = process.env.PORT || 3000; const slackWebhook = process.env.SLACK_WEBHOOK_URL; // Create table if not exists, now with direction (async () => { if (!await db.schema.hasTable('readings')) { await db.schema.createTable('readings', table => { table.increments('id').primary(); table.integer('dockDoor'); table.string('direction'); table.timestamp('timestamp'); table.float('temperature'); table.float('humidity'); table.float('heatIndex'); }); } })(); // Compute heat index (NOAA formula) function computeHeatIndex(T, R) { const c1 = -42.379, c2 = 2.04901523, c3 = 10.14333127; const c4 = -0.22475541, c5 = -6.83783e-3, c6 = -5.481717e-2; const c7 = 1.22874e-3, c8 = 8.5282e-4, c9 = -1.99e-6; const HI = c1 + c2*T + c3*R + c4*T*R + c5*T*T + c6*R*R + c7*T*T*R + c8*T*R*R + c9*T*T*R*R; return Math.round(HI * 100) / 100; } // Determine direction based on door number function getDirection(door) { door = Number(door); if (door >= 124 && door <= 138) return 'Inbound'; if (door >= 142 && door <= 201) return 'Outbound'; if (door >= 202 && door <= 209) return 'Inbound'; return 'Unknown'; } app.use(bodyParser.json()); app.use(express.static(path.join(__dirname, 'public'))); let clients = []; app.get('/api/stream', (req, res) => { res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' }); res.flushHeaders(); clients.push(res); req.on('close', () => { clients = clients.filter(c => c !== res); }); }); function broadcast(event, data) { const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; clients.forEach(res => res.write(payload)); } app.post('/api/readings', async (req, res) => { try { const { inbound, outbound } = req.body; // each: {dockDoor,temperature,humidity} const timestamp = new Date(); const entries = [inbound, outbound].map(r => { const direction = getDirection(r.dockDoor); const heatIndex = computeHeatIndex(r.temperature, r.humidity); return { ...r, direction, timestamp, heatIndex }; }); // Insert both const ids = await db('readings').insert(entries); const saved = entries.map((e, i) => ({ id: ids[i], ...e })); // Broadcast and respond saved.forEach(reading => broadcast('new-reading', reading)); // Slack notification with both if (slackWebhook) { const textLines = saved.map(r => `Door *${r.dockDoor}* (${r.direction}) – Temp: ${r.temperature}°F, Humidity: ${r.humidity}%, HI: ${r.heatIndex}` ); await axios.post(slackWebhook, { text: 'New dual readings:\n' + textLines.join('\n') }); } res.json(saved); } catch (err) { console.error('Error saving readings or sending Slack:', err); res.status(500).json({ error: err.message }); } }); app.get('/api/readings', async (req, res) => { try { const rows = await db('readings').orderBy('timestamp', 'asc'); res.json(rows); } catch (err) { res.status(500).json({ error: err.message }); } }); app.get('/api/export', async (req, res) => { try { const rows = await db('readings').orderBy('timestamp', 'asc'); res.setHeader('Content-disposition', 'attachment; filename=readings.csv'); res.set('Content-Type', 'text/csv'); res.write('id,dockDoor,direction,timestamp,temperature,humidity,heatIndex\n'); rows.forEach(r => res.write(`${r.id},${r.dockDoor},${r.direction},${r.timestamp},${r.temperature},${r.humidity},${r.heatIndex}\n`) ); res.end(); } catch (err) { res.status(500).send(err.message); } }); app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));