399 lines
17 KiB
HTML
399 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Ignition SCADA & Drawing 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 */
|
|
}
|
|
#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; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1 class="mb-4">SCADA & Drawing Device Placement Progress</h1>
|
|
|
|
<div id="overall-progress" class="chart-container">
|
|
<span class="chart-label">Overall Progress</span>
|
|
<canvas id="overall-chart-canvas" class="panel-chart-canvas" style="max-width: 200px; max-height: 200px;"></canvas>
|
|
<div id="overall-text" style="font-weight: bold; margin-top: 10px;">Found Both: 0/0 (0%)</div>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<h2>Progress by Control Panel</h2>
|
|
<div id="panels-progress">
|
|
<!-- Charts will be loaded here -->
|
|
<p>Loading panel data...</p>
|
|
</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>Expected Drawing File</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>
|
|
let chartInstances = {};
|
|
let progressDetailsData = {};
|
|
let detailsModalInstance = null;
|
|
|
|
// Define labels and colors consistently
|
|
const chartLabels = ['Found Both', 'SCADA Only', 'Drawing Only', 'Missing Both'];
|
|
const chartColors = [
|
|
'rgb(25, 135, 84)', // Green (Found Both)
|
|
'rgb(13, 202, 240)', // Cyan (SCADA Only)
|
|
'rgb(255, 193, 7)', // Yellow (Drawing Only)
|
|
'rgb(220, 53, 69)' // Red (Missing Both)
|
|
];
|
|
const listKeys = ['found_both_list', 'found_scada_only_list', 'found_drawing_only_list', 'missing_list'];
|
|
|
|
// --- Chart Click Handler (Updated) ---
|
|
function handleChartClick(event, elements, chart) {
|
|
if (elements.length > 0) {
|
|
const clickedElementIndex = elements[0].index;
|
|
const isOverallChart = chart.canvas.id === 'overall-chart-canvas';
|
|
const identifier = isOverallChart ? '__overall__' : chart.canvas.id.replace('chart-', '');
|
|
|
|
// Map clicked index to the correct list type/key
|
|
if (clickedElementIndex >= 0 && clickedElementIndex < listKeys.length) {
|
|
const listType = listKeys[clickedElementIndex];
|
|
showDetailsModal(identifier, listType);
|
|
} else {
|
|
console.warn("Clicked unknown chart segment index:", clickedElementIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- UI Update Function (Heavily Updated) ---
|
|
function updateUI(data) {
|
|
console.log("Updating UI with data:", data);
|
|
progressDetailsData = data.progress;
|
|
|
|
// Update status bar
|
|
document.getElementById('status-message').textContent = data.status;
|
|
document.getElementById('last-commit').textContent = data.last_commit || 'N/A';
|
|
|
|
// --- Update Overall Chart & Text ---
|
|
const overallData = progressDetailsData.overall;
|
|
const overallTotal = overallData.total_csv;
|
|
const overallChartCounts = [
|
|
overallData.found_both,
|
|
overallData.found_scada_only,
|
|
overallData.found_drawing_only,
|
|
overallData.missing_both
|
|
];
|
|
// Update text (showing found both %)
|
|
document.getElementById('overall-text').textContent = `Found Both: ${overallData.found_both}/${overallTotal} (${overallData.percentage_found_both}%)`;
|
|
|
|
const overallChartConfig = {
|
|
type: 'pie',
|
|
data: {
|
|
labels: chartLabels,
|
|
datasets: [{
|
|
label: 'Overall Aliases',
|
|
data: overallChartCounts,
|
|
backgroundColor: chartColors,
|
|
hoverOffset: 4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
onClick: handleChartClick,
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
let label = context.label || '';
|
|
if (label) label += ': ';
|
|
const value = context.parsed;
|
|
if (value !== null) label += value;
|
|
if (overallTotal > 0) {
|
|
label += ` (${((value / overallTotal) * 100).toFixed(1)}%)`;
|
|
}
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const overallCanvas = document.getElementById('overall-chart-canvas');
|
|
if (chartInstances['overall']) {
|
|
chartInstances['overall'].data = overallChartConfig.data;
|
|
chartInstances['overall'].update();
|
|
} else if (overallCanvas) {
|
|
const ctxOverall = overallCanvas.getContext('2d');
|
|
chartInstances['overall'] = new Chart(ctxOverall, overallChartConfig);
|
|
}
|
|
|
|
// --- Update Panel Charts ---
|
|
const panelsContainer = document.getElementById('panels-progress');
|
|
const panelsData = progressDetailsData.panels;
|
|
const sortedPanels = Object.keys(panelsData).sort();
|
|
const currentPanelsOnPage = new Set(Object.keys(chartInstances).filter(k => k !== 'overall'));
|
|
const incomingPanels = new Set(sortedPanels);
|
|
|
|
// Remove charts for panels no longer present
|
|
currentPanelsOnPage.forEach(panelName => {
|
|
if (!incomingPanels.has(panelName)) {
|
|
if(chartInstances[panelName]) { chartInstances[panelName].destroy(); delete chartInstances[panelName]; }
|
|
const chartElement = document.getElementById(`chart-container-${panelName}`);
|
|
if (chartElement) chartElement.remove();
|
|
}
|
|
});
|
|
|
|
// Update or create charts for current panels
|
|
if (sortedPanels.length === 0) {
|
|
panelsContainer.innerHTML = '<p>No panel data available yet.</p>';
|
|
} else {
|
|
// Remove loading message if it exists
|
|
const loadingMsg = panelsContainer.querySelector('p');
|
|
if (loadingMsg && loadingMsg.textContent.includes('Loading')) { loadingMsg.remove(); }
|
|
|
|
sortedPanels.forEach(panelName => {
|
|
const panel = panelsData[panelName];
|
|
const panelTotal = panel.total;
|
|
const panelChartCounts = [
|
|
panel.found_both,
|
|
panel.found_scada_only,
|
|
panel.found_drawing_only,
|
|
panel.missing_both
|
|
];
|
|
|
|
let chartContainer = document.getElementById(`chart-container-${panelName}`);
|
|
let canvas = document.getElementById(`chart-${panelName}`);
|
|
|
|
// Create container and canvas if they don't exist
|
|
if (!chartContainer) {
|
|
chartContainer = document.createElement('div');
|
|
chartContainer.id = `chart-container-${panelName}`;
|
|
chartContainer.className = 'chart-container';
|
|
const label = document.createElement('span');
|
|
label.className = 'chart-label'; label.textContent = panelName;
|
|
canvas = document.createElement('canvas');
|
|
canvas.id = `chart-${panelName}`;
|
|
canvas.className = 'panel-chart-canvas';
|
|
chartContainer.appendChild(label);
|
|
chartContainer.appendChild(canvas);
|
|
panelsContainer.appendChild(chartContainer);
|
|
}
|
|
|
|
const panelChartConfig = {
|
|
type: 'pie',
|
|
data: {
|
|
labels: chartLabels,
|
|
datasets: [{
|
|
label: 'Aliases',
|
|
data: panelChartCounts,
|
|
backgroundColor: chartColors,
|
|
hoverOffset: 4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
onClick: handleChartClick,
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
let label = context.label || '';
|
|
if (label) label += ': ';
|
|
const value = context.parsed;
|
|
if (value !== null) label += value;
|
|
if (panelTotal > 0) {
|
|
label += ` (${((value / panelTotal) * 100).toFixed(1)}%)`;
|
|
}
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Update existing chart or create new one
|
|
if (chartInstances[panelName]) {
|
|
chartInstances[panelName].data = panelChartConfig.data;
|
|
chartInstances[panelName].update();
|
|
} else if (canvas) {
|
|
const ctx = canvas.getContext('2d');
|
|
chartInstances[panelName] = new Chart(ctx, panelChartConfig);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// --- Modal Display Function (Heavily Updated) ---
|
|
function showDetailsModal(identifier, listKey) {
|
|
let sourceData = null;
|
|
let panelNameDisplay = ""; // Name to show in the title
|
|
const listTypeLabel = chartLabels[listKeys.indexOf(listKey)] || "Details"; // Get nice label
|
|
|
|
if (identifier === '__overall__') {
|
|
sourceData = progressDetailsData.overall;
|
|
panelNameDisplay = "Overall";
|
|
} else {
|
|
sourceData = progressDetailsData.panels[identifier];
|
|
panelNameDisplay = identifier; // Use panel name from identifier
|
|
}
|
|
|
|
if (!sourceData || !sourceData[listKey]) {
|
|
console.error("Data list not found for:", identifier, listKey);
|
|
alert(`Could not find data for ${listTypeLabel} in ${panelNameDisplay}.`);
|
|
return;
|
|
}
|
|
|
|
const dataList = sourceData[listKey];
|
|
|
|
if (!dataList || dataList.length === 0) {
|
|
console.log(`No items to show for:`, panelNameDisplay, listKey);
|
|
alert(`No ${listTypeLabel} items found for ${panelNameDisplay}.`);
|
|
return;
|
|
}
|
|
|
|
const modalTitleElement = document.getElementById('detailsModalLabel');
|
|
const modalTableBody = document.querySelector('#detailsModal .modal-body tbody');
|
|
|
|
// Update modal title dynamically
|
|
modalTitleElement.innerHTML = `${listTypeLabel} Items for ${panelNameDisplay} <span class="badge bg-secondary ms-2">${dataList.length}</span>`;
|
|
|
|
modalTableBody.innerHTML = ''; // Clear previous entries
|
|
|
|
// Populate table rows with detailed info
|
|
dataList.forEach(item => {
|
|
const row = document.createElement('tr');
|
|
|
|
row.insertCell().textContent = item.alias;
|
|
row.insertCell().textContent = item.control_panel;
|
|
|
|
// SCADA Status Cell
|
|
const scadaCell = row.insertCell();
|
|
scadaCell.innerHTML = item.found_scada
|
|
? '<span class="status-yes">Yes</span>'
|
|
: '<span class="status-no">No</span>';
|
|
|
|
// Drawing Status Cell
|
|
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.expected_drawing_filename || 'N/A';
|
|
row.insertCell().textContent = item.equipment_type || 'N/A';
|
|
row.insertCell().textContent = item.conveyor_type || 'N/A';
|
|
|
|
modalTableBody.appendChild(row);
|
|
});
|
|
|
|
// Initialize and show modal
|
|
if (!detailsModalInstance) {
|
|
detailsModalInstance = new bootstrap.Modal(document.getElementById('detailsModal'));
|
|
}
|
|
detailsModalInstance.show();
|
|
}
|
|
|
|
// --- Connect to SSE stream (Unchanged) ---
|
|
const eventSource = new EventSource("/stream");
|
|
|
|
eventSource.onmessage = function(event) {
|
|
console.log("SSE message received:", event.data);
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
updateUI(data); // Call the UI update function with the new data
|
|
} 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...';
|
|
// Note: browser usually attempts reconnection automatically
|
|
};
|
|
|
|
// No need for initial fetch here, SSE stream sends initial state on connect
|
|
|
|
</script>
|
|
</body>
|
|
</html> |