piazza 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {piazza-0.2.0/src/piazza.egg-info → piazza-0.2.2}/PKG-INFO +3 -1
  2. {piazza-0.2.0 → piazza-0.2.2}/README.md +2 -0
  3. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/__init__.py +1 -1
  4. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/admin/admin.html +294 -15
  5. piazza-0.2.2/src/piazza/admin/auth.py +303 -0
  6. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/admin/handlers.py +48 -4
  7. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/admin/routes/_shared.py +1 -1
  8. piazza-0.2.2/src/piazza/admin/routes/tokens.py +68 -0
  9. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/admin/server.py +20 -11
  10. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/bus.py +5 -23
  11. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/cli.py +42 -11
  12. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/frontends/http.py +183 -34
  13. piazza-0.2.2/src/piazza/token_store.py +256 -0
  14. {piazza-0.2.0 → piazza-0.2.2/src/piazza.egg-info}/PKG-INFO +3 -1
  15. {piazza-0.2.0 → piazza-0.2.2}/src/piazza.egg-info/SOURCES.txt +4 -1
  16. {piazza-0.2.0 → piazza-0.2.2}/tests/test_admin.py +76 -24
  17. piazza-0.2.2/tests/test_token_store.py +156 -0
  18. piazza-0.2.0/src/piazza/admin/auth.py +0 -85
  19. {piazza-0.2.0 → piazza-0.2.2}/LICENSE +0 -0
  20. {piazza-0.2.0 → piazza-0.2.2}/pyproject.toml +0 -0
  21. {piazza-0.2.0 → piazza-0.2.2}/setup.cfg +0 -0
  22. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/admin/__init__.py +0 -0
  23. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/admin/routes/__init__.py +0 -0
  24. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/admin/routes/channels.py +0 -0
  25. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/admin/routes/dashboard.py +0 -0
  26. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/admin/routes/messages.py +0 -0
  27. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/admin/routes/subscriptions.py +0 -0
  28. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/admin/routes/ui.py +0 -0
  29. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/admin/static.py +0 -0
  30. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/backends/__init__.py +0 -0
  31. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/backends/memory.py +0 -0
  32. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/backends/sqlite.py +0 -0
  33. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/client.py +0 -0
  34. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/frontends/__init__.py +0 -0
  35. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/frontends/base.py +0 -0
  36. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/frontends/irc.py +0 -0
  37. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/protocols.py +0 -0
  38. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/serializers.py +0 -0
  39. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/server.py +0 -0
  40. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/transport.py +0 -0
  41. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/transport_http.py +0 -0
  42. {piazza-0.2.0 → piazza-0.2.2}/src/piazza/types.py +0 -0
  43. {piazza-0.2.0 → piazza-0.2.2}/src/piazza.egg-info/dependency_links.txt +0 -0
  44. {piazza-0.2.0 → piazza-0.2.2}/src/piazza.egg-info/entry_points.txt +0 -0
  45. {piazza-0.2.0 → piazza-0.2.2}/src/piazza.egg-info/requires.txt +0 -0
  46. {piazza-0.2.0 → piazza-0.2.2}/src/piazza.egg-info/top_level.txt +0 -0
  47. {piazza-0.2.0 → piazza-0.2.2}/tests/test_bus.py +0 -0
  48. {piazza-0.2.0 → piazza-0.2.2}/tests/test_cli.py +0 -0
  49. {piazza-0.2.0 → piazza-0.2.2}/tests/test_client.py +0 -0
  50. {piazza-0.2.0 → piazza-0.2.2}/tests/test_irc_frontend.py +0 -0
  51. {piazza-0.2.0 → piazza-0.2.2}/tests/test_remote_transport.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: piazza
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Agent messaging hub for multi-agent collaboration.
5
5
  Author-email: Oaklight <oaklight@gmx.com>
6
6
  License-Expression: MIT
@@ -35,6 +35,8 @@ Dynamic: license-file
35
35
  # Piazza
36
36
 
