@nockdev/hsa 1.2.4 → 1.2.7

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 (3) hide show
  1. package/dashboard.html +89 -14
  2. package/logs.html +17 -2
  3. package/package.json +1 -1
package/dashboard.html CHANGED
@@ -1157,6 +1157,33 @@
1157
1157
  opacity: 0.4;
1158
1158
  }
1159
1159
 
1160
+ /* ERROR BANNER */
1161
+ .error-banner {
1162
+ display: none;
1163
+ align-items: center;
1164
+ gap: 8px;
1165
+ padding: 8px 16px;
1166
+ background: var(--red-subtle);
1167
+ border-bottom: 1px solid rgba(248, 81, 73, 0.3);
1168
+ color: var(--red);
1169
+ font-size: 12px;
1170
+ font-family: var(--font-mono);
1171
+ grid-area: header;
1172
+ }
1173
+ .error-banner.visible {
1174
+ display: flex;
1175
+ }
1176
+ .error-banner .error-dismiss {
1177
+ margin-left: auto;
1178
+ cursor: pointer;
1179
+ opacity: 0.6;
1180
+ background: none;
1181
+ border: none;
1182
+ color: var(--red);
1183
+ font-size: 14px;
1184
+ }
1185
+ .error-banner .error-dismiss:hover { opacity: 1; }
1186
+
1160
1187
  /* RESPONSIVE */
