Spaces:
Running
Running
File size: 5,842 Bytes
e16348e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | /**
* admin.js: ChessTB Admin Dashboard
*
* Client-side logic for templates/admin.html, which renders one of two
* states depending on authentication:
* - Login form: wires the token input + login button to POST /admin/login,
* then reloads the page on success (the new session cookie makes the
* next GET /admin render the dashboard instead).
* - Dashboard: polls the cache-stats endpoint, renders the two cache
* cards and the thread-pool card, and wires the refresh/clear/logout
* buttons.
* Kept as an external file - with clicks bound via addEventListener rather
* than inline onclick="" - because the app's Content-Security-Policy
* (`script-src 'self'`, set by app.py's after_request hook) blocks both
* inline <script> bodies and inline event handler attributes.
*/
function _setStatus(elId, msg, cls) {
const el = document.getElementById(elId);
if (!el) return;
el.textContent = msg;
el.className = 'status ' + (cls || '');
}
function setStatus(msg, cls) { _setStatus('status-msg', msg, cls); }
function setLoginStatus(msg, cls) { _setStatus('login-status', msg, cls); }
const JSON_HEADERS = { 'Content-Type': 'application/json' };
async function login() {
const input = document.getElementById('login-token-input');
try {
const r = await fetch('/admin/login', {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify({ token: input.value }),
});
const j = await r.json().catch(() => ({}));
if (r.ok) {
window.location.reload();
} else {
setLoginStatus(j.error || 'Login failed.', 'err');
}
} catch (e) {
setLoginStatus('Cannot reach backend.', 'err');
}
}
async function logout() {
try {
await fetch('/admin/logout', { method: 'POST' });
} finally {
window.location.reload();
}
}
// A brand-new or just-cleared cache has 0 hits and 0 misses, and a
// misconfigured one can have maxsize 0 - both make the ratios below NaN
// or Infinity rather than a real percentage.
function _pct(value) {
return Number.isFinite(value) ? value.toFixed(1) : '0.0';
}
function _renderStatRows(elId, rows) {
const container = document.getElementById(elId);
if (!container) return;
container.innerHTML = '';
for (const [label, value] of rows) {
const row = document.createElement('div');
row.className = 'stat';
const labelEl = document.createElement('span');
labelEl.className = 'label';
labelEl.textContent = label;
const valueEl = document.createElement('span');
valueEl.className = 'value';
valueEl.textContent = value;
row.append(labelEl, valueEl);
container.appendChild(row);
}
}
function renderCache(elId, barId, data) {
const pct = _pct(data.hit_rate * 100);
document.getElementById(barId).style.width = pct + '%';
_renderStatRows(elId, [
['Hit rate', pct + '%'],
['Hits', data.hits.toLocaleString()],
['Misses', data.misses.toLocaleString()],
['Used', data.currsize.toLocaleString() + ' / ' + data.maxsize.toLocaleString()],
['Fill', _pct((data.currsize / data.maxsize) * 100) + '%'],
]);
}
function renderPool(data) {
_renderStatRows('stats-pool', [
['Max workers', data.thread_pool.max_workers],
['Parallel threshold', data.thread_pool.parallel_threshold + ' child positions'],
['Probe timeout', data.thread_pool.probe_timeout_secs + ' s'],
['Eval cache size', data.config.evaluate_cache_size.toLocaleString()],
['Probe cache size', data.config.probe_cache_size.toLocaleString()],
['Block cache size', (data.config.block_cache_bytes / (1024 * 1024)).toLocaleString() + ' MB'],
]);
}
async function loadStats() {
try {
const r = await fetch('/admin/cache/stats'); // bodyless GET - no request body means no Content-Type to declare
if (!r.ok) {
const j = await r.json().catch(() => ({}));
setStatus('Error ' + r.status + ': ' + (j.error || r.statusText), 'err');
return;
}
const data = await r.json();
renderCache('stats-eval', 'bar-eval', data.evaluate_fen_cache);
renderCache('stats-probe', 'bar-probe', data.probe_fen_cache);
renderPool(data);
document.getElementById('last-updated').textContent =
'Last updated: ' + new Date().toLocaleTimeString();
setStatus('');
} catch (e) {
setStatus('Cannot reach backend.', 'err');
}
}
async function clearCaches() {
if (!confirm('Clear all caches? This will slow down the next few probes.')) return;
try {
const r = await fetch('/admin/cache/clear', { method: 'POST', headers: JSON_HEADERS });
const j = await r.json().catch(() => ({}));
if (r.ok) {
setStatus('Caches cleared successfully.', 'ok');
loadStats();
} else {
setStatus('Error: ' + (j.error || r.statusText), 'err');
}
} catch (e) {
setStatus('Cannot reach backend.', 'err');
}
}
document.addEventListener('DOMContentLoaded', () => {
const loginBtn = document.getElementById('login-btn');
if (loginBtn) {
loginBtn.addEventListener('click', login);
document.getElementById('login-token-input')?.addEventListener('keydown', e => {
if (e.key === 'Enter') login();
});
return; // login page only - the dashboard elements below are not rendered
}
document.getElementById('logout-btn')?.addEventListener('click', logout);
document.getElementById('refresh-btn')?.addEventListener('click', loadStats);
document.getElementById('clear-btn')?.addEventListener('click', clearCaches);
loadStats();
let countdown = 5;
setInterval(() => {
countdown--;
if (countdown <= 0) {
countdown = 5;
loadStats();
}
const el = document.getElementById('next-refresh');
if (el) el.textContent = 'Auto-refreshes in ' + countdown + ' s';
}, 1000);
});
|