Koddenbrock commited on
Commit
76bec74
Β·
1 Parent(s): 1ba3759

add admin dashboard for viewing DiffMT results

Browse files
Files changed (1) hide show
  1. public/admin.html +16 -84
public/admin.html CHANGED
@@ -14,14 +14,6 @@
14
  }
15
  body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100dvh; padding: 2rem 1rem; }
16
 
17
- #auth-screen { display: flex; justify-content: center; align-items: center; min-height: 80dvh; }
18
- .auth-card {
19
- background: var(--surface); border: 1px solid var(--border); border-radius: 16px;
20
- padding: 2.5rem; width: min(420px, 100%); box-shadow: 0 6px 32px rgba(26,58,112,.09);
21
- }
22
- .auth-card h2 { margin-bottom: 1.25rem; }
23
- .auth-card p { color: var(--muted); font-size: .9rem; margin-bottom: 1.5rem; }
24
-
25
  #dashboard { max-width: 920px; margin: 0 auto; }
26
 
27
  .page-header {
@@ -31,9 +23,7 @@
31
  .page-header h1 { font-size: 1.6rem; }
32
  .page-header .sub { color: var(--muted); font-size: .85rem; margin-top: .2rem; }
33
 
34
- .stats-row {
35
- display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.75rem;
36
- }
37
  .stat-card {
38
  flex: 1; min-width: 120px; background: var(--surface);
39
  border: 1px solid var(--border); border-radius: var(--r);
@@ -52,8 +42,7 @@
52
  background: var(--surface2); padding: .7rem 1rem; text-align: left;
53
  color: var(--muted); font-weight: 600; font-size: .75rem;
54
  text-transform: uppercase; letter-spacing: .06em;
55
- border-bottom: 1px solid var(--border);
56
- white-space: nowrap;
57
  }
58
  th.sortable { cursor: pointer; user-select: none; }
59
  th.sortable:hover { color: var(--accent); }
@@ -70,38 +59,13 @@
70
  text-decoration: none; transition: opacity .15s;
71
  }
72
  .btn:hover { opacity: .85; }
73
- .btn.outline {
74
- background: none; color: var(--accent);
75
- border: 1px solid var(--accent);
76
- }
77
 
78
- .input {
79
- background: var(--surface2); border: 1px solid var(--border);
80
- color: var(--text); padding: .65rem 1rem; border-radius: var(--r);
81
- font-size: .95rem; font-family: var(--font); outline: none; width: 100%;
82
- margin-bottom: .75rem;
83
- }
84
- .input:focus { border-color: var(--accent); }
85
-
86
- .error-msg { color: var(--red); font-size: .85rem; margin-top: .5rem; }
87
  #status { color: var(--muted); font-size: .82rem; font-style: italic; }
88
  </style>
89
  </head>
90
  <body>
91
-
92
- <!-- ── Auth gate ────────────────────────────────────────────── -->
93
- <div id="auth-screen">
94
- <div class="auth-card">
95
- <h2>Admin Dashboard</h2>
96
- <p>Enter your admin token to view results.</p>
97
- <input id="token-input" class="input" type="password" placeholder="Admin token…" autocomplete="off">
98
- <button class="btn" style="width:100%" onclick="login()">Unlock</button>
99
- <div id="auth-error" class="error-msg"></div>
100
- </div>
101
- </div>
102
-
103
- <!-- ── Dashboard ────────────────────────────────────────────── -->
104
- <div id="dashboard" style="display:none">
105
  <div class="page-header">
106
  <div>
107
  <h1>DiffMT Results</h1>
@@ -124,11 +88,11 @@
124
  <table>
125
  <thead>
126
  <tr>
