diff --git a/.env b/.env index e69de29..56d681d 100644 --- a/.env +++ b/.env @@ -0,0 +1,10 @@ +# Server port +PORT=3000 + +# MariaDB connection +DB_CLIENT=mysql +DB_HOST=localhost +DB_PORT=3306 +DB_USER=your_db_user +DB_PASSWORD=your_db_password +DB_NAME=warehouse_heatmap \ No newline at end of file diff --git a/package.json b/package.json index 6b2faf7..35b3037 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ }, "dependencies": { "express": "^4.18.2", - "sqlite3": "^5.1.6", + "mysql2": "^3.2.0", + "knex": "^2.4.2", "body-parser": "^1.20.2" } - } - \ No newline at end of file + } \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..bc341ab Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/heatmap.html b/public/heatmap.html index a4ef415..c2d9e75 100644 --- a/public/heatmap.html +++ b/public/heatmap.html @@ -3,24 +3,26 @@ + - Heat Map + Warehouse Heat Map
- - Warehouse Heat Map - - - + + | Fuego - Heat Tracker + + +
-
+
+
+
+
- - \ No newline at end of file diff --git a/public/image/logo.png b/public/image/logo.png new file mode 100644 index 0000000..c27109c Binary files /dev/null and b/public/image/logo.png differ diff --git a/public/input.html b/public/input.html index dd2fd28..bf64a1d 100644 --- a/public/input.html +++ b/public/input.html @@ -3,29 +3,37 @@ + - Log Reading + Log Dual Readings -
- - Warehouse Heat Logger - - - + + | Fuego - Heat Tracker + + +
-

Enter Trailer Reading

+

Enter Inbound & Outbound Readings

