@jagilber-org/index-server 1.27.0 → 1.27.2

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 (33) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/dashboard/client/admin.html +57 -27
  3. package/dist/dashboard/client/css/admin.css +54 -0
  4. package/dist/dashboard/client/js/admin.config.js +3 -6
  5. package/dist/dashboard/client/js/admin.embeddings.js +63 -4
  6. package/dist/dashboard/client/js/admin.events.js +256 -0
  7. package/dist/dashboard/client/js/admin.maintenance.js +75 -32
  8. package/dist/dashboard/client/js/admin.sessions.js +1 -1
  9. package/dist/dashboard/server/AdminPanel.js +83 -6
  10. package/dist/dashboard/server/AdminPanelConfig.d.ts +11 -0
  11. package/dist/dashboard/server/AdminPanelConfig.js +47 -17
  12. package/dist/dashboard/server/DashboardServer.js +13 -0
  13. package/dist/dashboard/server/routes/admin.routes.js +143 -17
  14. package/dist/dashboard/server/routes/embeddings.routes.js +91 -1
  15. package/dist/server/sdkServer.js +12 -4
  16. package/dist/services/embeddingService.d.ts +2 -0
  17. package/dist/services/embeddingService.js +16 -4
  18. package/dist/services/embeddingTrigger.d.ts +33 -0
  19. package/dist/services/embeddingTrigger.js +86 -0
  20. package/dist/services/eventBuffer.d.ts +45 -0
  21. package/dist/services/eventBuffer.js +110 -0
  22. package/dist/services/handlers/instructions.import.js +71 -13
  23. package/dist/services/handlers.dashboardConfig.js +81 -0
  24. package/dist/services/indexContext.d.ts +18 -0
  25. package/dist/services/indexContext.js +133 -24
  26. package/dist/services/logger.js +9 -0
  27. package/dist/services/manifestManager.js +11 -1
  28. package/dist/services/seedBootstrap.js +5 -1
  29. package/dist/services/storage/factory.d.ts +2 -0
  30. package/dist/services/storage/factory.js +12 -1
  31. package/dist/services/tracing.js +3 -1
  32. package/package.json +12 -2
  33. package/server.json +3 -3
package/CHANGELOG.md CHANGED
@@ -6,6 +6,34 @@ The format is based on Keep a Changelog and this project adheres to Semantic Ver
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.27.2] - 2026-05-01
10
+
11
+ ### Fixed
12
+
13
+ - **Release workflow** (`.github/workflows/release.yml`): trim `description` in `package.json` and `server.json` from 146 to 98 chars to satisfy the MCP Registry validator (`expected length <= 100` on `body.description`). The MCP Registry publish step had been failing on every release since v1.26.3 with HTTP 422.
14
+ - **Release workflow** (`scripts/Invoke-ReleaseWorkflow.ps1`): add a fail-fast parity guard that aborts before tagging when `package.json.version` does not match the `-Tag` argument (sans `v` prefix). The v1.27.1 release shipped a tag whose `package.json` still said `1.27.0`, which caused the `Check npmjs version status` step to skip publish (it found `1.27.0` already on npmjs and set `already_published=true`). This guard prevents the same class of mismatch from reaching the public mirror again.
15
+
16
+ ### Added
17
+
18
+ - **Release workflow** (`.github/workflows/release.yml`): publish to the GitHub Packages npm registry (`https://npm.pkg.github.com`) in addition to npmjs, so `https://github.com/jagilber-org/index-server/pkgs/npm/index-server` populates on each release. Auth uses the workflow-scoped `GITHUB_TOKEN` (no extra secret needed).
19
+
20
+ ### Notes
21
+
22
+ - v1.27.1 was tagged on the public mirror but is **incomplete**: the npmjs publish step was skipped (stale version in tag), the GitHub Packages registry was never wired (now fixed), and the MCP Registry rejected the description (now fixed). v1.27.2 is the corrected release. v1.27.1 will not be retroactively republished.
23
+
24
+ ## [1.27.1] - 2026-05-01
25
+
26
+ ### Changed
27
+
28
+ - **Security**: pin `mermaid` and `@mermaid-js/layout-elk` transitive `uuid` to `^14.0.0` via npm overrides to clear GHSA-w5hq-g745-h8pq. Server-side bundle has no mermaid imports; dashboard remains lazy-loaded.
29
+ - **Build**: pin `@types/express-serve-static-core` to `5.0.7` to keep dashboard route handler signatures stable after lockfile regen pulled `5.1.1` (which changed `req.params` typing).
30
+
31
+ ### Fixed
32
+
33
+ - **Deploy** (`scripts/deploy-local.ps1`): runtime `package.json` written to the deploy target now preserves the `overrides` block from the source manifest. Without it, `npm ci --omit=dev` failed with `EUSAGE Missing: uuid@11.1.1 from lock file` because the override-resolved lockfile didn't match the unstripped manifest.
34
+ - **CodeQL pre-push gate** (`scripts/run-codeql-pre-push.ps1`): now sources `scripts/Load-RepoEnv.ps1` and honors absolute paths from `.env` (`CODEQL_DB_PATH`, `CODEQL_LOG_DIR`, `CODEQL_OUTPUT_PATH`, `CODEQL_LANGUAGE`, `CODEQL_THREADS`, `CODEQL_RAM`). Reuses pre-built databases instead of always rebuilding in-repo.
35
+ - **Publish workflow** (`scripts/Publish-ToMirror.ps1`): after opening a publish PR via `-CreatePR`, now prints a copy-pasteable next-steps block (`gh api …/git/refs` + `gh release create --generate-notes`) so the operator can tag the merge commit and kick off the GitHub release in one shot. The `-WaitForMerge` success path also surfaces the `gh release create` command.
36
+
9
37
  ## [1.27.0] - 2026-04-30
