@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.
- package/dashboard.html +89 -14
- package/logs.html +17 -2
- 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") ||
|
|
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(
|
|
1561
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2336
|
+
const delay = sessionSSERetryMs;
|
|
2337
|
+
sessionSSERetryMs = Math.min(sessionSSERetryMs * 2, 30000);
|
|
2338
|
+
sseReconnectTimer = setTimeout(() => {
|
|
2339
|
+
sseReconnectTimer = null;
|
|
2270
2340
|
connectSessionSSE();
|
|
2271
|
-
|
|
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
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
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 ───────────────────────────────────────────────
|