From 12fdab583da123f2833ad3ecf2f1b85cf657e8d1 Mon Sep 17 00:00:00 2001 From: JoshBaneyCS Date: Tue, 29 Apr 2025 01:21:06 +0000 Subject: [PATCH] Upload files to "/" --- server.js | 309 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 server.js diff --git a/server.js b/server.js new file mode 100644 index 0000000..80fd67c --- /dev/null +++ b/server.js @@ -0,0 +1,309 @@ +// server.js +require('dotenv').config(); +const express = require('express'); +const mysql = require('mysql2/promise'); +const bodyParser = require('body-parser'); +const path = require('path'); +const axios = require('axios'); +const { uploadTrendsCsv } = require('./s3'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// In-memory shift counters +const shiftCounters = {}; + +/** Helpers **/ + +// Format a JS Date in EST as SQL DATETIME +function formatDateEST(date) { + const pad = n => n.toString().padStart(2,'0'); + return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())} ` + + `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; +} + +// Format a JS Date in EST as an ISO-like string (no Z) +function isoStringEST(date) { + const pad = n => n.toString().padStart(2,'0'); + return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())}` + + `T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; +} + +// Compute heat index (NOAA formula) +function computeHeatIndex(T, R) { + const [c1,c2,c3,c4,c5,c6,c7,c8,c9] = + [-42.379,2.04901523,10.14333127,-0.22475541, + -0.00683783,-0.05481717,0.00122874,0.00085282,-0.00000199]; + const HI = c1 + c2*T + c3*R + c4*T*R + + c5*T*T + c6*R*R + c7*T*T*R + + c8*T*R*R + c9*T*T*R*R; + return Math.round(HI*100)/100; +} + +// Determine shift (Day/Night), shiftStart, key & estNow in EST +function getShiftInfo(now) { + const estNow = new Date(now.toLocaleString('en-US',{ timeZone:'America/New_York' })); + const [h,m] = [estNow.getHours(), estNow.getMinutes()]; + let shift, shiftStart = new Date(estNow); + + if (h>7 || (h===7 && m>=0)) { + if (h<17 || (h===17 && m<30)) { + shift = 'Day'; shiftStart.setHours(7,0,0,0); + } else { + shift = 'Night'; shiftStart.setHours(17,30,0,0); + } + } else { + shift = 'Night'; + shiftStart.setDate(shiftStart.getDate()-1); + shiftStart.setHours(17,30,0,0); + } + + const key = `${shift}-${shiftStart.toISOString().slice(0,10)}-` + + `${shiftStart.getHours()}${shiftStart.getMinutes()}`; + return { shift, shiftStart, key, estNow }; +} + +// Fetch current weather from OpenWeatherMap +async function fetchCurrentWeather() { + const apiKey = process.env.WEATHER_API_KEY; + const zip = process.env.ZIP_CODE; + if (!apiKey || !zip) return null; + try { + const { data } = await axios.get( + 'https://api.openweathermap.org/data/2.5/weather', + { params: { zip:`${zip},us`, appid:apiKey, units:'imperial' } } + ); + return { + description: data.weather[0].description, + humidity: data.main.humidity, + temperature: data.main.temp + }; + } catch (err) { + console.error('Weather API error:', err.message); + return null; + } +} + +/** MariaDB pool **/ +const pool = mysql.createPool({ + host: process.env.DB_HOST, + 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, + enableKeepAlive: true, + keepAliveInitialDelay:10000 +}); + +// Ensure readings table exists (for dock doors & areas) +(async ()=>{ + const createSQL = ` + CREATE TABLE IF NOT EXISTS readings ( + id INT AUTO_INCREMENT PRIMARY KEY, + dockDoor INT, + direction VARCHAR(10), + region VARCHAR(20), + stationCode VARCHAR(10), + timestamp DATETIME NOT NULL, + temperature DOUBLE, + humidity DOUBLE, + heatIndex DOUBLE + ); + `; + await pool.execute(createSQL); +})(); + +/** SSE for real-time updates **/ +let clients = []; +app.get('/api/stream',(req,res)=>{ + res.set({ + '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); }); +}); +function broadcast(event,data){ + const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; + clients.forEach(c=>c.write(msg)); +} + +// Middleware & static +app.use(bodyParser.json()); +app.use(express.static(path.join(__dirname,'public'))); + +/** Dual-dock-door readings **/ +app.post('/api/readings',async(req,res)=>{ + try { + const { inbound={}, outbound={} } = req.body; + const { dockDoor: inDoor, temperature: inTemp, humidity: inHum } = inbound; + const { dockDoor: outDoor,temperature: outTemp,humidity: outHum } = outbound; + if ([inDoor,inTemp,inHum,outDoor,outTemp,outHum].some(v=>v===undefined)) + return res.status(400).json({error:'Missing inbound/outbound fields'}); + + const hiIn = computeHeatIndex(inTemp,inHum); + const hiOut = computeHeatIndex(outTemp,outHum); + const { shift, shiftStart, key, estNow } = getShiftInfo(new Date()); + shiftCounters[key]=(shiftCounters[key]||0)+1; + const period = shiftCounters[key]; + + const sqlTs = formatDateEST(estNow); + const bcTs = isoStringEST(estNow); + + // Insert dock readings + const insertSQL = ` + INSERT INTO readings + (dockDoor,direction,timestamp,temperature,humidity,heatIndex) + VALUES (?,?,?,?,?,?) + `; + await pool.execute(insertSQL,[inDoor,'inbound',sqlTs,inTemp,inHum,hiIn]); + await pool.execute(insertSQL,[outDoor,'outbound',sqlTs,outTemp,outHum,hiOut]); + + // Broadcast SSE + broadcast('new-reading',{ dockDoor:inDoor, direction:'inbound',timestamp:bcTs, + temperature:inTemp, humidity:inHum, heatIndex:hiIn }); + broadcast('new-reading',{ dockDoor:outDoor,direction:'outbound',timestamp:bcTs, + temperature:outTemp,humidity:outHum,heatIndex:hiOut }); + + // Build CSV & upload + const dateKey = `${estNow.getFullYear()}${String(estNow.getMonth()+1).padStart(2,'0')}${String(estNow.getDate()).padStart(2,'0')}`; + const [rows] = await pool.execute( + `SELECT * FROM readings WHERE DATE(timestamp)=CURDATE() ORDER BY timestamp ASC` + ); + let csvUrl=null; + try{ csvUrl = await uploadTrendsCsv(dateKey,rows); } + catch(e){ console.error('CSV upload error:',e); } + + // Weather + const weather = await fetchCurrentWeather(); + + // Slack payload + const slackPayload = { + text: 'New temperature readings recorded', + inbound_dockDoor: inDoor, + inbound_temperature: inTemp, + inbound_humidity: inHum, + hiIn, + outbound_dockDoor: outDoor, + outbound_temperature:outTemp, + outbound_humidity: outHum, + hiOut, + shift, + period, + timestamp: bcTs, + csvUrl, + current_weather: weather?.description || null, + current_humidity: weather?.humidity || null, + current_temperature: weather?.temperature || null + }; + await axios.post(process.env.SLACK_WEBHOOK_URL,slackPayload); + + res.json({ success:true, shift,period,csvUrl }); + } catch(err) { + console.error('POST /api/readings error:',err); + res.status(500).json({ error: err.message }); + } +}); + +/** Area (A-Mod / AFE-1 / AFE-2 / B-Mod) readings **/ +app.post('/api/area-readings',async(req,res)=>{ + try { + const { area, stationCode, temperature, humidity } = req.body; + if (!area||!stationCode||temperature==null||humidity==null) + return res.status(400).json({error:'Missing area,stationCode,temp,humidity'}); + + const hi = computeHeatIndex(temperature,humidity); + const { shift, shiftStart, key, estNow } = getShiftInfo(new Date()); + shiftCounters[key]=(shiftCounters[key]||0)+1; + const period = shiftCounters[key]; + const sqlTs = formatDateEST(estNow); + const bcTs = isoStringEST(estNow); + + // Insert area reading + const sql = ` + INSERT INTO readings + (region,stationCode,timestamp,temperature,humidity,heatIndex) + VALUES (?,?,?,?,?,?) + `; + await pool.execute(sql,[area,stationCode,sqlTs,temperature,humidity,hi]); + + // Broadcast SSE + broadcast('new-area-reading',{ region:area,stationCode,timestamp:bcTs, + temperature,humidity,heatIndex:hi }); + + // Build CSV & upload + const dateKey = `${estNow.getFullYear()}${String(estNow.getMonth()+1).padStart(2,'0')}${String(estNow.getDate()).padStart(2,'0')}`; + const [rows] = await pool.execute( + `SELECT * FROM readings WHERE DATE(timestamp)=CURDATE() ORDER BY timestamp ASC` + ); + let csvUrl=null; + try{ csvUrl = await uploadTrendsCsv(dateKey,rows); } + catch(e){ console.error('CSV upload error:',e); } + + // Weather + const weather = await fetchCurrentWeather(); + + // Slack payload + const slackPayload = { + text: 'New area temperature reading', + area, + stationCode, + temperature, + humidity, + heatIndex: hi, + shift, + period, + timestamp: bcTs, + csvUrl, + current_weather: weather?.description || null, + current_humidity: weather?.humidity || null, + current_temperature: weather?.temperature || null + }; + await axios.post(process.env.SLACK_WEBHOOK_URL,slackPayload); + + res.json({ success:true, shift,period,csvUrl }); + } catch(err) { + console.error('POST /api/area-readings error:',err); + res.status(500).json({ error: err.message }); + } +}); + +/** Return all readings **/ +app.get('/api/readings',async(req,res)=>{ + try { + const [rows] = await pool.execute(`SELECT * FROM readings ORDER BY timestamp ASC`); + res.json(rows); + } catch(err) { + console.error('GET /api/readings error:',err); + res.status(500).json({ error: err.message }); + } +}); + +/** Export CSV of all readings **/ +app.get('/api/export',async(req,res)=>{ + try { + const [rows] = await pool.execute(`SELECT * FROM readings ORDER BY timestamp ASC`); + res.setHeader('Content-disposition','attachment; filename=readings.csv'); + res.set('Content-Type','text/csv'); + res.write('id,dockDoor,direction,region,stationCode,timestamp,temperature,humidity,heatIndex\n'); + rows.forEach(r=>{ + const ts = r.timestamp instanceof Date ? formatDateEST(r.timestamp) : r.timestamp; + res.write(`${r.id},${r.dockDoor||''},${r.direction||''},${r.region||''},${r.stationCode||''},${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); + } +}); + +// Start server +app.listen(PORT,()=>{ + console.log(`Server running on http://localhost:${PORT}`); +});