CuteBoard/controller/static/js/main.js

331 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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 +=
'<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-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 = '<p class="text-sm text-slate-500">Every discovered board has a unique display_id.</p>';
} 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 = '<div class="font-semibold mb-2">display_id ' + esc(String(conflict.display_id)) + '</div>';
(conflict.boards || []).forEach(function (b) {
html += '<div class="flex items-center justify-between py-1" style="border-top:1px solid #1e1e30;">' +
'<div><span class="text-xs text-slate-600">' + esc(b.ip) + '</span>' +
'<span class="ml-2">' + esc(b.display_name || '?') + '</span></div>' +
'<button class="btn btn-edit" onclick="window.openConfig(\'' + esc(b.ip) + '\')">⚙ Edit</button>' +
'</div>';
});
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;
})();