719 lines
31 KiB
HTML
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> |