Update server.js

This commit is contained in:
JoshBaneyCS 2025-04-30 02:28:32 +00:00
parent 1c312ab0a5
commit b584087a4e

115
server.js
View File

@ -11,10 +11,18 @@ const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
// ─── Helpers ──────────────────────────────────────────────────────────────── // ─── Helpers ────────────────────────────────────────────────────────────────
// zero-pad // Fetch current New York time (with DST) via worldtimeapi.org
const pad2 = n => n.toString().padStart(2,'0'); async function getNYTime() {
try {
const res = await axios.get('http://worldtimeapi.org/api/timezone/America/New_York');
return new Date(res.data.datetime); // ISO string with offset
} catch (e) {
console.error('Time API error:', e.message);
return new Date(); // fallback to local clock
}
}
// Build an SQLcompatible DATETIME string in America/New_York (with DST) // Format a JS Date into "YYYY-MM-DD HH:mm:ss" in NY time for SQL DATETIME
function localDatetimeSQL(date) { function localDatetimeSQL(date) {
// datePart = "YYYY-MM-DD" // datePart = "YYYY-MM-DD"
const datePart = new Intl.DateTimeFormat('en-CA', { const datePart = new Intl.DateTimeFormat('en-CA', {
@ -34,7 +42,7 @@ function localDatetimeSQL(date) {
return `${datePart} ${timePart}`; return `${datePart} ${timePart}`;
} }
// Format a Date in EST as “M/D/YY @HH:mm” (24-hour, for Slack) // Format a JS Date into "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',
@ -53,9 +61,10 @@ function shortEST(date) {
// 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] = [
[-42.379,2.04901523,10.14333127,-0.22475541, -42.379, 2.04901523, 10.14333127, -0.22475541,
-0.00683783,-0.05481717,0.00122874,0.00085282,-0.00000199]; -0.00683783, -0.05481717, 0.00122874, 0.00085282, -0.00000199
];
const HI = c1 + c2*T + c3*R + c4*T*R const HI = c1 + c2*T + c3*R + c4*T*R
+ c5*T*T + c6*R*R + c7*T*T*R + c5*T*T + c6*R*R + c7*T*T*R
+ c8*T*R*R + c9*T*T*R*R; + c8*T*R*R + c9*T*T*R*R;
@ -84,10 +93,9 @@ function getShiftInfo(now) {
return { shift, key, estNow: est }; return { shift, key, estNow: est };
} }
// Fetch current Baltimore forecast // Fetch current weather for Baltimore
async function fetchCurrentWeather() { async function fetchCurrentWeather() {
const key = process.env.WEATHER_API_KEY; const key = process.env.WEATHER_API_KEY, zip = process.env.ZIP_CODE;
const zip = process.env.ZIP_CODE;
if (!key || !zip) return 'Unavailable'; if (!key || !zip) return 'Unavailable';
try { try {
const { data } = await axios.get( const { data } = await axios.get(
@ -104,10 +112,10 @@ async function fetchCurrentWeather() {
} }
} }
// ─── MariaDB Pool ───────────────────────────────────────────────────────────── // ─── MariaDB Pool & Table Setup ───────────────────────────────────────────────
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: +process.env.DB_PORT||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,
@ -115,23 +123,19 @@ const pool = mysql.createPool({
connectionLimit: 10, connectionLimit: 10,
queueLimit: 0, queueLimit: 0,
connectTimeout: 10000, connectTimeout: 10000,
dateStrings: ['DATETIME'] // ensure we get raw strings back dateStrings: ['DATETIME']
}); });
// Ensure table exists ;(async()=>{
(async () => { await pool.execute(`
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, heatIndex DOUBLE
humidity DOUBLE, ) CHARSET=utf8mb4;
heatIndex DOUBLE `);
);
`;
await pool.execute(sql);
})(); })();
// ─── Middleware & Static ───────────────────────────────────────────────────── // ─── Middleware & Static ─────────────────────────────────────────────────────
@ -141,21 +145,16 @@ app.use(express.static(path.join(__dirname,'public'), { 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', 'Cache-Control':'no-cache', Connection:'keep-alive' });
'Content-Type':'text/event-stream', res.flushHeaders(); clients.push(res);
'Cache-Control':'no-cache', req.on('close',()=> clients=clients.filter(c=>c!==res));
Connection:'keep-alive'
});
res.flushHeaders();
clients.push(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 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;
@ -164,38 +163,39 @@ 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' });
// compute
const hiIn = computeHeatIndex(inT,inH); const hiIn = computeHeatIndex(inT,inH);
const hiOut = computeHeatIndex(outT,outH); const hiOut = computeHeatIndex(outT,outH);
const now = new Date(); const now = await getNYTime();
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];
// timestamps
const sqlTs = localDatetimeSQL(estNow); const sqlTs = localDatetimeSQL(estNow);
const shortTs = shortEST(estNow); const shortTs = shortEST(estNow);
// insert await pool.execute(
const ins = ` `INSERT INTO readings(location,stationDockDoor,timestamp,temperature,humidity,heatIndex)
INSERT INTO readings VALUES(?,?,?,?,?,?)`,
(location,stationDockDoor,timestamp,temperature,humidity,heatIndex) ['Inbound',String(inD),sqlTs,inT,inH,hiIn]
VALUES(?,?,?,?,?,?)`; );
await pool.execute(ins, ['Inbound', String(inD), sqlTs, inT, inH, hiIn]); await pool.execute(
await pool.execute(ins, ['Outbound', String(outD), sqlTs, outT, outH, hiOut]); `INSERT INTO readings(location,stationDockDoor,timestamp,temperature,humidity,heatIndex)
VALUES(?,?,?,?,?,?)`,
['Outbound',String(outD),sqlTs,outT,outH,hiOut]
);
// SSE
broadcast('new-reading',{location:'Inbound',stationDockDoor:String(inD),timestamp:shortTs,temperature:inT,humidity:inH,heatIndex:hiIn}); broadcast('new-reading',{location:'Inbound',stationDockDoor:String(inD),timestamp:shortTs,temperature:inT,humidity:inH,heatIndex:hiIn});
broadcast('new-reading',{location:'Outbound',stationDockDoor:String(outD),timestamp:shortTs,temperature:outT,humidity:outH,heatIndex:hiOut}); broadcast('new-reading',{location:'Outbound',stationDockDoor:String(outD),timestamp:shortTs,temperature:outT,humidity:outH,heatIndex:hiOut});
// upload CSV // upload CSV
const y=estNow.getFullYear(), m=pad2(estNow.getMonth()+1), d=pad2(estNow.getDate()); const [rows] = await pool.execute(
const dateKey = `${y}${m}${d}`; `SELECT * FROM readings WHERE DATE(timestamp)=CURDATE() ORDER BY timestamp`
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) }catch(e){console.error(e)} try{ csvUrl = await uploadTrendsCsv(
shortTs.slice(6,8)+shortTs.slice(0,2)+shortTs.slice(3,5), rows
); }catch(_){}
// 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`+
@ -230,8 +230,9 @@ app.post('/api/area-readings', async (req,res) => {
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 = await getNYTime();
const { shift, estNow } = getShiftInfo(now); const { shift, estNow } = getShiftInfo(now);
const sqlTs = localDatetimeSQL(estNow); const sqlTs = localDatetimeSQL(estNow);
const shortTs = shortEST(estNow); const shortTs = shortEST(estNow);
@ -243,11 +244,13 @@ app.post('/api/area-readings', async (req,res) => {
broadcast('new-area-reading',{location:area,stationDockDoor:stationCode,timestamp:shortTs,temperature:T,humidity:H,heatIndex:hi}); 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 [rows] = await pool.execute(
const dateKey = `${y}${m}${d}`; `SELECT * FROM readings WHERE DATE(timestamp)=CURDATE() ORDER BY timestamp`
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) }catch(e){console.error(e)} try{ csvUrl = await uploadTrendsCsv(
shortTs.slice(6,8)+shortTs.slice(0,2)+shortTs.slice(3,5), rows
); }catch(_){}
const weather = await fetchCurrentWeather(); const weather = await fetchCurrentWeather();
const text = const text =
@ -271,7 +274,7 @@ app.post('/api/area-readings', async (req,res) => {
} }
}); });
// ─── Export & Fetch ────────────────────────────────────────────────────────── // ─── Fetch & Export ─────────────────────────────────────────────────────────
app.get('/api/readings', async (req,res)=>{ app.get('/api/readings', async (req,res)=>{
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);
@ -282,14 +285,10 @@ app.get('/api/export', async (req,res)=>{
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
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},${r.timestamp},${r.temperature},${r.humidity},${r.heatIndex}\n`);
}); });
res.end(); res.end();
}); });
// ─── Start ─────────────────────────────────────────────────────────────────── // ─── Start ───────────────────────────────────────────────────────────────────
app.listen(PORT, ()=>{ app.listen(PORT, ()=>console.log(`Server running http://localhost:${PORT}`));
console.log(`Server running on http://localhost:${PORT}`);
});