(function () { 'use strict'; var grid = document.getElementById('board-grid'); var emptyMsg = document.getElementById('empty-msg'); var modalOverlay = document.getElementById('config-modal'); var configForm = document.getElementById('config-form'); var editingBoard = null; function esc(s) { if (typeof s !== 'string') return ''; var d = document.createElement('div'); d.appendChild(document.createTextNode(s)); return d.innerHTML; } // ---- Fetch boards ---- function fetchBoards() { fetch('/api/boards') .then(function (r) { return r.json(); }) .then(function (boards) { renderBoards(boards); }) .catch(function () {}); } // ---- Render board cards ---- function renderBoards(boards) { if (boards.length === 0) { emptyMsg.style.display = 'block'; grid.innerHTML = ''; grid.appendChild(emptyMsg); return; } emptyMsg.style.display = 'none'; var sorted = boards.slice().sort(function (a, b) { var na = (a.display_name || '').toLowerCase(); var nb = (b.display_name || '').toLowerCase(); return na.localeCompare(nb); }); var html = ''; sorted.forEach(function (board) { var statusClass = board.online ? 'online' : 'offline'; var badgeClass = board.online ? 'badge-online' : 'badge-offline'; var left = board.config && board.config.left || {}; var right = board.config && board.config.right || {}; var scores = board.scores || {}; var leftScore = scores.left != null ? scores.left : '-'; var rightScore = scores.right != null ? scores.right : '-'; var leftColor = left.color || '#dc2626'; var rightColor = right.color || '#2563eb'; html += '
' + '
' + '
' + '' + '' + esc(board.display_name || board.ip) + '' + '' + board.ip + '' + '
' + '
' + '
' + // Left team '
' + '
' + esc(left.name || '???') + '
' + '
' + leftScore + '
' + '
' + '' + '' + '' + '
' + '
' + // Right team '
' + '
' + esc(right.name || '???') + '
' + '
' + rightScore + '
' + '
' + '' + '' + '' + '
' + '
' + '
' + '
' + '' + '' + '
' + '
'; }); grid.innerHTML = html; } // ---- Optimistic score updates ---- function updateScoreLocal(card, side, delta) { var col = card.querySelector('.team-column[data-side="' + side + '"]'); if (!col) return; var el = col.querySelector('.score'); if (!el) return; var cur = parseInt(el.textContent); if (isNaN(cur)) return; var next = Math.max(0, cur + delta); el.textContent = next; } function setScoreLocal(card, side, value) { var col = card.querySelector('.team-column[data-side="' + side + '"]'); if (!col) return; var el = col.querySelector('.score'); if (!el) return; el.textContent = value; } // ---- Re-poll single board directly (bypasses controller cache) ---- function refetchBoard(ip) { fetch('/api/boards/' + ip + '/api/status') .then(function (r) { return r.json(); }) .then(function (data) { var card = document.querySelector('.card[data-board="' + ip + '"]'); if (!card) return; var scores = data.scores || {}; var leftEl = card.querySelector('.team-column[data-side="left"] .score'); var rightEl = card.querySelector('.team-column[data-side="right"] .score'); if (leftEl) leftEl.textContent = scores.left != null ? scores.left : '-'; if (rightEl) rightEl.textContent = scores.right != null ? scores.right : '-'; }) .catch(function () {}); } // ---- Actions (optimistic + re-poll board directly) ---- function increment(ip, side, card) { updateScoreLocal(card, side, 1); fetch('/api/boards/' + ip + '/api/score/' + side + '/increment', { method: 'POST' }).then(function () { refetchBoard(ip); }).catch(function () {}); } function decrement(ip, side, card) { updateScoreLocal(card, side, -1); fetch('/api/boards/' + ip + '/api/score/' + side + '/decrement', { method: 'POST' }).then(function () { refetchBoard(ip); }).catch(function () {}); } function resetSide(ip, side, card) { setScoreLocal(card, side, 0); fetch('/api/boards/' + ip + '/api/score/' + side + '/reset', { method: 'POST' }).then(function () { refetchBoard(ip); }).catch(function () {}); } function resetAll(ip, card) { setScoreLocal(card, 'left', 0); setScoreLocal(card, 'right', 0); fetch('/api/boards/' + ip + '/api/score/reset', { method: 'POST' }).then(function () { refetchBoard(ip); }).catch(function () {}); } function scanNetwork() { fetch('/api/scan', { method: 'POST' }).catch(function () {}); } // ---- Event delegation ---- grid.addEventListener('click', function (e) { var btn = e.target.closest('button'); if (!btn) return; var card = btn.closest('.card'); if (!card) return; var ip = card.dataset.board; if (!ip) return; var side = btn.dataset.side; if (btn.classList.contains('btn-inc') && side) { increment(ip, side, card); } else if (btn.classList.contains('btn-dec') && side) { decrement(ip, side, card); } else if (btn.classList.contains('btn-reset') && side) { resetSide(ip, side, card); } else if (btn.classList.contains('btn-all-reset')) { resetAll(ip, card); } }); // ---- Edit Config Modal ---- window.openConfig = function (ip) { editingBoard = ip; document.getElementById('modal-title').textContent = 'Edit Config — ' + ip; fetch('/api/boards/' + ip + '/api/status') .then(function (r) { return r.json(); }) .then(function (data) { var cfg = data.config || {}; document.getElementById('edit-display-name').value = data.display_name || ''; document.getElementById('edit-display-id').value = data.display_id != null ? data.display_id : ''; document.getElementById('edit-header').value = data.header || ''; document.getElementById('edit-header-font-size').value = data.header_font_size || ''; document.getElementById('edit-content-padding-top').value = data.content_padding_top || ''; document.getElementById('edit-content-gap').value = data.content_gap || ''; document.getElementById('edit-left-name').value = (cfg.left && cfg.left.name) || ''; document.getElementById('edit-left-subtitle').value = (cfg.left && cfg.left.subtitle) || ''; document.getElementById('edit-left-color').value = (cfg.left && cfg.left.color) || '#dc2626'; document.getElementById('edit-left-font-size').value = (cfg.left && cfg.left.name_font_size) || ''; document.getElementById('edit-left-subtitle-font-size').value = (cfg.left && cfg.left.subtitle_font_size) || ''; document.getElementById('edit-left-score-font-size').value = (cfg.left && cfg.left.score_font_size) || ''; document.getElementById('edit-right-name').value = (cfg.right && cfg.right.name) || ''; document.getElementById('edit-right-subtitle').value = (cfg.right && cfg.right.subtitle) || ''; document.getElementById('edit-right-color').value = (cfg.right && cfg.right.color) || '#2563eb'; document.getElementById('edit-right-font-size').value = (cfg.right && cfg.right.name_font_size) || ''; document.getElementById('edit-right-subtitle-font-size').value = (cfg.right && cfg.right.subtitle_font_size) || ''; document.getElementById('edit-right-score-font-size').value = (cfg.right && cfg.right.score_font_size) || ''; modalOverlay.classList.add('open'); }) .catch(function () { alert('Could not fetch board config'); }); }; function closeModal() { modalOverlay.classList.remove('open'); editingBoard = null; } window.closeModal = closeModal; configForm.addEventListener('submit', function (e) { e.preventDefault(); if (!editingBoard) return; var payload = { display_name: document.getElementById('edit-display-name').value, header: document.getElementById('edit-header').value, header_font_size: parseFloat(document.getElementById('edit-header-font-size').value) || 0, content_padding_top: parseFloat(document.getElementById('edit-content-padding-top').value) || 0, content_gap: parseFloat(document.getElementById('edit-content-gap').value) || 1, left: { name: document.getElementById('edit-left-name').value, subtitle: document.getElementById('edit-left-subtitle').value, color: document.getElementById('edit-left-color').value, name_font_size: parseFloat(document.getElementById('edit-left-font-size').value) || 0, subtitle_font_size: parseFloat(document.getElementById('edit-left-subtitle-font-size').value) || 0, score_font_size: parseFloat(document.getElementById('edit-left-score-font-size').value) || 0, }, right: { name: document.getElementById('edit-right-name').value, subtitle: document.getElementById('edit-right-subtitle').value, color: document.getElementById('edit-right-color').value, name_font_size: parseFloat(document.getElementById('edit-right-font-size').value) || 0, subtitle_font_size: parseFloat(document.getElementById('edit-right-subtitle-font-size').value) || 0, score_font_size: parseFloat(document.getElementById('edit-right-score-font-size').value) || 0, }, }; var did = parseInt(document.getElementById('edit-display-id').value, 10); if (!isNaN(did) && did > 0) payload.display_id = did; var saveBtn = configForm.querySelector('.btn-save'); saveBtn.textContent = 'Saving...'; saveBtn.disabled = true; fetch('/api/boards/' + editingBoard + '/api/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) .then(function (r) { if (r.ok) closeModal(); else alert('Failed to save config'); }) .catch(function () { alert('Failed to save config'); }) .finally(function () { saveBtn.textContent = 'Save Config'; saveBtn.disabled = false; }); }); modalOverlay.addEventListener('click', function (e) { if (e.target === modalOverlay) closeModal(); }); // ---- Conflicts (F9) ---- function showConflicts() { fetch('/api/conflicts') .then(function (r) { return r.json(); }) .then(function (list) { var summary = document.getElementById('conflicts-summary'); var listEl = document.getElementById('conflicts-list'); listEl.innerHTML = ''; if (!list || list.length === 0) { summary.textContent = 'No display ID conflicts detected.'; listEl.innerHTML = '

Every discovered board has a unique display_id.

'; } else { summary.textContent = list.length + ' conflicted display ID' + (list.length > 1 ? 's' : '') + '. Give each board a unique display_id to fix (use Edit).'; list.forEach(function (conflict) { var box = document.createElement('div'); box.className = 'card p-4 mb-3'; var html = '
display_id ' + esc(String(conflict.display_id)) + '
'; (conflict.boards || []).forEach(function (b) { html += '
' + '
' + esc(b.ip) + '' + '' + esc(b.display_name || '?') + '
' + '' + '
'; }); box.innerHTML = html; listEl.appendChild(box); }); } document.getElementById('conflicts-modal').classList.add('open'); }) .catch(function () { alert('Could not fetch conflicts'); }); } window.closeConflicts = function () { document.getElementById('conflicts-modal').classList.remove('open'); }; document.getElementById('conflicts-modal').addEventListener('click', function (e) { if (e.target === document.getElementById('conflicts-modal')) window.closeConflicts(); }); document.addEventListener('keydown', function (e) { if (e.key === 'F9') { e.preventDefault(); showConflicts(); } }); // ---- Polling ---- fetchBoards(); setInterval(fetchBoards, 3000); window.scanNetwork = scanNetwork; })();