37
37
  [![CI](https://github.com/Oaklight/piazza/actions/workflows/ci.yml/badge.svg)](https://github.com/Oaklight/piazza/actions/workflows/ci.yml)
38
+ [![PyPI](https://img.shields.io/pypi/v/piazza?color=%23800020&label=PyPI)](https://pypi.org/project/piazza/)
39
+ [![Release](https://img.shields.io/github/v/release/Oaklight/piazza?color=%23800020&label=Release)](https://github.com/Oaklight/piazza/releases/latest)
38
40
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
39
41
 
40
42
  English Version | [中文版](README_zh.md)
@@ -1,6 +1,8 @@
1
1
  # Piazza
2
2
 
3
3
  [![CI](https://github.com/Oaklight/piazza/actions/workflows/ci.yml/badge.svg)](https://github.com/Oaklight/piazza/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/piazza?color=%23800020&label=PyPI)](https://pypi.org/project/piazza/)
5
+ [![Release](https://img.shields.io/github/v/release/Oaklight/piazza?color=%23800020&label=Release)](https://github.com/Oaklight/piazza/releases/latest)
4
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
5
7
 
6
8
  English Version | [中文版](README_zh.md)
@@ -1,6 +1,6 @@
1
1
  """piazza - Agent messaging hub for multi-agent collaboration."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.2"
4
4
 
5
5
  from piazza.backends import MemoryBackend, SQLiteBackend
6
6
  from piazza.bus import Bus, SQLiteBus
@@ -29,6 +29,8 @@
29
29
 
30
30
  /* ============== Reset & Base ============== */
31
31
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
32
+ /* Hide main content until auth check completes to prevent flash of unauthenticated content */
33
+ body.auth-pending .app { display: none !important; }
32
34
  body {
33
35
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
34
36
  background: var(--bg-primary);
@@ -426,6 +428,54 @@ tr:hover { background: var(--accent-light); }
426
428
  .toast-success { background: var(--success); color: #fff; }
427
429
  .toast-error { background: var(--error); color: #fff; }
428
430
  .toast-info { background: var(--accent); color: #fff; }
431
+
432
+ /* ============== Login Overlay ============== */
433
+ .login-overlay {
434
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
435
+ background: rgba(0,0,0,0.5); z-index: 2000;
436
+ display: flex; align-items: center; justify-content: center;
437
+ }
438
+ .login-overlay.hidden { display: none; }
439
+ .login-card {
440
+ background: var(--bg-secondary); border-radius: var(--radius-lg);
441
+ padding: 32px; width: 360px; box-shadow: var(--shadow-lg);
442
+ }
443
+ .login-card h2 { margin-bottom: 20px; font-size: 1.25rem; }
444
+ .login-card input {
445
+ width: 100%; padding: 10px 14px; border: 1px solid var(--border);
446
+ border-radius: var(--radius-sm); font-size: 0.9rem; margin-bottom: 12px;
447
+ outline: none; background: var(--bg-primary);
448
+ }
449
+ .login-card input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-light); }
450
+ .login-error { color: var(--error); font-size: 0.8rem; margin-bottom: 8px; min-height: 1.2em; }
451
+
452
+ /* ============== Modal ============== */
453
+ .modal-overlay {
454
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
455
+ background: rgba(0,0,0,0.4); z-index: 1500;
456
+ display: flex; align-items: center; justify-content: center;
457
+ }
458
+ .modal-overlay.hidden { display: none; }
459
+ .modal-card {
460
+ background: var(--bg-secondary); border-radius: var(--radius-lg);
461
+ padding: 28px; width: 440px; box-shadow: var(--shadow-lg);
462
+ }
463
+ .modal-card h3 { margin-bottom: 16px; }
464
+
465
+ /* ============== Token Reveal ============== */
466
+ .token-reveal {
467
+ background: #FFF8E1; border: 1px solid #F9A825; border-radius: var(--radius-md);
468
+ padding: 16px; margin: 12px 0;
469
+ }
470
+ .token-reveal-label { font-size: 0.8rem; font-weight: 600; color: #E65100; margin-bottom: 6px; }
471
+ .token-reveal-value {
472
+ font-family: 'SFMono-Regular', Consolas, monospace; font-size: 0.85rem;
473
+ word-break: break-all; background: #FFF3E0; padding: 8px; border-radius: 4px;
474
+ margin-bottom: 8px;
475
+ }
476
+ .btn-danger { background: var(--error); color: #fff; }
477
+ .btn-danger:hover { background: #A33636; }
478
+ .btn-sm-action { padding: 3px 8px; font-size: 0.7rem; margin: 0 2px; }
429
479
  @keyframes slideIn { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: translateX(0); } }
430
480
  @keyframes slideOut { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(40px); } }
431
481
 
@@ -440,7 +490,7 @@ tr:hover { background: var(--accent-light); }
440
490
  }
441
491
  </style>
442
492
  </head>
443
- <body>
493
+ <body class="auth-pending">
444
494
  <div class="app">
445
495
  <!-- Header -->
446
496
  <header class="header">
@@ -462,6 +512,7 @@ tr:hover { background: var(--accent-light); }
462
512
  </svg>
463
513
  Refresh
464
514
  </button>
515
+ <button class="btn btn-secondary btn-sm" id="logoutBtn" onclick="doLogout()" style="display:none">Logout</button>
465
516
  </header>
466
517
 
467
518
  <!-- Tabs -->
@@ -470,6 +521,7 @@ tr:hover { background: var(--accent-light); }
470
521
  <button class="tab-btn" onclick="switchTab('channels')">Channels</button>
471
522
  <button class="tab-btn" onclick="switchTab('messages')">Messages</button>
472
523
  <button class="tab-btn" onclick="switchTab('subscriptions')">Subscriptions</button>
524
+ <button class="tab-btn" onclick="switchTab('tokens')">Tokens</button>
473
525
  </div>
474
526
 
475
527
  <!-- Dashboard Tab -->
@@ -587,6 +639,52 @@ tr:hover { background: var(--accent-light); }
587
639
  </table>
588
640
  </div>
589
641
  </div>
642
+
643
+ <!-- Tokens Tab -->
644
+ <div class="tab-content" id="tab-tokens">
645
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
646
+ <span style="font-size:0.875rem;color:var(--text-secondary)" id="tokenStatus"></span>
647
+ <button class="btn btn-primary btn-sm" onclick="showCreateTokenModal()">+ Create Token</button>
648
+ </div>
649
+ <div id="tokenRevealArea"></div>
650
+ <div class="table-container">
651
+ <table>
652
+ <thead><tr>
653
+ <th>ID</th><th>Agent ID</th><th>Label</th><th>Prefix</th><th>Created</th><th>Last Used</th><th>Actions</th>
654
+ </tr></thead>
655
+ <tbody id="tokenList"></tbody>
656
+ </table>
657
+ </div>
658
+ </div>
659
+ </div>
660
+
661
+ <!-- Login Overlay -->
662
+ <div class="login-overlay hidden" id="loginOverlay">
663
+ <div class="login-card">
664
+ <h2>🔐 Piazza Admin</h2>
665
+ <div class="login-error" id="loginError"></div>
666
+ <input type="password" id="loginPassword" placeholder="Admin password" onkeydown="if(event.key==='Enter')doLogin()">
667
+ <button class="btn btn-primary" style="width:100%" onclick="doLogin()">Login</button>
668
+ </div>
669
+ </div>
670
+
671
+ <!-- Create Token Modal -->
672
+ <div class="modal-overlay hidden" id="createTokenModal">
673
+ <div class="modal-card">
674
+ <h3>Create Agent Token</h3>
675
+ <div class="form-group" style="margin-bottom:12px">
676
+ <label>Agent ID</label>
677
+ <input id="newTokenAgentId" type="text" placeholder="agent-alice (leave empty for supertoken)">
678
+ </div>
679
+ <div class="form-group" style="margin-bottom:16px">
680
+ <label>Label</label>
681
+ <input id="newTokenLabel" type="text" placeholder="Description">
682
+ </div>
683
+ <div style="display:flex;gap:8px;justify-content:flex-end">
684
+ <button class="btn btn-secondary" onclick="hideCreateTokenModal()">Cancel</button>
685
+ <button class="btn btn-primary" onclick="submitCreateToken()">Create</button>
686
+ </div>
687
+ </div>
590
688
  </div>
591
689
 
592
690
  <!-- Toast Container -->
@@ -597,28 +695,17 @@ tr:hover { background: var(--accent-light); }
597
695
  class PiazzaAPI {
598
696
  constructor() {
599
697
  this.baseUrl = window.location.origin;
600
- this.token = localStorage.getItem('piazza_token') || null;
601
- }
602
-
603
- getHeaders() {
604
- const headers = { 'Content-Type': 'application/json' };
605
- if (this.token) headers['Authorization'] = 'Bearer ' + this.token;
606
- return headers;
607
698
  }
608
699
 
609
700
  async request(path, options = {}) {
610
701
  try {
611
702
  const resp = await fetch(this.baseUrl + path, {
612
- headers: this.getHeaders(),
703
+ credentials: 'same-origin',
704
+ headers: { 'Content-Type': 'application/json' },
613
705
  ...options,
614
706
  });
615
707
  if (resp.status === 401) {
616
- const token = prompt('Enter authentication token:');
617
- if (token) {
618
- this.token = token;
619
- localStorage.setItem('piazza_token', token);
620
- return this.request(path, options);
621
- }
708
+ showLoginOverlay();
622
709
  throw new Error('Authentication required');
623
710
  }
624
711
  updateStatus(true);
@@ -631,6 +718,26 @@ class PiazzaAPI {
631
718
  }
632
719
  }
633
720
 
721
+ async login(password) {
722
+ const resp = await fetch(this.baseUrl + '/api/login', {
723
+ method: 'POST', credentials: 'same-origin',
724
+ headers: { 'Content-Type': 'application/json' },
725
+ body: JSON.stringify({ password }),
726
+ });
727
+ return { ok: resp.ok, data: await resp.json() };
728
+ }
729
+
730
+ async logout() {
731
+ return fetch(this.baseUrl + '/api/logout', {
732
+ method: 'POST', credentials: 'same-origin',
733
+ });
734
+ }
735
+
736
+ async checkAuth() {
737
+ const resp = await fetch(this.baseUrl + '/api/auth-check', { credentials: 'same-origin' });
738
+ return resp.json();
739
+ }
740
+
634
741
  async getStats() { return this.request('/api/stats'); }
635
742
  async getThroughput() { return this.request('/api/stats/throughput'); }
636
743
  async getChannels() { return this.request('/api/channels'); }
@@ -646,6 +753,21 @@ class PiazzaAPI {
646
753
  return this.request('/api/messages', { method: 'POST', body: JSON.stringify(data) });
647
754
  }
648
755
  async getSubscriptions() { return this.request('/api/subscriptions'); }
756
+
757
+ // Token management
758
+ async listTokens() { return this.request('/api/tokens'); }
759
+ async createToken(agentId, label) {
760
+ return this.request('/api/tokens', {
761
+ method: 'POST',
762
+ body: JSON.stringify({ agent_id: agentId || null, label: label || '' }),
763
+ });
764
+ }
765
+ async deleteToken(id) {
766
+ return this.request('/api/tokens/' + id, { method: 'DELETE' });
767
+ }
768
+ async rotateToken(id) {
769
+ return this.request('/api/tokens/' + id + '/rotate', { method: 'POST' });
770
+ }
649
771
  }
650
772
 
651
773
  const api = new PiazzaAPI();
@@ -702,6 +824,7 @@ function refreshCurrentTab() {
702
824
  if (document.getElementById('msgAutoRefresh').checked) loadMessages();
703
825
  break;
704
826
  case 'subscriptions': loadSubscriptions(); break;
827
+ case 'tokens': loadTokens(); break;
705
828
  }
706
829
  }
707
830
 
@@ -1047,8 +1170,164 @@ function formatTime(isoStr) {
1047
1170
  } catch(e) { return isoStr; }
1048
1171
  }
1049
1172
 
1173
+ /* ============== Login / Logout ============== */
1174
+ function showLoginOverlay() {
1175
+ document.body.classList.add('auth-pending');
1176
+ document.getElementById('loginOverlay').classList.remove('hidden');
1177
+ document.getElementById('loginError').textContent = '';
1178
+ document.getElementById('loginPassword').value = '';
1179
+ document.getElementById('loginPassword').focus();
1180
+ if (refreshInterval) { clearInterval(refreshInterval); refreshInterval = null; }
1181
+ }
1182
+
1183
+ function hideLoginOverlay() {
1184
+ document.getElementById('loginOverlay').classList.add('hidden');
1185
+ document.body.classList.remove('auth-pending');
1186
+ }
1187
+
1188
+ async function doLogin() {
1189
+ const pw = document.getElementById('loginPassword').value;
1190
+ if (!pw) return;
1191
+ try {
1192
+ const result = await api.login(pw);
1193
+ if (result.ok) {
1194
+ hideLoginOverlay();
1195
+ document.getElementById('logoutBtn').style.display = '';
1196
+ await loadDashboard();
1197
+ populateChannelSelector();
1198
+ refreshInterval = setInterval(refreshCurrentTab, 3000);
1199
+ } else {
1200
+ document.getElementById('loginError').textContent = result.data.error || 'Invalid password';
1201
+ }
1202
+ } catch (e) {
1203
+ document.getElementById('loginError').textContent = 'Connection error';
1204
+ }
1205
+ }
1206
+
1207
+ async function doLogout() {
1208
+ await api.logout();
1209
+ document.getElementById('logoutBtn').style.display = 'none';
1210
+ showLoginOverlay();
1211
+ }
1212
+
1213
+ /* ============== Token Management ============== */
1214
+ async function loadTokens() {
1215
+ try {
1216
+ const data = await api.listTokens();
1217
+ const tbody = document.getElementById('tokenList');
1218
+ const status = document.getElementById('tokenStatus');
1219
+ status.textContent = data.enabled
1220
+ ? `${data.tokens.length} token(s) configured`
1221
+ : 'Token auth not configured';
1222
+ if (data.tokens.length === 0) {
1223
+ tbody.innerHTML = '<tr><td colspan="7" style="color:var(--text-secondary);text-align:center;padding:40px">No tokens yet — API is open until the first token is created</td></tr>';
1224
+ return;
1225
+ }
1226
+ tbody.innerHTML = data.tokens.map(t => `
1227
+ <tr>
1228
+ <td style="font-family:monospace;font-size:0.8rem">${esc(t.id)}</td>
1229
+ <td>${t.agent_id ? esc(t.agent_id) : '<em style="color:var(--warning)">supertoken</em>'}</td>
1230
+ <td>${esc(t.label)}</td>
1231
+ <td style="font-family:monospace;font-size:0.8rem">${esc(t.token_prefix)}…</td>
1232
+ <td style="font-size:0.8rem;color:var(--text-secondary)">${formatTime(t.created_at)}</td>
1233
+ <td style="font-size:0.8rem;color:var(--text-secondary)">${t.last_used_at ? formatTime(t.last_used_at) : '-'}</td>
1234
+ <td>
1235
+ <button class="btn btn-secondary btn-sm-action" onclick="rotateToken('${t.id}')">Rotate</button>
1236
+ <button class="btn btn-danger btn-sm-action" onclick="deleteToken('${t.id}')">Delete</button>
1237
+ </td>
1238
+ </tr>
1239
+ `).join('');
1240
+ } catch (e) {
1241
+ console.error('Tokens load error:', e);
1242
+ }
1243
+ }
1244
+
1245
+ function showCreateTokenModal() {
1246
+ document.getElementById('createTokenModal').classList.remove('hidden');
1247
+ document.getElementById('newTokenAgentId').value = '';
1248
+ document.getElementById('newTokenLabel').value = '';
1249
+ document.getElementById('newTokenAgentId').focus();
1250
+ }
1251
+
1252
+ function hideCreateTokenModal() {
1253
+ document.getElementById('createTokenModal').classList.add('hidden');
1254
+ }
1255
+
1256
+ async function submitCreateToken() {
1257
+ const agentId = document.getElementById('newTokenAgentId').value.trim();
1258
+ const label = document.getElementById('newTokenLabel').value.trim();
1259
+ try {
1260
+ const result = await api.createToken(agentId, label);
1261
+ hideCreateTokenModal();
1262
+ if (result.ok && result.token) {
1263
+ showTokenReveal(result.token.token, 'New token created — copy it now, it won\'t be shown again:');
1264
+ showToast('Token created for ' + (agentId || 'supertoken'), 'success');
1265
+ }
1266
+ loadTokens();
1267
+ } catch (e) {
1268
+ showToast('Failed to create token', 'error');
1269
+ }
1270
+ }
1271
+
1272
+ async function deleteToken(id) {
1273
+ if (!confirm('Delete this token? Agents using it will be locked out immediately.')) return;
1274
+ try {
1275
+ await api.deleteToken(id);
1276
+ showToast('Token deleted', 'success');
1277
+ loadTokens();
1278
+ } catch (e) {
1279
+ showToast('Failed to delete token', 'error');
1280
+ }
1281
+ }
1282
+
1283
+ async function rotateToken(id) {
1284
+ if (!confirm('Rotate this token? The old value will stop working immediately.')) return;
1285
+ try {
1286
+ const result = await api.rotateToken(id);
1287
+ if (result.ok && result.token) {
1288
+ showTokenReveal(result.token.token, 'Token rotated — copy the new value:');
1289
+ showToast('Token rotated', 'success');
1290
+ }
1291
+ loadTokens();
1292
+ } catch (e) {
1293
+ showToast('Failed to rotate token', 'error');
1294
+ }
1295
+ }
1296
+
1297
+ function showTokenReveal(tokenValue, label) {
1298
+ const area = document.getElementById('tokenRevealArea');
1299
+ area.innerHTML = `
1300
+ <div class="token-reveal">
1301
+ <div class="token-reveal-label">⚠️ ${esc(label)}</div>
1302
+ <div class="token-reveal-value">${esc(tokenValue)}</div>
1303
+ <button class="btn btn-primary btn-sm" onclick="copyToken('${esc(tokenValue)}')">📋 Copy to Clipboard</button>
1304
+ <button class="btn btn-secondary btn-sm" onclick="this.parentElement.remove()" style="margin-left:8px">Dismiss</button>
1305
+ </div>
1306
+ `;
1307
+ }
1308
+
1309
+ function copyToken(token) {
1310
+ navigator.clipboard.writeText(token).then(
1311
+ () => showToast('Token copied to clipboard', 'success'),
1312
+ () => showToast('Failed to copy — select and copy manually', 'error')
1313
+ );
1314
+ }
1315
+
1050
1316
  /* ============== Init ============== */
1051
1317
  async function init() {
1318
+ try {
1319
+ const auth = await api.checkAuth();
1320
+ if (auth.required && !auth.authenticated) {
1321
+ showLoginOverlay();
1322
+ return;
1323
+ }
1324
+ if (auth.required) {
1325
+ document.getElementById('logoutBtn').style.display = '';
1326
+ }
1327
+ } catch (e) {
1328
+ // No auth configured or server unreachable — proceed without login
1329
+ }
1330
+ document.body.classList.remove('auth-pending');
1052
1331
  await loadDashboard();
1053
1332
  populateChannelSelector();
1054
1333
  refreshInterval = setInterval(refreshCurrentTab, 3000);