First Commit

This commit is contained in:
Baney 2025-04-21 22:52:26 -04:00
commit f36cfa8ca0
10 changed files with 301 additions and 0 deletions

0
.env Normal file
View File

14
package.json Normal file
View File

@ -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"
}
}

26
public/heatmap.html Normal file
View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
<title>Heat Map</title>
</head>
<body>
<header class="main-header">
<img src="/images/logo.png" alt="Company Logo" class="logo">
<span class="header-title">Warehouse Heat Map</span>
<button class="nav-btn" onclick="window.location.href='/input.html'">Log Reading</button>
<button class="nav-btn" onclick="window.location.href='/heatmap.html'">Heat Map</button>
<button class="nav-btn" onclick="window.location.href='/trends.html'">Trends</button>
</header>
<div class="page-container">
<div id="heatmap-container"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.3/leaflet.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.3/leaflet.css"/>
<script src="scripts/heatmap.js"></script>
</body>
</html>

31
public/input.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
<title>Log Reading</title>
</head>
<body>
<!-- Header -->
<header class="main-header">
<img src="/images/logo.png" alt="Company Logo" class="logo">
<span class="header-title">Warehouse Heat Logger</span>
<button class="nav-btn" onclick="window.location.href='/input.html'">Log Reading</button>
<button class="nav-btn" onclick="window.location.href='/heatmap.html'">Heat Map</button>
<button class="nav-btn" onclick="window.location.href='/trends.html'">Trends</button>
</header>
<div class="page-container">
<h1>Enter Trailer Reading</h1>
<form id="reading-form">
<input class="form-input" type="number" id="dockDoor" placeholder="Dock Door #" min="106">
<input class="form-input" type="datetime-local" id="timestamp">
<input class="form-input" type="number" id="temperature" step="0.1" placeholder="Temperature (°F)">
<input class="form-input" type="number" id="humidity" step="0.1" placeholder="Humidity (%)">
<button type="submit" class="big-button">Submit</button>
</form>
</div>
<script src="scripts/input.js"></script>
</body>
</html>

24
public/scripts/heatmap.js Normal file
View File

@ -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)));

13
public/scripts/input.js Normal file
View File

@ -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());
});

40
public/scripts/trends.js Normal file
View File

@ -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));

43
public/styles.css Normal file
View File

@ -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; }

26
public/trends.html Normal file
View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="styles.css">
<title>Trend Graphs</title>
</head>
<body>
<header class="main-header">
<img src="/images/logo.png" alt="Company Logo" class="logo">
<span class="header-title">Temperature & Heat Index Trends</span>
<button class="nav-btn" onclick="window.location.href='/input.html'">Log Reading</button>
<button class="nav-btn" onclick="window.location.href='/heatmap.html'">Heat Map</button>
<button class="nav-btn" onclick="window.location.href='/trends.html'">Trends</button>
</header>
<div class="page-container">
<canvas id="dailyChart"></canvas>
<canvas id="weeklyChart"></canvas>
<canvas id="monthlyChart"></canvas>
</div>
<script src="scripts/trends.js"></script>
</body>
</html>

84
server.js Normal file
View File

@ -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}`));