541 lines
25 KiB
HTML
541 lines
25 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PlcVanguard | Industrial Dashboard</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');
|
|
body { font-family: 'Inter', sans-serif; background-color: #f3f4f6; }
|
|
.warning-bg { background-color: #fef08a; } /* yellow-200 */
|
|
.error-bg { background-color: #fee2e2; } /* red-200 */
|
|
</style>
|
|
</head>
|
|
<body class="text-gray-800">
|
|
|
|
<!-- Navbar -->
|
|
<nav class="bg-slate-900 text-white p-4 shadow-lg">
|
|
<div class="container mx-auto flex justify-between items-center">
|
|
<div class="flex items-center space-x-2">
|
|
<div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center font-bold text-white">P</div>
|
|
<span class="text-xl font-bold tracking-tight">PlcVanguard</span>
|
|
</div>
|
|
<div class="flex items-center space-x-4">
|
|
<span class="text-sm text-green-400 flex items-center">
|
|
<span class="w-2 h-2 bg-green-400 rounded-full mr-2 animate-pulse"></span>
|
|
Connected: 192.168.0.102 (S7-1500)
|
|
</span>
|
|
<button class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm font-semibold transition">Settings</button>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="container mx-auto py-8 px-4">
|
|
<header class="mb-8">
|
|
<h1 id="mainTitle" class="text-3xl font-bold mb-2">Live Monitoring Dashboard</h1>
|
|
<p id="mainSubtitle" class="text-gray-600">Real-time polling of DB100 and event-triggered acquisition from DB101.</p>
|
|
</header>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
|
<!-- Status Cards -->
|
|
<div class="lg:col-span-2 bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 id="chartTitle" class="font-semibold text-lg">Live Acquisition (5Hz)</h2>
|
|
<div id="timerDisplay" class="text-blue-600 font-mono font-bold">Time Remaining: 30s</div>
|
|
</div>
|
|
<div class="h-[450px] relative">
|
|
<canvas id="liveChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Simulation Controls & Status -->
|
|
<div class="space-y-6">
|
|
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
|
<h2 class="font-semibold text-lg mb-4">Control Panel</h2>
|
|
<div class="space-y-4">
|
|
<p class="text-sm text-gray-500">Target Mean: 210mm<br>Control Limits: ±2mm (Warning), ±3mm (Error)</p>
|
|
<button id="startBtn" class="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3 rounded-lg transition shadow-md">
|
|
Start Acquisition
|
|
</button>
|
|
<div id="progressContainer" class="hidden">
|
|
<div class="w-full bg-gray-200 rounded-full h-2.5">
|
|
<div id="progressBar" class="bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div>
|
|
</div>
|
|
<p class="text-xs text-gray-400 mt-2">Data collection in progress...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-blue-900 text-white p-6 rounded-xl shadow-lg">
|
|
<h2 class="font-semibold text-lg mb-2">System Overview</h2>
|
|
<ul class="text-sm space-y-2 opacity-90">
|
|
<li>• PLC: Siemens S7-1500</li>
|
|
<li>• Area: DB100 (Continuous)</li>
|
|
<li>• Mode: 5Hz Polling</li>
|
|
<li>• Target: File System (JSON)</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- LEFT COLUMN: Session Selection (History) -->
|
|
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
|
<h2 id="historyHeader" class="font-semibold text-lg mb-4">Recording Sessions</h2>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left text-xs">
|
|
<thead class="bg-gray-50">
|
|
<tr class="border-b">
|
|
<th class="p-2">ID</th>
|
|
<th class="p-2">Timestamp</th>
|
|
<th class="p-2">Status</th>
|
|
<th class="p-2">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="historyTableBody">
|
|
<!-- Sessions injected by JS -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="flex justify-between items-center mt-4">
|
|
<button id="prevPage" class="px-2 py-1 bg-gray-200 rounded text-xs">Prev</button>
|
|
<span id="pageInfo" class="text-xs">Page 1</span>
|
|
<button id="nextPage" class="px-2 py-1 bg-gray-200 rounded text-xs">Next</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RIGHT COLUMN: Dynamic Detail View -->
|
|
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
|
<h2 id="detailTitle" class="font-semibold text-lg mb-4">Detail View</h2>
|
|
|
|
<div id="detailPlaceholder" class="text-gray-400 italic text-center py-20">
|
|
Click a row in the history table to view all captured data points.
|
|
</div>
|
|
|
|
<!-- Live Data View (Only during acquisition) -->
|
|
<div id="liveDataContainer" class="hidden space-y-4">
|
|
<h3 class="text-[10px] uppercase font-bold text-gray-400">Live Session Stream</h3>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left text-[10px]">
|
|
<thead class="bg-gray-50">
|
|
<tr class="border-b">
|
|
<th class="p-2">Index</th>
|
|
<th class="p-2">Timestamp</th>
|
|
<th class="p-2">Value (mm)</th>
|
|
<th class="p-2">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="fullDataTableBody">
|
|
<!-- Data points injected by JS -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="flex justify-between items-center mt-4">
|
|
<button id="fullPrevPage" class="px-2 py-1 bg-gray-200 rounded text-xs">Prev</button>
|
|
<span id="fullPageInfo" class="text-xs">Page 1</span>
|
|
<button id="fullNextPage" class="px-2 py-1 bg-gray-200 rounded text-xs">Next</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- History Detail View (Only for loaded sessions) -->
|
|
<div id="historyDetailContainer" class="hidden space-y-4">
|
|
<h3 class="text-[10px] uppercase font-bold text-gray-400">Session Summary</h3>
|
|
<div class="p-3 bg-gray-50 rounded-lg border border-gray-100">
|
|
<table class="w-full text-left text-[10px]">
|
|
<thead class="bg-gray-50">
|
|
<tr class="border-b">
|
|
<th class="p-3">Timestamp</th>
|
|
<th class="p-3">Value (mm)</th>
|
|
<th class="p-3">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="measurementsTable">
|
|
<!-- Last 3 rows injected by JS -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<!-- FIX: Added missing detailContent container -->
|
|
<div id="detailContent" class="hidden mt-4 p-4 bg-blue-50 border border-blue-200 rounded">
|
|
<div id="selectedData" class="text-xs font-mono"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
// Configuration
|
|
const MEAN = 210;
|
|
const WARNING_LIMIT = 2;
|
|
const ERROR_LIMIT = 3;
|
|
const SAMPLE_RATE = 5;
|
|
const TOTAL_TIME = 30;
|
|
const TOTAL_SAMPLES = TOTAL_TIME * SAMPLE_RATE;
|
|
const TARGET_VALUE = 210;
|
|
const ACQUISITION_LEN = 500;
|
|
const itemsPerPage = 10;
|
|
|
|
// State
|
|
let dataPoints = [];
|
|
let chart;
|
|
let timerInterval;
|
|
let isAcquiring = false;
|
|
let currentMode = 'live';
|
|
let activeSessionData = [];
|
|
let currentPage = 1;
|
|
let fullDataPage = 1;
|
|
|
|
// Mock Recording Sessions
|
|
const mockSessions = [
|
|
{ id: 'REC-001', ts: new Date(Date.now() - 3600000).toISOString(), status: 'Success', data: Array.from({length: 100}, (_, i) => ({
|
|
ts: new Date(Date.now() - (3600000 - (i * 1000))),
|
|
val: generateNormal(210, 1.5),
|
|
x: (i / (100 - 1)) * ACQUISITION_LEN
|
|
})) },
|
|
{ id: 'REC-002', ts: new Date(Date.now() - 7200000).toISOString(), status: 'Success', data: Array.from({length: 100}, (_, i) => ({
|
|
ts: new Date(Date.now() - (7200000 - (i * 1000))),
|
|
val: generateNormal(212, 2.0),
|
|
x: (i / (100 - 1)) * ACQUISITION_LEN
|
|
})) },
|
|
{ id: 'REC-003', ts: new Date(Date.now() - 10800000).toISOString(), status: 'Completed', data: Array.from({length: 100}, (_, i) => ({
|
|
ts: new Date(Date.now() - (10800000 - (i * 1000))),
|
|
val: generateNormal(208, 0.5),
|
|
x: (i / (100 - 1)) * ACQUISITION_LEN
|
|
})) }
|
|
];
|
|
|
|
// DOM Elements
|
|
const startBtn = document.getElementById('startBtn');
|
|
const progressBar = document.getElementById('progressBar');
|
|
const progressContainer = document.getElementById('progressContainer');
|
|
const timerDisplay = document.getElementById('timerDisplay');
|
|
const tableBody = document.getElementById('measurementsTable');
|
|
const detailPlaceholder = document.getElementById('detailPlaceholder');
|
|
const detailContent = document.getElementById('detailContent');
|
|
const fullDataTableBody = document.getElementById('fullDataTableBody');
|
|
const historyTableBody = document.getElementById('historyTableBody');
|
|
const mainTitle = document.getElementById('mainTitle');
|
|
const mainSubtitle = document.getElementById('mainSubtitle');
|
|
const chartTitle = document.getElementById('chartTitle');
|
|
const pageInfo = document.getElementById('pageInfo');
|
|
const prevBtn = document.getElementById('prevPage');
|
|
const nextBtn = document.getElementById('nextPage');
|
|
const fullPrevBtn = document.getElementById('fullPrevPage');
|
|
const fullNextBtn = document.getElementById('fullNextPage');
|
|
const liveDataContainer = document.getElementById('liveDataContainer');
|
|
const historyDetailContainer = document.getElementById('historyDetailContainer');
|
|
const selectedData = document.getElementById('selectedData');
|
|
|
|
// Initialize Chart
|
|
const ctx = document.getElementById('liveChart').getContext('2d');
|
|
chart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
datasets: [
|
|
{ label: 'Value (mm)', data: [], borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', fill: true, pointRadius: 0, borderWidth: 2, tension: 0 },
|
|
{ label: 'Warning UCL', data: [], borderColor: '#facc15', borderWidth: 1, pointRadius: 0, fill: false, borderDash: [5, 5] },
|
|
{ label: 'Warning LCL', data: [], borderColor: '#facc15', borderWidth: 1, pointRadius: 0, fill: false, borderDash: [5, 5] },
|
|
{ label: 'Error UCL', data: [], borderColor: '#f87171', borderWidth: 1, pointRadius: 0, fill: false, borderDash: [5, 5] },
|
|
{ label: 'Error LCL', data: [], borderColor: '#f87171', borderWidth: 1, pointRadius: 0, fill: false, borderDash: [5, 5] },
|
|
{ label: 'Target Value', data: [], borderColor: '#22c55e', borderWidth: 1.5, pointRadius: 0, fill: false, tension: 0 }
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: { type: 'linear', min: 0, max: ACQUISITION_LEN, title: { display: true, text: 'Position (mm)' } },
|
|
y: { min: 200, max: 220 }
|
|
},
|
|
animation: false
|
|
}
|
|
});
|
|
|
|
function generateNormal(mean, stdDev) {
|
|
const u1 = Math.random();
|
|
const u2 = Math.random();
|
|
const z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
|
|
return z0 * stdDev + mean;
|
|
}
|
|
|
|
function updateTable() {
|
|
tableBody.innerHTML = '';
|
|
const lastData = dataPoints.slice(-3).reverse();
|
|
lastData.forEach((point) => {
|
|
const diff = Math.abs(point.val - MEAN);
|
|
let rowClass = 'hover:bg-blue-50 cursor-pointer transition';
|
|
let status = 'OK';
|
|
let colorClass = 'text-green-600 border-green-600';
|
|
|
|
if (diff > ERROR_LIMIT) {
|
|
rowClass += ' error-bg';
|
|
status = 'ERROR';
|
|
colorClass = 'text-red-600 border-red-600';
|
|
} else if (diff > WARNING_LIMIT) {
|
|
rowClass += ' warning-bg';
|
|
status = 'WARNING';
|
|
colorClass = 'text-yellow-600 border-yellow-600';
|
|
}
|
|
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td class="p-2 text-[10px] text-gray-500">${point.ts.toLocaleTimeString()}</td>
|
|
<td class="p-2 font-mono font-bold">${point.val.toFixed(2)}</td>
|
|
<td class="p-2">
|
|
<span class="px-1 py-0.5 bg-white rounded-full text-[9px] font-bold border ${colorClass}">${status}</span>
|
|
</td>
|
|
`;
|
|
tableBody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
function toggleDetailVisibility() {
|
|
detailPlaceholder.classList.add('hidden');
|
|
detailContent.classList.add('hidden');
|
|
|
|
if (isAcquiring || currentMode === 'history') {
|
|
liveDataContainer.classList.remove('hidden');
|
|
historyDetailContainer.classList.add('hidden');
|
|
|
|
if (isAcquiring) {
|
|
activeSessionData = [...dataPoints];
|
|
renderFullSessionTable();
|
|
} else {
|
|
// For history, activeSessionData is already sorted descending in loadSession
|
|
renderFullSessionTable();
|
|
}
|
|
} else {
|
|
liveDataContainer.classList.add('hidden');
|
|
historyDetailContainer.classList.add('hidden');
|
|
detailPlaceholder.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
startBtn.addEventListener('click', () => {
|
|
if (isAcquiring) return;
|
|
|
|
dataPoints = [];
|
|
activeSessionData = [];
|
|
chart.data.labels = [];
|
|
chart.data.datasets.forEach(ds => ds.data = []);
|
|
chart.update();
|
|
|
|
isAcquiring = true;
|
|
startBtn.disabled = true;
|
|
startBtn.classList.replace('bg-green-600', 'bg-gray-400');
|
|
progressContainer.classList.remove('hidden');
|
|
|
|
currentMode = 'live';
|
|
toggleDetailVisibility();
|
|
|
|
let currentStep = 0;
|
|
|
|
timerInterval = setInterval(() => {
|
|
const point = {
|
|
ts: new Date(),
|
|
val: generateNormal(MEAN, 1),
|
|
x: (currentStep / (TOTAL_SAMPLES - 1)) * ACQUISITION_LEN
|
|
};
|
|
dataPoints.push(point);
|
|
activeSessionData = [...dataPoints];
|
|
|
|
chart.data.datasets[0].data.push({x: point.x, y: point.val});
|
|
chart.data.datasets[1].data.push({x: point.x, y: MEAN + WARNING_LIMIT});
|
|
chart.data.datasets[2].data.push({x: point.x, y: MEAN - WARNING_LIMIT});
|
|
chart.data.datasets[3].data.push({x: point.x, y: MEAN + ERROR_LIMIT});
|
|
chart.data.datasets[4].data.push({x: point.x, y: MEAN - ERROR_LIMIT});
|
|
chart.data.datasets[5].data.push({x: point.x, y: TARGET_VALUE});
|
|
|
|
if (chart.data.labels.length > 100) {
|
|
chart.data.labels.shift();
|
|
chart.data.datasets.forEach(ds => ds.data.shift());
|
|
}
|
|
chart.update();
|
|
|
|
if (dataPoints.length % 3 === 0) {
|
|
updateTable();
|
|
}
|
|
|
|
currentStep++;
|
|
const progress = (currentStep / TOTAL_SAMPLES) * 100;
|
|
progressBar.style.width = `${progress}%`;
|
|
timerDisplay.innerText = `Time Remaining: ${Math.max(0, Math.ceil((TOTAL_SAMPLES - currentStep) / SAMPLE_RATE))}s`;
|
|
|
|
if (currentStep >= TOTAL_SAMPLES) {
|
|
clearInterval(timerInterval);
|
|
completeAcquisition();
|
|
}
|
|
}, 1000 / SAMPLE_RATE);
|
|
});
|
|
|
|
function completeAcquisition() {
|
|
isAcquiring = false;
|
|
startBtn.disabled = false;
|
|
startBtn.classList.replace('bg-gray-400', 'bg-green-600');
|
|
timerDisplay.innerText = "Acquisition Complete";
|
|
timerDisplay.classList.replace('text-blue-600', 'text-green-600');
|
|
|
|
// Sort data descending by timestamp for the Detail View (Newest First)
|
|
activeSessionData.sort((a, b) => b.ts - a.ts);
|
|
|
|
// Ensure the live data container (table) is visible
|
|
liveDataContainer.classList.remove('hidden');
|
|
historyDetailContainer.classList.add('hidden');
|
|
|
|
renderFullSessionTable();
|
|
|
|
const highResData = Array.from({length: 4096}, () => generateNormal(MEAN, 1));
|
|
detailPlaceholder.classList.add('hidden');
|
|
detailContent.classList.remove('hidden');
|
|
selectedData.innerText = `High Resolution Data (First 5):\n\n[Success: 4096 points captured]\nMean: ${highResData.reduce((a,b)=>a+b,0)/4096}\nMax: ${Math.max(...highResData)}\nMin: ${Math.min(...highResData)}\n\nRaw Array (Sample): ${highResData.slice(0, 5).join(', ')}...`;
|
|
}
|
|
|
|
window.loadSession = (id) => {
|
|
currentMode = 'history';
|
|
mainTitle.innerText = "Recording History View";
|
|
mainSubtitle.innerText = "Visualizing historical data from recorded PLC sessions.";
|
|
chartTitle.innerText = `Session: ${id}`;
|
|
|
|
const session = mockSessions.find(s => s.id === id);
|
|
if (session) {
|
|
// Sort descending for the table/detail view (Newest First)
|
|
activeSessionData = [...session.data].sort((a, b) => b.ts - a.ts);
|
|
// Sort ascending for the chart (Oldest -> Newest left to right)
|
|
dataPoints = [...session.data].sort((a, b) => a.ts - b.ts);
|
|
} else {
|
|
activeSessionData = [];
|
|
dataPoints = [];
|
|
}
|
|
|
|
updateTable();
|
|
|
|
chart.data.labels = dataPoints.map((_, i) => (i / (dataPoints.length - 1)) * ACQUISITION_LEN);
|
|
chart.data.datasets[0].data = dataPoints.map(p => p.val);
|
|
chart.data.datasets[1].data = Array(dataPoints.length).fill(MEAN + WARNING_LIMIT);
|
|
chart.data.datasets[2].data = Array(dataPoints.length).fill(MEAN - WARNING_LIMIT);
|
|
chart.data.datasets[3].data = Array(dataPoints.length).fill(MEAN + ERROR_LIMIT);
|
|
chart.data.datasets[4].data = Array(dataPoints.length).fill(MEAN - ERROR_LIMIT);
|
|
chart.data.datasets[5].data = Array(dataPoints.length).fill(TARGET_VALUE);
|
|
chart.update();
|
|
|
|
fullDataPage = 1;
|
|
renderFullSessionTable();
|
|
toggleDetailVisibility();
|
|
};
|
|
|
|
function renderFullSessionTable() {
|
|
fullDataTableBody.innerHTML = '';
|
|
const start = (fullDataPage - 1) * itemsPerPage;
|
|
const end = start + itemsPerPage;
|
|
const pageData = activeSessionData.slice(start, end);
|
|
|
|
pageData.forEach((p, i) => {
|
|
const row = document.createElement('tr');
|
|
const diff = Math.abs(p.val - MEAN);
|
|
let status = 'OK';
|
|
let colorClass = 'text-green-600 border-green-600';
|
|
|
|
if (diff > ERROR_LIMIT) {
|
|
status = 'ERROR';
|
|
colorClass = 'text-red-600 border-red-600';
|
|
} else if (diff > WARNING_LIMIT) {
|
|
status = 'WARNING';
|
|
colorClass = 'text-yellow-600 border-yellow-600';
|
|
}
|
|
|
|
row.innerHTML = `
|
|
<td class="p-2 border-b">${start + i + 1}</td>
|
|
<td class="p-2 border-b">${p.ts.toLocaleTimeString()}</td>
|
|
<td class="p-2 border-b font-mono">${p.val.toFixed(2)}</td>
|
|
<td class="p-2 border-b">
|
|
<span class="px-1 py-0.5 bg-white rounded-full text-[9px] font-bold border ${colorClass}">${status}</span>
|
|
</td>
|
|
`;
|
|
fullDataTableBody.appendChild(row);
|
|
});
|
|
|
|
fullPageInfo.innerText = `Page ${fullDataPage} of ${Math.ceil(activeSessionData.length / itemsPerPage)}`;
|
|
fullPrevBtn.disabled = fullDataPage === 1;
|
|
fullNextBtn.disabled = fullDataPage * itemsPerPage >= activeSessionData.length;
|
|
}
|
|
|
|
// Event Listeners for Pagination
|
|
fullPrevBtn.onclick = () => {
|
|
if (fullDataPage > 1) {
|
|
fullDataPage--;
|
|
renderFullSessionTable();
|
|
}
|
|
};
|
|
|
|
fullNextBtn.onclick = () => {
|
|
if (fullDataPage * itemsPerPage < activeSessionData.length) {
|
|
fullDataPage++;
|
|
renderFullSessionTable();
|
|
}
|
|
};
|
|
|
|
prevBtn.onclick = () => {
|
|
if (currentPage > 1) {
|
|
currentPage--;
|
|
renderHistoryTable();
|
|
}
|
|
};
|
|
|
|
nextBtn.onclick = () => {
|
|
if (currentPage * itemsPerPage < mockSessions.length) {
|
|
currentPage++;
|
|
renderHistoryTable();
|
|
}
|
|
};
|
|
|
|
function renderHistoryTable() {
|
|
historyTableBody.innerHTML = '';
|
|
const start = (currentPage - 1) * itemsPerPage;
|
|
const end = start + itemsPerPage;
|
|
const pageData = mockSessions.slice(start, end);
|
|
|
|
pageData.forEach(s => {
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td class="p-2 border-b">${s.id}</td>
|
|
<td class="p-2 border-b">${s.ts}</td>
|
|
<td class="p-2 border-b">${s.status}</td>
|
|
<td class="p-2 border-b">
|
|
<button class="text-blue-600 hover:underline" onclick="loadSession('${s.id}')">Reload</button>
|
|
</td>
|
|
`;
|
|
historyTableBody.appendChild(row);
|
|
});
|
|
pageInfo.innerText = `Page ${currentPage} of ${Math.ceil(mockSessions.length / itemsPerPage)}`;
|
|
}
|
|
|
|
// Initial Render
|
|
window.onload = () => {
|
|
const sessions = [
|
|
{ id: 'REC-001', ts: new Date(Date.now() - 3600000).toISOString(), status: 'Success' },
|
|
{ id: 'REC-002', ts: new Date(Date.now() - 7200000).toISOString(), status: 'Success' },
|
|
{ id: 'REC-003', ts: new Date(Date.now() - 10800000).toISOString(), status: 'Completed' }
|
|
];
|
|
|
|
const tableBody = document.getElementById('historyTableBody');
|
|
sessions.forEach(s => {
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td class="p-2 border-b">${s.id}</td>
|
|
<td class="p-2 border-b">${s.ts}</td>
|
|
<td class="p-2 border-b">${s.status}</td>
|
|
<td class="p-2 border-b">
|
|
<button class="text-blue-600 hover:underline" onclick="loadSession('${s.id}')">Reload</button>
|
|
</td>
|
|
`;
|
|
tableBody.appendChild(row);
|
|
});
|
|
|
|
renderHistoryTable();
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|