- - - - - +
+ Inbound + + + +
+
+ Outbound + + + +
+
- + \ No newline at end of file diff --git a/public/scripts/heatmap.js b/public/scripts/heatmap.js index c6c3395..896f90a 100644 --- a/public/scripts/heatmap.js +++ b/public/scripts/heatmap.js @@ -1,24 +1,51 @@ -// Map setup -const map = L.map('heatmap-container').setView([0, 0], 1); -L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map); - -// Dock door coordinates config -const coords = {}; -for (let d=106; d<=138; d++) coords[d] = [0, d-122]; -for (let d=142; d<=201; d++) coords[d] = [ -1, d-172]; -for (let d=202; d<=210; d++) coords[d] = [ 1, d-206]; - -// Color scale -function getColor(h) { - const pct = (h - 70) / 30; - return `rgba(${255},${Math.round(255*(1-pct))},0,0.7)`; -} - -// Render markers & listen SSE -fetch('/api/readings').then(r=>r.json()).then(all => all.forEach(plot)); -function plot({dockDoor, heatIndex}) { - const [lat, lon] = coords[dockDoor]; - L.rectangle([[lat-0.1, lon-0.1],[lat+0.1, lon+0.1]],{color:getColor(heatIndex)}).addTo(map); -} - -new EventSource('/api/stream').addEventListener('new-reading', e => plot(JSON.parse(e.data))); +// door ranges +const doors = [ + ...Array.from({length: 138 - 124 + 1}, (_, i) => 124 + i), + ...Array.from({length: 201 - 142 + 1}, (_, i) => 142 + i), + ...Array.from({length: 209 - 202 + 1}, (_, i) => 202 + i), + ]; + + // NOAA heat‐index formula + function getColorFromHI(H) { + const pct = Math.min(Math.max((H - 70) / 30, 0), 1); + const r = 255; + const g = Math.round(255 * (1 - pct)); + return `rgba(${r},${g},0,0.8)`; + } + + function createGrid() { + const row = document.getElementById('dock-row'); + doors.forEach(d => { + const sq = document.createElement('div'); + sq.className = 'dock-square'; + sq.dataset.door = d; + row.appendChild(sq); + }); + } + + // apply a new reading to its square + function colorize(door, hi) { + const sq = document.querySelector(`.dock-square[data-door="${door}"]`); + if (!sq) return; + sq.style.background = getColorFromHI(hi); + } + + async function init() { + createGrid(); + // initial fill + const all = await fetch('/api/readings').then(r=>r.json()); + // pick latest per door + const latest = {}; + all.forEach(r => { latest[r.dockDoor] = r.heatIndex; }); + Object.entries(latest).forEach(([door, hi]) => colorize(door, hi)); + + // subscribe SSE + const es = new EventSource('/api/stream'); + es.addEventListener('new-reading', e => { + const { dockDoor, heatIndex } = JSON.parse(e.data); + colorize(dockDoor, heatIndex); + }); + } + + document.addEventListener('DOMContentLoaded', init); + \ No newline at end of file diff --git a/public/scripts/input.js b/public/scripts/input.js index df0848e..7936441 100644 --- a/public/scripts/input.js +++ b/public/scripts/input.js @@ -1,13 +1,29 @@ const form = document.getElementById('reading-form'); +const inDoor = document.getElementById('inboundDoor'); +const outDoor = document.getElementById('outboundDoor'); +const inTemp = document.getElementById('inboundTemp'); +const outTemp = document.getElementById('outboundTemp'); +const inHum = document.getElementById('inboundHum'); +const outHum = document.getElementById('outboundHum'); + +// Auto-set direction fields (readonly) if you want display +// omitted here since direction hidden in dual-input + form.addEventListener('submit', e => { e.preventDefault(); - const data = { - dockDoor: +document.getElementById('dockDoor').value, - timestamp: document.getElementById('timestamp').value, - temperature: +document.getElementById('temperature').value, - humidity: +document.getElementById('humidity').value, + const payload = { + inbound: { + dockDoor: +inDoor.value, + temperature: +inTemp.value, + humidity: +inHum.value + }, + outbound: { + dockDoor: +outDoor.value, + temperature: +outTemp.value, + humidity: +outHum.value + } }; fetch('/api/readings', { - method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).then(res => res.json()).then(() => form.reset()); -}); +}); \ No newline at end of file diff --git a/public/styles.css b/public/styles.css index 4249c77..a466360 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,3 +1,5 @@ +/* Favicon support (no CSS needed) */ + /* BINS Project Header & Buttons */ .main-header { background-color: #232F3E; @@ -7,37 +9,55 @@ position: fixed; top: 0; left: 0; right: 0; width: 100%; z-index: 1000; } - .logo { height: 40px; margin-right: 1rem; } - .header-title { color: #fff; font-size: 1.2rem; font-weight: bold; margin-right: auto; } - .nav-btn { - background-color: #FF9900; color: #111; - border: none; border-radius: 5px; - padding: 0.5rem 1rem; margin-left: 0.5rem; - cursor: pointer; font-weight: bold; + .main-header .logo { + height: 40px; + margin-right: 1rem; + } + .main-header .header-title { + color: #fff; + font-size: 1.2rem; + font-weight: bold; + margin-right: auto; + } + .main-header .nav-btn { + background-color: #FF9900; + color: #111; + border: none; + border-radius: 5px; + padding: 0.5rem 1rem; + margin-left: 0.5rem; + cursor: pointer; + font-weight: bold; + } + .main-header .nav-btn:hover { + background-color: #e48f00; } - .nav-btn:hover { background-color: #e48f00; } .page-container { margin-top: 70px; width: 95%; margin: 0 auto; padding: 1rem; - background-color: #fff; border-radius: 6px; + background-color: #fff; + border-radius: 6px; } .form-input, .form-textarea { - width: 90%; margin: 0.5rem auto; - display: block; padding: 0.5rem; - border: 1px solid #ccc; border-radius: 6px; + width: 90%; margin: 0.5rem auto; display: block; + padding: 0.5rem; border: 1px solid #ccc; border-radius: 6px; } .big-button { background-color: #28a745; color: white; - padding: 0.75rem 1.5rem; - border: none; border-radius: 6px; + padding: 0.75rem 1.5rem; border: none; border-radius: 6px; cursor: pointer; font-size: 1rem; margin: 1rem auto; display: block; } .big-button:hover { background-color: #218838; } .hidden { display: none; } - /* Existing styles */ - body { font-family: Arial, sans-serif; margin: 20px; } - #heatmap-container { height: 500px; width: 100%; } - canvas { max-width: 600px; margin: 20px auto; display: block; } \ No newline at end of file + /* Diagram styling */ + .diagram-container { width: 95%; margin: 0 auto; text-align: center; } + .dock-row { display: grid; grid-template-columns: repeat(83, 1fr); gap: 4px; margin-bottom: 8px; } + .dock-square { width: 100%; padding-top: 100%; position: relative; background: #ddd; border-radius: 2px; } + .dock-square::after { content: attr(data-door); position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.6); color: #333; } + .warehouse { width: 95%; height: 200px; margin: 0 auto; background: #ccc; border-radius: 6px; } + + /* Existing element styles */ + body { font-family: Arial, sans-serif; margin: 20px; } \ No newline at end of file diff --git a/public/trends.html b/public/trends.html index 304c74c..e6169f4 100644 --- a/public/trends.html +++ b/public/trends.html @@ -3,17 +3,18 @@ + - Trend Graphs + | Fuego - Heat Tracker
- - Temperature & Heat Index Trends - - - + + | Fuego - Heat Tracker + + +
diff --git a/server.js b/server.js index 56ac5a0..606a794 100644 --- a/server.js +++ b/server.js @@ -1,25 +1,40 @@ +require('dotenv').config(); const express = require('express'); -const sqlite3 = require('sqlite3').verbose(); const bodyParser = require('body-parser'); const path = require('path'); +const knex = require('knex'); +const axios = require('axios'); + +// Initialize MariaDB connection via Knex +const db = knex({ + client: process.env.DB_CLIENT, + connection: { + host: process.env.DB_HOST, + port: process.env.DB_PORT, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME + } +}); const app = express(); const PORT = process.env.PORT || 3000; +const slackWebhook = process.env.SLACK_WEBHOOK_URL; -// Initialize SQLite database -const db = new sqlite3.Database('./readings.db'); -db.serialize(() => { - db.run(` - CREATE TABLE IF NOT EXISTS readings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - dockDoor INTEGER, - timestamp TEXT, - temperature REAL, - humidity REAL, - heatIndex REAL - ) - `); -}); +// Create table if not exists, now with direction +(async () => { + if (!await db.schema.hasTable('readings')) { + await db.schema.createTable('readings', table => { + table.increments('id').primary(); + table.integer('dockDoor'); + table.string('direction'); + table.timestamp('timestamp'); + table.float('temperature'); + table.float('humidity'); + table.float('heatIndex'); + }); + } +})(); // Compute heat index (NOAA formula) function computeHeatIndex(T, R) { @@ -30,11 +45,18 @@ function computeHeatIndex(T, R) { return Math.round(HI * 100) / 100; } -// Middleware & static +// Determine direction based on door number +function getDirection(door) { + door = Number(door); + if (door >= 124 && door <= 138) return 'Inbound'; + if (door >= 142 && door <= 201) return 'Outbound'; + if (door >= 202 && door <= 209) return 'Inbound'; + return 'Unknown'; +} + app.use(bodyParser.json()); app.use(express.static(path.join(__dirname, 'public'))); -// SSE clients let clients = []; app.get('/api/stream', (req, res) => { res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' }); @@ -47,38 +69,58 @@ function broadcast(event, data) { clients.forEach(res => res.write(payload)); } -// APIs -app.post('/api/readings', (req, res) => { - const { dockDoor, timestamp, temperature, humidity } = req.body; - const heatIndex = computeHeatIndex(temperature, humidity); - db.run( - `INSERT INTO readings (dockDoor, timestamp, temperature, humidity, heatIndex) VALUES (?, ?, ?, ?, ?)`, - [dockDoor, timestamp, temperature, humidity, heatIndex], - function(err) { - if (err) return res.status(500).json({ error: err.message }); - const reading = { id: this.lastID, dockDoor, timestamp, temperature, humidity, heatIndex }; - broadcast('new-reading', reading); - res.json(reading); +app.post('/api/readings', async (req, res) => { + try { + const { inbound, outbound } = req.body; // each: {dockDoor,temperature,humidity} + const timestamp = new Date(); + const entries = [inbound, outbound].map(r => { + const direction = getDirection(r.dockDoor); + const heatIndex = computeHeatIndex(r.temperature, r.humidity); + return { ...r, direction, timestamp, heatIndex }; + }); + // Insert both + const ids = await db('readings').insert(entries); + const saved = entries.map((e, i) => ({ id: ids[i], ...e })); + + // Broadcast and respond + saved.forEach(reading => broadcast('new-reading', reading)); + + // Slack notification with both + if (slackWebhook) { + const textLines = saved.map(r => + `Door *${r.dockDoor}* (${r.direction}) – Temp: ${r.temperature}°F, Humidity: ${r.humidity}%, HI: ${r.heatIndex}` + ); + await axios.post(slackWebhook, { text: 'New dual readings:\n' + textLines.join('\n') }); } - ); + res.json(saved); + } catch (err) { + console.error('Error saving readings or sending Slack:', err); + res.status(500).json({ error: err.message }); + } }); -app.get('/api/readings', (req, res) => { - db.all(`SELECT * FROM readings ORDER BY timestamp ASC`, (err, rows) => { - if (err) return res.status(500).json({ error: err.message }); +app.get('/api/readings', async (req, res) => { + try { + const rows = await db('readings').orderBy('timestamp', 'asc'); res.json(rows); - }); + } catch (err) { + res.status(500).json({ error: err.message }); + } }); -app.get('/api/export', (req, res) => { - db.all(`SELECT * FROM readings ORDER BY timestamp ASC`, (err, rows) => { - if (err) return res.status(500).send(err.message); +app.get('/api/export', async (req, res) => { + try { + const rows = await db('readings').orderBy('timestamp', 'asc'); res.setHeader('Content-disposition', 'attachment; filename=readings.csv'); res.set('Content-Type', 'text/csv'); - res.write('id,dockDoor,timestamp,temperature,humidity,heatIndex\n'); - rows.forEach(r => res.write(`${r.id},${r.dockDoor},${r.timestamp},${r.temperature},${r.humidity},${r.heatIndex}\n`)); + res.write('id,dockDoor,direction,timestamp,temperature,humidity,heatIndex\n'); + rows.forEach(r => + res.write(`${r.id},${r.dockDoor},${r.direction},${r.timestamp},${r.temperature},${r.humidity},${r.heatIndex}\n`) + ); res.end(); - }); + } catch (err) { + res.status(500).send(err.message); + } }); -app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); +app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); \ No newline at end of file