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);
});