10
38
 
11
39
  ### Changed (BREAKING)
@@ -1,29 +1,30 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <meta name="dashboard-build-version" content="1.27.0-9badd8dd">
4
+ <meta name="dashboard-build-version" content="1.27.2-d7f5ec0e">
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>Index Server Admin</title>
8
- <link rel="stylesheet" href="css/admin.css?v=1.27.0-9badd8dd">
9
- <script defer src="js/admin.utils.js?v=1.27.0-9badd8dd"></script>
10
- <script defer src="js/admin.auth.js?v=1.27.0-9badd8dd"></script>
11
- <script defer src="js/admin.overview.js?v=1.27.0-9badd8dd"></script>
12
- <script defer src="js/admin.sessions.js?v=1.27.0-9badd8dd"></script>
13
- <script defer src="js/admin.monitor.js?v=1.27.0-9badd8dd"></script>
14
- <script defer src="js/admin.graph.js?v=1.27.0-9badd8dd"></script>
8
+ <link rel="stylesheet" href="css/admin.css?v=1.27.2-d7f5ec0e">
9
+ <script defer src="js/admin.utils.js?v=1.27.2-d7f5ec0e"></script>
10
+ <script defer src="js/admin.auth.js?v=1.27.2-d7f5ec0e"></script>
11
+ <script defer src="js/admin.overview.js?v=1.27.2-d7f5ec0e"></script>
12
+ <script defer src="js/admin.sessions.js?v=1.27.2-d7f5ec0e"></script>
13
+ <script defer src="js/admin.monitor.js?v=1.27.2-d7f5ec0e"></script>
14
+ <script defer src="js/admin.events.js?v=1.27.2-d7f5ec0e"></script>
15
+ <script defer src="js/admin.graph.js?v=1.27.2-d7f5ec0e"></script>
15
16
  <script defer src="js/marked.umd.js"></script>
16
- <script defer src="js/admin.instructions.js?v=1.27.0-9badd8dd"></script>
17
- <script defer src="js/admin.logs.js?v=1.27.0-9badd8dd"></script>
18
- <script defer src="js/admin.maintenance.js?v=1.27.0-9badd8dd"></script>
19
- <script defer src="js/admin.config.js?v=1.27.0-9badd8dd"></script>
20
- <script defer src="js/admin.performance.js?v=1.27.0-9badd8dd"></script>
21
- <script defer src="js/admin.instances.js?v=1.27.0-9badd8dd"></script>
22
- <script defer src="js/admin.embeddings.js?v=1.27.0-9badd8dd"></script>
23
- <script defer src="js/admin.messaging.js?v=1.27.0-9badd8dd"></script>
24
- <script defer src="js/admin.sqlite.js?v=1.27.0-9badd8dd"></script>
25
- <script defer src="js/admin.boot.js?v=1.27.0-9badd8dd"></script>
26
- <script defer src="js/admin.feedback.js?v=1.27.0-9badd8dd"></script>
17
+ <script defer src="js/admin.instructions.js?v=1.27.2-d7f5ec0e"></script>
18
+ <script defer src="js/admin.logs.js?v=1.27.2-d7f5ec0e"></script>
19
+ <script defer src="js/admin.maintenance.js?v=1.27.2-d7f5ec0e"></script>
20
+ <script defer src="js/admin.config.js?v=1.27.2-d7f5ec0e"></script>
21
+ <script defer src="js/admin.performance.js?v=1.27.2-d7f5ec0e"></script>
22
+ <script defer src="js/admin.instances.js?v=1.27.2-d7f5ec0e"></script>
23
+ <script defer src="js/admin.embeddings.js?v=1.27.2-d7f5ec0e"></script>
24
+ <script defer src="js/admin.messaging.js?v=1.27.2-d7f5ec0e"></script>
25
+ <script defer src="js/admin.sqlite.js?v=1.27.2-d7f5ec0e"></script>
26
+ <script defer src="js/admin.boot.js?v=1.27.2-d7f5ec0e"></script>
27
+ <script defer src="js/admin.feedback.js?v=1.27.2-d7f5ec0e"></script>
27
28
  </head>
