Initial commit of SOP Monitoring Dashboard

This commit is contained in:
2026-07-28 12:50:28 +05:30
commit 0894a9c21e
40 changed files with 5676 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules
dist
.env
+7
View File
@@ -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"]
+14
View File
@@ -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
View File
@@ -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}`);
});
+1963
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -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"
}
}