Update server.js

fixed time.
This commit is contained in:
JoshBaneyCS 2025-04-30 01:12:34 +00:00
parent 5f029f492a
commit 6461fc8e85

214
server.js
View File

@ -1,4 +1,3 @@
// 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');
@ -13,24 +12,41 @@ const PORT = process.env.PORT || 3000;
// In-memory shift counters // In-memory shift counters
const shiftCounters = {}; const shiftCounters = {};
// Helpers // ─── Helpers ──────────────────────────────────────────────────────────────────
const pad2 = n => n.toString().padStart(2, '0'); // zero-pad
const pad2 = n => n.toString().padStart(2,'0');
// Format Date in EST as “M/D/YY @HH:mm” using Intl (no round-trip through string)
function shortEST(date) { function shortEST(date) {
const est = new Date(date.toLocaleString('en-US', { timeZone: 'America/New_York' })); const dateFmt = new Intl.DateTimeFormat('en-US', {
const M = est.getMonth() + 1, D = est.getDate(), YY = String(est.getFullYear()).slice(-2); timeZone: 'America/New_York',
const hh = pad2(est.getHours()), mm = pad2(est.getMinutes()); month: 'numeric',
return `${M}/${D}/${YY} @${hh}:${mm}`; day: 'numeric',
year: '2-digit'
});
const timeFmt = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
hour12: false,
hour: '2-digit',
minute: '2-digit'
});
return `${dateFmt.format(date)} @${timeFmt.format(date)}`;
} }
// Format Date in EST as SQL DATETIME “YYYY-MM-DD HH:mm:ss”
function formatDateEST(date) { function formatDateEST(date) {
const est = new Date(date.toLocaleString('en-US', { timeZone: 'America/New_York' })); const est = new Date(date.toLocaleString('en-US', { timeZone: 'America/New_York' }));
const y = est.getFullYear(), M = pad2(est.getMonth()+1), D = pad2(est.getDate()); const Y = est.getFullYear();
const hh = pad2(est.getHours()), mm = pad2(est.getMinutes()), ss = pad2(est.getSeconds()); const M = pad2(est.getMonth() + 1);
return `${y}-${M}-${D} ${hh}:${mm}:${ss}`; const D = pad2(est.getDate());
const h = pad2(est.getHours());
const m = pad2(est.getMinutes());
const s = pad2(est.getSeconds());
return `${Y}-${M}-${D} ${h}:${m}:${s}`;
} }
function computeHeatIndex(T, R) { // NOAA heat-index formula
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];
@ -40,6 +56,7 @@ function computeHeatIndex(T, R) {
return Math.round(HI * 100) / 100; return Math.round(HI * 100) / 100;
} }
// Determine Day/Night shift and period key
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();
@ -47,7 +64,7 @@ function getShiftInfo(now) {
if (h > 7 || (h === 7 && m >= 0)) { if (h > 7 || (h === 7 && m >= 0)) {
if (h < 17 || (h === 17 && m < 30)) { if (h < 17 || (h === 17 && m < 30)) {
shift = 'Day'; start.setHours(7,0,0,0); shift = 'Day'; start.setHours(7,0,0,0);
} else { } else {
shift = 'Night'; start.setHours(17,30,0,0); shift = 'Night'; start.setHours(17,30,0,0);
} }
@ -61,25 +78,26 @@ 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, zip = process.env.ZIP_CODE; const key = process.env.WEATHER_API_KEY;
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('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.replace(/^\w/,c=>c.toUpperCase()); 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}. Hi of ${hi}, Humidity ${hum}%`; return `${desc}. Hi of ${hi}, Humidity ${hum}%`;
} catch (err) { } catch (e) {
console.error('Weather API error:', err.message); console.error('Weather API error:', e.message);
return 'Unavailable'; return 'Unavailable';
} }
} }
// 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: parseInt(process.env.DB_PORT,10) || 3306,
@ -92,29 +110,28 @@ const pool = mysql.createPool({
connectTimeout: 10000 connectTimeout: 10000
}); });
// Ensure readings 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 at '/') // ─── Middleware & Static (default to heatmap.html) ───────────────────────────
app.use(bodyParser.json()); app.use(bodyParser.json());
const publicDir = path.join(__dirname, 'public'); const publicDir = path.join(__dirname,'public');
app.use(express.static(publicDir, { 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',
@ -122,72 +139,61 @@ 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(event, data) { function broadcast(evt,data){
const msg = `event: ${event}\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 Readings 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))
return res.status(400).json({ error: 'Missing fields' }); return res.status(400).json({ error:'Missing fields' });
}
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, 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 const insertSQL = `
const ins = `
INSERT INTO readings(location,stationDockDoor,timestamp,temperature,humidity,heatIndex) INSERT INTO readings(location,stationDockDoor,timestamp,temperature,humidity,heatIndex)
VALUES(?,?,?,?,?,?)`; VALUES(?,?,?,?,?,?)`;
await pool.execute(ins, ['Inbound', String(inD), sqlTs, inT, inH, hiIn]); await pool.execute(insertSQL, ['Inbound', String(inD), sqlTs, inT, inH, hiIn]);
await pool.execute(ins, ['Outbound', String(outD), sqlTs, outT, outH, hiOut]); await pool.execute(insertSQL, ['Outbound', String(outD), sqlTs, outT, outH, hiOut]);
// SSE broadcast
broadcast('new-reading', { broadcast('new-reading', {
location: 'Inbound', location: 'Inbound', stationDockDoor:String(inD),
stationDockDoor: String(inD), timestamp: shortTs, temperature:inT,
timestamp: shortTs, humidity: inH, heatIndex: hiIn
temperature: inT,
humidity: inH,
heatIndex: hiIn
}); });
broadcast('new-reading', { broadcast('new-reading', {
location: 'Outbound', location:'Outbound', stationDockDoor:String(outD),
stationDockDoor: String(outD), timestamp: shortTs, temperature:outT,
timestamp: shortTs, humidity: outH, heatIndex: hiOut
temperature: outT,
humidity: outH,
heatIndex: hiOut
}); });
// Generate/upload CSV // CSV Upload
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('CSV upload error:', e); } catch(e){ console.error('CSV upload error',e); }
// Fetch weather
const weather = await fetchCurrentWeather(); const weather = await fetchCurrentWeather();
// Build Slack message text
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` +
@ -200,64 +206,56 @@ app.post('/api/readings', async (req, res) => {
`*_Humidity:_* ${outH} % 💦\n` + `*_Humidity:_* ${outH} % 💦\n` +
`*_Heat Index:_* ${hiOut} °F 🥵`; `*_Heat Index:_* ${hiOut} °F 🥵`;
// Send JSON with top-level "text" field // Send to Slack workflow trigger
await axios.post( await axios.post(
process.env.SLACK_WEBHOOK_URL, process.env.SLACK_WEBHOOK_URL,
{ text, shift, period, timestamp: shortTs }, { text },
{ headers: { 'Content-Type': 'application/json' } } { headers: {'Content-Type':'application/json'} }
); );
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 readings endpoint === // ─── Area/Mod Station Readings 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 = formatDateEST(estNow); const sqlTs = formatDateEST(estNow);
const shortTs = shortEST(estNow); const shortTs = shortEST(estNow);
// Insert const insertSQL = `
const ins = `
INSERT INTO readings(location,stationDockDoor,timestamp,temperature,humidity,heatIndex) INSERT INTO readings(location,stationDockDoor,timestamp,temperature,humidity,heatIndex)
VALUES(?,?,?,?,?,?)`; VALUES(?,?,?,?,?,?)`;
await pool.execute(ins, [area, stationCode, sqlTs, T, H, hi]); await pool.execute(insertSQL, [area, stationCode, sqlTs, T, H, hi]);
// SSE
broadcast('new-area-reading', { broadcast('new-area-reading', {
location: area, location:area, stationDockDoor:stationCode,
stationDockDoor: stationCode, timestamp:shortTs, temperature:T,
timestamp: shortTs, humidity:H, heatIndex:hi
temperature: T,
humidity: H,
heatIndex: hi
}); });
// 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('CSV upload error:', e); } catch(e){ console.error('CSV upload error',e); }
// Fetch weather
const weather = await fetchCurrentWeather(); const weather = await fetchCurrentWeather();
// Build Slack message text
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` +
@ -266,22 +264,21 @@ app.post('/api/area-readings', async (req, res) => {
`*_Humidity:_* ${H} % 💦\n` + `*_Humidity:_* ${H} % 💦\n` +
`*_Heat Index:_* ${hi} °F 🥵`; `*_Heat Index:_* ${hi} °F 🥵`;
// Send JSON with top-level "text" field
await axios.post( await axios.post(
process.env.SLACK_WEBHOOK_URL, process.env.SLACK_WEBHOOK_URL,
{ text, location: area, station_dock_door: stationCode, temperature: T, humidity: H, heat_index: hi }, { text },
{ headers: { 'Content-Type': 'application/json' } } { headers: {'Content-Type':'application/json'} }
); );
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 & CSV export ───────────────────────────────────────────
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);
@ -290,16 +287,14 @@ app.get('/api/readings', async (req, res) => {
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} }
}); });
app.get('/api/export', async (req,res) => {
// Export CSV
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();
@ -309,8 +304,7 @@ app.get('/api/export', async (req, res) => {
} }
}); });
// 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}`);
}); });