Initial commit of SOP Monitoring Dashboard
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
RUN npm install -g pm2
|
||||
COPY . .
|
||||
CMD ["pm2-runtime", "ecosystem.config.js"]
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: "sop-api",
|
||||
script: "./index.js",
|
||||
instances: "max",
|
||||
exec_mode: "cluster",
|
||||
env: {
|
||||
NODE_ENV: "development",
|
||||
},
|
||||
env_production: {
|
||||
NODE_ENV: "production",
|
||||
}
|
||||
}]
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
const express = require('express');
|
||||
const { Pool } = require('pg');
|
||||
const cors = require('cors');
|
||||
require('dotenv').config();
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
const pool = new Pool({
|
||||
user: process.env.DB_USER || 'admin',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
database: process.env.DB_NAME || 'sop_monitoring',
|
||||
password: process.env.DB_PASS || 'password',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
});
|
||||
|
||||
app.get('/api/changeovers', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT DISTINCT changeover_id, changeover_date FROM events ORDER BY changeover_date DESC');
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/events', async (req, res) => {
|
||||
try {
|
||||
const { camera_zone, state, changeover_id } = req.query;
|
||||
let query = 'SELECT * FROM events WHERE 1=1';
|
||||
let values = [];
|
||||
|
||||
if (camera_zone) {
|
||||
values.push(camera_zone);
|
||||
query += ` AND camera_zone = $${values.length}`;
|
||||
}
|
||||
if (state) {
|
||||
values.push(state);
|
||||
query += ` AND state = $${values.length}`;
|
||||
}
|
||||
if (changeover_id) {
|
||||
values.push(changeover_id);
|
||||
query += ` AND changeover_id = $${values.length}`;
|
||||
}
|
||||
query += ' ORDER BY start_time ASC';
|
||||
|
||||
const result = await pool.query(query, values);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/analytics/idle-time', async (req, res) => {
|
||||
try {
|
||||
const { changeover_id } = req.query;
|
||||
let filter = changeover_id ? `AND changeover_id = $1` : '';
|
||||
let params = changeover_id ? [changeover_id] : [];
|
||||
|
||||
// 1. CO Idle Time: Sum of all events marked Waste in CO
|
||||
const coResult = await pool.query(`
|
||||
SELECT SUM(state_duration_seconds) as total_idle
|
||||
FROM events
|
||||
WHERE camera_zone = 'CO' AND state = 'Waste' ${filter}
|
||||
`, params);
|
||||
const coIdle = parseInt(coResult.rows[0].total_idle || 0);
|
||||
|
||||
// 2. Mill Idle Time: Sum of Waste in Mill + specific Mill gap/idle
|
||||
const millResult = await pool.query(`
|
||||
SELECT SUM(state_duration_seconds) as total_idle
|
||||
FROM events
|
||||
WHERE camera_zone = 'Mill' AND (state = 'Waste' OR state = 'Idle') ${filter}
|
||||
`, params);
|
||||
const millIdle = parseInt(millResult.rows[0].total_idle || 0);
|
||||
|
||||
// 3. Conveyor Idle Time: Sum of No Load (Waste) + Load waiting before start
|
||||
const conveyorResult = await pool.query(`
|
||||
SELECT SUM(state_duration_seconds) as total_idle
|
||||
FROM events
|
||||
WHERE camera_zone = 'Conveyor' AND (state = 'Waste' OR state = 'Load waiting') ${filter}
|
||||
`, params);
|
||||
const conveyorIdle = parseInt(conveyorResult.rows[0].total_idle || 0);
|
||||
|
||||
res.json({
|
||||
CO: coIdle,
|
||||
Mill: millIdle,
|
||||
Conveyor: conveyorIdle
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/analytics/bottlenecks', async (req, res) => {
|
||||
try {
|
||||
const { changeover_id } = req.query;
|
||||
let filter = changeover_id ? `AND changeover_id = $1` : '';
|
||||
let params = changeover_id ? [changeover_id] : [];
|
||||
|
||||
const result = await pool.query(`
|
||||
SELECT event_description, camera_zone, SUM(state_duration_seconds) as total_duration, COUNT(*) as occurrences
|
||||
FROM events
|
||||
WHERE state IN ('Waste', 'Idle', 'Load waiting') ${filter}
|
||||
GROUP BY event_description, camera_zone
|
||||
ORDER BY total_duration DESC
|
||||
LIMIT 10
|
||||
`, params);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/analytics/sop-compliance', async (req, res) => {
|
||||
try {
|
||||
const { changeover_id } = req.query;
|
||||
let filter = changeover_id ? `AND changeover_id = $1` : '';
|
||||
let params = changeover_id ? [changeover_id] : [];
|
||||
|
||||
// 1. Conveyor must be started 15 mins before changeover
|
||||
const coStartResult = await pool.query(`SELECT start_time FROM events WHERE camera_zone = 'CO' ${filter} ORDER BY start_time ASC LIMIT 1`, params);
|
||||
const convStartResult = await pool.query(`SELECT start_time FROM events WHERE camera_zone = 'Conveyor' AND (event_description ILIKE '%Started%' OR event_description ILIKE '%Active%') ${filter} ORDER BY start_time ASC LIMIT 1`, params);
|
||||
|
||||
let conveyorCompliance = { rule: "Conveyor started 15 min before CO", status: "Unknown", detail: "" };
|
||||
if (coStartResult.rows.length > 0 && convStartResult.rows.length > 0) {
|
||||
const coTime = coStartResult.rows[0].start_time;
|
||||
const convTime = convStartResult.rows[0].start_time;
|
||||
// Compare times (assuming same day)
|
||||
const coDate = new Date(`1970-01-01T${coTime}Z`);
|
||||
const convDate = new Date(`1970-01-01T${convTime}Z`);
|
||||
const diffMins = (coDate - convDate) / 60000;
|
||||
|
||||
if (diffMins >= 15) {
|
||||
conveyorCompliance.status = "Pass";
|
||||
conveyorCompliance.detail = `Started ${diffMins.toFixed(1)} mins before CO`;
|
||||
} else {
|
||||
conveyorCompliance.status = "Fail";
|
||||
if (diffMins > 0) conveyorCompliance.detail = `Started only ${diffMins.toFixed(1)} mins before CO`;
|
||||
else conveyorCompliance.detail = `Started ${Math.abs(diffMins).toFixed(1)} mins AFTER CO`;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Mill should be completed within 15 mins after pushout rubber collection
|
||||
const pushoutResult = await pool.query(`SELECT end_time FROM events WHERE event_description ILIKE '%Pushout of Rubber - 4%' ${filter} ORDER BY end_time DESC LIMIT 1`, params);
|
||||
const millCompleteResult = await pool.query(`SELECT end_time FROM events WHERE camera_zone = 'Mill' AND event_description ILIKE '%Sheeting Out - 3%' ${filter} ORDER BY end_time DESC LIMIT 1`, params);
|
||||
|
||||
let millCompliance = { rule: "Mill completed within 15 min of Pushout", status: "Unknown", detail: "" };
|
||||
if (pushoutResult.rows.length > 0 && millCompleteResult.rows.length > 0) {
|
||||
const pushoutTime = pushoutResult.rows[0].end_time;
|
||||
const millTime = millCompleteResult.rows[0].end_time;
|
||||
const pushoutDate = new Date(`1970-01-01T${pushoutTime}Z`);
|
||||
const millDate = new Date(`1970-01-01T${millTime}Z`);
|
||||
const diffMins = (millDate - pushoutDate) / 60000;
|
||||
|
||||
if (diffMins <= 15 && diffMins >= 0) {
|
||||
millCompliance.status = "Pass";
|
||||
millCompliance.detail = `Completed in ${diffMins.toFixed(1)} mins`;
|
||||
} else {
|
||||
millCompliance.status = "Fail";
|
||||
millCompliance.detail = `Completed in ${diffMins.toFixed(1)} mins (limit 15)`;
|
||||
}
|
||||
}
|
||||
|
||||
res.json([conveyorCompliance, millCompliance]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
Generated
+1963
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "api",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"pg": "^8.22.0",
|
||||
"pm2": "^7.0.3"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user