Update server.js

This commit is contained in:
JoshBaneyCS 2025-04-29 23:17:45 +00:00
parent c372146ba8
commit b280a720d7

257
server.js
View File

@ -10,13 +10,12 @@ const { uploadTrendsCsv } = require('./s3');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
// Inmemory shift counters // In-memory shift counters
const shiftCounters = {}; const shiftCounters = {};
// ===== Helpers ===== // Helpers
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”
function shortEST(d) { function shortEST(d) {
const est = new Date(d.toLocaleString('en-US', { timeZone: 'America/New_York' })); const est = new Date(d.toLocaleString('en-US', { timeZone: 'America/New_York' }));
const M = est.getMonth() + 1, D = est.getDate(), YY = String(est.getFullYear()).slice(-2); const M = est.getMonth() + 1, D = est.getDate(), YY = String(est.getFullYear()).slice(-2);
@ -24,7 +23,6 @@ function shortEST(d) {
return `${M}/${D}/${YY} @${hh}:${mm}`; return `${M}/${D}/${YY} @${hh}:${mm}`;
} }
// Format Date in EST as SQL DATETIME
function formatDateEST(d) { function formatDateEST(d) {
const est = new Date(d.toLocaleString('en-US', { timeZone: 'America/New_York' })); const est = new Date(d.toLocaleString('en-US', { timeZone: 'America/New_York' }));
const y = est.getFullYear(), M = pad2(est.getMonth()+1), D = pad2(est.getDate()); const y = est.getFullYear(), M = pad2(est.getMonth()+1), D = pad2(est.getDate());
@ -32,18 +30,16 @@ function formatDateEST(d) {
return `${y}-${M}-${D} ${hh}:${mm}:${ss}`; return `${y}-${M}-${D} ${hh}:${mm}:${ss}`;
} }
// Compute heat index (NOAA)
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;
return Math.round(HI * 100) / 100; return Math.round(HI * 100) / 100;
} }
// Determine shift info in EST
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();
@ -65,30 +61,28 @@ function getShiftInfo(now) {
return { shift, start, key, estNow: est }; return { shift, start, key, estNow: est };
} }
// Fetch current weather from OpenWeatherMap
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 null;
try { try {
const { data } = await axios.get( const { data } = await axios.get(
'https://api.openweathermap.org/data/2.5/weather', 'https://api.openweathermap.org/data/2.5/weather',
{ params: { zip:`${zip},us`, appid:key, units:'imperial' } } { params:{ zip:`${zip},us`, appid:key, units:'imperial' } }
); );
const desc = data.weather[0].description; const desc = data.weather[0].description.replace(/^\w/, c => c.toUpperCase());
const hi = Math.round(data.main.temp_max); const hi = Math.round(data.main.temp_max);
const hum = data.main.humidity; const hum = data.main.humidity;
return `${desc.charAt(0).toUpperCase()+desc.slice(1)}. Hi of ${hi}, Humidity ${hum}%`; return `${desc}. Hi of ${hi}, Humidity ${hum}%`;
} catch (e) { } catch (e) {
console.error('Weather API error:', e.message); console.error('Weather API error:', e.message);
return null; return 'Unavailable';
} }
} }
// MariaDB pool // 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,
@ -99,20 +93,25 @@ const pool = mysql.createPool({
}); });
// Ensure table exists // Ensure table exists
(async () => { (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)
app.use(bodyParser.json());
const publicDir = path.join(__dirname,'public');
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)=>{
@ -123,26 +122,17 @@ app.get('/api/stream',(req,res)=>{
}); });
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));
} }
// Middleware
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname,'public')));
const publicDir = path.join(__dirname, 'public');
// Serve heatmap.html as the index page
app.use(express.static(publicDir, { index: 'heatmap.html' }));
// ---- 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;
const { dockDoor: inD, temperature: inT, humidity: inH } = inbound; const { dockDoor: inD, temperature: inT, humidity: inH } = inbound;
const { dockDoor: outD, temperature: outT, humidity: outH } = outbound; const { dockDoor: outD, temperature: outT, humidity: outH } = outbound;
if ([inD,inT,inH,outD,outT,outH].some(v=>v==null)) if ([inD,inT,inH,outD,outT,outH].some(v=>v==null))
@ -151,162 +141,147 @@ app.post('/api/readings', async (req, res) => {
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 = new Date();
const { shift, start, 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);
const sqlTs = formatDateEST(estNow);
const shortTs = shortEST(estNow); const shortTs = shortEST(estNow);
// Insert inbound & outbound // insert
const ins = `INSERT INTO readings(location,stationDockDoor,timestamp,temperature,humidity,heatIndex) const ins = `
VALUES(?,?,?,?,?,?)`; INSERT INTO readings(location,stationDockDoor,timestamp,temperature,humidity,heatIndex)
await pool.execute(ins, ['Inbound', String(inD), sqlTs, inT, inH, hiIn]); VALUES(?,?,?,?,?,?)`;
await pool.execute(ins, ['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]);
// SSE // 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 });
// CSV 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(`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); } try{ csvUrl=await uploadTrendsCsv(dateKey,rows) }catch(e){console.error(e)}
catch(e){ console.error('CSV upload error',e); }
// Weather // weather
const weather = await fetchCurrentWeather() || 'Unavailable'; const weather = await fetchCurrentWeather();
// Slack payload // build text
const text = `*_${shift} shift Period ${period} dock/ trailer temperature checks for ${shortTs}_*\n` + const text =
`*_Current Weather Forecast for Baltimore: ${weather}_*\n\n` + `*_${shift} shift Period ${period} dock/ trailer temperature checks for ${shortTs}_*\n`+
`*_⬇ Inbound Dock Door 🚛 :_* ${inD}\n` + `*_Current Weather Forecast for Baltimore: ${weather}_*\n\n`+
`*_Temp:_* ${inT} °F 🌡️\n` + `*_⬇ Inbound Dock Door 🚛 :_* ${inD}\n`+
`*_Humidity:_* ${inH} % 💦\n` + `*_Temp:_* ${inT} °F 🌡️\n`+
`*_Heat Index:_* ${hiIn} °F 🥵\n\n` + `*_Humidity:_* ${inH} % 💦\n`+
`*_⬆ Outbound Dock Door 🚛 :_* ${outD}\n` + `*_Heat Index:_* ${hiIn} °F 🥵\n\n`+
`*_Temp:_* ${outT} °F 🌡️\n` + `*_⬆ Outbound Dock Door 🚛 :_* ${outD}\n`+
`*_Humidity:_* ${outH} % 💦\n` + `*_Temp:_* ${outT} °F 🌡️\n`+
`*_Heat Index:_* ${hiOut} °F 🥵`; `*_Humidity:_* ${outH} % 💦\n`+
`*_Heat Index:_* ${hiOut} °F 🥵`;
const payload = { const payload = { text };
text,
shift, // send as url-encoded string
period, const body = `payload=${encodeURIComponent(JSON.stringify(payload))}`;
timestamp: shortTs, await axios.post(process.env.SLACK_WEBHOOK_URL, body, {
current_weather: weather, headers:{ 'Content-Type':'application/x-www-form-urlencoded' }
inbound_dock_door: inD, });
inbound_temperature: inT,
inbound_humidity: inH,
inbound_heat_index: hiIn,
outbound_dock_door: outD,
outbound_temperature: outT,
outbound_humidity: outH,
outbound_heat_index: hiOut
};
await axios.post(process.env.SLACK_WEBHOOK_URL, payload);
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('POST /api/readings error:',err);
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} }
}); });
// ---- Area (Mod/AFE) 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, start, key, estNow } = getShiftInfo(now); const { shift, estNow } = getShiftInfo(now);
// NOTE: area checks do NOT increment period counter
const shortTs = shortEST(estNow);
const sqlTs = formatDateEST(estNow); const sqlTs = formatDateEST(estNow);
const shortTs = shortEST(estNow);
// Insert // insert
const ins = `INSERT INTO readings(location,stationDockDoor,timestamp,temperature,humidity,heatIndex) const ins = `
VALUES(?,?,?,?,?,?)`; INSERT INTO readings(location,stationDockDoor,timestamp,temperature,humidity,heatIndex)
await pool.execute(ins, [area, stationCode, sqlTs, T, H, hi]); VALUES(?,?,?,?,?,?)`;
await pool.execute(ins,[area,stationCode,sqlTs,T,H,hi]);
// SSE // SSE
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 });
// CSV 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(`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); } try{ csvUrl=await uploadTrendsCsv(dateKey,rows) }catch(e){console.error(e)}
catch(e){ console.error('CSV upload error',e); }
// Weather // weather
const weather = await fetchCurrentWeather() || 'Unavailable'; const weather = await fetchCurrentWeather();
// Slack text // build text
const text = `*_${shift} shift ${area} temp check for ${shortTs}_*\n` + const text =
`*_Current Weather Forecast for Baltimore: ${weather}_*\n\n` + `*_${shift} shift ${area} temp check for ${shortTs}_*\n`+
`*_${area.toUpperCase()} station:_* ${stationCode}\n` + `*_Current Weather Forecast for Baltimore: ${weather}_*\n\n`+
`*_Temp:_* ${T} °F 🌡️\n` + `*_${area.toUpperCase()} station:_* ${stationCode}\n`+
`*_Humidity:_* ${H} % 💦\n` + `*_Temp:_* ${T} °F 🌡️\n`+
`*_Heat Index:_* ${hi} °F 🥵`; `*_Humidity:_* ${H} % 💦\n`+
`*_Heat Index:_* ${hi} °F 🥵`;
const payload = { const payload = { text };
text, const body = `payload=${encodeURIComponent(JSON.stringify(payload))}`;
shift, await axios.post(process.env.SLACK_WEBHOOK_URL, body, {
timestamp: shortTs, headers:{ 'Content-Type':'application/x-www-form-urlencoded' }
current_weather: weather, });
location: area,
station_dock_door: stationCode,
temperature: T,
humidity: H,
heat_index: hi
};
await axios.post(process.env.SLACK_WEBHOOK_URL, payload);
res.json({ success:true, csvUrl }); res.json({ success:true, csvUrl });
} catch (err) { } catch(err) {
console.error('POST /api/area-readings error:', err); console.error('POST /api/area-readings error:',err);
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} }
}); });
// ---- GET all readings ---- // ---- GET all readings ----
app.get('/api/readings', async (req, res) => { app.get('/api/readings', async (req,res)=>{
try { 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) { } catch(err) {
console.error('GET /api/readings error:', err); console.error('GET /api/readings error:',err);
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} }
}); });
// ---- CSV export ---- // ---- CSV export ----
app.get('/api/export', async (req, res) => { app.get('/api/export', async (req,res)=>{
try { 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=>{
const ts = r.timestamp instanceof Date ? formatDateEST(r.timestamp) : r.timestamp; const ts = r.timestamp instanceof Date ? formatDateEST(r.timestamp) : r.timestamp;
res.write(`${r.id},${r.location},${r.stationDockDoor},${ts},${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) { } catch(err) {
console.error('GET /api/export error:', err); console.error('GET /api/export error:',err);
res.status(500).send(err.message); res.status(500).send(err.message);
} }
}); });
// Start server // Start server
app.listen(PORT, () => { app.listen(PORT, ()=>console.log(`Server running on http://localhost:${PORT}`));
console.log(`Server running on http://localhost:${PORT}`);
});