127
- <th class="sortable" data-col="rank" onclick="sortBy('rank')">#<span class="arrow" id="arr-rank"></span></th>
128
- <th class="sortable" data-col="player_name" onclick="sortBy('player_name')">Name<span class="arrow" id="arr-player_name"></span></th>
129
- <th class="sortable" data-col="score" onclick="sortBy('score')">Score<span class="arrow" id="arr-score"></span></th>
130
- <th class="sortable" data-col="percentage" onclick="sortBy('percentage')">Accuracy<span class="arrow" id="arr-percentage">↓</span></th>
131
- <th class="sortable" data-col="timestamp" onclick="sortBy('timestamp')">Date<span class="arrow" id="arr-timestamp"></span></th>
132
  <th>Session ID</th>
133
  </tr>
134
  </thead>
@@ -138,53 +102,22 @@
138
  </div>
139
 
140
  <script>
141
- let token = '';
142
  let entries = [];
143
  let sortCol = 'percentage';
144
  let sortAsc = false;
145
 
146
- // ── Auth ──────────────────────────────────────────────────────
147
- // Accept token from ?token= URL param so you can bookmark the page
148
- const urlToken = new URLSearchParams(location.search).get('token');
149
- if (urlToken) {
150
- token = urlToken;
151
- // Remove token from URL bar without reloading
152
- history.replaceState(null, '', location.pathname);
153
- showDashboard();
154
- }
155
-
156
- async function login() {
157
- token = document.getElementById('token-input').value.trim();
158
- if (!token) return;
159
- const ok = await showDashboard();
160
- if (!ok) {
161
- document.getElementById('auth-error').textContent = 'Invalid token.';
162
- token = '';
163
- }
164
- }
165
- document.getElementById('token-input').addEventListener('keydown', e => {
166
- if (e.key === 'Enter') login();
167
- });
168
-
169
- // ── Load data ─────────────────────────────────────────────────
170
- async function showDashboard() {
171
- const res = await fetch('/api/admin/sessions', {
172
- headers: { 'X-Admin-Token': token }
173
- });
174
- if (!res.ok) return false;
175
  entries = await res.json();
176
- document.getElementById('auth-screen').style.display = 'none';
177
- document.getElementById('dashboard').style.display = '';
178
  renderAll();
179
- return true;
180
  }
181
 
182
  async function refresh() {
183
  document.getElementById('status').textContent = 'Refreshing…';
184
- await showDashboard();
185
  }
186
 
187
- // ── Render ────────────────────────────────────────────────────
188
  function renderAll() {
189
  renderStats();
190
  renderTable();
@@ -212,12 +145,10 @@
212
  const sorted = [...entries].sort((a, b) => {
213
  let va = a[sortCol], vb = b[sortCol];
214
  if (sortCol === 'timestamp') { va = new Date(va); vb = new Date(vb); }
215
- if (sortCol === 'rank') return 0; // handled by order
216
  return sortAsc ? (va > vb ? 1 : -1) : (va < vb ? 1 : -1);
217
  });
218
 
219
- // Update arrow indicators
220
- ['rank','player_name','score','percentage','timestamp'].forEach(col => {
221
  const el = document.getElementById('arr-' + col);
222
  if (!el) return;
223
  el.textContent = col === sortCol ? (sortAsc ? '↑' : '↓') : '';
@@ -255,7 +186,6 @@
255
  renderTable();
256
  }
257
 
258
- // ── CSV download ──────────────────────────────────────────────
259
  function downloadCSV() {
260
  const header = ['rank','player_name','score','total','percentage','timestamp','session_id'];
261
  const sorted = [...entries].sort((a, b) => b.percentage - a.percentage || new Date(b.timestamp) - new Date(a.timestamp));
@@ -283,6 +213,8 @@
283
  return String(str ?? '').replace(/[&<>"']/g, c =>
284
  ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c]));
285
  }
 
 
286
  </script>
287
  </body>
288
  </html>
 
14
  }
15
  body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100dvh; padding: 2rem 1rem; }
16
 
 
 
 
 
 
 
 
 
17
  #dashboard { max-width: 920px; margin: 0 auto; }
