272 lines
12 KiB
JavaScript
272 lines
12 KiB
JavaScript
(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 html = '';
|
||
boards.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 +=
|
||
'<div class="card ' + statusClass + ' p-4" data-board="' + board.ip + '">' +
|
||
'<div class="flex items-center justify-between mb-3">' +
|
||
'<div class="flex items-center gap-2">' +
|
||
'<span class="' + badgeClass + '"></span>' +
|
||
'<span class="font-semibold text-base">' + esc(board.display_name || board.ip) + '</span>' +
|
||
'<span class="text-xs text-slate-600">' + board.ip + '</span>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
|
||
'<div class="flex gap-4">' +
|
||
// Left team
|
||
'<div class="flex-1 bg-black/20 rounded-lg p-3 text-center team-column"' +
|
||
' style="border-left: 3px solid ' + leftColor + ';" data-side="left">' +
|
||
'<div class="team-name text-sm mb-1" style="color:' + leftColor + ';">' + esc(left.name || '???') + '</div>' +
|
||
'<div class="score text-3xl font-black mb-2" style="color:' + leftColor + ';">' + leftScore + '</div>' +
|
||
'<div class="flex gap-1 justify-center">' +
|
||
'<button class="btn btn-dec" data-side="left">−1</button>' +
|
||
'<button class="btn btn-inc" data-side="left">+1</button>' +
|
||
'<button class="btn btn-reset" data-side="left">↺</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
// Right team
|
||
'<div class="flex-1 bg-black/20 rounded-lg p-3 text-center team-column"' +
|
||
' style="border-right: 3px solid ' + rightColor + ';" data-side="right">' +
|
||
'<div class="team-name text-sm mb-1" style="color:' + rightColor + ';">' + esc(right.name || '???') + '</div>' +
|
||
'<div class="score text-3xl font-black mb-2" style="color:' + rightColor + ';">' + rightScore + '</div>' +
|
||
'<div class="flex gap-1 justify-center">' +
|
||
'<button class="btn btn-dec" data-side="right">−1</button>' +
|
||
'<button class="btn btn-inc" data-side="right">+1</button>' +
|
||
'<button class="btn btn-reset" data-side="right">↺</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
|
||
'<div class="flex gap-2 mt-3">' +
|
||
'<button class="btn btn-edit flex-1" onclick="window.openConfig(\'' + board.ip + '\')">⚙ Edit Config</button>' +
|
||
'<button class="btn btn-all-reset flex-1">⟳ Reset All</button>' +
|
||
'</div>' +
|
||
'</div>';
|
||
});
|
||
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-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 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();
|
||
});
|
||
|
||
// ---- Polling ----
|
||
fetchBoards();
|
||
setInterval(fetchBoards, 3000);
|
||
|
||
window.scanNetwork = scanNetwork;
|
||
|
||
})();
|