1161
1188
  @media (max-width: 1200px) {
1162
1189
  .kpi-grid {
@@ -1240,6 +1267,12 @@
1240
1267
  </button>
1241
1268
  </div>
1242
1269
  </header>
1270
+ <!-- ERROR BANNER -->
1271
+ <div class="error-banner" id="errorBanner">
1272
+ <span>⚠</span>
1273
+ <span id="errorBannerMsg">Connection failed</span>
1274
+ <button class="error-dismiss" onclick="hideErrorBanner()">✕</button>
1275
+ </div>
1243
1276
 
1244
1277
  <!-- CONTEXT SELECTOR BAR -->
1245
1278
  <div class="context-bar">
@@ -1481,7 +1514,7 @@
1481
1514
  STATE
1482
1515
  ================================================================ */
1483
1516
  let endpoint =
1484
- localStorage.getItem("hsa_endpoint") || "http://localhost:13100";
1517
+ localStorage.getItem("hsa_endpoint") || window.location.origin;
1485
1518
  let autoRefresh = true;
1486
1519
  let refreshInterval = null;
1487
1520
  let cacheHistory = [];
@@ -1493,7 +1526,9 @@
1493
1526
  let sessionSSERetryMs = 1000;
1494
1527
  let sessionSSERetryCount = 0;
1495
1528
  const MAX_SSE_RETRIES = 5;
1529
+ let sseReconnectTimer = null;
1496
1530
  let fetchErrorCount = 0;
1531
+ let dashboardAbortCtrl = null;
1497
1532
  const BASE_REFRESH_MS = 5000;
1498
1533
  const MAX_REFRESH_MS = 30000;
1499
1534
 
@@ -1510,6 +1545,11 @@
1510
1545
  setStatus("connecting");
1511
1546
  fetchErrorCount = 0; // Reset error counter on manual connect
1512
1547
  sessionSSERetryCount = 0; // Reset SSE retries so reconnect works
1548
+ sessionSSERetryMs = 1000; // Reset backoff delay
1549
+ if (sseReconnectTimer) {
1550
+ clearTimeout(sseReconnectTimer);
1551
+ sseReconnectTimer = null;
1552
+ }
1513
1553
  fetchDashboard();
1514
1554
  fetchSessions();
1515
1555
  connectSessionSSE(); // Re-establish SSE stream
@@ -1518,6 +1558,16 @@
1518
1558
  function setStatus(status) {
1519
1559
  const dot = document.getElementById("statusDot");
1520
1560
  dot.className = "logo-dot " + (status === "connected" ? "" : status);
1561
+ if (status === "connected") hideErrorBanner();
1562
+ }
1563
+
1564
+ function showErrorBanner(msg) {
1565
+ const banner = document.getElementById("errorBanner");
1566
+ document.getElementById("errorBannerMsg").textContent = msg || "Connection failed";
1567
+ banner.classList.add("visible");
1568
+ }
1569
+ function hideErrorBanner() {
1570
+ document.getElementById("errorBanner").classList.remove("visible");
1521
1571
  }
1522
1572
 
1523
1573
  function toggleAutoRefresh() {
@@ -1557,8 +1607,11 @@
1557
1607
  startRefresh();
1558
1608
  }
1559
1609
  function updateRefreshBadge(badge) {
1560
- const ms = Math.min(BASE_REFRESH_MS * Math.pow(2, fetchErrorCount), MAX_REFRESH_MS);
1561
- badge.textContent = ms >= 1000 ? (ms / 1000) + "s" : ms + "ms";
1610
+ const ms = Math.min(
1611
+ BASE_REFRESH_MS * Math.pow(2, fetchErrorCount),
1612
+ MAX_REFRESH_MS,
1613
+ );
1614
+ badge.textContent = ms >= 1000 ? ms / 1000 + "s" : ms + "ms";
1562
1615
  }
1563
1616
 
1564
1617
  /* ================================================================
@@ -1617,10 +1670,16 @@
1617
1670
  FETCH & RENDER
1618
1671
  ================================================================ */
1619
1672
  async function fetchDashboard() {
1673
+ // Cancel any in-flight request to prevent NS_BINDING_ABORTED
1674
+ if (dashboardAbortCtrl) dashboardAbortCtrl.abort();
1675
+ dashboardAbortCtrl = new AbortController();
1676
+ const ctrl = dashboardAbortCtrl;
1620
1677
  try {
1621
1678
  let url = endpoint + "/api/dashboard";
1622
1679
  // Note: /api/dashboard doesn't support session filtering yet, handled client-side
1623
- const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
1680
+ const timeoutId = setTimeout(() => ctrl.abort(), 8000);
1681
+ const res = await fetch(url, { signal: ctrl.signal });
1682
+ clearTimeout(timeoutId);
1624
1683
  if (!res.ok) throw new Error("HTTP " + res.status);
1625
1684
  const data = await res.json();
1626
1685
  setStatus("connected");
@@ -1630,10 +1689,13 @@
1630
1689
  }
1631
1690
  render(data);
1632
1691
  } catch (err) {
1692
+ if (err.name === "AbortError") return; // Intentional cancel, ignore
1633
1693
  setStatus("error");
1634
1694
  fetchErrorCount++;
1635
1695
  adjustRefreshInterval();
1636
- console.warn("HSA fetch error:", err.message);
1696
+ const msg = err.message || "Connection failed";
1697
+ console.warn("HSA fetch error:", msg);
1698
+ showErrorBanner(msg);
1637
1699
  }
1638
1700
  }
1639
1701
 
@@ -2156,7 +2218,10 @@
2156
2218
  for (let i = 1; i < cacheHistory.length; i++) {
2157
2219
  deltas.push({
2158
2220
  hits: Math.max(0, cacheHistory[i].hits - cacheHistory[i - 1].hits),
2159
- misses: Math.max(0, cacheHistory[i].misses - cacheHistory[i - 1].misses),
2221
+ misses: Math.max(
2222
+ 0,
2223
+ cacheHistory[i].misses - cacheHistory[i - 1].misses,
2224
+ ),
2160
2225
  });
2161
2226
  }
2162
2227
  const maxVal = Math.max(
@@ -2235,12 +2300,14 @@
2235
2300
  renderSidebar(allSessions);
2236
2301
  populateContextSelectors(allSessions);
2237
2302
  })
2238
- .catch(() => {});
2303
+ .catch((err) => { console.warn("fetchSessions failed:", err.message || err); });
2239
2304
  }
2240
2305
 