28
29
  <body>
29
30
  <div class="admin-container admin-root">
@@ -48,7 +49,7 @@
48
49
  <button class="nav-btn" data-section="config" onclick="window.showSection && window.showSection('config')">Configuration</button>
49
50
  <button id="nav-sessions" class="nav-btn" data-section="sessions" onclick="window.showSection && window.showSection('sessions')">Sessions</button>
50
51
  <button class="nav-btn" data-section="maintenance" onclick="window.showSection && window.showSection('maintenance')">Maintenance</button>
51
- <button class="nav-btn" data-section="monitoring" onclick="window.showSection && window.showSection('monitoring')">Monitoring</button>
52
+ <button class="nav-btn" data-section="monitoring" onclick="window.showSection && window.showSection('monitoring')">Monitoring<span id="nav-events-bubble" class="nav-bubble" hidden></span></button>
52
53
  <button class="nav-btn" data-section="instructions" onclick="window.showSection && window.showSection('instructions')">Instructions</button>
53
54
  <button class="nav-btn" data-section="graph" onclick="window.showSection && window.showSection('graph')">Graph</button>
54
55
  <button class="nav-btn" data-section="embeddings" onclick="window.showSection && window.showSection('embeddings')">Embeddings</button>
@@ -328,6 +329,34 @@
328
329
  <!-- Monitoring Section -->
329
330
  <div id="monitoring-section" class="admin-section hidden">
330
331
  <div class="admin-card">
332
+ <div class="card-header">
333
+ <div class="card-icon">⚠️</div>
334
+ <div class="card-title">Recent Events <span id="events-counts-summary" class="muted small"></span></div>
335
+ <div class="card-actions">
336
+ <input id="events-search" class="form-input form-input-sm events-search" type="search" placeholder="Search msg/detail…" />
337
+ <select id="events-level-filter" class="form-input form-input-sm">
338
+ <option value="">All severities</option>
339
+ <option value="WARN">WARN only</option>
340
+ <option value="ERROR">ERROR only</option>
341
+ </select>
342
+ <select id="events-page-size" class="form-input form-input-sm" title="Page size">
343
+ <option value="25">25 / page</option>
344
+ <option value="50" selected>50 / page</option>
345
+ <option value="100">100 / page</option>
346
+ <option value="200">200 / page</option>
347
+ </select>
348
+ <button id="events-refresh-btn" class="action-btn-sm" onclick="window.loadEvents && window.loadEvents()">Refresh</button>
349
+ <button id="events-clear-btn" class="action-btn-sm" onclick="window.clearEvents && window.clearEvents()">Mark All Read</button>
350
+ </div>
351
+ </div>
352
+ <div id="events-panel" class="loading">Loading events…</div>
353
+ <div id="events-pager" class="events-pager hidden">
354
+ <button id="events-prev" class="action-btn-sm" type="button">‹ Newer</button>
355
+ <span id="events-page-info" class="muted small"></span>
356
+ <button id="events-next" class="action-btn-sm" type="button">Older ›</button>
357
+ </div>
358
+ </div>
359
+ <div class="admin-card mt-xxl">
331
360
  <div class="card-header">
332
361
  <div class="card-icon"><svg viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 14 10 14 12 10 16 20 18 14 22 14"/><circle cx="6" cy="6" r="2"/></svg></div>
333
362
  <div class="card-title">Real-time Monitoring <a class="doc-link" href="/api/docs/monitoring" target="_blank" title="Panel documentation">?</a></div>
@@ -404,6 +433,7 @@
404
433
 
405
434
  <!-- Embeddings Visualization Section -->
406
435
  <div id="embeddings-section" class="admin-section hidden">
436
+ <div id="emb-status-banner" class="emb-status-banner hidden" role="status" aria-live="polite"></div>
407
437
  <div class="admin-card" style="padding:0;overflow:hidden">
408
438
  <div class="emb-layout">
409
439
  <!-- Sidebar -->
@@ -879,10 +909,10 @@
879
909
  }
880
910
  }
881
911
 
882
- // Graph logic was extracted to js/admin.graph.js?v=1.27.0-9badd8dd
912
+ // Graph logic was extracted to js/admin.graph.js?v=1.27.2-d7f5ec0e
883
913
  // Functions available globally: reloadGraphMermaid, initGraphScopeDefaults, copyMermaidSource, toggleGraphEdit, applyGraphEdit, cancelGraphEdit, refreshDrillCategories, loadDrillInstructions, clearSelections
884
914
 
885
- <!-- overview functions moved to js/admin.overview.js?v=1.27.0-9badd8dd -->
915
+ <!-- overview functions moved to js/admin.overview.js?v=1.27.2-d7f5ec0e -->
886
916
 
887
917
  // Lightweight overview-level maintenance display (optional)
888
918
  // Intentionally minimal to avoid blocking overview rendering.
@@ -1067,7 +1097,7 @@
1067
1097
  }
