First Commit
This commit is contained in:
parent
f36cfa8ca0
commit
433b7c4681
10
.env
10
.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
|
@ -7,8 +7,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"sqlite3": "^5.1.6",
|
"mysql2": "^3.2.0",
|
||||||
|
"knex": "^2.4.2",
|
||||||
"body-parser": "^1.20.2"
|
"body-parser": "^1.20.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@ -3,24 +3,26 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
<title>Heat Map</title>
|
<title>Warehouse Heat Map</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="main-header">
|
<header class="main-header">
|
||||||
<img src="/images/logo.png" alt="Company Logo" class="logo">
|
<img src="/image/logo.png" alt="Amazon Logo" class="logo">
|
||||||
<span class="header-title">Warehouse Heat Map</span>
|
<span class="header-title">| Fuego - Heat Tracker</span>
|
||||||
<button class="nav-btn" onclick="window.location.href='/input.html'">Log Reading</button>
|
<button class="nav-btn" onclick="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="location.href='/heatmap.html'">Heat Map</button>
|
||||||
<button class="nav-btn" onclick="window.location.href='/trends.html'">Trends</button>
|
<button class="nav-btn" onclick="location.href='/trends.html'">Trends</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<div id="heatmap-container"></div>
|
<div class="diagram-container">
|
||||||
|
<div id="dock-row" class="dock-row"></div>
|
||||||
|
<div class="warehouse"></div>
|
||||||
|
</div>
|
||||||
</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>
|
<script src="scripts/heatmap.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
BIN
public/image/logo.png
Normal file
BIN
public/image/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
@ -3,27 +3,35 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
<title>Log Reading</title>
|
<title>Log Dual Readings</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Header -->
|
|
||||||
<header class="main-header">
|
<header class="main-header">
|
||||||
<img src="/images/logo.png" alt="Company Logo" class="logo">
|
<img src="/image/logo.png" alt="Amazon Logo" class="logo">
|
||||||
<span class="header-title">Warehouse Heat Logger</span>
|
<span class="header-title"> | Fuego - Heat Tracker</span>
|
||||||
<button class="nav-btn" onclick="window.location.href='/input.html'">Log Reading</button>
|
<button class="nav-btn" onclick="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="location.href='/heatmap.html'">Heat Map</button>
|
||||||
<button class="nav-btn" onclick="window.location.href='/trends.html'">Trends</button>
|
<button class="nav-btn" onclick="location.href='/trends.html'">Trends</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<h1>Enter Trailer Reading</h1>
|
<h1>Enter Inbound & Outbound Readings</h1>
|
||||||
<form id="reading-form">
|
<form id="reading-form">
|
||||||
<input class="form-input" type="number" id="dockDoor" placeholder="Dock Door #" min="106">
|
<fieldset>
|
||||||
<input class="form-input" type="datetime-local" id="timestamp">
|
<legend>Inbound</legend>
|
||||||
<input class="form-input" type="number" id="temperature" step="0.1" placeholder="Temperature (°F)">
|
<input class="form-input" id="inboundDoor" type="number" placeholder="Dock Door # (124–138,202–209)" required>
|
||||||
<input class="form-input" type="number" id="humidity" step="0.1" placeholder="Humidity (%)">
|
<input class="form-input" id="inboundTemp" type="number" step="0.1" placeholder="Temperature (°F)" required>
|
||||||
<button type="submit" class="big-button">Submit</button>
|
<input class="form-input" id="inboundHum" type="number" step="0.1" placeholder="Humidity (%)" required>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Outbound</legend>
|
||||||
|
<input class="form-input" id="outboundDoor" type="number" placeholder="Dock Door # (142–201)" required>
|
||||||
|
<input class="form-input" id="outboundTemp" type="number" step="0.1" placeholder="Temperature (°F)" required>
|
||||||
|
<input class="form-input" id="outboundHum" type="number" step="0.1" placeholder="Humidity (%)" required>
|
||||||
|
</fieldset>
|
||||||
|
<button type="submit" class="big-button">Submit Both</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<script src="scripts/input.js"></script>
|
<script src="scripts/input.js"></script>
|
||||||
|
@ -1,24 +1,51 @@
|
|||||||
// Map setup
|
// door ranges
|
||||||
const map = L.map('heatmap-container').setView([0, 0], 1);
|
const doors = [
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
|
...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),
|
||||||
|
];
|
||||||
|
|
||||||
// Dock door coordinates config
|
// NOAA heat‐index formula
|
||||||
const coords = {};
|
function getColorFromHI(H) {
|
||||||
for (let d=106; d<=138; d++) coords[d] = [0, d-122];
|
const pct = Math.min(Math.max((H - 70) / 30, 0), 1);
|
||||||
for (let d=142; d<=201; d++) coords[d] = [ -1, d-172];
|
const r = 255;
|
||||||
for (let d=202; d<=210; d++) coords[d] = [ 1, d-206];
|
const g = Math.round(255 * (1 - pct));
|
||||||
|
return `rgba(${r},${g},0,0.8)`;
|
||||||
|
}
|
||||||
|
|
||||||
// Color scale
|
function createGrid() {
|
||||||
function getColor(h) {
|
const row = document.getElementById('dock-row');
|
||||||
const pct = (h - 70) / 30;
|
doors.forEach(d => {
|
||||||
return `rgba(${255},${Math.round(255*(1-pct))},0,0.7)`;
|
const sq = document.createElement('div');
|
||||||
}
|
sq.className = 'dock-square';
|
||||||
|
sq.dataset.door = d;
|
||||||
|
row.appendChild(sq);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Render markers & listen SSE
|
// apply a new reading to its square
|
||||||
fetch('/api/readings').then(r=>r.json()).then(all => all.forEach(plot));
|
function colorize(door, hi) {
|
||||||
function plot({dockDoor, heatIndex}) {
|
const sq = document.querySelector(`.dock-square[data-door="${door}"]`);
|
||||||
const [lat, lon] = coords[dockDoor];
|
if (!sq) return;
|
||||||
L.rectangle([[lat-0.1, lon-0.1],[lat+0.1, lon+0.1]],{color:getColor(heatIndex)}).addTo(map);
|
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);
|
||||||
|
|
||||||
new EventSource('/api/stream').addEventListener('new-reading', e => plot(JSON.parse(e.data)));
|
|
||||||
|
@ -1,13 +1,29 @@
|
|||||||
const form = document.getElementById('reading-form');
|
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 => {
|
form.addEventListener('submit', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = {
|
const payload = {
|
||||||
dockDoor: +document.getElementById('dockDoor').value,
|
inbound: {
|
||||||
timestamp: document.getElementById('timestamp').value,
|
dockDoor: +inDoor.value,
|
||||||
temperature: +document.getElementById('temperature').value,
|
temperature: +inTemp.value,
|
||||||
humidity: +document.getElementById('humidity').value,
|
humidity: +inHum.value
|
||||||
|
},
|
||||||
|
outbound: {
|
||||||
|
dockDoor: +outDoor.value,
|
||||||
|
temperature: +outTemp.value,
|
||||||
|
humidity: +outHum.value
|
||||||
|
}
|
||||||
};
|
};
|
||||||
fetch('/api/readings', {
|
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());
|
}).then(res => res.json()).then(() => form.reset());
|
||||||
});
|
});
|
@ -1,3 +1,5 @@
|
|||||||
|
/* Favicon support (no CSS needed) */
|
||||||
|
|
||||||
/* BINS Project Header & Buttons */
|
/* BINS Project Header & Buttons */
|
||||||
.main-header {
|
.main-header {
|
||||||
background-color: #232F3E;
|
background-color: #232F3E;
|
||||||
@ -7,37 +9,55 @@
|
|||||||
position: fixed; top: 0; left: 0; right: 0;
|
position: fixed; top: 0; left: 0; right: 0;
|
||||||
width: 100%; z-index: 1000;
|
width: 100%; z-index: 1000;
|
||||||
}
|
}
|
||||||
.logo { height: 40px; margin-right: 1rem; }
|
.main-header .logo {
|
||||||
.header-title { color: #fff; font-size: 1.2rem; font-weight: bold; margin-right: auto; }
|
height: 40px;
|
||||||
.nav-btn {
|
margin-right: 1rem;
|
||||||
background-color: #FF9900; color: #111;
|
}
|
||||||
border: none; border-radius: 5px;
|
.main-header .header-title {
|
||||||
padding: 0.5rem 1rem; margin-left: 0.5rem;
|
color: #fff;
|
||||||
cursor: pointer; font-weight: bold;
|
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 {
|
.page-container {
|
||||||
margin-top: 70px;
|
margin-top: 70px;
|
||||||
width: 95%; margin: 0 auto;
|
width: 95%; margin: 0 auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background-color: #fff; border-radius: 6px;
|
background-color: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
.form-input, .form-textarea {
|
.form-input, .form-textarea {
|
||||||
width: 90%; margin: 0.5rem auto;
|
width: 90%; margin: 0.5rem auto; display: block;
|
||||||
display: block; padding: 0.5rem;
|
padding: 0.5rem; border: 1px solid #ccc; border-radius: 6px;
|
||||||
border: 1px solid #ccc; border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
.big-button {
|
.big-button {
|
||||||
background-color: #28a745; color: white;
|
background-color: #28a745; color: white;
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem; border: none; border-radius: 6px;
|
||||||
border: none; border-radius: 6px;
|
|
||||||
cursor: pointer; font-size: 1rem;
|
cursor: pointer; font-size: 1rem;
|
||||||
margin: 1rem auto; display: block;
|
margin: 1rem auto; display: block;
|
||||||
}
|
}
|
||||||
.big-button:hover { background-color: #218838; }
|
.big-button:hover { background-color: #218838; }
|
||||||
.hidden { display: none; }
|
.hidden { display: none; }
|
||||||
|
|
||||||
/* Existing styles */
|
/* 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; }
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||||
#heatmap-container { height: 500px; width: 100%; }
|
|
||||||
canvas { max-width: 600px; margin: 20px auto; display: block; }
|
|
@ -3,17 +3,18 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
<title>Trend Graphs</title>
|
<title>| Fuego - Heat Tracker</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="main-header">
|
<header class="main-header">
|
||||||
<img src="/images/logo.png" alt="Company Logo" class="logo">
|
<img src="/image/logo.png" alt="Amazon Logo" class="logo">
|
||||||
<span class="header-title">Temperature & Heat Index Trends</span>
|
<span class="header-title">| Fuego - Heat Tracker</span>
|
||||||
<button class="nav-btn" onclick="window.location.href='/input.html'">Log Reading</button>
|
<button class="nav-btn" onclick="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="location.href='/heatmap.html'">Heat Map</button>
|
||||||
<button class="nav-btn" onclick="window.location.href='/trends.html'">Trends</button>
|
<button class="nav-btn" onclick="location.href='/trends.html'">Trends</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
|
122
server.js
122
server.js
@ -1,25 +1,40 @@
|
|||||||
|
require('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const sqlite3 = require('sqlite3').verbose();
|
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
const path = require('path');
|
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 app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const slackWebhook = process.env.SLACK_WEBHOOK_URL;
|
||||||
|
|
||||||
// Initialize SQLite database
|
// Create table if not exists, now with direction
|
||||||
const db = new sqlite3.Database('./readings.db');
|
(async () => {
|
||||||
db.serialize(() => {
|
if (!await db.schema.hasTable('readings')) {
|
||||||
db.run(`
|
await db.schema.createTable('readings', table => {
|
||||||
CREATE TABLE IF NOT EXISTS readings (
|
table.increments('id').primary();
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
table.integer('dockDoor');
|
||||||
dockDoor INTEGER,
|
table.string('direction');
|
||||||
timestamp TEXT,
|
table.timestamp('timestamp');
|
||||||
temperature REAL,
|
table.float('temperature');
|
||||||
humidity REAL,
|
table.float('humidity');
|
||||||
heatIndex REAL
|
table.float('heatIndex');
|
||||||
)
|
});
|
||||||
`);
|
}
|
||||||
});
|
})();
|
||||||
|
|
||||||
// Compute heat index (NOAA formula)
|
// Compute heat index (NOAA formula)
|
||||||
function computeHeatIndex(T, R) {
|
function computeHeatIndex(T, R) {
|
||||||
@ -30,11 +45,18 @@ function computeHeatIndex(T, R) {
|
|||||||
return Math.round(HI * 100) / 100;
|
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(bodyParser.json());
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
// SSE clients
|
|
||||||
let clients = [];
|
let clients = [];
|
||||||
app.get('/api/stream', (req, res) => {
|
app.get('/api/stream', (req, res) => {
|
||||||
res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
|
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));
|
clients.forEach(res => res.write(payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIs
|
app.post('/api/readings', async (req, res) => {
|
||||||
app.post('/api/readings', (req, res) => {
|
try {
|
||||||
const { dockDoor, timestamp, temperature, humidity } = req.body;
|
const { inbound, outbound } = req.body; // each: {dockDoor,temperature,humidity}
|
||||||
const heatIndex = computeHeatIndex(temperature, humidity);
|
const timestamp = new Date();
|
||||||
db.run(
|
const entries = [inbound, outbound].map(r => {
|
||||||
`INSERT INTO readings (dockDoor, timestamp, temperature, humidity, heatIndex) VALUES (?, ?, ?, ?, ?)`,
|
const direction = getDirection(r.dockDoor);
|
||||||
[dockDoor, timestamp, temperature, humidity, heatIndex],
|
const heatIndex = computeHeatIndex(r.temperature, r.humidity);
|
||||||
function(err) {
|
return { ...r, direction, timestamp, heatIndex };
|
||||||
if (err) return res.status(500).json({ error: err.message });
|
});
|
||||||
const reading = { id: this.lastID, dockDoor, timestamp, temperature, humidity, heatIndex };
|
// Insert both
|
||||||
broadcast('new-reading', reading);
|
const ids = await db('readings').insert(entries);
|
||||||
res.json(reading);
|
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) => {
|
app.get('/api/readings', async (req, res) => {
|
||||||
db.all(`SELECT * FROM readings ORDER BY timestamp ASC`, (err, rows) => {
|
try {
|
||||||
if (err) return res.status(500).json({ error: err.message });
|
const rows = await db('readings').orderBy('timestamp', 'asc');
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
});
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/export', (req, res) => {
|
app.get('/api/export', async (req, res) => {
|
||||||
db.all(`SELECT * FROM readings ORDER BY timestamp ASC`, (err, rows) => {
|
try {
|
||||||
if (err) return res.status(500).send(err.message);
|
const rows = await db('readings').orderBy('timestamp', 'asc');
|
||||||
res.setHeader('Content-disposition', 'attachment; filename=readings.csv');
|
res.setHeader('Content-disposition', 'attachment; filename=readings.csv');
|
||||||
res.set('Content-Type', 'text/csv');
|
res.set('Content-Type', 'text/csv');
|
||||||
res.write('id,dockDoor,timestamp,temperature,humidity,heatIndex\n');
|
res.write('id,dockDoor,direction,timestamp,temperature,humidity,heatIndex\n');
|
||||||
rows.forEach(r => res.write(`${r.id},${r.dockDoor},${r.timestamp},${r.temperature},${r.humidity},${r.heatIndex}\n`));
|
rows.forEach(r =>
|
||||||
|
res.write(`${r.id},${r.dockDoor},${r.direction},${r.timestamp},${r.temperature},${r.humidity},${r.heatIndex}\n`)
|
||||||
|
);
|
||||||
res.end();
|
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}`));
|
Loading…
x
Reference in New Issue
Block a user