ilia.gurielidze@autStand.com d51d597e87 No idea
2025-04-09 19:09:01 +04:00

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>