PLC_Generation/templates/job_status.html
2025-08-05 14:38:54 +04:00

719 lines
31 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PLC Generation Status</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
.log-container {
height: 400px;
overflow-y: auto;
background-color: #1e1e1e;
border-radius: 5px;
padding: 15px;
font-family: 'Courier New', monospace;
color: #ffffff;
}
.log-entry {
margin-bottom: 5px;
word-wrap: break-word;
}
.log-timestamp {
color: #888;
margin-right: 10px;
}
.log-info {
color: #ffffff;
}
.log-success {
color: #28a745;
}
.log-error {
color: #dc3545;
}
.log-warning {
color: #ffc107;
}
.status-badge {
font-size: 1.2rem;
padding: 8px 16px;
}
.progress-container {
position: relative;
}
.progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
color: #000;
z-index: 10;
}
.progress-bar {
transition: width 0.3s ease-in-out;
}
.download-section {
display: none;
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.header-icon {
font-size: 2rem;
margin-right: 10px;
}
.data-table-container {
max-height: 500px;
overflow-y: auto;
}
.desc-ip-section {
display: none;
}
.desc-ip-section.show {
display: block;
}
.table th {
position: sticky;
top: 0;
background-color: #f8f9fa;
z-index: 10;
}
.search-controls {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 15px;
}
.pagination-controls {
display: flex;
justify-content: between;
align-items: center;
padding: 10px 0;
}
.acd-download-highlight {
background: linear-gradient(135deg, #28a745, #20c997);
border: none;
box-shadow: 0 4px 8px rgba(40, 167, 69, 0.3);
}
.acd-download-highlight:hover {
background: linear-gradient(135deg, #218838, #1c9a7a);
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(40, 167, 69, 0.4);
}
</style>
</head>
<body>
<div class="container-fluid mt-5">
<div class="row">
<div class="col-md-12">
<!-- Header -->
<div class="d-flex align-items-center mb-4">
<a href="/" class="btn btn-outline-secondary me-3">
<i class="fas fa-arrow-left"></i> Back
</a>
<div>
<h1><i class="fas fa-cogs header-icon"></i>PLC Generation Status</h1>
<p class="text-muted mb-0">Job ID: <code>{{ job_id }}</code></p>
</div>
</div>
<!-- Navigation Tabs -->
<ul class="nav nav-tabs" id="statusTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="progress-tab" data-bs-toggle="tab" data-bs-target="#progress" type="button" role="tab">
<i class="fas fa-tasks me-2"></i>Progress & Logs
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="data-tab" data-bs-toggle="tab" data-bs-target="#data" type="button" role="tab">
<i class="fas fa-table me-2"></i>Data
</button>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content mt-3" id="statusTabContent">
<!-- Progress & Logs Tab -->
<div class="tab-pane fade show active" id="progress" role="tabpanel">
<!-- Status Card -->
<div class="card shadow mb-4">
<div class="card-header">
<div class="row align-items-center">
<div class="col-md-6">
<h5 class="mb-0">
<i class="fas fa-project-diagram me-2"></i>
<span id="projectName">Loading...</span>
</h5>
</div>
<div class="col-md-6 text-end">
<span id="statusBadge" class="badge status-badge">
<i class="fas fa-spinner fa-spin me-2"></i>Loading...
</span>
</div>
</div>
</div>
<div class="card-body">
<!-- Progress Bar -->
<div class="progress-container mb-3">
<div class="progress" style="height: 30px;">
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%"></div>
</div>
<div class="progress-text" id="progressText">0%</div>
</div>
<!-- Job Info -->
<div class="row">
<div class="col-md-4">
<small class="text-muted">Start Time:</small>
<div id="startTime">-</div>
</div>
<div class="col-md-4">
<small class="text-muted">Duration:</small>
<div id="duration">-</div>
</div>
<div class="col-md-4">
<small class="text-muted">Status:</small>
<div id="statusText">-</div>
</div>
</div>
</div>
</div>
<!-- Download Section -->
<div class="card shadow mb-4 download-section" id="downloadSection">
<div class="card-header bg-success text-white">
<h5 class="mb-0">
<i class="fas fa-download me-2"></i>Download Files
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="d-grid">
<button id="downloadL5X" class="btn btn-outline-primary btn-lg" disabled>
<i class="fas fa-file-code me-2"></i>Download L5X File
</button>
</div>
</div>
<div class="col-md-6">
<div class="d-grid">
<button id="downloadACD" class="btn btn-lg acd-download-highlight text-white" disabled>
<i class="fas fa-microchip me-2"></i>Download ACD File
</button>
</div>
</div>
</div>
<div class="text-center mt-3">
<small class="text-muted">
<i class="fas fa-info-circle me-1"></i>
ACD files are ready for direct import into Studio 5000
</small>
</div>
</div>
</div>
<!-- Restart Section -->
<div class="card shadow mb-4 restart-section" id="restartSection" style="display: none;">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0">
<i class="fas fa-redo me-2"></i>Restart Job
</h5>
</div>
<div class="card-body">
<p class="mb-3">This job failed or was interrupted. You can restart it without uploading the file again.</p>
<div class="d-grid">
<button id="restartBtn" class="btn btn-warning">
<i class="fas fa-redo me-2"></i>Restart Generation
</button>
</div>
</div>
</div>
<!-- Logs Section -->
<div class="card shadow">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-terminal me-2"></i>Generation Logs
</h5>
<div>
<button id="clearLogs" class="btn btn-sm btn-outline-secondary me-2">
<i class="fas fa-eraser"></i> Clear
</button>
<button id="autoScroll" class="btn btn-sm btn-outline-primary active">
<i class="fas fa-arrow-down"></i> Auto-scroll
</button>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="log-container" id="logContainer">
<div class="log-entry log-info">
<span class="log-timestamp">[--:--:--]</span>
Waiting for logs...
</div>
</div>
</div>
</div>
</div>
<!-- DESC_IP Data Tab -->
<div class="tab-pane fade" id="data" role="tabpanel">
<div class="card shadow">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-table me-2"></i>DESC_IP Data
<span class="badge bg-secondary" id="dataCount">0 records</span>
</h5>
</div>
<div class="card-body">
<!-- Search Controls -->
<div class="search-controls">
<div class="row">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" id="searchInput"
placeholder="Search in all columns...">
</div>
</div>
<div class="col-md-3">
<select class="form-select" id="rowsPerPage">
<option value="10">10 rows</option>
<option value="25">25 rows</option>
<option value="50">50 rows</option>
<option value="100">100 rows</option>
</select>
</div>
<div class="col-md-3">
<button class="btn btn-outline-secondary" id="refreshData">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
</div>
</div>
<!-- Data Table -->
<div class="data-table-container">
<table class="table table-striped table-hover" id="descIpTable">
<thead>
<tr>
<th>TAGNAME</th>
<th>TERM</th>
<th>DESCA</th>
<th>DESCB</th>
</tr>
</thead>
<tbody id="tableBody">
<tr>
<td colspan="4" class="text-center text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>Loading data...
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="pagination-controls">
<div class="d-flex justify-content-between align-items-center w-100">
<div class="text-muted" id="paginationInfo">
Showing 0 to 0 of 0 entries
</div>
<nav>
<ul class="pagination mb-0" id="pagination">
<!-- Pagination buttons will be inserted here -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const jobId = '{{ job_id }}';
let autoScrollEnabled = true;
let lastLogCount = 0;
let currentPage = 1;
let rowsPerPage = 10;
let searchTerm = '';
let descIpData = [];
// DOM elements
const projectNameEl = document.getElementById('projectName');
const statusBadge = document.getElementById('statusBadge');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const startTimeEl = document.getElementById('startTime');
const durationEl = document.getElementById('duration');
const statusTextEl = document.getElementById('statusText');
const logContainer = document.getElementById('logContainer');
const downloadSection = document.getElementById('downloadSection');
const downloadL5X = document.getElementById('downloadL5X');
const downloadACD = document.getElementById('downloadACD');
const restartSection = document.getElementById('restartSection');
const restartBtn = document.getElementById('restartBtn');
const clearLogsBtn = document.getElementById('clearLogs');
const autoScrollBtn = document.getElementById('autoScroll');
// DESC_IP Data elements
const dataCount = document.getElementById('dataCount');
const searchInput = document.getElementById('searchInput');
const rowsPerPageSelect = document.getElementById('rowsPerPage');
const refreshDataBtn = document.getElementById('refreshData');
const tableBody = document.getElementById('tableBody');
const paginationInfo = document.getElementById('paginationInfo');
const pagination = document.getElementById('pagination');
// Status colors and icons
const statusConfig = {
'queued': { class: 'bg-secondary', icon: 'fa-clock', text: 'Queued' },
'running': { class: 'bg-primary pulse', icon: 'fa-spinner fa-spin', text: 'Running' },
'completed': { class: 'bg-success', icon: 'fa-check', text: 'Completed' },
'failed': { class: 'bg-danger', icon: 'fa-times', text: 'Failed' }
};
function updateStatus(job) {
const config = statusConfig[job.status] || statusConfig['queued'];
// Update project name
projectNameEl.textContent = job.project_name;
// Update status badge
statusBadge.className = `badge status-badge text-white ${config.class}`;
statusBadge.innerHTML = `<i class="fas ${config.icon} me-2"></i>${config.text}`;
// Update progress bar with smooth transition
const currentProgress = Math.max(0, Math.min(100, job.progress || 0));
progressBar.style.width = `${currentProgress}%`;
progressText.textContent = `${currentProgress}%`;
// Update progress bar appearance based on status
if (job.status === 'completed') {
progressBar.className = 'progress-bar bg-success';
} else if (job.status === 'failed') {
progressBar.className = 'progress-bar bg-danger';
} else if (job.status === 'running') {
progressBar.className = 'progress-bar progress-bar-striped progress-bar-animated bg-primary';
} else {
progressBar.className = 'progress-bar bg-secondary';
}
// Update job info
if (job.start_time) {
startTimeEl.textContent = new Date(job.start_time).toLocaleString();
}
statusTextEl.textContent = config.text;
// Start duration timer if job is running
if (job.status === 'running' && !durationInterval) {
startDurationTimer();
} else if ((job.status === 'completed' || job.status === 'failed') && durationInterval) {
// Stop the timer when job is done
clearInterval(durationInterval);
durationInterval = null;
}
// Handle downloads
if (job.status === 'completed') {
downloadSection.style.display = 'block';
restartSection.style.display = 'none';
if (job.output_files && job.output_files.l5x) {
downloadL5X.disabled = false;
downloadL5X.onclick = () => downloadFile('l5x');
}
if (job.output_files && job.output_files.acd) {
downloadACD.disabled = false;
downloadACD.onclick = () => downloadFile('acd');
}
}
// Handle restart option for failed jobs
if (job.status === 'failed') {
downloadSection.style.display = 'none';
restartSection.style.display = 'block';
} else {
restartSection.style.display = 'none';
}
}
function updateLogs(logs) {
if (logs.length > lastLogCount) {
// Only add new logs
const newLogs = logs.slice(lastLogCount);
newLogs.forEach(log => {
const logDiv = document.createElement('div');
logDiv.className = `log-entry log-${log.level}`;
logDiv.innerHTML = `
<span class="log-timestamp">[${log.timestamp}]</span>
${escapeHtml(log.message)}
`;
logContainer.appendChild(logDiv);
});
lastLogCount = logs.length;
// Auto-scroll to bottom if enabled
if (autoScrollEnabled) {
logContainer.scrollTop = logContainer.scrollHeight;
}
}
}
async function loadDescIpData() {
try {
const response = await fetch(`/desc_ip_data/${jobId}?page=${currentPage}&per_page=${rowsPerPage}&search=${encodeURIComponent(searchTerm)}`);
const result = await response.json();
if (response.ok) {
displayDescIpData(result.data, result.pagination);
} else {
console.error('Failed to load DESC_IP data:', result.error);
tableBody.innerHTML = `
<tr>
<td colspan="4" class="text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>Failed to load data
</td>
</tr>
`;
}
} catch (error) {
console.error('Network error loading DESC_IP data:', error);
tableBody.innerHTML = `
<tr>
<td colspan="4" class="text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>Network error
</td>
</tr>
`;
}
}
function displayDescIpData(data, paginationData) {
if (data.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="4" class="text-center text-muted">
<i class="fas fa-info-circle me-2"></i>No data found
</td>
</tr>
`;
dataCount.textContent = '0 records';
paginationInfo.textContent = 'Showing 0 to 0 of 0 entries';
pagination.innerHTML = '';
return;
}
// Update data count
dataCount.textContent = `${paginationData.total} records`;
// Update table body
tableBody.innerHTML = '';
data.forEach(row => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td><code>${escapeHtml(row.TAGNAME)}</code></td>
<td><span class="badge bg-secondary">${escapeHtml(row.TERM)}</span></td>
<td>${escapeHtml(row.DESCA)}</td>
<td>${escapeHtml(row.DESCB)}</td>
`;
tableBody.appendChild(tr);
});
// Update pagination info
const start = (paginationData.page - 1) * paginationData.per_page + 1;
const end = Math.min(start + data.length - 1, paginationData.total);
paginationInfo.textContent = `Showing ${start} to ${end} of ${paginationData.total} entries`;
// Update pagination buttons
updatePagination(paginationData);
}
function updatePagination(paginationData) {
pagination.innerHTML = '';
if (paginationData.pages <= 1) return;
// Previous button
const prevLi = document.createElement('li');
prevLi.className = `page-item ${paginationData.page === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" data-page="${paginationData.page - 1}">Previous</a>`;
pagination.appendChild(prevLi);
// Page numbers
const startPage = Math.max(1, paginationData.page - 2);
const endPage = Math.min(paginationData.pages, paginationData.page + 2);
for (let i = startPage; i <= endPage; i++) {
const li = document.createElement('li');
li.className = `page-item ${i === paginationData.page ? 'active' : ''}`;
li.innerHTML = `<a class="page-link" href="#" data-page="${i}">${i}</a>`;
pagination.appendChild(li);
}
// Next button
const nextLi = document.createElement('li');
nextLi.className = `page-item ${paginationData.page === paginationData.pages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" data-page="${paginationData.page + 1}">Next</a>`;
pagination.appendChild(nextLi);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function downloadFile(fileType) {
window.open(`/download/${jobId}/${fileType}`, '_blank');
}
// Event handlers
clearLogsBtn.addEventListener('click', () => {
logContainer.innerHTML = '';
lastLogCount = 0;
});
autoScrollBtn.addEventListener('click', () => {
autoScrollEnabled = !autoScrollEnabled;
autoScrollBtn.classList.toggle('active');
if (autoScrollEnabled) {
logContainer.scrollTop = logContainer.scrollHeight;
}
});
restartBtn.addEventListener('click', async () => {
restartBtn.disabled = true;
restartBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Restarting...';
try {
const response = await fetch(`/restart/${jobId}`, {
method: 'POST'
});
if (response.ok) {
// Clear logs and start polling again
logContainer.innerHTML = '';
lastLogCount = 0;
restartSection.style.display = 'none';
pollStatus();
} else {
const error = await response.json();
alert(`Failed to restart: ${error.error}`);
restartBtn.disabled = false;
restartBtn.innerHTML = '<i class="fas fa-redo me-2"></i>Restart Generation';
}
} catch (error) {
alert(`Network error: ${error.message}`);
restartBtn.disabled = false;
restartBtn.innerHTML = '<i class="fas fa-redo me-2"></i>Restart Generation';
}
});
// DESC_IP Data event handlers
searchInput.addEventListener('input', (e) => {
searchTerm = e.target.value;
currentPage = 1;
loadDescIpData();
});
rowsPerPageSelect.addEventListener('change', (e) => {
rowsPerPage = parseInt(e.target.value);
currentPage = 1;
loadDescIpData();
});
refreshDataBtn.addEventListener('click', () => {
loadDescIpData();
});
pagination.addEventListener('click', (e) => {
e.preventDefault();
if (e.target.tagName === 'A' && !e.target.parentElement.classList.contains('disabled')) {
currentPage = parseInt(e.target.dataset.page);
loadDescIpData();
}
});
// Initialize UI with default values
function initializeUI() {
// Set initial progress to 0
progressBar.style.width = '0%';
progressText.textContent = '0%';
progressBar.className = 'progress-bar bg-secondary';
// Set initial duration to 0
durationEl.textContent = '0s';
// Set initial status
statusTextEl.textContent = 'Initializing...';
}
// Poll for updates
async function pollStatus() {
try {
const response = await fetch(`/status/${jobId}`);
const job = await response.json();
if (response.ok) {
updateStatus(job);
updateLogs(job.logs || []);
// Continue polling if job is not finished
if (job.status === 'queued' || job.status === 'running') {
setTimeout(pollStatus, 1000);
}
} else {
console.error('Failed to fetch job status:', job.error);
}
} catch (error) {
console.error('Network error:', error);
setTimeout(pollStatus, 5000); // Retry after 5 seconds
}
}
// Update duration every second for running jobs
let durationInterval = null;
let durationCounter = 0;
function startDurationTimer() {
if (durationInterval) clearInterval(durationInterval);
durationCounter = 0;
durationEl.textContent = '0s';
durationInterval = setInterval(() => {
durationCounter++;
durationEl.textContent = `${durationCounter}s`;
}, 1000);
}
// Initialize
initializeUI();
loadDescIpData();
pollStatus();
</script>
</body>
</html>