1068
1098
 
1069
1099
  // --- Backup / Restore ---
1070
- // Extracted to js/admin.maintenance.js?v=1.27.0-9badd8dd
1100
+ // Extracted to js/admin.maintenance.js?v=1.27.2-d7f5ec0e
1071
1101
 
1072
1102
  async function performBackup() {
1073
1103
  try {
@@ -1133,7 +1163,7 @@
1133
1163
  }
1134
1164
 
1135
1165
  async function loadConfiguration() {
1136
- // Primary implementation in js/admin.config.js?v=1.27.0-9badd8dd (loaded via defer).
1166
+ // Primary implementation in js/admin.config.js?v=1.27.2-d7f5ec0e (loaded via defer).
1137
1167
  // This inline fallback only fires if the external script failed to load.
1138
1168
  if (window.__configExternalLoaded) return;
1139
1169
  try {
@@ -1193,10 +1223,10 @@
1193
1223
  return false;
1194
1224
  }
1195
1225
 
1196
- // Monitoring functions moved to js/admin.monitor.js?v=1.27.0-9badd8dd
1226
+ // Monitoring functions moved to js/admin.monitor.js?v=1.27.2-d7f5ec0e
1197
1227
 
1198
1228
  // ===== Log Viewer =====
1199
- // Extracted to js/admin.logs.js?v=1.27.0-9badd8dd
1229
+ // Extracted to js/admin.logs.js?v=1.27.2-d7f5ec0e
1200
1230
 
1201
1231
  // ===== Instruction Management =====
1202
1232
  let instructionEditing = null;
@@ -1693,7 +1723,7 @@
1693
1723
  setInterval(fetchResourceTrends, 10000);
1694
1724
  })();
1695
1725
 
1696
- // Instruction management logic extracted to js/admin.instructions.js?v=1.27.0-9badd8dd
1726
+ // Instruction management logic extracted to js/admin.instructions.js?v=1.27.2-d7f5ec0e
1697
1727
  // Functions exposed globally: loadInstructions, renderInstructionList, editInstruction, saveInstruction, deleteInstruction, etc.
1698
1728
 
1699
1729
  function startAutoRefresh() {
@@ -1159,6 +1159,36 @@ a.mermaid-link { color: var(--admin-accent); }
1159
1159
  font-size: 12px;
1160
1160
  color: var(--admin-text-dim);
1161
1161
  }
1162
+ .emb-status-banner {
1163
+ margin: 0 0 12px 0;
1164
+ padding: 12px 16px;
1165
+ border-radius: 6px;
1166
+ border-left: 4px solid var(--admin-text-dim);
1167
+ background: rgba(255,255,255,0.04);
1168
+ font-size: 13px;
1169
+ line-height: 1.45;
1170
+ color: var(--admin-text);
1171
+ }
1172
+ .emb-status-banner.hidden { display: none; }
1173
+ .emb-status-banner .title {
1174
+ font-weight: 600;
1175
+ margin-bottom: 4px;
1176
+ display: flex;
1177
+ align-items: center;
1178
+ gap: 8px;
1179
+ }
1180
+ .emb-status-banner .meta {
1181
+ color: var(--admin-text-dim);
1182
+ font-size: 12px;
1183
+ margin-top: 6px;
1184
+ font-family: var(--mcp-font-mono);
1185
+ word-break: break-all;
1186
+ }
1187
+ .emb-status-banner.state-ready { border-left-color: var(--admin-success); }
1188
+ .emb-status-banner.state-will-download { border-left-color: var(--admin-accent); }
1189
+ .emb-status-banner.state-no-embeddings { border-left-color: var(--admin-warn); }
1190
+ .emb-status-banner.state-missing,
1191
+ .emb-status-banner.state-disabled { border-left-color: var(--admin-danger); }
1162
1192
  .emb-stat {
1163
1193
  margin-bottom: 4px;
1164
1194
  font-size: 13px;
@@ -1736,3 +1766,27 @@ a.mermaid-link { color: var(--admin-accent); }
1736
1766
  #backup-warning-banner {
1737
1767
  animation: rl-fade-in 0.3s ease-out;
1738
1768
  }
