662 lines
34 KiB
HTML
662 lines
34 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SCADA Progress Monitor</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<style>
|
|
body { padding: 20px; padding-bottom: 60px; /* Account for status bar */ }
|
|
.progress-container, .chart-container {
|
|
margin-bottom: 25px;
|
|
text-align: center; /* Center chart labels */
|
|
}
|
|
.chart-label {
|
|
font-weight: bold;
|
|
margin-bottom: 5px;
|
|
display: block;
|
|
}
|
|
.status-bar {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
background-color: #f8f9fa;
|
|
border-top: 1px solid #dee2e6;
|
|
padding: 5px 15px;
|
|
font-size: 0.9em;
|
|
z-index: 1000;
|
|
}
|
|
/* Style for the overall progress bar - Removed as using Pie chart */
|
|
|
|
/* Style for panel charts */
|
|
.panel-chart-canvas {
|
|
max-width: 150px; /* Control pie chart size */
|
|
max-height: 150px;
|
|
margin: 0 auto; /* Center the canvas */
|
|
cursor: pointer; /* Indicate clickable */
|
|
}
|
|
#scada-panels-progress, #drawing-panels-progress {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); /* Responsive grid */
|
|
gap: 20px;
|
|
}
|
|
.modal-body table { width: 100%; }
|
|
.modal-body th, .modal-body td { padding: 5px 10px; border-bottom: 1px solid #eee; vertical-align: middle; }
|
|
.modal-body th { background-color: #f8f9fa; text-align: left; }
|
|
.status-yes { color: green; font-weight: bold; }
|
|
.status-no { color: red; font-weight: bold; }
|
|
nav { margin-bottom: 20px; } /* Added for nav spacing */
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<!-- Added Navigation -->
|
|
<nav class="nav nav-pills">
|
|
<a class="nav-link active" aria-current="page" href="/">SCADA Progress</a>
|
|
<a class="nav-link" href="/drawings">Drawing Progress</a>
|
|
<a class="nav-link" href="/conflicts">Conflicts</a>
|
|
</nav>
|
|
|
|
<!-- SCADA Content Section -->
|
|
<div id="scada-content">
|
|
<h1 class="mb-4">SCADA Device Placement Progress</h1>
|
|
<p>Compares the Equipment Manifest against the SCADA view.json files.</p>
|
|
|
|
<div id="overall-scada-progress" class="chart-container">
|
|
<span class="chart-label">Overall SCADA Progress</span>
|
|
<canvas id="overall-scada-chart-canvas" class="panel-chart-canvas" style="max-width: 200px; max-height: 200px;"></canvas>
|
|
<div id="overall-scada-text" style="font-weight: bold; margin-top: 10px;">Found in SCADA: 0/0 (0%)</div>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<h2>SCADA Progress by Control Panel</h2>
|
|
<div id="scada-panels-progress">
|
|
<p>Loading panel data...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Drawing Content Section (Initially Hidden) -->
|
|
<div id="drawings-content" style="display: none;">
|
|
<h1 class="mb-4">Drawing Device Placement Progress</h1>
|
|
<p>Compares the Equipment Manifest against the extracted text from drawing files (.txt).</p>
|
|
|
|
<div id="overall-drawing-progress" class="chart-container">
|
|
<span class="chart-label">Overall Drawing Progress</span>
|
|
<canvas id="overall-drawing-chart-canvas" class="panel-chart-canvas" style="max-width: 200px; max-height: 200px;"></canvas>
|
|
<div id="overall-drawing-text" style="font-weight: bold; margin-top: 10px;">Found in Drawing: 0/0 (0%)</div>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<h2>Drawing Progress by Control Panel</h2>
|
|
<div id="drawing-panels-progress">
|
|
<p>Loading panel data...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Conflicts Content Section (Initially Hidden) -->
|
|
<div id="conflicts-content" style="display: none;">
|
|
<h1 class="mb-4">SCADA/Drawing Conflicts <span id="conflict-count" class="badge bg-warning ms-2">0</span></h1>
|
|
<p>Items found in SCADA views but <strong>not</strong> found in the extracted drawing text files.</p>
|
|
|
|
<div id="panels-conflicts">
|
|
<p>Loading conflict data...</p>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Status Bar -->
|
|
<div class="status-bar">
|
|
<span id="status-message">Initializing...</span> | Last Commit: <span id="last-commit">N/A</span>
|
|
</div>
|
|
|
|
<!-- Bootstrap Modal for Details -->
|
|
<div class="modal fade" id="detailsModal" tabindex="-1" aria-labelledby="detailsModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="detailsModalLabel">Details for Panel: <span></span></h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<table class="table table-sm table-striped">
|
|
<thead>
|
|
<tr>
|
|
<th>Alias</th>
|
|
<th>Panel</th>
|
|
<th>SCADA Status</th>
|
|
<th>Drawing Status</th>
|
|
<th>Equipment Type</th>
|
|
<th>Type of Conveyor</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<!-- Missing/Found items will be populated here -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
// --- Global State Variables ---
|
|
let chartInstancesScada = {}; // Separate instances for SCADA
|
|
let chartInstancesDrawing = {}; // Separate instances for Drawing
|
|
let progressDetailsData = {}; // Stores the raw data from SSE (shared)
|
|
let previousCommitHash = null; // Single hash for the whole page
|
|
let detailsModalInstance = null;
|
|
let currentVisibleSection = 'scada'; // Track visible section: 'scada', 'drawing', 'conflicts'
|
|
|
|
// --- Chart Configurations ---
|
|
const scadaChartLabels = ['Found in SCADA', 'Not Found in SCADA'];
|
|
const scadaChartColors = ['rgb(13, 110, 253)', 'rgb(220, 53, 69)'];
|
|
const drawingChartLabels = ['Found in Drawing', 'Not Found in Drawing'];
|
|
const drawingChartColors = ['rgb(25, 135, 84)', 'rgb(220, 53, 69)'];
|
|
|
|
// Map backend list keys for modal clicks (can be combined or kept separate if needed)
|
|
const scadaListKeysMap = {
|
|
found: ['found_both_list', 'found_scada_only_list'],
|
|
notFound: ['found_drawing_only_list', 'missing_list']
|
|
};
|
|
const drawingListKeysMap = {
|
|
found: ['found_both_list', 'found_drawing_only_list'],
|
|
notFound: ['found_scada_only_list', 'missing_list']
|
|
};
|
|
|
|
// --- Debounce Utility (Only need one) ---
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
// --- Chart Click Handler (Needs context: SCADA or Drawing?) ---
|
|
function handleChartClick(event, elements, chart, context) { // Added context
|
|
if (elements.length > 0) {
|
|
const clickedElementIndex = elements[0].index;
|
|
const isOverallChart = chart.canvas.id.startsWith('overall-'); // More robust check
|
|
const identifier = isOverallChart ? '__overall__' : chart.canvas.id.replace(`chart-${context}-`, ''); // Use context
|
|
const categoryType = clickedElementIndex === 0 ? 'found' : 'notFound';
|
|
|
|
showDetailsModal(identifier, categoryType, context); // Pass context to modal
|
|
}
|
|
}
|
|
|
|
// --- Core UI Update Functions (One for each section) ---
|
|
|
|
function updateUIScadaCore(data) {
|
|
console.log("Running core SCADA UI redraw logic for commit:", data.last_commit);
|
|
progressDetailsData = data.progress; // Update shared raw data
|
|
|
|
// --- Overall SCADA Chart ---
|
|
const overallData = progressDetailsData.overall;
|
|
const overallTotal = overallData.total_csv;
|
|
const overallFoundScada = overallData.found_both + overallData.found_scada_only;
|
|
const overallNotFoundScada = overallData.found_drawing_only + overallData.missing_both;
|
|
const overallPercentageFound = overallTotal > 0 ? ((overallFoundScada / overallTotal) * 100).toFixed(1) : 0;
|
|
const overallChartCounts = [overallFoundScada, overallNotFoundScada];
|
|
|
|
document.getElementById('overall-scada-text').textContent = `Found in SCADA: ${overallFoundScada}/${overallTotal} (${overallPercentageFound}%)`;
|
|
|
|
// --- Only update/create chart if section is visible ---
|
|
const isSectionVisible = (currentVisibleSection === 'scada');
|
|
if (isSectionVisible) {
|
|
const overallScadaCanvas = document.getElementById('overall-scada-chart-canvas');
|
|
if (chartInstancesScada['overall']) {
|
|
if (JSON.stringify(chartInstancesScada['overall'].data.datasets[0].data) !== JSON.stringify(overallChartCounts)) {
|
|
chartInstancesScada['overall'].data.datasets[0].data = overallChartCounts;
|
|
chartInstancesScada['overall'].update('none');
|
|
}
|
|
} else if (overallScadaCanvas) {
|
|
console.log("Creating overall SCADA chart (visible).");
|
|
const ctxOverall = overallScadaCanvas.getContext('2d');
|
|
chartInstancesScada['overall'] = new Chart(ctxOverall, createChartConfig(overallChartCounts, overallTotal, 'scada', 'overall'));
|
|
}
|
|
} else {
|
|
// If section is not visible, destroy the chart instance if it exists
|
|
if (chartInstancesScada['overall']) {
|
|
console.log("Destroying hidden overall SCADA chart.");
|
|
chartInstancesScada['overall'].destroy();
|
|
delete chartInstancesScada['overall'];
|
|
}
|
|
}
|
|
|
|
// --- SCADA Panel Charts ---
|
|
const panelsContainer = document.getElementById('scada-panels-progress');
|
|
const panelsData = progressDetailsData.panels || {};
|
|
updatePanelCharts(panelsContainer, panelsData, chartInstancesScada, 'scada');
|
|
|
|
console.log("Finished SCADA UI core redraw.");
|
|
}
|
|
|
|
function updateUIDrawingCore(data) {
|
|
console.log("Running core Drawing UI redraw logic for commit:", data.last_commit);
|
|
progressDetailsData = data.progress; // Update shared raw data
|
|
|
|
// --- Overall Drawing Chart ---
|
|
const overallData = progressDetailsData.overall;
|
|
const overallTotal = overallData.total_csv;
|
|
const overallFoundDrawing = overallData.found_both + overallData.found_drawing_only;
|
|
const overallNotFoundDrawing = overallData.found_scada_only + overallData.missing_both;
|
|
const overallPercentageFound = overallTotal > 0 ? ((overallFoundDrawing / overallTotal) * 100).toFixed(1) : 0;
|
|
const overallChartCounts = [overallFoundDrawing, overallNotFoundDrawing];
|
|
|
|
document.getElementById('overall-drawing-text').textContent = `Found in Drawing: ${overallFoundDrawing}/${overallTotal} (${overallPercentageFound}%)`;
|
|
|
|
// --- Only update/create chart if section is visible ---
|
|
const isSectionVisible = (currentVisibleSection === 'drawings');
|
|
if (isSectionVisible) {
|
|
const overallDrawingCanvas = document.getElementById('overall-drawing-chart-canvas');
|
|
if (chartInstancesDrawing['overall']) {
|
|
if (JSON.stringify(chartInstancesDrawing['overall'].data.datasets[0].data) !== JSON.stringify(overallChartCounts)) {
|
|
chartInstancesDrawing['overall'].data.datasets[0].data = overallChartCounts;
|
|
chartInstancesDrawing['overall'].update('none');
|
|
}
|
|
} else if (overallDrawingCanvas) {
|
|
console.log("Creating overall drawing chart (visible).");
|
|
const ctxOverall = overallDrawingCanvas.getContext('2d');
|
|
chartInstancesDrawing['overall'] = new Chart(ctxOverall, createChartConfig(overallChartCounts, overallTotal, 'drawing', 'overall'));
|
|
}
|
|
} else {
|
|
// If section is not visible, destroy the chart instance if it exists
|
|
if (chartInstancesDrawing['overall']) {
|
|
console.log("Destroying hidden overall Drawing chart.");
|
|
chartInstancesDrawing['overall'].destroy();
|
|
delete chartInstancesDrawing['overall'];
|
|
}
|
|
}
|
|
|
|
// --- Drawing Panel Charts (call updatePanelCharts, which also checks visibility/destroys) ---
|
|
const panelsContainer = document.getElementById('drawing-panels-progress');
|
|
const panelsData = progressDetailsData.panels || {};
|
|
console.log(`[updateUIDrawingCore] Found drawing panels container:`, panelsContainer ? panelsContainer.id : 'Not Found'); // Added Log
|
|
updatePanelCharts(panelsContainer, panelsData, chartInstancesDrawing, 'drawings'); // Changed context to plural 'drawings'
|
|
|
|
console.log("Finished Drawing UI core redraw.");
|
|
}
|
|
|
|
function updateUIConflictsCore(data) {
|
|
console.log("Running core Conflicts UI redraw logic for commit:", data.last_commit);
|
|
progressDetailsData = data.progress; // Update shared raw data
|
|
|
|
const panelsContainer = document.getElementById('panels-conflicts');
|
|
panelsContainer.innerHTML = ''; // Clear previous
|
|
|
|
const panelsData = progressDetailsData.panels;
|
|
let totalConflicts = 0;
|
|
let panelsWithConflicts = 0;
|
|
|
|
if (!panelsData || Object.keys(panelsData).length === 0) {
|
|
panelsContainer.innerHTML = '<p class="text-center fst-italic">No panel data available yet.</p>';
|
|
} else {
|
|
const sortedPanels = Object.keys(panelsData).sort();
|
|
sortedPanels.forEach(panelName => {
|
|
const panel = panelsData[panelName];
|
|
const conflictsList = panel.found_scada_only_list || [];
|
|
if (conflictsList.length > 0) {
|
|
panelsWithConflicts++;
|
|
totalConflicts += conflictsList.length;
|
|
// ... (Create header and table as in conflicts.html) ...
|
|
const panelHeader = document.createElement('h4');
|
|
panelHeader.className = 'mt-4 mb-2';
|
|
panelHeader.textContent = `${panelName} (${conflictsList.length} conflicts)`;
|
|
panelsContainer.appendChild(panelHeader);
|
|
|
|
const table = document.createElement('table');
|
|
table.className = 'table table-sm table-striped table-hover table-bordered';
|
|
const thead = table.createTHead();
|
|
thead.innerHTML = `<tr><th>Alias</th><th>Panel</th><th>SCADA Status</th><th>Drawing Status</th><th>Equipment Type</th><th>Type of Conveyor</th></tr>`;
|
|
const tbody = table.createTBody();
|
|
conflictsList.sort((a, b) => a.alias.localeCompare(b.alias)).forEach(item => {
|
|
const row = tbody.insertRow();
|
|
row.classList.add('table-warning');
|
|
row.insertCell().textContent = item.alias;
|
|
row.insertCell().textContent = item.control_panel;
|
|
row.insertCell().innerHTML = '<span class="status-yes">Yes</span>';
|
|
row.insertCell().innerHTML = '<span class="status-no">No</span>';
|
|
row.insertCell().textContent = item.equipment_type || 'N/A';
|
|
row.insertCell().textContent = item.conveyor_type || 'N/A';
|
|
});
|
|
panelsContainer.appendChild(table);
|
|
}
|
|
});
|
|
if (panelsWithConflicts === 0) {
|
|
panelsContainer.innerHTML = '<p class="text-center fst-italic">No conflicts found across all panels.</p>';
|
|
}
|
|
}
|
|
// Update total count badge
|
|
const countBadge = document.getElementById('conflict-count');
|
|
if (countBadge) {
|
|
countBadge.textContent = totalConflicts;
|
|
countBadge.style.display = totalConflicts > 0 ? 'inline-block' : 'none';
|
|
}
|
|
console.log("Finished Conflicts UI core redraw.");
|
|
}
|
|
|
|
// --- Generic Panel Chart Update Logic ---
|
|
function updatePanelCharts(panelsContainer, panelsData, chartInstances, context) { // context: 'scada' or 'drawing'
|
|
const incomingPanelNames = new Set(Object.keys(panelsData).sort());
|
|
const existingInstanceNames = new Set(Object.keys(chartInstances).filter(k => k !== 'overall'));
|
|
|
|
// --- Check if the context matches the currently visible section ---
|
|
const isSectionVisible = (context === currentVisibleSection);
|
|
if (!isSectionVisible) {
|
|
// If section is not visible, destroy existing panel chart instances for this context
|
|
console.log(`Destroying hidden panel charts for context: ${context}`);
|
|
existingInstanceNames.forEach(panelName => {
|
|
if (chartInstances[panelName]) {
|
|
chartInstances[panelName].destroy();
|
|
delete chartInstances[panelName];
|
|
}
|
|
});
|
|
// Don't proceed further if the section is hidden
|
|
return;
|
|
}
|
|
|
|
if (incomingPanelNames.size > 0) {
|
|
const loadingMsg = panelsContainer.querySelector('p');
|
|
if (loadingMsg) { loadingMsg.remove(); }
|
|
|
|
incomingPanelNames.forEach(panelName => {
|
|
const panel = panelsData[panelName];
|
|
const panelTotal = panel.total;
|
|
let panelChartCounts;
|
|
if (context === 'scada') {
|
|
panelChartCounts = [panel.found_both + panel.found_scada_only, panel.found_drawing_only + panel.missing_both];
|
|
} else { // drawing
|
|
panelChartCounts = [panel.found_both + panel.found_drawing_only, panel.found_scada_only + panel.missing_both];
|
|
}
|
|
|
|
// --- Only update/create chart if section is visible ---
|
|
if (isSectionVisible) {
|
|
if (chartInstances[panelName]) {
|
|
if (JSON.stringify(chartInstances[panelName].data.datasets[0].data) !== JSON.stringify(panelChartCounts)) {
|
|
chartInstances[panelName].data.datasets[0].data = panelChartCounts;
|
|
chartInstances[panelName].update('none');
|
|
}
|
|
} else {
|
|
let canvas = document.getElementById(`chart-${context}-${panelName}`); // Use context in ID
|
|
if (canvas) {
|
|
console.log(`Recreating ${context} chart instance for panel (visible): ${panelName}`);
|
|
const ctx = canvas.getContext('2d');
|
|
chartInstances[panelName] = new Chart(ctx, createChartConfig(panelChartCounts, panelTotal, context, panelName));
|
|
} else {
|
|
console.log(`Creating new ${context} panel elements and chart (visible) for: ${panelName}`);
|
|
const chartContainer = document.createElement('div');
|
|
chartContainer.id = `chart-container-${context}-${panelName}`; // Use context in ID
|
|
chartContainer.className = 'chart-container';
|
|
const label = document.createElement('span');
|
|
label.className = 'chart-label'; label.textContent = panelName;
|
|
canvas = document.createElement('canvas'); // Reassign canvas variable
|
|
canvas.id = `chart-${context}-${panelName}`; // Use context in ID
|
|
canvas.className = 'panel-chart-canvas';
|
|
chartContainer.appendChild(label);
|
|
chartContainer.appendChild(canvas);
|
|
// Added Log before append
|
|
console.log(`[updatePanelCharts] Appending chartContainer (${chartContainer.id}) to panelsContainer (${panelsContainer ? panelsContainer.id : 'null'})`);
|
|
panelsContainer.appendChild(chartContainer); // Append to the main panels progress div
|
|
const ctx = canvas.getContext('2d');
|
|
chartInstances[panelName] = new Chart(ctx, createChartConfig(panelChartCounts, panelTotal, context, panelName));
|
|
}
|
|
}
|
|
}
|
|
// --- End visibility check ---
|
|
});
|
|
} else {
|
|
if (!panelsContainer.querySelector('p')) {
|
|
panelsContainer.innerHTML = '<p class="text-center fst-italic">No panel data available yet.</p>';
|
|
}
|
|
}
|
|
|
|
existingInstanceNames.forEach(panelName => {
|
|
if (!incomingPanelNames.has(panelName)) {
|
|
console.log(`Removing ${context} panel elements and chart for: ${panelName}`);
|
|
// Ensure chart is destroyed before removing element
|
|
if (chartInstances[panelName]) {
|
|
chartInstances[panelName].destroy();
|
|
delete chartInstances[panelName];
|
|
}
|
|
const chartElement = document.getElementById(`chart-container-${context}-${panelName}`); // Use context
|
|
if (chartElement) {
|
|
chartElement.remove();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Generic Helper to create chart config --- Needs context ---
|
|
function createChartConfig(chartCounts, total, context, identifier) { // identifier is 'overall' or panelName
|
|
const labels = context === 'scada' ? scadaChartLabels : drawingChartLabels;
|
|
const colors = context === 'scada' ? scadaChartColors : drawingChartColors;
|
|
const datasetLabel = context === 'scada' ? 'SCADA Match' : 'Drawing Match';
|
|
|
|
return {
|
|
type: 'pie',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: datasetLabel,
|
|
data: chartCounts,
|
|
backgroundColor: colors,
|
|
hoverOffset: 4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
onClick: (event, elements, chart) => handleChartClick(event, elements, chart, context), // Pass context
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(ctxTooltip) {
|
|
let label = ctxTooltip.label || '';
|
|
if (label) label += ': ';
|
|
const value = ctxTooltip.parsed;
|
|
if (value !== null) label += value;
|
|
// Use overallTotal for overall chart, panelTotal otherwise (How to get panelTotal here? Needs rethinking)
|
|
// Workaround: Don't show percentage on panel tooltips for now
|
|
const chartTotal = (identifier === 'overall' && progressDetailsData.overall) ? progressDetailsData.overall.total_csv : null;
|
|
if (chartTotal && chartTotal > 0) {
|
|
label += ` (${((value / chartTotal) * 100).toFixed(1)}%)`;
|
|
}
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// --- Wrapper function called by debouncer (Handles all sections) ---
|
|
function processUpdate(data) {
|
|
console.log("Processing update for commit:", data.last_commit);
|
|
|
|
// Always update status bar and commit hash text immediately
|
|
document.getElementById('status-message').textContent = data.status;
|
|
document.getElementById('last-commit').textContent = data.last_commit || 'N/A';
|
|
|
|
// *** Strict Check: Only proceed if commit hash has changed ***
|
|
if (data.last_commit && data.last_commit !== previousCommitHash) {
|
|
console.log("Commit hash changed (" + (previousCommitHash || 'None') + " -> " + data.last_commit + ") or initial load. Queueing core redraw.");
|
|
previousCommitHash = data.last_commit;
|
|
// Defer the core UI update calls
|
|
setTimeout(() => {
|
|
// Update all sections - they have internal checks/efficiency
|
|
updateUIScadaCore(data);
|
|
updateUIDrawingCore(data);
|
|
updateUIConflictsCore(data);
|
|
}, 0);
|
|
} else {
|
|
console.log("Commit hash unchanged (" + previousCommitHash + "), skipping core UI redraw.");
|
|
}
|
|
}
|
|
|
|
// --- Debounced version of the processing function ---
|
|
const debouncedProcessUpdate = debounce(processUpdate, 250); // Single debouncer
|
|
|
|
// --- Modal Display Function (Needs context) ---
|
|
function showDetailsModal(identifier, categoryType, context) { // Added context
|
|
let sourceData = null;
|
|
let panelNameDisplay = "";
|
|
const listKeysMap = context === 'scada' ? scadaListKeysMap : drawingListKeysMap;
|
|
const listTypeLabel = categoryType === 'found'
|
|
? (context === 'scada' ? 'Found in SCADA' : 'Found in Drawing')
|
|
: (context === 'scada' ? 'Not Found in SCADA' : 'Not Found in Drawing');
|
|
|
|
if (identifier === '__overall__') {
|
|
sourceData = progressDetailsData.overall;
|
|
panelNameDisplay = "Overall";
|
|
} else {
|
|
sourceData = progressDetailsData.panels ? progressDetailsData.panels[identifier] : null;
|
|
panelNameDisplay = identifier;
|
|
}
|
|
|
|
if (!sourceData) { /* ... error handling ... */ return; }
|
|
|
|
const backendListKeys = listKeysMap[categoryType];
|
|
if (!backendListKeys) { /* ... error handling ... */ return; }
|
|
|
|
let combinedDataList = [];
|
|
backendListKeys.forEach(key => {
|
|
if (sourceData[key]) {
|
|
combinedDataList = combinedDataList.concat(sourceData[key]);
|
|
}
|
|
});
|
|
|
|
if (combinedDataList.length === 0) { /* ... alert handling ... */ return; }
|
|
|
|
const modalTitleElement = document.getElementById('detailsModalLabel');
|
|
const modalTableBody = document.querySelector('#detailsModal .modal-body tbody');
|
|
|
|
modalTitleElement.innerHTML = `${listTypeLabel} Items for ${panelNameDisplay} <span class="badge bg-secondary ms-2">${combinedDataList.length}</span>`;
|
|
modalTableBody.innerHTML = '';
|
|
|
|
combinedDataList.sort((a, b) => a.alias.localeCompare(b.alias)).forEach(item => {
|
|
const row = document.createElement('tr');
|
|
row.insertCell().textContent = item.alias;
|
|
row.insertCell().textContent = item.control_panel;
|
|
const scadaCell = row.insertCell(); scadaCell.innerHTML = item.found_scada ? '<span class="status-yes">Yes</span>' : '<span class="status-no">No</span>';
|
|
const drawingCell = row.insertCell(); drawingCell.innerHTML = item.found_drawing ? '<span class="status-yes">Yes</span>' : '<span class="status-no">No</span>';
|
|
row.insertCell().textContent = item.equipment_type || 'N/A';
|
|
row.insertCell().textContent = item.conveyor_type || 'N/A';
|
|
if (item.found_scada && !item.found_drawing) { row.classList.add('table-warning'); }
|
|
modalTableBody.appendChild(row);
|
|
});
|
|
|
|
if (!detailsModalInstance) {
|
|
detailsModalInstance = new bootstrap.Modal(document.getElementById('detailsModal'));
|
|
}
|
|
detailsModalInstance.show();
|
|
}
|
|
|
|
// --- Navigation Handling ---
|
|
function showSection(sectionId) {
|
|
console.log("Showing section:", sectionId);
|
|
document.getElementById('scada-content').style.display = 'none';
|
|
document.getElementById('drawings-content').style.display = 'none';
|
|
document.getElementById('conflicts-content').style.display = 'none';
|
|
|
|
const elementToShow = document.getElementById(`${sectionId}-content`);
|
|
if (elementToShow) {
|
|
elementToShow.style.display = 'block';
|
|
currentVisibleSection = sectionId;
|
|
|
|
// --- Trigger update for the now-visible section ---
|
|
// The update function will check visibility internally before drawing charts.
|
|
if (progressDetailsData && Object.keys(progressDetailsData).length > 0) {
|
|
const updateData = { progress: progressDetailsData }; // Pass existing data
|
|
console.log(`Calling update function for now-visible section: ${sectionId}`);
|
|
// Use setTimeout to ensure DOM update (display: block) is processed first
|
|
if (sectionId === 'scada') {
|
|
updateUIScadaCore(updateData);
|
|
} else if (sectionId === 'drawings') {
|
|
updateUIDrawingCore(updateData);
|
|
} else if (sectionId === 'conflicts') {
|
|
updateUIConflictsCore(updateData);
|
|
}
|
|
} else {
|
|
console.log(`Section ${sectionId} shown, but no progress data yet.`);
|
|
// If data arrives later, the debouncedProcessUpdate will handle drawing
|
|
// for the currently visible section.
|
|
}
|
|
// --- End section update trigger ---
|
|
|
|
} else {
|
|
console.error("Attempted to show unknown section:", sectionId);
|
|
document.getElementById('scada-content').style.display = 'block'; // Default back to SCADA
|
|
currentVisibleSection = 'scada';
|
|
}
|
|
|
|
// Update active nav link
|
|
document.querySelectorAll('.nav-link').forEach(link => {
|
|
link.classList.remove('active');
|
|
// Use href attribute to match sectionId
|
|
const targetSection = link.getAttribute('data-target-section');
|
|
if (targetSection === sectionId) {
|
|
link.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
console.log("DOM Loaded, setting up navigation...");
|
|
document.querySelectorAll('.nav-link').forEach(link => {
|
|
// Store target section ID in a data attribute from href
|
|
const href = link.getAttribute('href');
|
|
let targetSection = 'scada'; // Default
|
|
if (href === '/drawings') targetSection = 'drawings'; // Use plural to match ID
|
|
else if (href === '/conflicts') targetSection = 'conflicts'; // Use plural to match ID
|
|
link.setAttribute('data-target-section', targetSection);
|
|
|
|
link.addEventListener('click', (event) => {
|
|
event.preventDefault(); // Prevent page reload
|
|
const sectionId = link.getAttribute('data-target-section');
|
|
showSection(sectionId);
|
|
});
|
|
});
|
|
|
|
// Show initial section (SCADA by default)
|
|
showSection('scada');
|
|
});
|
|
|
|
// --- Connect to SSE stream (Single connection) ---
|
|
console.log("Initializing SSE connection...");
|
|
const eventSource = new EventSource("/stream");
|
|
|
|
eventSource.onmessage = function(event) {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
debouncedProcessUpdate(data); // Call the single debounced processor
|
|
} catch (error) {
|
|
console.error("Error parsing SSE data:", error);
|
|
document.getElementById('status-message').textContent = 'Error processing update from server.';
|
|
}
|
|
};
|
|
|
|
eventSource.onerror = function(err) {
|
|
console.error("EventSource failed:", err);
|
|
document.getElementById('status-message').textContent = 'Connection to server lost. Retrying...';
|
|
};
|
|
|
|
console.log("SSE handler set up.");
|
|
|
|
</script>
|
|
</body>
|
|
</html> |