truenas-burnin/app/static/app.js
Brandon Walter b73b5251ae Initial commit — TrueNAS Burn-In Dashboard v0.5.0
Full-stack burn-in orchestration dashboard (Stages 1–6d complete):
FastAPI backend, SQLite/WAL, SSE live dashboard, mock TrueNAS server,
SMTP/webhook notifications, batch burn-in, settings UI, audit log,
stats page, cancel SMART/burn-in, drag-to-reorder stages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 00:08:29 -05:00

848 lines
30 KiB
JavaScript

(function () {
'use strict';
// -----------------------------------------------------------------------
// Filter bar + stats bar
// -----------------------------------------------------------------------
var activeFilter = 'all';
function getRows() {
return Array.from(document.querySelectorAll('#drives-tbody tr[data-status]'));
}
function updateCounts() {
var rows = getRows();
var counts = { all: rows.length, running: 0, failed: 0, passed: 0, idle: 0 };
rows.forEach(function (r) {
var s = r.dataset.status;
if (s && Object.prototype.hasOwnProperty.call(counts, s)) counts[s]++;
});
// Update filter bar badges
document.querySelectorAll('.filter-btn[data-filter]').forEach(function (btn) {
var badge = btn.querySelector('.badge');
if (badge) badge.textContent = counts[btn.dataset.filter] != null ? counts[btn.dataset.filter] : 0;
});
// Update stats bar
['all', 'running', 'failed', 'passed', 'idle'].forEach(function (s) {
var el = document.getElementById('stat-' + s);
if (el) el.textContent = counts[s] != null ? counts[s] : 0;
});
// Show/hide failed banner
var banner = document.getElementById('failed-banner');
if (banner) {
var failedCount = counts.failed || 0;
banner.hidden = failedCount === 0;
var fc = banner.querySelector('.failed-count');
if (fc) fc.textContent = failedCount;
}
// Show/hide "Cancel All Burn-Ins" button based on whether any .btn-cancel exist
var cancelAllBtn = document.getElementById('cancel-all-btn');
if (cancelAllBtn) {
var hasCancelable = document.querySelectorAll('.btn-cancel[data-job-id]').length > 0;
cancelAllBtn.hidden = !hasCancelable;
}
}
function applyFilter(filter) {
activeFilter = filter;
getRows().forEach(function (row) {
row.style.display = (filter === 'all' || row.dataset.status === filter) ? '' : 'none';
});
document.querySelectorAll('.filter-btn[data-filter]').forEach(function (btn) {
btn.classList.toggle('active', btn.dataset.filter === filter);
});
updateCounts();
}
document.addEventListener('click', function (e) {
var btn = e.target.closest('.filter-btn[data-filter]');
if (btn) applyFilter(btn.dataset.filter);
});
document.addEventListener('htmx:afterSwap', function () {
applyFilter(activeFilter);
restoreCheckboxes();
initElapsedTimers();
initLocationEdits();
});
updateCounts();
// -----------------------------------------------------------------------
// Toast notifications
// -----------------------------------------------------------------------
function showToast(msg, type) {
type = type || 'info';
var container = document.getElementById('toast-container');
if (!container) return;
var el = document.createElement('div');
el.className = 'toast toast-' + type;
el.textContent = msg;
container.appendChild(el);
setTimeout(function () { el.remove(); }, 5000);
}
// -----------------------------------------------------------------------
// Browser push notifications
// -----------------------------------------------------------------------
function updateNotifBtn() {
var btn = document.getElementById('notif-btn');
if (!btn) return;
var perm = Notification.permission;
btn.classList.remove('notif-active', 'notif-denied');
if (perm === 'granted') {
btn.classList.add('notif-active');
btn.title = 'Notifications enabled';
} else if (perm === 'denied') {
btn.classList.add('notif-denied');
btn.title = 'Notifications blocked — allow in browser settings';
} else {
btn.title = 'Enable browser notifications';
}
}
if ('Notification' in window) {
updateNotifBtn();
document.addEventListener('click', function (e) {
if (!e.target.closest('#notif-btn')) return;
if (Notification.permission === 'denied') {
showToast('Notifications blocked — allow in browser settings', 'error');
return;
}
Notification.requestPermission().then(function (perm) {
updateNotifBtn();
if (perm === 'granted') {
showToast('Browser notifications enabled', 'success');
new Notification('TrueNAS Burn-In', {
body: 'You will be notified when burn-in jobs complete.',
});
}
});
});
} else {
var nb = document.getElementById('notif-btn');
if (nb) nb.style.display = 'none';
}
// Handle job-alert SSE events for browser notifications
document.addEventListener('htmx:sseMessage', function (e) {
if (!e.detail || e.detail.type !== 'job-alert') return;
try {
handleJobAlert(JSON.parse(e.detail.data));
} catch (_) {}
});
function handleJobAlert(data) {
var isPass = data.state === 'passed';
var icon = isPass ? '✓' : '✕';
var title = icon + ' ' + (data.devname || 'Drive') + ' — Burn-In ' + (data.state || '').toUpperCase();
var bodyText = (data.model || '') + (data.serial ? ' · ' + data.serial : '');
if (!isPass && data.error_text) bodyText += '\n' + data.error_text;
showToast(title + (data.error_text ? ' · ' + data.error_text : ''), isPass ? 'success' : 'error');
if (Notification.permission === 'granted') {
try {
new Notification(title, { body: bodyText || undefined });
} catch (_) {}
}
}
// -----------------------------------------------------------------------
// Elapsed time timers
// -----------------------------------------------------------------------
var _elapsedInterval = null;
function formatElapsed(seconds) {
if (seconds < 0) return '';
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
var s = seconds % 60;
if (h > 0) return h + 'h ' + m + 'm';
if (m > 0) return m + 'm ' + s + 's';
return s + 's';
}
function tickElapsedTimers() {
var now = Date.now();
document.querySelectorAll('.elapsed-timer[data-started]').forEach(function (el) {
var started = new Date(el.dataset.started).getTime();
if (isNaN(started)) return;
var elapsed = Math.floor((now - started) / 1000);
el.textContent = formatElapsed(elapsed);
});
}
function initElapsedTimers() {
if (_elapsedInterval) return; // Already running
var timers = document.querySelectorAll('.elapsed-timer[data-started]');
if (timers.length === 0) return;
_elapsedInterval = setInterval(function () {
var remaining = document.querySelectorAll('.elapsed-timer[data-started]');
if (remaining.length === 0) {
clearInterval(_elapsedInterval);
_elapsedInterval = null;
return;
}
tickElapsedTimers();
}, 1000);
tickElapsedTimers();
}
initElapsedTimers();
// -----------------------------------------------------------------------
// Inline location / notes edit
// -----------------------------------------------------------------------
function initLocationEdits() {
document.querySelectorAll('.drive-location').forEach(function (el) {
if (el._locationInited) return;
el._locationInited = true;
el.addEventListener('click', function (evt) {
evt.stopPropagation();
var driveId = el.dataset.driveId;
var current = el.classList.contains('drive-location-empty') ? '' : el.textContent.trim();
var input = document.createElement('input');
input.type = 'text';
input.className = 'drive-location-input';
input.value = current;
input.placeholder = 'e.g. Bay 3 Shelf 2';
input.maxLength = 64;
el.replaceWith(input);
input.focus();
input.select();
async function save() {
var newVal = input.value.trim();
try {
var resp = await fetch('/api/v1/drives/' + driveId, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ location: newVal || null }),
});
if (!resp.ok) throw new Error('save failed');
} catch (_) {
showToast('Failed to save location', 'error');
}
// The SSE update will replace the whole row; nothing more needed
}
function cancel() {
var span = document.createElement('span');
span.className = 'drive-location' + (current ? '' : ' drive-location-empty');
span.dataset.driveId = driveId;
span.dataset.field = 'location';
span.title = current ? 'Click to edit location' : 'Click to set location';
span.textContent = current || '+ location';
input.replaceWith(span);
initLocationEdits(); // re-attach listener
}
input.addEventListener('blur', function () { save(); });
input.addEventListener('keydown', function (e) {
if (e.key === 'Enter') { input.blur(); }
if (e.key === 'Escape') { cancel(); }
});
});
});
}
initLocationEdits();
// -----------------------------------------------------------------------
// Stage drag-and-drop reordering
// -----------------------------------------------------------------------
function initStageDrag(listId) {
var list = document.getElementById(listId);
if (!list || list._dragInited) return;
list._dragInited = true;
var draggingEl = null;
list.addEventListener('dragstart', function (e) {
draggingEl = e.target.closest('.stage-check');
if (!draggingEl) return;
draggingEl.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
});
list.addEventListener('dragend', function () {
if (draggingEl) draggingEl.classList.remove('dragging');
list.querySelectorAll('.stage-check.drag-over').forEach(function (el) {
el.classList.remove('drag-over');
});
draggingEl = null;
});
list.addEventListener('dragover', function (e) {
e.preventDefault();
if (!draggingEl) return;
var target = e.target.closest('.stage-check');
if (!target || target === draggingEl) return;
var rect = target.getBoundingClientRect();
var midY = rect.top + rect.height / 2;
if (e.clientY < midY) {
list.insertBefore(draggingEl, target);
} else {
list.insertBefore(draggingEl, target.nextSibling);
}
});
}
// Map checkbox id → backend stage name
var _STAGE_ID_MAP = {
'stage-surface': 'surface_validate',
'stage-short': 'short_smart',
'stage-long': 'long_smart',
'batch-stage-surface': 'surface_validate',
'batch-stage-short': 'short_smart',
'batch-stage-long': 'long_smart',
};
// Read DOM order of checked stages from the given list element
function getStageOrder(listId) {
var items = Array.from(document.querySelectorAll('#' + listId + ' .stage-check'));
var order = [];
items.forEach(function (item) {
var cb = item.querySelector('input[type=checkbox]');
if (cb && cb.checked && _STAGE_ID_MAP[cb.id]) {
order.push(_STAGE_ID_MAP[cb.id]);
}
});
return order;
}
// -----------------------------------------------------------------------
// Standalone SMART test
// -----------------------------------------------------------------------
async function startSmartTest(btn) {
var driveId = btn.dataset.driveId;
var testType = btn.dataset.testType;
var operator = localStorage.getItem('burnin_operator') || 'unknown';
btn.disabled = true;
try {
var resp = await fetch('/api/v1/drives/' + driveId + '/smart/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: testType, operator: operator }),
});
var data = await resp.json();
if (!resp.ok) {
showToast(data.detail || 'Failed to start test', 'error');
btn.disabled = false;
} else {
var label = testType === 'SHORT' ? 'Short' : 'Long';
showToast(label + ' SMART test started on ' + data.devname, 'success');
}
} catch (err) {
showToast('Network error', 'error');
btn.disabled = false;
}
}
// -----------------------------------------------------------------------
// Cancel standalone SMART test
// -----------------------------------------------------------------------
async function cancelSmartTest(btn) {
var driveId = btn.dataset.driveId;
var testType = btn.dataset.testType; // 'short' or 'long'
var label = testType === 'short' ? 'Short' : 'Long';
if (!confirm('Cancel the ' + label + ' SMART test? This cannot be undone.')) return;
btn.disabled = true;
try {
var resp = await fetch('/api/v1/drives/' + driveId + '/smart/cancel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: testType }),
});
var data = await resp.json();
if (!resp.ok) {
showToast(data.detail || 'Failed to cancel test', 'error');
btn.disabled = false;
} else {
var label = testType === 'short' ? 'Short' : 'Long';
showToast(label + ' SMART test cancelled on ' + (data.devname || ''), 'info');
}
} catch (err) {
showToast('Network error', 'error');
btn.disabled = false;
}
}
// -----------------------------------------------------------------------
// Cancel ALL running/queued burn-in jobs
// -----------------------------------------------------------------------
async function cancelAllBurnins() {
var cancelBtns = Array.from(document.querySelectorAll('.btn-cancel[data-job-id]'));
if (cancelBtns.length === 0) {
showToast('No active burn-in jobs to cancel', 'info');
return;
}
if (!confirm('Cancel ALL ' + cancelBtns.length + ' active burn-in job(s)? This cannot be undone.')) return;
var operator = localStorage.getItem('burnin_operator') || 'unknown';
var count = 0;
for (var i = 0; i < cancelBtns.length; i++) {
var jobId = cancelBtns[i].dataset.jobId;
try {
var resp = await fetch('/api/v1/burnin/' + jobId + '/cancel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ operator: operator }),
});
if (resp.ok) count++;
} catch (_) {}
}
showToast(count + ' burn-in job(s) cancelled', 'info');
}
// -----------------------------------------------------------------------
// Single drive Burn-In modal
// -----------------------------------------------------------------------
var modalDriveId = null;
var modalSerial = null;
function _stageLabel() {
// Read labels in DOM order from stage-order-list
var items = Array.from(document.querySelectorAll('#stage-order-list .stage-check'));
var labelMap = {
'stage-surface': 'Surface',
'stage-short': 'Short SMART',
'stage-long': 'Long SMART',
};
var parts = [];
items.forEach(function (item) {
var cb = item.querySelector('input[type=checkbox]');
if (cb && cb.checked && labelMap[cb.id]) parts.push(labelMap[cb.id]);
});
return parts.length ? parts.join(' + ') : 'No stages';
}
function handleStageChange() {
var surfaceChecked = document.getElementById('stage-surface') && document.getElementById('stage-surface').checked;
var warning = document.getElementById('surface-warning');
var serialField = document.getElementById('serial-field');
if (warning) warning.style.display = surfaceChecked ? '' : 'none';
if (serialField) serialField.style.display = surfaceChecked ? '' : 'none';
// Update title
var title = document.getElementById('modal-title');
if (title) title.textContent = 'Burn-In — ' + _stageLabel();
validateModal();
}
function openModal(btn) {
modalDriveId = btn.dataset.driveId;
modalSerial = btn.dataset.serial || '';
document.getElementById('modal-devname').textContent = btn.dataset.devname || '—';
document.getElementById('modal-model').textContent = btn.dataset.model || '—';
document.getElementById('modal-serial-display').textContent = btn.dataset.serial || '—';
document.getElementById('modal-size').textContent = btn.dataset.size || '—';
var healthEl = document.getElementById('modal-health');
var health = btn.dataset.health || 'UNKNOWN';
healthEl.textContent = health;
healthEl.className = 'chip chip-' + health.toLowerCase();
// Reset stage checkboxes to all-on (keep user's drag order)
['stage-surface', 'stage-short', 'stage-long'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.checked = true;
});
document.getElementById('confirm-serial').value = '';
document.getElementById('confirm-hint').textContent = 'Expected: ' + modalSerial;
var savedOp = localStorage.getItem('burnin_operator') || '';
document.getElementById('operator-input').value = savedOp;
// Init drag on first open (list is in static DOM)
initStageDrag('stage-order-list');
handleStageChange(); // sets warning visibility + title + validates
document.getElementById('start-modal').removeAttribute('hidden');
setTimeout(function () {
document.getElementById('operator-input').focus();
}, 50);
}
function closeModal() {
document.getElementById('start-modal').setAttribute('hidden', '');
modalDriveId = null;
modalSerial = null;
}
function validateModal() {
var operator = (document.getElementById('operator-input').value || '').trim();
var surfaceChecked = document.getElementById('stage-surface') && document.getElementById('stage-surface').checked;
var shortChecked = document.getElementById('stage-short') && document.getElementById('stage-short').checked;
var longChecked = document.getElementById('stage-long') && document.getElementById('stage-long').checked;
var anyStage = surfaceChecked || shortChecked || longChecked;
var valid;
if (surfaceChecked) {
var typed = (document.getElementById('confirm-serial').value || '').trim();
valid = operator.length > 0 && typed === modalSerial && modalSerial !== '' && anyStage;
} else {
valid = operator.length > 0 && anyStage;
}
document.getElementById('modal-start-btn').disabled = !valid;
}
async function submitStart() {
var operator = (document.getElementById('operator-input').value || '').trim();
localStorage.setItem('burnin_operator', operator);
var runSurface = document.getElementById('stage-surface').checked;
var runShort = document.getElementById('stage-short').checked;
var runLong = document.getElementById('stage-long').checked;
var stageOrder = getStageOrder('stage-order-list');
try {
var resp = await fetch('/api/v1/burnin/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
drive_ids: [parseInt(modalDriveId, 10)],
operator: operator,
run_surface: runSurface,
run_short: runShort,
run_long: runLong,
stage_order: stageOrder,
}),
});
var data = await resp.json();
if (!resp.ok) {
showToast(data.detail || 'Failed to start burn-in', 'error');
return;
}
closeModal();
showToast('Burn-in queued: ' + _stageLabel(), 'success');
} catch (err) {
showToast('Network error', 'error');
}
}
// -----------------------------------------------------------------------
// Batch Burn-In
// -----------------------------------------------------------------------
var checkedDriveIds = new Set();
function updateBatchBar() {
var bar = document.getElementById('batch-bar');
if (!bar) return;
var count = checkedDriveIds.size;
bar.hidden = count === 0;
var countEl = document.getElementById('batch-count');
if (countEl) countEl.textContent = count;
}
function restoreCheckboxes() {
// Re-check boxes that were checked before the SSE swap
document.querySelectorAll('.drive-checkbox').forEach(function (cb) {
cb.checked = checkedDriveIds.has(cb.dataset.driveId);
});
// Update select-all state
var selectAll = document.getElementById('select-all-cb');
if (selectAll) {
var allBoxes = document.querySelectorAll('.drive-checkbox');
selectAll.checked = allBoxes.length > 0 && Array.from(allBoxes).every(function (c) { return c.checked; });
selectAll.indeterminate = checkedDriveIds.size > 0 && !selectAll.checked;
}
updateBatchBar();
}
// Toggle individual checkbox
document.addEventListener('change', function (e) {
if (e.target.classList.contains('drive-checkbox')) {
var id = e.target.dataset.driveId;
if (e.target.checked) {
checkedDriveIds.add(id);
} else {
checkedDriveIds.delete(id);
}
updateBatchBar();
// Update select-all indeterminate state
var selectAll = document.getElementById('select-all-cb');
if (selectAll) {
var allBoxes = Array.from(document.querySelectorAll('.drive-checkbox'));
selectAll.checked = allBoxes.length > 0 && allBoxes.every(function (c) { return c.checked; });
selectAll.indeterminate = checkedDriveIds.size > 0 && !selectAll.checked;
}
return;
}
// Select-all checkbox
if (e.target.id === 'select-all-cb') {
var boxes = document.querySelectorAll('.drive-checkbox');
boxes.forEach(function (cb) {
cb.checked = e.target.checked;
if (e.target.checked) {
checkedDriveIds.add(cb.dataset.driveId);
} else {
checkedDriveIds.delete(cb.dataset.driveId);
}
});
updateBatchBar();
return;
}
// Batch modal inputs validation
if (['batch-confirm-cb', 'batch-stage-surface', 'batch-stage-short', 'batch-stage-long'].indexOf(e.target.id) !== -1) {
validateBatchModal();
}
});
// Batch bar buttons
document.addEventListener('click', function (e) {
if (e.target.id === 'batch-start-btn' || e.target.closest('#batch-start-btn')) {
openBatchModal();
return;
}
if (e.target.id === 'batch-clear-btn') {
checkedDriveIds.clear();
document.querySelectorAll('.drive-checkbox').forEach(function (cb) { cb.checked = false; });
var sa = document.getElementById('select-all-cb');
if (sa) { sa.checked = false; sa.indeterminate = false; }
updateBatchBar();
return;
}
});
function openBatchModal() {
var modal = document.getElementById('batch-modal');
if (!modal) return;
var savedOp = localStorage.getItem('burnin_operator') || '';
document.getElementById('batch-operator-input').value = savedOp;
document.getElementById('batch-confirm-cb').checked = false;
// Reset stages to all-on (keep user's drag order)
['batch-stage-surface', 'batch-stage-short', 'batch-stage-long'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.checked = true;
});
var countEls = document.querySelectorAll('#batch-modal-count, #batch-modal-count-btn');
countEls.forEach(function (el) { el.textContent = checkedDriveIds.size; });
// Init drag on first open
initStageDrag('batch-stage-order-list');
validateBatchModal();
modal.removeAttribute('hidden');
setTimeout(function () {
document.getElementById('batch-operator-input').focus();
}, 50);
}
function closeBatchModal() {
var modal = document.getElementById('batch-modal');
if (modal) modal.setAttribute('hidden', '');
}
function validateBatchModal() {
var operator = (document.getElementById('batch-operator-input').value || '').trim();
var surfaceEl = document.getElementById('batch-stage-surface');
var surfaceChecked = surfaceEl && surfaceEl.checked;
// Show/hide destructive warning and confirm checkbox based on surface selection
var warning = document.getElementById('batch-surface-warning');
var confirmWrap = document.getElementById('batch-confirm-wrap');
if (warning) warning.style.display = surfaceChecked ? '' : 'none';
if (confirmWrap) confirmWrap.style.display = surfaceChecked ? '' : 'none';
var shortEl = document.getElementById('batch-stage-short');
var longEl = document.getElementById('batch-stage-long');
var anyStage = surfaceChecked ||
(shortEl && shortEl.checked) ||
(longEl && longEl.checked);
var valid;
if (surfaceChecked) {
var confirmed = document.getElementById('batch-confirm-cb').checked;
valid = operator.length > 0 && confirmed && anyStage;
} else {
valid = operator.length > 0 && anyStage;
}
var btn = document.getElementById('batch-modal-start-btn');
if (btn) btn.disabled = !valid;
}
document.addEventListener('input', function (e) {
if (e.target.id === 'operator-input' || e.target.id === 'confirm-serial') validateModal();
if (e.target.id === 'batch-operator-input') validateBatchModal();
});
async function submitBatchStart() {
var operator = (document.getElementById('batch-operator-input').value || '').trim();
localStorage.setItem('burnin_operator', operator);
var ids = Array.from(checkedDriveIds).map(function (id) { return parseInt(id, 10); });
if (ids.length === 0) return;
var btn = document.getElementById('batch-modal-start-btn');
if (btn) btn.disabled = true;
var runSurface = document.getElementById('batch-stage-surface').checked;
var runShort = document.getElementById('batch-stage-short').checked;
var runLong = document.getElementById('batch-stage-long').checked;
var stageOrder = getStageOrder('batch-stage-order-list');
try {
var resp = await fetch('/api/v1/burnin/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
drive_ids: ids,
operator: operator,
run_surface: runSurface,
run_short: runShort,
run_long: runLong,
stage_order: stageOrder,
}),
});
var data = await resp.json();
if (!resp.ok) {
showToast(data.detail || 'Failed to queue batch', 'error');
if (btn) btn.disabled = false;
return;
}
closeBatchModal();
checkedDriveIds.clear();
updateBatchBar();
var queued = (data.queued || []).length;
var errors = (data.errors || []).length;
var msg = queued + ' burn-in(s) queued';
if (errors) msg += ', ' + errors + ' skipped (already active)';
showToast(msg, errors && !queued ? 'error' : 'success');
} catch (err) {
showToast('Network error', 'error');
if (btn) btn.disabled = false;
}
}
// -----------------------------------------------------------------------
// Cancel burn-in (individual)
// -----------------------------------------------------------------------
async function cancelBurnin(btn) {
var jobId = btn.dataset.jobId;
var operator = localStorage.getItem('burnin_operator') || 'unknown';
if (!confirm('Cancel this burn-in job? This cannot be undone.')) return;
btn.disabled = true;
try {
var resp = await fetch('/api/v1/burnin/' + jobId + '/cancel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ operator: operator }),
});
if (resp.ok) {
showToast('Burn-in cancelled', 'info');
} else {
var data = await resp.json();
showToast(data.detail || 'Failed to cancel', 'error');
btn.disabled = false;
}
} catch (err) {
showToast('Network error', 'error');
btn.disabled = false;
}
}
// -----------------------------------------------------------------------
// Delegated event handlers (work after SSE swaps)
// -----------------------------------------------------------------------
document.addEventListener('click', function (e) {
// Short / Long SMART start buttons
var smartBtn = e.target.closest('.btn-smart-short, .btn-smart-long');
if (smartBtn && !smartBtn.disabled) { startSmartTest(smartBtn); return; }
// Cancel SMART test buttons
var cancelSmartBtn = e.target.closest('.btn-cancel-smart');
if (cancelSmartBtn && !cancelSmartBtn.disabled) { cancelSmartTest(cancelSmartBtn); return; }
// Burn-in start button (single drive)
var startBtn = e.target.closest('.btn-start');
if (startBtn && !startBtn.disabled) { openModal(startBtn); return; }
// Cancel burn-in button (individual)
var cancelBtn = e.target.closest('.btn-cancel');
if (cancelBtn) { cancelBurnin(cancelBtn); return; }
// Cancel ALL running burn-ins
if (e.target.id === 'cancel-all-btn' || e.target.closest('#cancel-all-btn')) {
cancelAllBurnins();
return;
}
// Single-drive modal close
if (e.target.closest('#modal-close-btn') || e.target.closest('#modal-cancel-btn')) {
closeModal();
return;
}
if (e.target.id === 'start-modal') {
closeModal();
return;
}
if (e.target.id === 'modal-start-btn') {
submitStart();
return;
}
// Batch modal close
if (e.target.closest('#batch-modal-close-btn') || e.target.closest('#batch-modal-cancel-btn')) {
closeBatchModal();
return;
}
if (e.target.id === 'batch-modal') {
closeBatchModal();
return;
}
if (e.target.id === 'batch-modal-start-btn') {
submitBatchStart();
return;
}
});
document.addEventListener('input', function (e) {
var id = e.target.id;
if (id === 'operator-input' || id === 'confirm-serial') validateModal();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
var modal = document.getElementById('start-modal');
if (modal && !modal.hidden) { closeModal(); return; }
var bModal = document.getElementById('batch-modal');
if (bModal && !bModal.hidden) { closeBatchModal(); return; }
}
});
}());