Update server.js

This commit is contained in:
JoshBaneyCS 2025-04-30 02:20:54 +00:00
parent bc3ff28ad2
commit 9283a6d31a

271
server.js
View File

@ -1,3 +1,4 @@
// server.js
require('dotenv').config(); require('dotenv').config();
const express = require('express'); const express = require('express');
const mysql = require('mysql2/promise'); const mysql = require('mysql2/promise');
@ -9,14 +10,31 @@ const { uploadTrendsCsv } = require('./s3');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
// In-memory shift counters // ─── Helpers ────────────────────────────────────────────────────────────────
const shiftCounters = {};
// ─── Helpers ──────────────────────────────────────────────────────────────────
// zero-pad // zero-pad
const pad2 = n => n.toString().padStart(2,'0'); const pad2 = n => n.toString().padStart(2,'0');
// Format Date in EST as “M/D/YY @HH:mm” using Intl // Build an SQLcompatible DATETIME string in America/New_York (with DST)
function localDatetimeSQL(date) {
// datePart = "YYYY-MM-DD"
const datePart = new Intl.DateTimeFormat('en-CA', {
timeZone: 'America/New_York',
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).format(date);
// timePart = "HH:MM:SS"
const timePart = new Intl.DateTimeFormat('en-GB', {
timeZone: 'America/New_York',
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(date);
return `${datePart} ${timePart}`;
}
// Format a Date in EST as “M/D/YY @HH:mm” (24-hour, for Slack)
function shortEST(date) { function shortEST(date) {
const dateFmt = new Intl.DateTimeFormat('en-US', { const dateFmt = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York', timeZone: 'America/New_York',
@ -33,26 +51,6 @@ function shortEST(date) {
return `${dateFmt.format(date)} @${timeFmt.format(date)}`; return `${dateFmt.format(date)} @${timeFmt.format(date)}`;
} }
// Format Date in EST as SQL DATETIME “YYYY-MM-DD HH:mm:ss”
function formatDateEST(date) {
// datePart as “YYYY-MM-DD”
const datePart = new Intl.DateTimeFormat('en-CA', {
timeZone: 'America/New_York',
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).format(date);
// timePart as “HH:MM:SS”
const timePart = new Intl.DateTimeFormat('en-GB', {
timeZone: 'America/New_York',
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(date);
return `${datePart} ${timePart}`;
}
// NOAA heat-index formula // NOAA heat-index formula
function computeHeatIndex(T, R) { function computeHeatIndex(T, R) {
const [c1,c2,c3,c4,c5,c6,c7,c8,c9] = const [c1,c2,c3,c4,c5,c6,c7,c8,c9] =
@ -64,7 +62,7 @@ function computeHeatIndex(T, R) {
return Math.round(HI * 100) / 100; return Math.round(HI * 100) / 100;
} }
// Determine Day/Night shift and period key // Determine Day/Night shift and rolling period
function getShiftInfo(now) { function getShiftInfo(now) {
const est = new Date(now.toLocaleString('en-US', { timeZone:'America/New_York' })); const est = new Date(now.toLocaleString('en-US', { timeZone:'America/New_York' }));
const h = est.getHours(), m = est.getMinutes(); const h = est.getHours(), m = est.getMinutes();
@ -83,10 +81,10 @@ function getShiftInfo(now) {
} }
const key = `${shift}-${start.toISOString().slice(0,10)}-${start.getHours()}${start.getMinutes()}`; const key = `${shift}-${start.toISOString().slice(0,10)}-${start.getHours()}${start.getMinutes()}`;
return { shift, start, key, estNow: est }; return { shift, key, estNow: est };
} }
// Fetch current weather from OpenWeatherMap // Fetch current Baltimore forecast
async function fetchCurrentWeather() { async function fetchCurrentWeather() {
const key = process.env.WEATHER_API_KEY; const key = process.env.WEATHER_API_KEY;
const zip = process.env.ZIP_CODE; const zip = process.env.ZIP_CODE;
@ -106,57 +104,58 @@ async function fetchCurrentWeather() {
} }
} }
// ─── MariaDB Pool & Table Setup ─────────────────────────────────────────────── // ─── MariaDB Pool ─────────────────────────────────────────────────────────────
const pool = mysql.createPool({ const pool = mysql.createPool({
host: process.env.DB_HOST, host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT,10) || 3306, port: parseInt(process.env.DB_PORT,10)||3306,
user: process.env.DB_USER, user: process.env.DB_USER,
password: process.env.DB_PASSWORD, password: process.env.DB_PASSWORD,
database: process.env.DB_NAME, database: process.env.DB_NAME,
waitForConnections: true, waitForConnections: true,
connectionLimit: 10, connectionLimit: 10,
queueLimit: 0, queueLimit: 0,
connectTimeout: 10000 connectTimeout: 10000,
dateStrings: [ 'DATETIME', 'TIMESTAMP' ] dateStrings: ['DATETIME'] // ensure we get raw strings back
}); });
(async ()=>{ // Ensure table exists
(async () => {
const sql = ` const sql = `
CREATE TABLE IF NOT EXISTS readings ( CREATE TABLE IF NOT EXISTS readings (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
location VARCHAR(20) NOT NULL, location VARCHAR(20) NOT NULL,
stationDockDoor VARCHAR(10) NOT NULL, stationDockDoor VARCHAR(10) NOT NULL,
timestamp DATETIME NOT NULL, timestamp DATETIME NOT NULL,
temperature DOUBLE, temperature DOUBLE,
humidity DOUBLE, humidity DOUBLE,
heatIndex DOUBLE heatIndex DOUBLE
);`; );
`;
await pool.execute(sql); await pool.execute(sql);
})(); })();
// ─── Middleware & Static (serve heatmap.html as index) ─────────────────────── // ─── Middleware & Static ─────────────────────────────────────────────────────
app.use(bodyParser.json()); app.use(bodyParser.json());
const publicDir = path.join(__dirname,'public'); app.use(express.static(path.join(__dirname,'public'), { index: 'heatmap.html' }));
app.use(express.static(publicDir, { index: 'heatmap.html' }));
// ─── SSE Setup ─────────────────────────────────────────────────────────────── // ─── SSE Setup ───────────────────────────────────────────────────────────────
let clients = []; let clients = [];
app.get('/api/stream',(req,res) => { app.get('/api/stream',(req,res)=>{
res.set({ res.set({
'Content-Type': 'text/event-stream', 'Content-Type':'text/event-stream',
'Cache-Control': 'no-cache', 'Cache-Control':'no-cache',
Connection: 'keep-alive' Connection:'keep-alive'
}); });
res.flushHeaders(); res.flushHeaders();
clients.push(res); clients.push(res);
req.on('close',()=>clients=clients.filter(c=>c!==res)); req.on('close',()=>{ clients = clients.filter(c=>c!==res); });
}); });
function broadcast(evt,data){ function broadcast(evt,data){
const msg = `event: ${evt}\ndata: ${JSON.stringify(data)}\n\n`; const msg = `event: ${evt}\ndata: ${JSON.stringify(data)}\n\n`;
clients.forEach(c=>c.write(msg)); clients.forEach(c=>c.write(msg));
} }
// ─── Dual Dock-Door Readings Endpoint ──────────────────────────────────────── // ─── Dual Dock-Door Endpoint ──────────────────────────────────────────────────
app.post('/api/readings', async (req,res) => { app.post('/api/readings', async (req,res) => {
try { try {
const { inbound={}, outbound={} } = req.body; const { inbound={}, outbound={} } = req.body;
@ -165,64 +164,51 @@ app.post('/api/readings', async (req,res) => {
if ([inD,inT,inH,outD,outT,outH].some(v=>v==null)) if ([inD,inT,inH,outD,outT,outH].some(v=>v==null))
return res.status(400).json({ error:'Missing fields' }); return res.status(400).json({ error:'Missing fields' });
const hiIn = computeHeatIndex(inT, inH); // compute
const hiOut = computeHeatIndex(outT, outH); const hiIn = computeHeatIndex(inT,inH);
const hiOut = computeHeatIndex(outT,outH);
const now = new Date(); const now = new Date();
const { shift, key, estNow } = getShiftInfo(now); const { shift, key, estNow } = getShiftInfo(now);
shiftCounters[key] = (shiftCounters[key]||0) + 1; shiftCounters[key] = (shiftCounters[key]||0)+1;
const period = shiftCounters[key]; const period = shiftCounters[key];
const sqlTs = formatDateEST(estNow); // timestamps
const sqlTs = localDatetimeSQL(estNow);
const shortTs = shortEST(estNow); const shortTs = shortEST(estNow);
const insertSQL = ` // insert
INSERT INTO readings(location,stationDockDoor,timestamp,temperature,humidity,heatIndex) const ins = `
INSERT INTO readings
(location,stationDockDoor,timestamp,temperature,humidity,heatIndex)
VALUES(?,?,?,?,?,?)`; VALUES(?,?,?,?,?,?)`;
await pool.execute(insertSQL, ['Inbound', String(inD), sqlTs, inT, inH, hiIn]); await pool.execute(ins, ['Inbound', String(inD), sqlTs, inT, inH, hiIn]);
await pool.execute(insertSQL, ['Outbound', String(outD), sqlTs, outT, outH, hiOut]); await pool.execute(ins, ['Outbound', String(outD), sqlTs, outT, outH, hiOut]);
broadcast('new-reading', { // SSE
location: 'Inbound', broadcast('new-reading', { location:'Inbound', stationDockDoor:String(inD), timestamp:shortTs, temperature:inT, humidity:inH, heatIndex:hiIn });
stationDockDoor: String(inD), broadcast('new-reading', { location:'Outbound', stationDockDoor:String(outD), timestamp:shortTs, temperature:outT, humidity:outH, heatIndex:hiOut });
timestamp: shortTs,
temperature: inT,
humidity: inH,
heatIndex: hiIn
});
broadcast('new-reading', {
location: 'Outbound',
stationDockDoor: String(outD),
timestamp: shortTs,
temperature: outT,
humidity: outH,
heatIndex: hiOut
});
// CSV Upload // upload CSV
const y=estNow.getFullYear(), m=pad2(estNow.getMonth()+1), d=pad2(estNow.getDate()); const y=estNow.getFullYear(), m=pad2(estNow.getMonth()+1), d=pad2(estNow.getDate());
const dateKey=`${y}${m}${d}`; const dateKey = `${y}${m}${d}`;
const [rows] = await pool.execute( const [rows] = await pool.execute(`SELECT * FROM readings WHERE DATE(timestamp)=CURDATE() ORDER BY timestamp`);
`SELECT * FROM readings WHERE DATE(timestamp)=CURDATE() ORDER BY timestamp`
);
let csvUrl=null; let csvUrl=null;
try{ csvUrl=await uploadTrendsCsv(dateKey,rows) } try{ csvUrl=await uploadTrendsCsv(dateKey,rows) }catch(e){console.error(e)}
catch(e){ console.error('CSV upload error',e) }
// weather + Slack
const weather = await fetchCurrentWeather(); const weather = await fetchCurrentWeather();
const text = const text =
`*_${shift} shift Period ${period} dock/ trailer temperature checks for ${shortTs}_*\n` + `*_${shift} shift Period ${period} dock/ trailer temperature checks for ${shortTs}_*\n`+
`*_Current Weather Forecast for Baltimore: ${weather}_*\n\n` + `*_Current Weather Forecast for Baltimore: ${weather}_*\n\n`+
`*_⬇ Inbound Dock Door 🚛 :_* ${inD}\n` + `*_⬇ Inbound Dock Door 🚛 :_* ${inD}\n`+
`*_Temp:_* ${inT} °F 🌡️\n` + `*_Temp:_* ${inT} °F 🌡️\n`+
`*_Humidity:_* ${inH} % 💦\n` + `*_Humidity:_* ${inH} % 💦\n`+
`*_Heat Index:_* ${hiIn} °F 🥵\n\n` + `*_Heat Index:_* ${hiIn} °F 🥵\n\n`+
`*_⬆ Outbound Dock Door 🚛 :_* ${outD}\n` + `*_⬆ Outbound Dock Door 🚛 :_* ${outD}\n`+
`*_Temp:_* ${outT} °F 🌡️\n` + `*_Temp:_* ${outT} °F 🌡️\n`+
`*_Humidity:_* ${outH} % 💦\n` + `*_Humidity:_* ${outH} % 💦\n`+
`*_Heat Index:_* ${hiOut} °F 🥵`; `*_Heat Index:_* ${hiOut} °F 🥵`;
// Send to Slack workflow trigger
await axios.post( await axios.post(
process.env.SLACK_WEBHOOK_URL, process.env.SLACK_WEBHOOK_URL,
{ text }, { text },
@ -230,58 +216,46 @@ app.post('/api/readings', async (req,res) => {
); );
res.json({ success:true, shift, period, csvUrl }); res.json({ success:true, shift, period, csvUrl });
} catch (err) { } catch(err) {
console.error('POST /api/readings error:', err); console.error(err);
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} }
}); });
// ─── Area/Mod Station Readings Endpoint ───────────────────────────────────── // ─── Area/Mod Endpoint ───────────────────────────────────────────────────────
app.post('/api/area-readings', async (req,res) => { app.post('/api/area-readings', async (req,res) => {
try { try {
const { area, stationCode, temperature: T, humidity: H } = req.body; const { area, stationCode, temperature:T, humidity:H } = req.body;
if (!area||!stationCode||T==null||H==null) if (!area||!stationCode||T==null||H==null)
return res.status(400).json({ error:'Missing fields' }); return res.status(400).json({ error:'Missing fields' });
const hi = computeHeatIndex(T, H); const hi = computeHeatIndex(T,H);
const now = new Date(); const now = new Date();
const { shift, estNow } = getShiftInfo(now); const { shift, estNow } = getShiftInfo(now);
const sqlTs = localDatetimeSQL(estNow);
const sqlTs = formatDateEST(estNow);
const shortTs = shortEST(estNow); const shortTs = shortEST(estNow);
const insertSQL = ` await pool.execute(
INSERT INTO readings(location,stationDockDoor,timestamp,temperature,humidity,heatIndex) `INSERT INTO readings(location,stationDockDoor,timestamp,temperature,humidity,heatIndex)
VALUES(?,?,?,?,?,?)`; VALUES(?,?,?,?,?,?)`,
await pool.execute(insertSQL, [area, stationCode, sqlTs, T, H, hi]); [area, stationCode, sqlTs, T, H, hi]
broadcast('new-area-reading', {
location: area,
stationDockDoor: stationCode,
timestamp: shortTs,
temperature: T,
humidity: H,
heatIndex: hi
});
// CSV Upload
const y=estNow.getFullYear(), m=pad2(estNow.getMonth()+1), d=pad2(estNow.getDate());
const dateKey=`${y}${m}${d}`;
const [rows] = await pool.execute(
`SELECT * FROM readings WHERE DATE(timestamp)=CURDATE() ORDER BY timestamp`
); );
broadcast('new-area-reading', { location:area, stationDockDoor:stationCode, timestamp:shortTs, temperature:T, humidity:H, heatIndex:hi });
const y=estNow.getFullYear(), m=pad2(estNow.getMonth()+1), d=pad2(estNow.getDate());
const dateKey = `${y}${m}${d}`;
const [rows] = await pool.execute(`SELECT * FROM readings WHERE DATE(timestamp)=CURDATE() ORDER BY timestamp`);
let csvUrl=null; let csvUrl=null;
try{ csvUrl=await uploadTrendsCsv(dateKey,rows) } try{ csvUrl=await uploadTrendsCsv(dateKey,rows) }catch(e){console.error(e)}
catch(e){ console.error('CSV upload error',e) }
const weather = await fetchCurrentWeather(); const weather = await fetchCurrentWeather();
const text = const text =
`*_${shift} shift ${area} temp check for ${shortTs}_*\n` + `*_${shift} shift ${area} temp check for ${shortTs}_*\n`+
`*_Current Weather Forecast for Baltimore: ${weather}_*\n\n` + `*_Current Weather Forecast for Baltimore: ${weather}_*\n\n`+
`*_${area.toUpperCase()} station:_* ${stationCode}\n` + `*_${area.toUpperCase()} station:_* ${stationCode}\n`+
`*_Temp:_* ${T} °F 🌡️\n` + `*_Temp:_* ${T} °F 🌡️\n`+
`*_Humidity:_* ${H} % 💦\n` + `*_Humidity:_* ${H} % 💦\n`+
`*_Heat Index:_* ${hi} °F 🥵`; `*_Heat Index:_* ${hi} °F 🥵`;
await axios.post( await axios.post(
@ -291,38 +265,31 @@ app.post('/api/area-readings', async (req,res) => {
); );
res.json({ success:true, csvUrl }); res.json({ success:true, csvUrl });
} catch (err) { } catch(err) {
console.error('POST /api/area-readings error:', err); console.error(err);
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} }
}); });
// ─── GET all readings & CSV export ─────────────────────────────────────────── // ─── Export & Fetch ──────────────────────────────────────────────────────────
app.get('/api/readings', async (req,res) => { app.get('/api/readings', async (req,res)=>{
try { const [rows] = await pool.execute(`SELECT * FROM readings ORDER BY timestamp`);
const [rows] = await pool.execute(`SELECT * FROM readings ORDER BY timestamp`); res.json(rows);
res.json(rows);
} catch (err) {
console.error('GET /api/readings error:', err);
res.status(500).json({ error: err.message });
}
}); });
app.get('/api/export', async (req,res) => { app.get('/api/export', async (req,res)=>{
try { const [rows] = await pool.execute(`SELECT * FROM readings ORDER BY timestamp`);
const [rows] = await pool.execute(`SELECT * FROM readings ORDER BY timestamp`); res.setHeader('Content-disposition','attachment; filename=readings.csv');
res.setHeader('Content-disposition','attachment; filename=readings.csv'); res.set('Content-Type','text/csv');
res.set('Content-Type','text/csv'); res.write('id,location,stationDockDoor,timestamp,temperature,humidity,heatIndex\n');
res.write('id,location,stationDockDoor,timestamp,temperature,humidity,heatIndex\n'); rows.forEach(r=>{
rows.forEach(r => { // r.timestamp is already "YYYY-MM-DD HH:mm:ss" in EST/EDT
const ts = formatDateEST(new Date(r.timestamp)); res.write(`${r.id},${r.location},${r.stationDockDoor},${r.timestamp},${r.temperature},${r.humidity},${r.heatIndex}\n`);
res.write(`${r.id},${r.location},${r.stationDockDoor},${ts},${r.temperature},${r.humidity},${r.heatIndex}\n`); });
}); res.end();
res.end();
} catch (err) {
console.error('GET /api/export error:', err);
res.status(500).send(err.message);
}
}); });
// ─── Start Server ─────────────────────────────────────────────────────────── // ─── Start ───────────────────────────────────────────────────────────────────
app.listen(PORT, ()=>console.log(`Server running http://localhost:${PORT}`)); app.listen(PORT, ()=>{
console.log(`Server running on http://localhost:${PORT}`);
});