Heat-Tracker/server.js
2025-04-22 09:41:26 -04:00

215 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
// Inmemory 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')));
// Dualreading 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 serverside 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 frontend)
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}`);
});