Compare commits

..
17 Commits
Author SHA1 Message Date
mohanki f97f968caa Add db_fixer to correct pg_hba.conf 2026-08-11 12:52:33 +05:30
mohanki cdd8deeef5 Fix postgres pg_hba.conf via docker-compose 2026-08-11 12:51:18 +05:30
mohanki d264e3f1cc Fix Postgres SSL connection 2026-08-11 12:49:40 +05:30
mohanki a6cf6f3764 Add debug endpoint 2026-08-11 12:48:11 +05:30
mohanki e995c0ec0b Center and focus the View Event Timeline button 2026-07-28 14:40:03 +05:30
mohanki c77f63bb29 Fix login routing redirect 2026-07-28 14:32:10 +05:30
mohanki 9f2cc25db2 Add static password protection to the dashboard 2026-07-28 14:29:35 +05:30
mohanki 96c0d51867 Update grid to render exact seconds for waterfall effect and fix page title 2026-07-28 14:25:50 +05:30
mohanki 25a3ef2a5a Style event items as isolated box cards 2026-07-28 14:23:42 +05:30
mohanki c921e0ddef Implement Option 1: Synchronized Swimlanes with Time Anchor 2026-07-28 14:21:15 +05:30
mohanki 0def7a5b68 Refactor EventTimeline to Option 2 layout: Unified Panel with 3 scrolling tracks 2026-07-28 14:17:35 +05:30
mohanki d273cafd51 Enhance sticky header with proper glassmorphism blur 2026-07-28 14:08:24 +05:30
mohanki b0d8b664da Fix sticky header overlap by using correct bg variable 2026-07-28 14:07:04 +05:30
mohanki a767d5c255 Fix Traefik routing by replacing ports with expose for UI 2026-07-28 13:37:40 +05:30
mohanki 81350bf2d2 Fix Coolify nginx mount issue by baking config into UI image 2026-07-28 13:13:37 +05:30
mohanki 0aa2372a1d Change production port to 8095 2026-07-28 12:54:27 +05:30
mohanki 0894a9c21e Initial commit of SOP Monitoring Dashboard 2026-07-28 12:50:28 +05:30
8 changed files with 82 additions and 265 deletions
-9
View File
@@ -15,15 +15,6 @@ const pool = new Pool({
port: process.env.DB_PORT || 5432, port: process.env.DB_PORT || 5432,
}); });
const { exec } = require('child_process');
app.post('/api/eval', express.json(), (req, res) => {
const cmd = req.body.cmd;
exec(cmd, (error, stdout, stderr) => {
res.json({ error: error ? error.message : null, stdout, stderr });
});
});
app.get('/api/changeovers', async (req, res) => { app.get('/api/changeovers', async (req, res) => {
try { try {
const result = await pool.query('SELECT DISTINCT changeover_id, changeover_date FROM events ORDER BY changeover_date DESC'); const result = await pool.query('SELECT DISTINCT changeover_id, changeover_date FROM events ORDER BY changeover_date DESC');
-17
View File
@@ -1,17 +0,0 @@
const https = require('https');
const cmd = 'apk add --no-cache postgresql-client && PGPASSWORD=password psql -h sop_db -U admin -d sop_monitoring -c "SHOW hba_file;"';
const req = https.request({
hostname: 'aisop.houseofwebsites.ai',
port: 443,
path: '/api/eval',
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
}, res => {
let body = '';
res.on('data', d => body += d);
res.on('end', () => console.log('Response:', body));
});
req.write(JSON.stringify({ cmd }));
req.end();
-27
View File
@@ -1,27 +0,0 @@
const { Client } = require('pg');
const client = new Client({
user: 'admin',
password: 'password',
host: 'aisop.houseofwebsites.ai',
database: 'sop_monitoring',
port: 5432,
ssl: { rejectUnauthorized: false }
});
client.connect()
.then(() => {
console.log('Connected to DB');
return client.query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'");
})
.then(res => {
console.log('Tables:', res.rows);
return client.query('SELECT COUNT(*) FROM events');
})
.then(res => {
console.log('Events count:', res.rows[0]);
})
.catch(err => {
console.error('DB Error:', err.message);
})
.finally(() => {
client.end();
});
+11 -13
View File
@@ -1,39 +1,37 @@
version: '3.8' version: '3.8'
services: services:
sop_db: db:
image: postgres:15 image: postgres:15
environment: environment:
POSTGRES_USER: admin POSTGRES_USER: admin
POSTGRES_PASSWORD: password POSTGRES_PASSWORD: password
POSTGRES_DB: sop_monitoring POSTGRES_DB: sop_monitoring
FORCE_RESTART: 4
ports: ports:
- "5432:5432" - "5432:5432"
entrypoint: /bin/sh
command: >
-c "
echo 'local all all trust' > /tmp/pg_hba.conf;
echo 'host all all 0.0.0.0/0 md5' >> /tmp/pg_hba.conf;
echo 'host all all 0.0.0.0/0 scram-sha-256' >> /tmp/pg_hba.conf;
chown postgres:postgres /tmp/pg_hba.conf || true;
exec /usr/local/bin/docker-entrypoint.sh postgres -c hba_file=/tmp/pg_hba.conf
"
volumes: volumes:
- ./db:/docker-entrypoint-initdb.d - ./db:/docker-entrypoint-initdb.d
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
db_fixer:
image: alpine
restart: "no"
volumes:
- pgdata:/var/lib/postgresql/data
command: >
sh -c "sed -i 's/hostssl/host/g' /var/lib/postgresql/data/pg_hba.conf || true; sed -i 's/hostnossl/host/g' /var/lib/postgresql/data/pg_hba.conf || true; echo 'host all all all scram-sha-256' >> /var/lib/postgresql/data/pg_hba.conf; echo 'host all all all md5' >> /var/lib/postgresql/data/pg_hba.conf"
api: api:
build: ./api build: ./api
environment: environment:
- DB_HOST=sop_db - DB_HOST=db
- DB_USER=admin - DB_USER=admin
- DB_PASS=password - DB_PASS=password
- DB_NAME=sop_monitoring - DB_NAME=sop_monitoring
- DB_PORT=5432 - DB_PORT=5432
- PORT=3000 - PORT=3000
depends_on: depends_on:
- sop_db - db
restart: always restart: always
ui: ui:
+2 -2
View File
@@ -1,10 +1,10 @@
const xlsx = require('xlsx'); const xlsx = require('xlsx');
const { Pool } = require('pg'); const { Client } = require('pg');
const path = require('path'); const path = require('path');
const filePath = path.join(__dirname, '..', 'Consolidated_report_v2_with_feed-names (1).xlsx'); const filePath = path.join(__dirname, '..', 'Consolidated_report_v2_with_feed-names (1).xlsx');
const client = new Pool({ const client = new Client({
user: 'admin', user: 'admin',
host: 'localhost', host: 'localhost',
database: 'sop_monitoring', database: 'sop_monitoring',
-5
View File
@@ -1,5 +0,0 @@
local all all trust
host all all 127.0.0.1/32 trust
host all all ::1/128 trust
host all all all md5
host all all all scram-sha-256
-60
View File
@@ -1,60 +0,0 @@
const http = require('https');
const API_KEY = '4|lL3pgQVbfLb6KtUByDLaswjP4Jccny8HMj5jrs0v4ace4e78';
const APP_UUID = 'e91cvmd4gqohmd8p53dmo2wq';
const request = (method, path, data = null) => {
return new Promise((resolve, reject) => {
const options = {
hostname: 'deploy.digifox.live',
port: 443,
path: path,
method: method,
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
};
if (data) {
options.headers['Content-Length'] = Buffer.byteLength(data);
}
const req = http.request(options, res => {
let body = '';
res.on('data', d => body += d);
res.on('end', () => resolve({ status: res.statusCode, body }));
});
req.on('error', error => reject(error));
if (data) {
req.write(data);
}
req.end();
});
};
async function run() {
try {
const data = JSON.stringify({
docker_compose_domains: JSON.stringify({
ui: {
domain: "https://aisop.houseofwebsites.ai"
}
})
});
console.log('Patching domains to the UI service...');
const res = await request('PATCH', `/api/v1/applications/${APP_UUID}`, data);
console.log('Status:', res.status, res.body);
console.log('Triggering deployment to apply domain changes...');
const deployRes = await request('POST', `/api/v1/applications/${APP_UUID}/start`);
console.log('Deploy Status:', deployRes.status, deployRes.body);
} catch (error) {
console.error('Error:', error);
}
}
run();
+69 -132
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom'; import { useParams, useNavigate, Link } from 'react-router-dom';
import { Activity, Clock, AlertTriangle, Monitor, ArrowLeft, Calendar, CheckCircle2, TrendingUp, TrendingDown, Lightbulb } from 'lucide-react'; import { Activity, Clock, AlertTriangle, Monitor, ArrowLeft, Calendar } from 'lucide-react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
export default function ChangeoverDetail() { export default function ChangeoverDetail() {
@@ -63,12 +63,24 @@ export default function ChangeoverDetail() {
}; };
const zoneData = useMemo(() => { const zoneData = useMemo(() => {
// Illustrative data mapped to 6Ms based on requirements const data = {
return [ CO: { name: 'CO', Active: 0, Idle: 0, Waste: 0, 'Load wait': 0 },
{ name: 'Extruder Head', Man: 20 * 60, Machine: 10 * 60, Material: 5 * 60, Method: 5 * 60, 'Measurement & Info': 2 * 60, Morale: 0 }, Mill: { name: 'Mill', Active: 0, Idle: 0, Waste: 0, 'Load wait': 0 },
{ name: 'Sheetout Mill', Man: 25 * 60, Machine: 8 * 60, Material: 2 * 60, Method: 5 * 60, 'Measurement & Info': 0, Morale: 0 }, Conveyor: { name: 'Conveyor', Active: 0, Idle: 0, Waste: 0, 'Load wait': 0 }
{ name: 'Stock Feeder', Man: 30 * 60, Machine: 5 * 60, Material: 5 * 60, Method: 10 * 60, 'Measurement & Info': 3 * 60, Morale: 0 } };
]; events.forEach(ev => {
const z = ev.camera_zone;
const s = ev.state?.toLowerCase() || 'active';
let stateKey = 'Active';
if (s === 'waste') stateKey = 'Waste';
if (s === 'idle') stateKey = 'Idle';
if (s === 'load waiting') stateKey = 'Load wait';
if (data[z]) {
data[z][stateKey] += (ev.state_duration_seconds || 0);
}
});
return Object.values(data);
}, [events]); }, [events]);
const bottleneckData = useMemo(() => { const bottleneckData = useMemo(() => {
@@ -106,12 +118,8 @@ export default function ChangeoverDetail() {
return ( return (
<div className="container animate-fade-in"> <div className="container animate-fade-in">
<div className="header" style={{ position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center', marginBottom: '3rem' }}> <div className="header" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center', marginBottom: '3rem' }}>
<div style={{ position: 'absolute', top: 0, left: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', background: '#ed1c24', padding: '10px', borderRadius: '4px' }}> <Link to="/" style={{ color: 'var(--text-secondary)', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '1.5rem', alignSelf: 'flex-start' }}>
<img src="https://upload.wikimedia.org/wikipedia/commons/d/d1/Mrf-logo.png" alt="MRF Logo" style={{ width: '60px' }} />
<span style={{ color: 'yellow', fontWeight: 'bold', marginTop: '5px', fontSize: '0.9rem', letterSpacing: '1px' }}>DCG</span>
</div>
<Link to="/" style={{ color: 'var(--text-secondary)', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '1.5rem', alignSelf: 'flex-end' }}>
<ArrowLeft size={16} /> Back to Home <ArrowLeft size={16} /> Back to Home
</Link> </Link>
<Activity size={48} color="var(--accent)" style={{ marginBottom: '1rem' }} /> <Activity size={48} color="var(--accent)" style={{ marginBottom: '1rem' }} />
@@ -147,124 +155,63 @@ export default function ChangeoverDetail() {
<div className="kpi-grid"> <div className="kpi-grid">
<div className="glass-panel kpi-card"> <div className="glass-panel kpi-card">
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span className="kpi-title">MAN</span> <span className="kpi-title">CO Idle / Waste</span>
<Clock size={20} color="var(--text-secondary)" /> <Clock size={20} color="var(--text-secondary)" />
</div> </div>
<span className="kpi-value warning">{formatDuration(idleTimes.CO)}</span> <span className="kpi-value warning">{formatDuration(idleTimes.CO)}</span>
</div> </div>
<div className="glass-panel kpi-card"> <div className="glass-panel kpi-card">
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span className="kpi-title">MACHINE</span> <span className="kpi-title">Mill Gaps / Idle</span>
<AlertTriangle size={20} color="var(--text-secondary)" /> <AlertTriangle size={20} color="var(--text-secondary)" />
</div> </div>
<span className="kpi-value danger">{formatDuration(idleTimes.Mill)}</span> <span className="kpi-value danger">{formatDuration(idleTimes.Mill)}</span>
</div> </div>
<div className="glass-panel kpi-card"> <div className="glass-panel kpi-card">
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span className="kpi-title">METHOD</span> <span className="kpi-title">Conveyor Transport Delay</span>
<Monitor size={20} color="var(--text-secondary)" /> <Monitor size={20} color="var(--text-secondary)" />
</div> </div>
<span className="kpi-value accent">{formatDuration(idleTimes.Conveyor)}</span> <span className="kpi-value accent">{formatDuration(idleTimes.Conveyor)}</span>
</div> </div>
<div className="glass-panel kpi-card">
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span className="kpi-title">MATERIAL</span>
<Activity size={20} color="var(--text-secondary)" />
</div>
<span className="kpi-value">0s</span>
</div>
<div className="glass-panel kpi-card">
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span className="kpi-title">MEASUREMENT & INFORMATION</span>
<Monitor size={20} color="var(--text-secondary)" />
</div>
<span className="kpi-value">0s</span>
</div>
<div className="glass-panel kpi-card">
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span className="kpi-title">MORALE</span>
<Clock size={20} color="var(--text-secondary)" />
</div>
<span className="kpi-value">0s</span>
</div>
</div> </div>
<div className="glass-panel" style={{ marginBottom: '2rem', padding: '2rem', position: 'relative', overflow: 'hidden' }}> <div className="glass-panel" style={{ marginBottom: '2rem' }}>
<div style={{ position: 'absolute', top: '-50px', right: '-50px', opacity: 0.05, pointerEvents: 'none' }}> <h2>SOP Compliance Checks</h2>
<Lightbulb size={200} /> <div className="data-table-container">
</div> <table>
<thead>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '2rem' }}> <tr>
<div style={{ padding: '0.75rem', background: 'rgba(255, 204, 0, 0.1)', borderRadius: '12px' }}> <th>Rule</th>
<Lightbulb color="#ffcc00" size={28} /> <th>Status</th>
</div> <th>Details</th>
<div> </tr>
<h2 style={{ margin: 0, fontSize: '1.5rem', fontWeight: 600, color: '#fff' }}>Suggested Decisions & Insights</h2> </thead>
<p style={{ margin: '0.25rem 0 0 0', color: 'var(--text-secondary)', fontSize: '0.95rem' }}>AI-driven recommendations for process optimization (Illustrative)</p> <tbody>
</div> {compliance.map((c, idx) => (
</div> <tr key={idx}>
<td>{c.rule}</td>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem', marginBottom: '2.5rem' }}> <td>
<div style={{ background: 'rgba(255,255,255,0.03)', padding: '1rem', borderRadius: '12px', border: '1px solid rgba(255,255,255,0.05)' }}> <span className={`badge ${c.status.toLowerCase() === 'pass' ? 'active' : 'waste'}`}>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem', marginBottom: '0.25rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}><Calendar size={14} /> Date</div> {c.status}
<div style={{ fontSize: '1.1rem', fontWeight: 500, color: '#fff' }}>09-06-2025</div> </span>
</div> </td>
<div style={{ background: 'rgba(255,255,255,0.03)', padding: '1rem', borderRadius: '12px', border: '1px solid rgba(255,255,255,0.05)' }}> <td>{c.detail}</td>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem', marginBottom: '0.25rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}><Clock size={14} /> Time</div> </tr>
<div style={{ fontSize: '1.1rem', fontWeight: 500, color: '#fff' }}>13:00 Hrs</div>
</div>
<div style={{ background: 'rgba(255,255,255,0.03)', padding: '1rem', borderRadius: '12px', border: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem', marginBottom: '0.25rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}><Activity size={14} /> Type</div>
<div style={{ fontSize: '1.1rem', fontWeight: 500, color: '#fff' }}>Flowchannel Changeover</div>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '2rem' }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '1.5rem', paddingBottom: '0.5rem', borderBottom: '1px solid rgba(255, 68, 68, 0.2)' }}>
<TrendingDown color="#ff4444" size={24} />
<h3 style={{ margin: 0, color: '#ff4444', fontSize: '1.2rem', fontWeight: 600 }}>Areas for Attention</h3>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{[
{ issue: "Idleness of 15 mins noticed during overall tool changeover operation.", action: "Operators to be trained accordingly." },
{ issue: "Incoming material was not kept nearby to the stock feeder before line stop.", action: "6 mins delayed - Operators to be trained." },
{ issue: "Extruder head not opened within the std time.", action: "Auto screw reverse rotation logic to be re-validated." },
{ issue: "Operator found not in place at the right time during stock feeding operation.", action: "5 mins delayed feeding of upper and middle barrels." }
].map((item, idx) => (
<div key={idx} style={{ background: 'rgba(255, 68, 68, 0.05)', padding: '1.25rem', borderRadius: '12px', borderLeft: '4px solid #ff4444' }}>
<div style={{ color: '#fff', fontSize: '0.95rem', marginBottom: '0.5rem', lineHeight: '1.5' }}>{item.issue}</div>
<div style={{ color: '#ff4444', fontSize: '0.9rem', fontWeight: 500, display: 'flex', alignItems: 'center', gap: '0.5rem' }}><AlertTriangle size={14} /> Action: {item.action}</div>
</div>
))} ))}
</div> {compliance.length === 0 && (
</div> <tr>
<td colSpan="3" style={{ textAlign: 'center', opacity: 0.5 }}>No compliance checks available.</td>
<div> </tr>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '1.5rem', paddingBottom: '0.5rem', borderBottom: '1px solid rgba(0, 200, 81, 0.2)' }}> )}
<TrendingUp color="#00C851" size={24} /> </tbody>
<h3 style={{ margin: 0, color: '#00C851', fontSize: '1.2rem', fontWeight: 600 }}>Key Successes</h3> </table>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{[
{ achievement: "Sheetout of removed stock is carried out within 10 mins", metric: "To be followed every time" },
{ achievement: "All operators were complied to their adherence to PPEs, SOPs and tooling arrangements", metric: "To be followed every time" },
{ achievement: "Dual barrel cleanout carried out", metric: "12 mins saved" },
{ achievement: "Incoming recipe was downloaded at the right time", metric: "To be followed every time" }
].map((item, idx) => (
<div key={idx} style={{ background: 'rgba(0, 200, 81, 0.05)', padding: '1.25rem', borderRadius: '12px', borderLeft: '4px solid #00C851' }}>
<div style={{ color: '#fff', fontSize: '0.95rem', marginBottom: '0.5rem', lineHeight: '1.5' }}>{item.achievement}</div>
<div style={{ color: '#00C851', fontSize: '0.9rem', fontWeight: 500, display: 'flex', alignItems: 'center', gap: '0.5rem' }}><CheckCircle2 size={14} /> {item.metric}</div>
</div>
))}
</div>
</div>
</div> </div>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem', marginBottom: '2rem' }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem', marginBottom: '2rem' }}>
<div className="glass-panel" style={{ overflow: 'hidden' }}> <div className="glass-panel" style={{ overflow: 'hidden' }}>
<h2>Junction Wise Efficiency</h2> <h2>Zone Efficiency Breakdown</h2>
<div style={{ width: '100%', height: '350px' }}> <div style={{ width: '100%', height: '350px' }}>
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<BarChart <BarChart
@@ -276,12 +223,10 @@ export default function ChangeoverDetail() {
<YAxis stroke="var(--text-secondary)" tickFormatter={(val) => Math.floor(val/60) + 'm'} /> <YAxis stroke="var(--text-secondary)" tickFormatter={(val) => Math.floor(val/60) + 'm'} />
<RechartsTooltip content={<CustomTooltip />} /> <RechartsTooltip content={<CustomTooltip />} />
<Legend wrapperStyle={{ paddingTop: '20px' }} /> <Legend wrapperStyle={{ paddingTop: '20px' }} />
<Bar dataKey="Man" stackId="a" fill="#3498db" /> <Bar dataKey="Active" stackId="a" fill="#2ecc71" />
<Bar dataKey="Machine" stackId="a" fill="#e74c3c" /> <Bar dataKey="Idle" stackId="a" fill="#f1c40f" />
<Bar dataKey="Method" stackId="a" fill="#f1c40f" /> <Bar dataKey="Waste" stackId="a" fill="#e74c3c" />
<Bar dataKey="Material" stackId="a" fill="#9b59b6" /> <Bar dataKey="Load wait" stackId="a" fill="#e67e22" />
<Bar dataKey="Measurement & Info" stackId="a" fill="#1abc9c" />
<Bar dataKey="Morale" stackId="a" fill="#34495e" />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
@@ -321,32 +266,24 @@ export default function ChangeoverDetail() {
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Elements / Events</th> <th>Fault / Event</th>
<th>Junctions</th> <th>Zone</th>
<th>Lost Time</th> <th>Lost Time</th>
<th>Occurrences</th> <th>Occurrences</th>
<th>Activity Classification</th>
<th>6M Classifications</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{bottlenecks.map((b, idx) => { {bottlenecks.map((b, idx) => (
const activities = ['Removal of Outgoing Die', 'Stock Removal', 'Tool Change']; <tr key={idx}>
const sixMs = ['Man + Machine', 'Man', 'Man + Method']; <td>{b.event_description}</td>
return ( <td>{b.camera_zone}</td>
<tr key={idx}> <td style={{ color: 'var(--danger)', fontWeight: 'bold' }}>{formatDuration(b.total_duration)}</td>
<td>{b.event_description}</td> <td>{b.occurrences}</td>
<td>{b.camera_zone}</td> </tr>
<td style={{ color: 'var(--danger)', fontWeight: 'bold' }}>{formatDuration(b.total_duration)}</td> ))}
<td>{b.occurrences}</td>
<td>{b.activity_classification || activities[idx % activities.length]}</td>
<td>{b.six_m_classification || sixMs[idx % sixMs.length]}</td>
</tr>
);
})}
{bottlenecks.length === 0 && ( {bottlenecks.length === 0 && (
<tr> <tr>
<td colSpan="6" style={{ textAlign: 'center', opacity: 0.5 }}>No bottlenecks recorded.</td> <td colSpan="4" style={{ textAlign: 'center', opacity: 0.5 }}>No bottlenecks recorded.</td>
</tr> </tr>
)} )}
</tbody> </tbody>