// server.js require('dotenv').config(); const express = require('express'); const mysql = require('mysql2/promise'); const bodyParser = require('body-parser'); const path = require('path'); const axios = require('axios'); const { uploadTrendsCsv } = require('./s3'); const app = express(); const PORT = process.env.PORT || 3000; // In‑memory shift counters const shiftCounters = {}; // Create MariaDB connection pool // server.js (pool section) const pool = mysql.createPool({ host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT, 10) || 3306, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, queueLimit: 0, connectTimeout: 10000 // give it up to 10s before timing out }); // Ensure readings table exists (async () => { const createSQL = ` CREATE TABLE IF NOT EXISTS readings ( id INT AUTO_INCREMENT PRIMARY KEY, dockDoor INT NOT NULL, timestamp DATETIME NOT NULL, temperature DOUBLE, humidity DOUBLE, heatIndex DOUBLE ); `; await pool.execute(createSQL); })(); // Simple SSE setup 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 msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; clients.forEach(c => c.write(msg)); } // 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=-0.05481717; const c7=0.00122874, c8=0.00085282, c9=-0.00000199; 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 shift info in EST function getShiftInfo(now) { // Convert to EST const estNow = new Date(now.toLocaleString('en-US', { timeZone: 'America/New_York' })); const h = estNow.getHours(), m = estNow.getMinutes(); let shift, shiftStart; if (h > 7 || (h === 7 && m >= 0)) { if (h < 17 || (h === 17 && m < 30)) { shift = 'Day'; shiftStart = new Date(estNow); shiftStart.setHours(7,0,0,0); } else { shift = 'Night'; shiftStart = new Date(estNow); shiftStart.setHours(17,30,0,0); } } else { shift = 'Night'; shiftStart = new Date(estNow); shiftStart.setDate(shiftStart.getDate() - 1); shiftStart.setHours(17,30,0,0); } const key = `${shift}-${shiftStart.toISOString().slice(0,10)}-${shiftStart.getHours()}${shiftStart.getMinutes()}`; return { shift, shiftStart, key, estNow }; } // Middleware & static files app.use(bodyParser.json()); app.use(express.static(path.join(__dirname, 'public'))); // Dual‐reading POST endpoint app.post('/api/readings', async (req, res) => { try { const { inbound, outbound } = req.body; const hiIn = computeHeatIndex(inbound.temperature, inbound.humidity); const hiOut = computeHeatIndex(outbound.temperature, outbound.humidity); // Shift & period logic const { shift, shiftStart, key, estNow } = getShiftInfo(new Date()); shiftCounters[key] = (shiftCounters[key] || 0) + 1; const period = shiftCounters[key]; // Insert both readings with server‐side NOW() const insertSQL = ` INSERT INTO readings (dockDoor, timestamp, temperature, humidity, heatIndex) VALUES (?, NOW(), ?, ?, ?) `; await pool.execute(insertSQL, [inbound.dockDoor, inbound.temperature, inbound.humidity, hiIn]); await pool.execute(insertSQL, [outbound.dockDoor, outbound.temperature, outbound.humidity, hiOut]); // Broadcast to SSE clients (use UTC ISO for front‐end) const isoNow = new Date().toISOString(); broadcast('new-reading', { dockDoor: inbound.dockDoor, timestamp: isoNow, ...inbound, heatIndex: hiIn }); broadcast('new-reading', { dockDoor: outbound.dockDoor, timestamp: isoNow, ...outbound, heatIndex: hiOut }); // Format EST timestamp for Slack const estString = estNow.toLocaleString('en-US', { timeZone: 'America/New_York', hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); // Base Slack text let slackText = `${shift} Shift ${period} temperature checks for ${estString} EST Inbound Dock Door: ${inbound.dockDoor} Temp: ${inbound.temperature}°F Humidity: ${inbound.humidity}% Heat Index: ${hiIn} Outbound Dock Door: ${outbound.dockDoor} Temp: ${outbound.temperature}°F Humidity: ${outbound.humidity}% Heat Index: ${hiOut}`; // Upload today's CSV and append URL const dateKey = `${String(estNow.getFullYear())}${String(estNow.getMonth()+1).padStart(2,'0')}${String(estNow.getDate()).padStart(2,'0')}`; const [rows] = await pool.execute( `SELECT * FROM readings WHERE DATE(timestamp) = CURDATE() ORDER BY timestamp ASC` ); try { const publicUrl = await uploadTrendsCsv(dateKey, rows); slackText += `\n\nDownload daily trends: ${publicUrl}`; } catch(uploadErr) { console.error('Failed to upload CSV to S3:', uploadErr); } // Send Slack notification await axios.post(process.env.SLACK_WEBHOOK_URL, { text: slackText }); res.json({ success: true, shift, period }); } catch (err) { console.error('POST /api/readings error:', err); res.status(500).json({ error: err.message }); } }); // Return all readings as JSON app.get('/api/readings', async (req, res) => { try { const [rows] = await pool.execute(`SELECT * FROM readings ORDER BY timestamp ASC`); res.json(rows); } catch (err) { console.error('GET /api/readings error:', err); res.status(500).json({ error: err.message }); } }); // Export CSV of all readings app.get('/api/export', async (req, res) => { try { const [rows] = await pool.execute(`SELECT * FROM readings ORDER BY timestamp ASC`); res.setHeader('Content-disposition', 'attachment; filename=readings.csv'); res.set('Content-Type', 'text/csv'); res.write('id,dockDoor,timestamp,temperature,humidity,heatIndex\n'); rows.forEach(r => { // ensure ISO timestamp const ts = (r.timestamp instanceof Date) ? r.timestamp.toISOString() : r.timestamp; res.write(`${r.id},${r.dockDoor},${ts},${r.temperature},${r.humidity},${r.heatIndex}\n`); }); res.end(); } catch (err) { console.error('GET /api/export error:', err); res.status(500).send(err.message); } }); app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });