Update server.js

updates
This commit is contained in:
JoshBaneyCS 2025-04-30 00:33:02 +00:00
parent 997f7c34e2
commit 5f029f492a

103
server.js
View File

@ -14,19 +14,19 @@ 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(date) { function shortEST(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 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(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(), 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,21 +37,19 @@ 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) {
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();
let shift, start = new Date(est); let shift, start = new Date(est);
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'; shift = 'Day'; start.setHours(7,0,0,0);
start.setHours(7,0,0,0);
} else { } else {
shift = 'Night'; shift = 'Night'; start.setHours(17,30,0,0);
start.setHours(17,30,0,0);
} }
} else { } else {
shift = 'Night'; shift = 'Night';
@ -64,14 +62,14 @@ function getShiftInfo(now) {
} }
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('https://api.openweathermap.org/data/2.5/weather', { const { data } = await axios.get(
params: { zip: `${zip},us`, appid: key, units: 'imperial' } 'https://api.openweathermap.org/data/2.5/weather',
}); { 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}%`;
@ -81,7 +79,7 @@ async function fetchCurrentWeather() {
} }
} }
// MariaDB connection 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,
@ -96,21 +94,20 @@ const pool = mysql.createPool({
// Ensure readings table exists // Ensure readings table exists
(async () => { (async () => {
const createSQL = ` 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(createSQL);
})(); })();
// Middleware & static files (serve heatmap.html at '/') // Middleware & static (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' }));
@ -132,13 +129,13 @@ function broadcast(event, data) {
clients.forEach(c => c.write(msg)); clients.forEach(c => c.write(msg));
} }
// Dual dockdoor 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' });
} }
@ -152,15 +149,14 @@ app.post('/api/readings', async (req, res) => {
const sqlTs = formatDateEST(estNow); const sqlTs = formatDateEST(estNow);
const shortTs = shortEST(estNow); const shortTs = shortEST(estNow);
// Insert readings // 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]);
// Broadcast SSE // SSE broadcast
broadcast('new-reading', { broadcast('new-reading', {
location: 'Inbound', location: 'Inbound',
stationDockDoor: String(inD), stationDockDoor: String(inD),
@ -178,7 +174,7 @@ app.post('/api/readings', async (req, res) => {
heatIndex: hiOut heatIndex: hiOut
}); });
// Upload CSV of todays readings // Generate/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( const [rows] = await pool.execute(
@ -204,10 +200,10 @@ app.post('/api/readings', async (req, res) => {
`*_Humidity:_* ${outH} % 💦\n` + `*_Humidity:_* ${outH} % 💦\n` +
`*_Heat Index:_* ${hiOut} °F 🥵`; `*_Heat Index:_* ${hiOut} °F 🥵`;
// Trigger Slack workflow via Inputs map // Send JSON with top-level "text" field
await axios.post( await axios.post(
process.env.SLACK_WEBHOOK_URL, process.env.SLACK_WEBHOOK_URL,
{ inputs: { message: text } }, { text, shift, period, timestamp: shortTs },
{ headers: { 'Content-Type': 'application/json' } } { headers: { 'Content-Type': 'application/json' } }
); );
@ -218,7 +214,7 @@ app.post('/api/readings', async (req, res) => {
} }
}); });
// Area/Mod station readings endpoint // === Area/mod 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;
@ -229,18 +225,16 @@ app.post('/api/area-readings', async (req, res) => {
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 reading // 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]);
// Broadcast SSE // SSE
broadcast('new-area-reading', { broadcast('new-area-reading', {
location: area, location: area,
stationDockDoor: stationCode, stationDockDoor: stationCode,
@ -250,7 +244,7 @@ app.post('/api/area-readings', async (req, res) => {
heatIndex: hi heatIndex: hi
}); });
// Upload CSV // 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(
@ -272,10 +266,10 @@ app.post('/api/area-readings', async (req, res) => {
`*_Humidity:_* ${H} % 💦\n` + `*_Humidity:_* ${H} % 💦\n` +
`*_Heat Index:_* ${hi} °F 🥵`; `*_Heat Index:_* ${hi} °F 🥵`;
// Trigger Slack workflow via Inputs map // Send JSON with top-level "text" field
await axios.post( await axios.post(
process.env.SLACK_WEBHOOK_URL, process.env.SLACK_WEBHOOK_URL,
{ inputs: { message: text } }, { text, location: area, station_dock_door: stationCode, temperature: T, humidity: H, heat_index: hi },
{ headers: { 'Content-Type': 'application/json' } } { headers: { 'Content-Type': 'application/json' } }
); );
@ -305,7 +299,7 @@ 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 => {
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();
@ -319,3 +313,4 @@ app.get('/api/export', async (req, res) => {
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`); console.log(`Server running on http://localhost:${PORT}`);
}); });