updated server.js

Updated server.js for prod build
This commit is contained in:
JoshBaneyCS 2025-04-30 00:07:57 +00:00
parent 1fa7d96b23
commit 997f7c34e2

312
server.js
View File

@ -14,17 +14,17 @@ const PORT = process.env.PORT || 3000;
const shiftCounters = {}; const shiftCounters = {};
// Helpers // Helpers
const pad2 = n => n.toString().padStart(2, '0'); const pad2 = n => n.toString().padStart(2,'0');
function shortEST(d) { function shortEST(date) {
const est = new Date(d.toLocaleString('en-US', { timeZone: 'America/New_York' })); const est = new Date(date.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);
const hh = pad2(est.getHours()), mm = pad2(est.getMinutes()); const hh = pad2(est.getHours()), mm = pad2(est.getMinutes());
return `${M}/${D}/${YY} @${hh}:${mm}`; return `${M}/${D}/${YY} @${hh}:${mm}`;
} }
function formatDateEST(d) { function formatDateEST(date) {
const est = new Date(d.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(), M = pad2(est.getMonth()+1), D = pad2(est.getDate());
const hh = pad2(est.getHours()), mm = pad2(est.getMinutes()), ss = pad2(est.getSeconds()); const hh = pad2(est.getHours()), mm = pad2(est.getMinutes()), ss = pad2(est.getSeconds());
return `${y}-${M}-${D} ${hh}:${mm}:${ss}`; return `${y}-${M}-${D} ${hh}:${mm}:${ss}`;
@ -37,7 +37,7 @@ function computeHeatIndex(T, R) {
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;
} }
function getShiftInfo(now) { function getShiftInfo(now) {
@ -47,13 +47,15 @@ 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);
} }
} else { } else {
shift = 'Night'; shift = 'Night';
start.setDate(start.getDate()-1); start.setDate(start.getDate() - 1);
start.setHours(17,30,0,0); start.setHours(17,30,0,0);
} }
@ -62,27 +64,27 @@ function getShiftInfo(now) {
} }
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 (e) { } catch (err) {
console.error('Weather API error:', e.message); console.error('Weather API error:', err.message);
return 'Unavailable'; return 'Unavailable';
} }
} }
// MariaDB pool // MariaDB connection 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,
@ -92,196 +94,228 @@ const pool = mysql.createPool({
connectTimeout: 10000 connectTimeout: 10000
}); });
// Ensure table exists // Ensure readings table exists
(async()=>{ (async () => {
const sql=` const createSQL = `
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(createSQL);
})(); })();
// Middleware & static (serve heatmap.html as index) // Middleware & static files (serve heatmap.html at '/')
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',
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(event, data) {
const msg = `event: ${evt}\ndata: ${JSON.stringify(data)}\n\n`; const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
clients.forEach(c=>c.write(msg)); clients.forEach(c => c.write(msg));
} }
// ---- Dual dock-door endpoint ---- // Dual dockdoor 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 // Insert readings
const ins = ` const insertSQL = `
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(ins,['Outbound',String(outD),sqlTs,outT,outH,hiOut]); await pool.execute(insertSQL, ['Inbound', String(inD), sqlTs, inT, inH, hiIn]);
await pool.execute(insertSQL, ['Outbound', String(outD), sqlTs, outT, outH, hiOut]);
// SSE // Broadcast SSE
broadcast('new-reading',{ location:'Inbound',stationDockDoor:String(inD),timestamp:shortTs,temperature:inT,humidity:inH,heatIndex:hiIn }); broadcast('new-reading', {
broadcast('new-reading',{ location:'Outbound',stationDockDoor:String(outD),timestamp:shortTs,temperature:outT,humidity:outH,heatIndex:hiOut }); 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 CSV of todays readings
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) }catch(e){console.error(e)} try { csvUrl = await uploadTrendsCsv(dateKey, rows); }
catch (e) { console.error('CSV upload error:', e); }
// weather // Fetch weather
const weather = await fetchCurrentWeather(); const weather = await fetchCurrentWeather();
// build text // 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` +
`*_⬇ 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 🥵`;
const payload = { text }; // Trigger Slack workflow via Inputs map
await axios.post(
process.env.SLACK_WEBHOOK_URL,
{ inputs: { message: text } },
{ headers: { 'Content-Type': 'application/json' } }
);
// send as url-encoded string res.json({ success: true, shift, period, csvUrl });
const body = `payload=${encodeURIComponent(JSON.stringify(payload))}`; } catch (err) {
await axios.post(process.env.SLACK_WEBHOOK_URL, body, { console.error('POST /api/readings error:', err);
headers:{ 'Content-Type':'application/x-www-form-urlencoded' }
});
res.json({ success:true, shift, period, csvUrl });
} catch(err) {
console.error('POST /api/readings error:',err);
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });
} }
}); });
// ---- Area/Mod 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 // Insert reading
const ins = ` const insertSQL = `
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 SSE
broadcast('new-area-reading',{ location:area,stationDockDoor:stationCode,timestamp:shortTs,temperature:T,humidity:H,heatIndex:hi }); broadcast('new-area-reading', {
location: area,
// CSV stationDockDoor: stationCode,
const y=estNow.getFullYear(), m=pad2(estNow.getMonth()+1), d=pad2(estNow.getDate()); timestamp: shortTs,
const dateKey=`${y}${m}${d}`; temperature: T,
const [rows] = await pool.execute( humidity: H,
`SELECT * FROM readings WHERE DATE(timestamp)=CURDATE() ORDER BY timestamp` heatIndex: hi
);
let csvUrl=null;
try{ csvUrl=await uploadTrendsCsv(dateKey,rows) }catch(e){console.error(e)}
// weather
const weather = await fetchCurrentWeather();
// build text
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`+
`*_Heat Index:_* ${hi} °F 🥵`;
const payload = { text };
const body = `payload=${encodeURIComponent(JSON.stringify(payload))}`;
await axios.post(process.env.SLACK_WEBHOOK_URL, body, {
headers:{ 'Content-Type':'application/x-www-form-urlencoded' }
}); });
res.json({ success:true, csvUrl }); // Upload CSV
} catch(err) { const y = estNow.getFullYear(), m = pad2(estNow.getMonth()+1), d = pad2(estNow.getDate());
console.error('POST /api/area-readings error:',err); 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); }
// Fetch weather
const weather = await fetchCurrentWeather();
// Build Slack message text
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` +
`*_Heat Index:_* ${hi} °F 🥵`;
// Trigger Slack workflow via Inputs map
await axios.post(
process.env.SLACK_WEBHOOK_URL,
{ inputs: { message: text } },
{ headers: { 'Content-Type': 'application/json' } }
);
res.json({ success: true, csvUrl });
} catch (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 ---- // Export CSV
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, ()=>console.log(`Server running on http://localhost:${PORT}`)); app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});