From f36cfa8ca0366f723f929444b090741c0990b874 Mon Sep 17 00:00:00 2001 From: Baney Date: Mon, 21 Apr 2025 22:52:26 -0400 Subject: [PATCH] First Commit --- .env | 0 package.json | 14 +++++++ public/heatmap.html | 26 ++++++++++++ public/input.html | 31 +++++++++++++++ public/scripts/heatmap.js | 24 +++++++++++ public/scripts/input.js | 13 ++++++ public/scripts/trends.js | 40 +++++++++++++++++++ public/styles.css | 43 ++++++++++++++++++++ public/trends.html | 26 ++++++++++++ server.js | 84 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 301 insertions(+) create mode 100644 .env create mode 100644 package.json create mode 100644 public/heatmap.html create mode 100644 public/input.html create mode 100644 public/scripts/heatmap.js create mode 100644 public/scripts/input.js create mode 100644 public/scripts/trends.js create mode 100644 public/styles.css create mode 100644 public/trends.html create mode 100644 server.js diff --git a/.env b/.env new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b2faf7 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "warehouse-heatmap-app", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.18.2", + "sqlite3": "^5.1.6", + "body-parser": "^1.20.2" + } + } + \ No newline at end of file diff --git a/public/heatmap.html b/public/heatmap.html new file mode 100644 index 0000000..a4ef415 --- /dev/null +++ b/public/heatmap.html @@ -0,0 +1,26 @@ + + + + + + + Heat Map + + +
+ + Warehouse Heat Map + + + +
+ +
+
+
+ + + + + + \ No newline at end of file diff --git a/public/input.html b/public/input.html new file mode 100644 index 0000000..dd2fd28 --- /dev/null +++ b/public/input.html @@ -0,0 +1,31 @@ + + + + + + + Log Reading + + + +
+ + Warehouse Heat Logger + + + +
+ +
+

Enter Trailer Reading

+
+ + + + + +
+
+ + + diff --git a/public/scripts/heatmap.js b/public/scripts/heatmap.js new file mode 100644 index 0000000..c6c3395 --- /dev/null +++ b/public/scripts/heatmap.js @@ -0,0 +1,24 @@ +// 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))); diff --git a/public/scripts/input.js b/public/scripts/input.js new file mode 100644 index 0000000..df0848e --- /dev/null +++ b/public/scripts/input.js @@ -0,0 +1,13 @@ +const form = document.getElementById('reading-form'); +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, + }; + fetch('/api/readings', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) + }).then(res => res.json()).then(() => form.reset()); +}); diff --git a/public/scripts/trends.js b/public/scripts/trends.js new file mode 100644 index 0000000..e54cdf1 --- /dev/null +++ b/public/scripts/trends.js @@ -0,0 +1,40 @@ +const ctxD = document.getElementById('dailyChart').getContext('2d'); +const ctxW = document.getElementById('weeklyChart').getContext('2d'); +const ctxM = document.getElementById('monthlyChart').getContext('2d'); + +async function drawTrend(ctx, period) { + const all = await fetch('/api/readings').then(r=>r.json()); + // group by day/week/month + const groups = {}; + all.forEach(r => { + const d = new Date(r.timestamp); + let key; + if (period==='daily') key = d.toISOString().slice(0,10); + if (period==='weekly') key = `${d.getFullYear()}-W${Math.ceil(d.getDate()/7)}`; + if (period==='monthly') key = d.toISOString().slice(0,7); + groups[key] = groups[key]||[]; + groups[key].push(r.heatIndex); + }); + const labels = Object.keys(groups); + const data = labels.map(k => { + const arr = groups[k]; + return { + max: Math.max(...arr), + min: Math.min(...arr), + avg: arr.reduce((a,b)=>a+b,0)/arr.length + }; + }); + new Chart(ctx, { + type: 'line', + data: { + labels, + datasets: [ + { label: 'Max', data: data.map(x=>x.max) }, + { label: 'Avg', data: data.map(x=>x.avg) }, + { label: 'Min', data: data.map(x=>x.min) } + ] + } + }); +} + +['daily','weekly','monthly'].forEach((p,i)=>drawTrend([ctxD,ctxW,ctxM][i], p)); diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..4249c77 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,43 @@ +/* BINS Project Header & Buttons */ +.main-header { + background-color: #232F3E; + display: flex; + align-items: center; + padding: 0.5rem 1rem; + 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; + } + .nav-btn:hover { background-color: #e48f00; } + .page-container { + margin-top: 70px; + width: 95%; margin: 0 auto; + padding: 1rem; + 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; + } + .big-button { + background-color: #28a745; color: white; + 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 diff --git a/public/trends.html b/public/trends.html new file mode 100644 index 0000000..304c74c --- /dev/null +++ b/public/trends.html @@ -0,0 +1,26 @@ + + + + + + + + Trend Graphs + + +
+ + Temperature & Heat Index Trends + + + +
+ +
+ + + +
+ + + \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..56ac5a0 --- /dev/null +++ b/server.js @@ -0,0 +1,84 @@ +const express = require('express'); +const sqlite3 = require('sqlite3').verbose(); +const bodyParser = require('body-parser'); +const path = require('path'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// 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 + ) + `); +}); + +// Compute heat index (NOAA formula) +function computeHeatIndex(T, R) { + const c1 = -42.379, c2 = 2.04901523, c3 = 10.14333127; + const c4 = -0.22475541, c5 = -6.83783e-3, c6 = -5.481717e-2; + const c7 = 1.22874e-3, c8 = 8.5282e-4, c9 = -1.99e-6; + 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; +} + +// Middleware & static +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' }); + res.flushHeaders(); + clients.push(res); + req.on('close', () => { clients = clients.filter(c => c !== res); }); +}); +function broadcast(event, data) { + const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; + 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.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 }); + res.json(rows); + }); +}); + +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); + 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.end(); + }); +}); + +app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));