215 lines
6.8 KiB
JavaScript
215 lines
6.8 KiB
JavaScript
// 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}`);
|
||
});
|