1769
+
1770
+
1771
+ /* Events panel + nav bubble (added for issue #282 events buffer) */
1772
+ .nav-bubble { display:inline-block; min-width:18px; padding:0 6px; margin-left:6px; border-radius:9px; background:var(--admin-warn); color:var(--admin-white); font-size:11px; line-height:18px; text-align:center; vertical-align:middle; }
1773
+ .nav-bubble.has-error { background:var(--admin-danger-dark); }
1774
+ .events-table { width:100%; border-collapse:collapse; }
1775
+ .events-table th, .events-table td { padding:6px 8px; text-align:left; border-bottom:1px solid var(--admin-border); font-size:13px; vertical-align:top; }
1776
+ .events-table th { color:var(--admin-text-dim); font-weight:500; }
1777
+ .event-badge { display:inline-block; padding:1px 6px; border-radius:3px; font-size:11px; font-weight:600; }
1778
+ .event-badge.level-warn { background:var(--admin-warn-dark); color:var(--admin-drill-gold); }
1779
+ .event-badge.level-error { background:var(--admin-danger-darker); color:var(--admin-red-bright); }
1780
+ .event-row.level-error { background:rgba(220,38,38,0.06); }
1781
+ .event-row.level-warn { background:rgba(245,158,11,0.04); }
1782
+ .event-detail { font-family:var(--mcp-font-mono); font-size:11px; white-space:pre-wrap; margin-top:3px; }
1783
+ .event-ts { white-space:nowrap; color:var(--admin-text-dim); font-family:var(--mcp-font-mono); font-size:12px; }
1784
+ .event-row .event-msg { cursor: pointer; }
1785
+ .event-row .event-detail { display: none; max-height: 240px; overflow: auto; }
1786
+ .event-row.expanded .event-detail { display: block; }
1787
+ .event-row .toggle-caret { display:inline-block; width:14px; color:var(--admin-text-dim); transition: transform 0.15s; }
1788
+ .event-row.expanded .toggle-caret { transform: rotate(90deg); }
1789
+ .events-pager { display:flex; align-items:center; gap:12px; padding:8px 4px; }
1790
+ .events-pager.hidden { display:none; }
1791
+ .events-search { min-width: 180px; }
1792
+ .events-table tr.event-row mark { background: var(--admin-drill-gold); color: var(--admin-text); padding: 0 1px; border-radius: 2px; }
@@ -90,10 +90,8 @@
90
90
  + '<div class="form-group"><label class="form-label">Enable Mutation</label>'
91
91
  + '<select class="form-input" id="cfg-mutation"><option value="1"' + (cfg.serverSettings.enableMutation ? ' selected' : '') + '>Enabled</option>'
92
92
  + '<option value="0"' + (!cfg.serverSettings.enableMutation ? ' selected' : '') + '>Disabled</option></select></div>'
93
- + '<div class="form-group"><label class="form-label">Rate Limit Window (ms)</label>'
94
- + '<input class="form-input" type="number" id="cfg-windowMs" value="' + cfg.serverSettings.rateLimit.windowMs + '" /></div>'
95
- + '<div class="form-group"><label class="form-label">Rate Limit Max Requests</label>'
96
- + '<input class="form-input" type="number" id="cfg-maxRequests" value="' + cfg.serverSettings.rateLimit.maxRequests + '" /></div>'
93
+ + '<div class="form-group"><label class="form-label">Rate Limit (req/min, 0 = off)</label>'
94
+ + '<input class="form-input" type="number" min="0" id="cfg-rateLimitPerMinute" value="' + (cfg.serverSettings.rateLimit && cfg.serverSettings.rateLimit.perMinute != null ? cfg.serverSettings.rateLimit.perMinute : 0) + '" /></div>'
97
95
  + '</div>'
98
96
  + '<div class="cfg-save-row"><button class="action-btn" type="submit">💾 Save Config</button></div>'
99
97
  + '</form>'
@@ -173,8 +171,7 @@
173
171
  enableVerboseLogging: document.getElementById('cfg-verbose').value === '1',
174
172
  enableMutation: document.getElementById('cfg-mutation').value === '1',
175
173
  rateLimit: {
176
- windowMs: parseInt(document.getElementById('cfg-windowMs').value),
177
- maxRequests: parseInt(document.getElementById('cfg-maxRequests').value)
174
+ perMinute: parseInt(document.getElementById('cfg-rateLimitPerMinute').value) || 0
178
175
  }
179
176
  },
180
177
  featureFlags: featureFlags
@@ -378,8 +378,61 @@
378
378
  var origShowSection = window.showSection;