2241
2306
  function connectSessionSSE() {
2242
2307
  if (sessionSSERetryCount >= MAX_SSE_RETRIES) {
2243
- console.warn("SSE: Max retries reached, stopping session stream reconnect");
2308
+ console.warn(
2309
+ "SSE: Max retries reached, stopping session stream reconnect",
2310
+ );
2244
2311
  return;
2245
2312
  }
2246
2313
  if (sessionSSE) {
@@ -2266,10 +2333,12 @@
2266
2333
  sessionSSE.close();
2267
2334
  sessionSSE = null;
2268
2335
  sessionSSERetryCount++;
2269
- setTimeout(() => {
2336
+ const delay = sessionSSERetryMs;
2337
+ sessionSSERetryMs = Math.min(sessionSSERetryMs * 2, 30000);
2338
+ sseReconnectTimer = setTimeout(() => {
2339
+ sseReconnectTimer = null;
2270
2340
  connectSessionSSE();
2271
- sessionSSERetryMs = Math.min(sessionSSERetryMs * 2, 30000);
2272
- }, sessionSSERetryMs);
2341
+ }, delay);
2273
2342
  };
2274
2343
  sessionSSE.onopen = () => {
2275
2344
  sessionSSERetryMs = 1000;
@@ -2390,9 +2459,15 @@
2390
2459
  INIT
2391
2460
  ================================================================ */
2392
2461
  document.getElementById("refreshToggle").classList.add("active");
2393
- connect();
2394
- startRefresh();
2395
- connectSessionSSE();
2462
+ // Fast health check first, then full data load
2463
+ (async () => {
2464
+ try {
2465
+ const res = await fetch(endpoint + "/api/health", { signal: AbortSignal.timeout(3000) });
2466
+ if (res.ok) { setStatus("connected"); hideErrorBanner(); }
2467
+ } catch { /* will fall through to connect() below */ }
2468
+ connect();
2469
+ startRefresh();
2470
+ })();
2396
2471
  window.addEventListener("resize", drawCacheChart);
2397
2472
  </script>
2398
2473
  </body>
package/logs.html CHANGED
@@ -628,6 +628,8 @@
628
628
  let filterTimeout = null;
629
629
  let allSessions = [];
630
630
  let sseRetryMs = 1000;
631
+ let sseRetryCount = 0;
632
+ const MAX_SSE_RETRIES = 5;
631
633
  let statsThrottleTimer = null;
632
634
 
633
635
  // ── Init ──────────────────────────────────────────────────
@@ -651,7 +653,9 @@
651
653
 
652
654
  setStatus("pending");
653
655
  try {
654
- const res = await fetch(baseUrl + "/api/logs?limit=200");
656
+ const res = await fetch(baseUrl + "/api/logs?limit=200", {
657
+ signal: AbortSignal.timeout(8000),
658
+ });
655
659
  if (!res.ok) throw new Error(res.statusText);
656
660
  const data = await res.json();
657
661
  entries = data.entries || [];
@@ -758,6 +762,14 @@
758
762
  eventSource.close();
759
763
  eventSource = null;
760
764
  }
765
+ sseRetryCount++;
766
+ if (sseRetryCount >= MAX_SSE_RETRIES) {
767
+ console.warn("SSE: Max retries reached, stopping log stream reconnect");
768
+ isStreaming = false;
769
+ document.getElementById("streamBtn").textContent = "▶ Stream";
770
+ document.getElementById("streamBtn").classList.remove("active");
771
+ return;
772
+ }
761
773
  // FE-01: Auto-reconnect with exponential backoff
762
774
  setTimeout(() => {
763
775
  if (isStreaming) startStream();
@@ -767,6 +779,7 @@
767
779
  eventSource.onopen = () => {
768
780
  setStatus("ok");
769
781
  sseRetryMs = 1000;
782
+ sseRetryCount = 0;
770
783
  };
771
784
  isStreaming = true;
772
785
  document.getElementById("streamBtn").textContent = "⏸ Pause";
@@ -804,7 +817,9 @@
804
817
  document.getElementById("statErrors").textContent = s.errorCount;
805
818
  document.getElementById("statAvg").textContent = s.avgDurationMs;
806
819
  document.getElementById("statRate").textContent = s.callsPerMinute;
807
- } catch {}
820
+ } catch (err) {
821
+ console.warn("fetchStats error:", err.message || err);
822
+ }
808
823
  }
809
824
 
810
825
  // ── Filters ───────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nockdev/hsa",
3
- "version": "1.2.4",
3
+ "version": "1.2.7",
4
4
  "description": "HSA - Hierarchical Semantic Analysis MCP Server for AI Code Agents",
5
5
  "type": "module",
6
6
  "main": "dist/hsa-lib.bundle.js",