diff --git a/server.js b/server.js index a922de3..78b25f9 100644 --- a/server.js +++ b/server.js @@ -10,16 +10,14 @@ const { uploadTrendsCsv } = require('./s3'); const app = express(); const PORT = process.env.PORT || 3000; -// In‐memory shift counters +// In-memory shift counters const shiftCounters = {}; -// ─── Helpers ───────────────────────────────────────────────────────────────── -// pad two digits -function pad2(n) { - return n.toString().padStart(2, '0'); -} +// ─── Helpers ────────────────────────────────────────────────────────────────── +// Pad to 2 digits +function pad2(n) { return n.toString().padStart(2, '0'); } -// Format an epoch‐ms timestamp for Slack: "M/D/YY @HH:mm" in America/New_York +// Format epoch_ms → "M/D/YY @HH:mm" in America/New_York for Slack/SSE function formatForSlack(epoch) { return new Date(epoch).toLocaleString('en-US', { timeZone: 'America/New_York', @@ -32,7 +30,7 @@ function formatForSlack(epoch) { }).replace(',', ' @'); } -// Compute NOAA heat‐index +// NOAA heat‐index formula function computeHeatIndex(T, R) { const [c1,c2,c3,c4,c5,c6,c7,c8,c9] = [ -42.379, 2.04901523, 10.14333127, -0.22475541, @@ -44,9 +42,9 @@ function computeHeatIndex(T, R) { return Math.round(HI * 100) / 100; } -// Determine Day/Night shift & rolling period key based on a JS epoch +// Determine shift (Day/Night) and period key from epoch_ms function getShiftInfo(epoch) { - // convert epoch to an EST Date object by round‐trip through toLocaleString + // Convert epoch to EST Date by string-roundtrip const estString = new Date(epoch) .toLocaleString('en-US', { timeZone: 'America/New_York' }); const est = new Date(estString); @@ -55,11 +53,9 @@ function getShiftInfo(epoch) { if (h > 7 || (h === 7 && m >= 0)) { if (h < 17 || (h === 17 && m < 30)) { - shift = 'Day'; - start.setHours(7, 0, 0, 0); + shift = 'Day'; start.setHours(7, 0, 0, 0); } else { - shift = 'Night'; - start.setHours(17, 30, 0, 0); + shift = 'Night'; start.setHours(17, 30, 0, 0); } } else { shift = 'Night'; @@ -73,12 +69,14 @@ function getShiftInfo(epoch) { // Fetch current weather forecast for Baltimore 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'; try { const { data } = await axios.get( - 'https://api.openweathermap.org/data/2.5/weather', - { 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 hi = Math.round(data.main.temp_max); @@ -104,18 +102,18 @@ const pool = mysql.createPool({ }); (async () => { - // Create readings table with epoch_ms instead of timestamp + // Create readings table with only epoch_ms (no timestamp column) await pool.execute(` CREATE TABLE IF NOT EXISTS readings ( - id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - location VARCHAR(20) NOT NULL, - stationDockDoor VARCHAR(10) NOT NULL, - epoch_ms BIGINT NOT NULL, - temperature DOUBLE, - humidity DOUBLE, - heatIndex DOUBLE, - INDEX idx_time (epoch_ms), - INDEX idx_loc (location) + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + location VARCHAR(20) NOT NULL, + stationDockDoor VARCHAR(10) NOT NULL, + epoch_ms BIGINT NOT NULL, + temperature DOUBLE, + humidity DOUBLE, + heatIndex DOUBLE, + INDEX idx_time (epoch_ms), + INDEX idx_loc (location) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; `); })(); @@ -151,14 +149,15 @@ app.post('/api/readings', async (req, res) => { return res.status(400).json({ error: 'Missing fields' }); } - const epoch = Date.now(); - const hiIn = computeHeatIndex(inT, inH); - const hiOut = computeHeatIndex(outT, outH); + const epoch = Date.now(); + const hiIn = computeHeatIndex(inT, inH); + const hiOut = computeHeatIndex(outT, outH); const { shift, key, estNow } = getShiftInfo(epoch); shiftCounters[key] = (shiftCounters[key] || 0) + 1; - const period = shiftCounters[key]; + const period = shiftCounters[key]; + const slackTs = formatForSlack(epoch); - // Insert inbound and outbound + // Insert inbound/outbound into epoch_ms const insertSQL = ` INSERT INTO readings(location,stationDockDoor,epoch_ms,temperature,humidity,heatIndex) VALUES (?, ?, ?, ?, ?, ?) @@ -166,41 +165,37 @@ app.post('/api/readings', async (req, res) => { await pool.execute(insertSQL, ['Inbound', String(inD), epoch, inT, inH, hiIn]); await pool.execute(insertSQL, ['Outbound', String(outD), epoch, outT, outH, hiOut]); - // Broadcast via SSE - const slackTs = formatForSlack(epoch); + // SSE broadcast broadcast('new-reading', { - location: 'Inbound', + location: 'Inbound', stationDockDoor: String(inD), - timestamp: slackTs, - temperature: inT, - humidity: inH, - heatIndex: hiIn + timestamp: slackTs, + temperature: inT, + humidity: inH, + heatIndex: hiIn }); broadcast('new-reading', { - location: 'Outbound', + location: 'Outbound', stationDockDoor: String(outD), - timestamp: slackTs, - temperature: outT, - humidity: outH, - heatIndex: hiOut + timestamp: slackTs, + temperature: outT, + humidity: outH, + heatIndex: hiOut }); - // Upload today's CSV + // Upload CSV of today’s readings const y = estNow.getFullYear(), m = pad2(estNow.getMonth()+1), d = pad2(estNow.getDate()); const dateKey = `${y}${m}${d}`; - const [rows] = await pool.execute( - `SELECT * FROM readings + const [rows] = await pool.execute(` + SELECT * FROM readings WHERE DATE(FROM_UNIXTIME(epoch_ms/1000)) = CURDATE() - ORDER BY epoch_ms` - ); + ORDER BY epoch_ms + `); let csvUrl = null; - try { - csvUrl = await uploadTrendsCsv(dateKey, rows); - } catch (e) { - console.error('CSV upload error:', e); - } + try { csvUrl = await uploadTrendsCsv(dateKey, rows); } + catch (e) { console.error('CSV upload error:', e); } - // Send Slack notification + // Slack notification const weather = await fetchCurrentWeather(); const text = `*_${shift} shift Period ${period} dock/trailer temperature checks for ${slackTs}_*\n` + @@ -235,39 +230,35 @@ app.post('/api/area-readings', async (req, res) => { return res.status(400).json({ error: 'Missing fields' }); } - const epoch = Date.now(); - const hi = computeHeatIndex(T, H); + const epoch = Date.now(); + const hi = computeHeatIndex(T, H); const { shift, key, estNow } = getShiftInfo(epoch); - - await pool.execute( - `INSERT INTO readings(location,stationDockDoor,epoch_ms,temperature,humidity,heatIndex) - VALUES (?, ?, ?, ?, ?, ?)`, - [area, stationCode, epoch, T, H, hi] - ); - const slackTs = formatForSlack(epoch); + + await pool.execute(` + INSERT INTO readings(location,stationDockDoor,epoch_ms,temperature,humidity,heatIndex) + VALUES (?, ?, ?, ?, ?, ?) + `, [area, stationCode, epoch, T, H, hi]); + broadcast('new-area-reading', { - location: area, + location: area, stationDockDoor: stationCode, - timestamp: slackTs, - temperature: T, - humidity: H, - heatIndex: hi + timestamp: slackTs, + temperature: T, + humidity: H, + heatIndex: hi }); const y = estNow.getFullYear(), m = pad2(estNow.getMonth()+1), d = pad2(estNow.getDate()); const dateKey = `${y}${m}${d}`; - const [rows] = await pool.execute( - `SELECT * FROM readings + const [rows] = await pool.execute(` + SELECT * FROM readings WHERE DATE(FROM_UNIXTIME(epoch_ms/1000)) = CURDATE() - ORDER BY epoch_ms` - ); + ORDER BY epoch_ms + `); let csvUrl = null; - try { - csvUrl = await uploadTrendsCsv(dateKey, rows); - } catch (e) { - console.error('CSV upload error:', e); - } + try { csvUrl = await uploadTrendsCsv(dateKey, rows); } + catch (e) { console.error('CSV upload error:', e); } const weather = await fetchCurrentWeather(); const text = @@ -291,7 +282,7 @@ app.post('/api/area-readings', async (req, res) => { } }); -// ─── Fetch All Readings ──────────────────────────────────────────────────────── +// ─── Fetch All & Export ─────────────────────────────────────────────────────── app.get('/api/readings', async (req, res) => { try { const [rows] = await pool.execute(`SELECT * FROM readings ORDER BY epoch_ms`); @@ -301,8 +292,6 @@ app.get('/api/readings', async (req, res) => { res.status(500).json({ error: err.message }); } }); - -// ─── Export CSV ─────────────────────────────────────────────────────────────── app.get('/api/export', async (req, res) => { try { const [rows] = await pool.execute(`SELECT * FROM readings ORDER BY epoch_ms`); @@ -310,10 +299,7 @@ app.get('/api/export', async (req, res) => { res.set('Content-Type', 'text/csv'); res.write('id,location,stationDockDoor,epoch_ms,temperature,humidity,heatIndex\n'); rows.forEach(r => { - res.write( - `${r.id},${r.location},${r.stationDockDoor},${r.epoch_ms},` + - `${r.temperature},${r.humidity},${r.heatIndex}\n` - ); + res.write(`${r.id},${r.location},${r.stationDockDoor},${r.epoch_ms},${r.temperature},${r.humidity},${r.heatIndex}\n`); }); res.end(); } catch (err) {