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