18
 
19
  .page-header {
 
23
  .page-header h1 { font-size: 1.6rem; }
24
  .page-header .sub { color: var(--muted); font-size: .85rem; margin-top: .2rem; }
25
 
26
+ .stats-row { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.75rem; }
 
 
27
  .stat-card {
28
  flex: 1; min-width: 120px; background: var(--surface);
29
  border: 1px solid var(--border); border-radius: var(--r);
 
42
  background: var(--surface2); padding: .7rem 1rem; text-align: left;
43
  color: var(--muted); font-weight: 600; font-size: .75rem;
44
  text-transform: uppercase; letter-spacing: .06em;
45
+ border-bottom: 1px solid var(--border); white-space: nowrap;
 
46
  }
47
  th.sortable { cursor: pointer; user-select: none; }
48
  th.sortable:hover { color: var(--accent); }
 
59
  text-decoration: none; transition: opacity .15s;
60
  }
61
  .btn:hover { opacity: .85; }
62
+ .btn.outline { background: none; color: var(--accent); border: 1px solid var(--accent); }
 
 
 
63
 
 
 
 
 
 
 
 
 
 
64
  #status { color: var(--muted); font-size: .82rem; font-style: italic; }
65
  </style>
66
  </head>
67
  <body>
68
+ <div id="dashboard">
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  <div class="page-header">
70
  <div>
71
  <h1>DiffMT Results</h1>
 
88
  <table>
89
  <thead>
90
  <tr>
91
+ <th>#</th>
92
+ <th class="sortable" onclick="sortBy('player_name')">Name<span class="arrow" id="arr-player_name"></span></th>
93
+ <th class="sortable" onclick="sortBy('score')">Score<span class="arrow" id="arr-score"></span></th>
94
+ <th class="sortable" onclick="sortBy('percentage')">Accuracy<span class="arrow" id="arr-percentage">↓</span></th>
95
+ <th class="sortable" onclick="sortBy('timestamp')">Date<span class="arrow" id="arr-timestamp"></span></th>
96
  <th>Session ID</th>
97
  </tr>
98
  </thead>
 
102
  </div>
103
 
104
  <script>
 
105
  let entries = [];
106
  let sortCol = 'percentage';
107
  let sortAsc = false;
108
 
109
+ async function load() {
110
+ const res = await fetch('/api/admin/sessions');
111
+ if (!res.ok) { document.getElementById('status').textContent = 'Error loading data.'; return; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  entries = await res.json();
 
 
113
  renderAll();
 
114
  }
115
 
116
  async function refresh() {
117
  document.getElementById('status').textContent = 'Refreshing…';
118
+ await load();
119
  }
120
 
 
121
  function renderAll() {
122
  renderStats();
123
  renderTable();
 
145
  const sorted = [...entries].sort((a, b) => {
146
  let va = a[sortCol], vb = b[sortCol];
147
  if (sortCol === 'timestamp') { va = new Date(va); vb = new Date(vb); }
 
148
  return sortAsc ? (va > vb ? 1 : -1) : (va < vb ? 1 : -1);
149
  });
150
 
151
+ ['player_name','score','percentage','timestamp'].forEach(col => {
 
152
  const el = document.getElementById('arr-' + col);
153
  if (!el) return;
154
  el.textContent = col === sortCol ? (sortAsc ? '↑' : '↓') : '';
 
186
  renderTable();
187
  }
188
 
 
189
  function downloadCSV() {
190
  const header = ['rank','player_name','score','total','percentage','timestamp','session_id'];
191
  const sorted = [...entries].sort((a, b) => b.percentage - a.percentage || new Date(b.timestamp) - new Date(a.timestamp));
 
213
  return String(str ?? '').replace(/[&<>"']/g, c =>
214
  ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c]));
215
  }
216
+
217
+ load();
218
  </script>
219
  </body>
220
  </html>