379
379
  window.showSection = function (name) {
380
380
  if (origShowSection) origShowSection(name);
381
- if (name === 'embeddings' && !embData) {
382
- setTimeout(init, 50);
381
+ if (name === 'embeddings') {
382
+ window.loadEmbeddingsStatus();
383
+ if (!embData) setTimeout(init, 50);
384
+ }
385
+ };
386
+
387
+ function escapeHtml(s) {
388
+ if (s == null) return '';
389
+ return String(s)
390
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
391
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
392
+ }
393
+
394
+ function renderStatusBanner(status) {
395
+ var banner = document.getElementById('emb-status-banner');
396
+ if (!banner) return;
397
+ banner.className = 'emb-status-banner';
398
+ if (!status || status.success === false) {
399
+ banner.classList.add('hidden');
400
+ return;
401
+ }
402
+ var state = status.state || 'unknown';
403
+ var titleText = '', icon = '';
404
+ if (state === 'disabled') { icon = '🚫'; titleText = 'Semantic embeddings are disabled'; }
405
+ else if (state === 'missing') { icon = '⛔'; titleText = 'Model not available — compute will fail'; }
406
+ else if (state === 'will-download') { icon = '⬇️'; titleText = 'Model will download on first compute'; }
407
+ else if (state === 'no-embeddings') { icon = '⚠️'; titleText = 'No embeddings computed yet'; }
408
+ else if (state === 'ready') { icon = '✅'; titleText = 'Embeddings ready'; }
409
+ else { icon = 'ℹ️'; titleText = 'Embeddings status'; }
410
+ banner.classList.add('state-' + state);
411
+ var parts = [];
412
+ parts.push('<div class="title">' + icon + ' <span>' + escapeHtml(titleText) + '</span></div>');
413
+ if (status.message) parts.push('<div>' + escapeHtml(status.message) + '</div>');
414
+ var meta = [];
415
+ if (status.model) meta.push('model=' + status.model);
416
+ if (status.device) meta.push('device=' + status.device);
417
+ if (typeof status.localOnly === 'boolean') meta.push('localOnly=' + status.localOnly);
418
+ if (typeof status.modelCached === 'boolean') meta.push('modelCached=' + status.modelCached);
419
+ if (typeof status.embeddingsCount === 'number') meta.push('embeddings=' + status.embeddingsCount);
420
+ if (status.cacheDir) meta.push('cacheDir=' + status.cacheDir);
421
+ if (status.embeddingPath) meta.push('embeddingPath=' + status.embeddingPath);
422
+ if (meta.length) parts.push('<div class="meta">' + escapeHtml(meta.join(' · ')) + '</div>');
423
+ banner.innerHTML = parts.join('');
424
+ banner.classList.remove('hidden');
425
+ }
426
+
427
+ window.loadEmbeddingsStatus = async function loadEmbeddingsStatus() {
428
+ try {
429
+ var res = await adminAuth.adminFetch('/api/embeddings/status');
430
+ if (!res.ok) { renderStatusBanner(null); return; }
431
+ var data = await res.json();
432
+ renderStatusBanner(data);
433
+ } catch (_err) {
434
+ void _err;
435
+ renderStatusBanner(null);
383
436
  }
384
437
  };
385
438
 
@@ -391,12 +444,18 @@
391
444
  var res = await adminAuth.adminFetch('/api/embeddings/compute', { method: 'POST', headers: { 'Content-Type': 'application/json' } });
392
445
  if (!res.ok) {
393
446
  var err = await res.json().catch(function () { return {}; });
394
- if (statusEl) statusEl.textContent = 'Error: ' + (err.error || res.statusText) + (err.hint ? ' — ' + err.hint : '');
447
+ var detail = err.error || res.statusText;
448
+ if (err.hint) detail += ' — ' + err.hint;
449
+ else if (err.message) detail += ' — ' + err.message;
450
+ if (statusEl) statusEl.textContent = 'Error: ' + detail;
451
+ // Refresh banner so user sees the actionable state machine.
452
+ window.loadEmbeddingsStatus();
395
453
  return;
396
454
  }
397
455
  var result = await res.json();
398
456
  if (statusEl) statusEl.textContent = 'Computed ' + result.count + ' embeddings (' + result.model + ', ' + result.elapsedMs + 'ms). Loading visualization…';
399
- // Auto-load the projection after compute
457
+ // Auto-load the projection + refresh banner
458
+ window.loadEmbeddingsStatus();
400
459
  await window.loadEmbeddings();
401
460
  } catch (e) {
402
461
  if (statusEl) statusEl.textContent = 'Compute failed: ' + e.message;
@@ -0,0 +1,256 @@
1
+ /**
2
+ * admin.events.js — Recent events panel + nav-bubble polling.
3
+ *
4
+ * Polls /api/admin/events/counts every 10s for an unread bubble on the
5
+ * Monitoring nav button. When the Monitoring section is active, also fetches
6
+ * the full event list and renders into #events-panel.
7
+ *
8
+ * Client-side pagination + text search + per-row collapse-to-detail.
9
+ */
10
+ (function() {
11
+ var lastSeenId = 0;
12
+ var pollTimer = null;
13
+ var refreshTimer = null;
14
+ var allEvents = []; // most-recent-first cache
15
+ var filterText = '';
16
+ var filterLevel = '';
17
+ var pageIndex = 0;
18
+ var pageSize = 50;
19
+
20
+ function adminFetch(url, opts) {
21
+ if (window.adminAuth && typeof window.adminAuth.adminFetch === 'function') {
22
+ return window.adminAuth.adminFetch(url, opts);
23
+ }
24
+ return fetch(url, opts);
25
+ }
26
+
27
+ function escapeText(s) {
28
+ if (s == null) return '';
29
+ return String(s)
30
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
31
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
32
+ }
33
+
34
+ function highlight(text, term) {
35
+ var escaped = escapeText(text);
36
+ if (!term) return escaped;
37
+ try {
38
+ // Escape regex meta in user input.
39
+ var re = new RegExp(term.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi');
40
+ return escaped.replace(re, function(m) { return '<mark>' + m + '</mark>'; });
41
+ } catch (_err) { void _err; return escaped; }
42
+ }
43
+
44
+ function applyFilters() {
45
+ var t = filterText.trim().toLowerCase();
46
+ return allEvents.filter(function(e) {
47
+ if (filterLevel && e.level !== filterLevel) return false;
48
+ if (!t) return true;
49
+ var hay = (e.msg || '') + ' ' + (e.detail || '');
50
+ return hay.toLowerCase().indexOf(t) !== -1;
51
+ });
52
+ }
53
+
54
+ function renderEvents() {
55
+ var panel = document.getElementById('events-panel');
56
+ var pager = document.getElementById('events-pager');
57
+ var pageInfo = document.getElementById('events-page-info');
58
+ if (!panel) return;
59
+
60
+ var filtered = applyFilters();
61
+ var totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
62
+ if (pageIndex >= totalPages) pageIndex = totalPages - 1;
63
+ if (pageIndex < 0) pageIndex = 0;
64
+ var start = pageIndex * pageSize;
65
+ var slice = filtered.slice(start, start + pageSize);
66
+
67
+ panel.classList.remove('loading');
68
+ if (filtered.length === 0) {
69
+ panel.innerHTML = '<div class="muted">' + (allEvents.length === 0 ? 'No recent events.' : 'No events match the current filter.') + '</div>';
70
+ if (pager) pager.classList.add('hidden');
71
+ return;
72
+ }
73
+
74
+ var rows = slice.map(function(e) {
75
+ var lvlClass = e.level === 'ERROR' ? 'level-error' : 'level-warn';
76
+ var ts = e.ts ? new Date(e.ts).toLocaleTimeString() : '';
77
+ var msgHtml = highlight(e.msg || '', filterText);
78
+ var detailHtml = e.detail ? '<div class="event-detail muted">' + highlight(e.detail, filterText) + '</div>' : '';
79
+ var caret = e.detail ? '<span class="toggle-caret">▶</span> ' : '';
80
+ return '<tr class="event-row ' + lvlClass + '" data-evt-id="' + e.id + '">'
81
+ + '<td class="event-ts">' + escapeText(ts) + '</td>'
82
+ + '<td class="event-level"><span class="event-badge ' + lvlClass + '">' + escapeText(e.level) + '</span></td>'
83
+ + '<td class="event-msg">' + caret + msgHtml + detailHtml + '</td>'
84
+ + '</tr>';
85
+ }).join('');
86
+
87
+ panel.innerHTML = '<table class="events-table">'
88
+ + '<thead><tr><th>Time</th><th>Level</th><th>Message</th></tr></thead>'
89
+ + '<tbody>' + rows + '</tbody></table>';
90
+
91
+ if (pager) {
92
+ pager.classList.remove('hidden');
93
+ if (pageInfo) {
94
+ pageInfo.textContent = 'Page ' + (pageIndex + 1) + ' of ' + totalPages
95
+ + ' · ' + filtered.length + ' event' + (filtered.length === 1 ? '' : 's')
96
+ + (filtered.length !== allEvents.length ? ' (filtered from ' + allEvents.length + ')' : '');
97
+ }
98
+ var prev = document.getElementById('events-prev');
99
+ var next = document.getElementById('events-next');
100
+ if (prev) prev.disabled = pageIndex === 0;
101
+ if (next) next.disabled = pageIndex >= totalPages - 1;
102
+ }
103
+ }
104
+
105
+ function loadEvents() {
106
+ var levelSel = document.getElementById('events-level-filter');
107
+ filterLevel = levelSel ? levelSel.value : '';
108
+ // Always fetch the buffer max — pagination/filter is client-side so
109
+ // searching can hit older events without round-trips.
110
+ adminFetch('/api/admin/events?limit=1000')
111
+ .then(function(r) { return r.json(); })
112
+ .then(function(data) {
113
+ if (!data || !data.success) return;
114
+ // listEvents returns oldest→newest; reverse for newest-first paging.
115
+ allEvents = (data.events || []).slice().reverse();
116
+ if (data.counts) {
117
+ lastSeenId = data.counts.latestId || lastSeenId;
118
+ updateBubble({ warn: 0, error: 0, total: 0 });
119
+ var summary = document.getElementById('events-counts-summary');
120
+ if (summary) summary.textContent = '(' + (data.counts.warn || 0) + ' warn / ' + (data.counts.error || 0) + ' error in buffer)';
121
+ }
122
+ renderEvents();
123
+ })
124
+ .catch(function() { /* ignore transient errors */ });
125
+ }
126
+
127
+ function clearEventsBuffer() {
128
+ adminFetch('/api/admin/events', { method: 'DELETE' })
129
+ .then(function(r) { return r.json(); })
130
+ .then(function() { allEvents = []; pageIndex = 0; loadEvents(); })
131
+ .catch(function() { /* ignore */ });
132
+ }
133
+
134
+ function updateBubble(counts) {
135
+ var bubble = document.getElementById('nav-events-bubble');
136
+ if (!bubble) return;
137
+ var total = (counts.warn || 0) + (counts.error || 0);
138
+ if (total > 0) {
139
+ bubble.textContent = total > 99 ? '99+' : String(total);
140
+ bubble.hidden = false;
141
+ bubble.classList.toggle('has-error', (counts.error || 0) > 0);
142
+ } else {
143
+ bubble.hidden = true;
144
+ bubble.classList.remove('has-error');
145
+ }
146
+ }
147
+
148
+ function pollCounts() {
149
+ adminFetch('/api/admin/events/counts?since=' + encodeURIComponent(lastSeenId))
150
+ .then(function(r) { return r.json(); })
151
+ .then(function(data) {
152
+ if (!data || !data.success || !data.counts) return;
153
+ updateBubble(data.counts);
154
+ })
155
+ .catch(function() { /* ignore */ });
156
+ }
157
+
158
+ function startPolling() {
159
+ stopPolling();
160
+ pollCounts();
161
+ pollTimer = setInterval(pollCounts, 10000);
162
+ }
163
+ function stopPolling() {
164
+ if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
165
+ }
166
+
167
+ function startMonitoringRefresh() {
168
+ stopMonitoringRefresh();
169
+ refreshTimer = setInterval(function() {
170
+ var section = document.getElementById('monitoring-section');
171
+ if (section && !section.classList.contains('hidden')) loadEvents();
172
+ }, 15000);
173
+ }
174
+ function stopMonitoringRefresh() {
175
+ if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
176
+ }
177
+
178
+ // When user navigates to Monitoring, load events; mark-as-read by adopting latestId.
179
+ document.addEventListener('click', function(ev) {
180
+ var t = ev.target;
181
+ if (t && t.getAttribute && t.getAttribute('data-section') === 'monitoring') {
182
+ setTimeout(loadEvents, 50);
183
+ }
184
+ }, true);
185
+
186
+ // Row expand/collapse (delegated; only inside events-panel).
187
+ function attachPanelHandlers() {
188
+ var panel = document.getElementById('events-panel');
189
+ if (!panel || panel._evtBound) return;
190
+ panel._evtBound = true;
191
+ panel.addEventListener('click', function(ev) {
192
+ var row = ev.target.closest && ev.target.closest('tr.event-row');
193
+ if (!row) return;
194
+ // Don't toggle on selection of text within already-expanded detail.
195
+ var sel = window.getSelection && window.getSelection();
196
+ if (sel && sel.toString().length > 0) return;
197
+ row.classList.toggle('expanded');
198
+ });
199
+ }
200
+
201
+ function attachControlHandlers() {
202
+ var search = document.getElementById('events-search');
203
+ if (search && !search._evtBound) {
204
+ search._evtBound = true;
205
+ var debounce;
206
+ search.addEventListener('input', function() {
207
+ clearTimeout(debounce);
208
+ debounce = setTimeout(function() {
209
+ filterText = search.value || '';
210
+ pageIndex = 0;
211
+ renderEvents();
212
+ }, 120);
213
+ });
214
+ }
215
+ var pageSel = document.getElementById('events-page-size');
216
+ if (pageSel && !pageSel._evtBound) {
217
+ pageSel._evtBound = true;
218
+ pageSel.addEventListener('change', function() {
219
+ var n = parseInt(pageSel.value, 10);
220
+ if (Number.isFinite(n) && n > 0) { pageSize = n; pageIndex = 0; renderEvents(); }
221
+ });
222
+ }
223
+ var prev = document.getElementById('events-prev');
224
+ if (prev && !prev._evtBound) {
225
+ prev._evtBound = true;
226
+ prev.addEventListener('click', function() { if (pageIndex > 0) { pageIndex--; renderEvents(); } });
227
+ }
228
+ var next = document.getElementById('events-next');
229
+ if (next && !next._evtBound) {
230
+ next._evtBound = true;
231
+ next.addEventListener('click', function() { pageIndex++; renderEvents(); });
232
+ }
233
+ }
234
+
235
+ // Init after DOM ready (script is `defer`).
236
+ function init() {
237
+ startPolling();
238
+ startMonitoringRefresh();
239
+ attachPanelHandlers();
240
+ attachControlHandlers();
241
+ var levelFilter = document.getElementById('events-level-filter');
242
+ if (levelFilter && !levelFilter._evtBound) {
243
+ levelFilter._evtBound = true;
244
+ levelFilter.addEventListener('change', function() { pageIndex = 0; loadEvents(); });
245
+ }
246
+ }
247
+
248
+ if (document.readyState === 'loading') {
249
+ document.addEventListener('DOMContentLoaded', init);
250
+ } else {
251
+ init();
252
+ }
253
+
254
+ window.loadEvents = loadEvents;
255
+ window.clearEvents = clearEventsBuffer;
256
+ })();