@nockdev/hsa 1.0.0 → 1.0.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.
- package/README.md +1 -1
- package/dashboard.html +2348 -0
- package/dist/hsa-cli.bundle.js +1 -1
- package/dist/hsa-daemon.bundle.js +1 -1
- package/dist/hsa-http.bundle.js +1 -1
- package/dist/hsa-lib.bundle.js +1 -1
- package/dist/integrity.json +173 -80
- package/logs.html +492 -0
- package/package.json +3 -1
package/logs.html
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>HSA Logs</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
9
|
+
<style>
|
|
10
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
11
|
+
:root{
|
|
12
|
+
--bg:#0d1117;--surface:#161b22;--surface2:#1c2129;--border:rgba(240,246,252,0.1);--border-h:rgba(240,246,252,0.2);
|
|
13
|
+
--text:#e6edf3;--text2:#8b949e;--text3:#6e7681;
|
|
14
|
+
--ok:#3fb950;--err:#f85149;--warn:#d29922;--accent:#388bfd;
|
|
15
|
+
--accent-subtle:rgba(56,139,253,0.1);
|
|
16
|
+
--font:Inter,system-ui,sans-serif;--mono:'JetBrains Mono',monospace;
|
|
17
|
+
}
|
|
18
|
+
html,body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:14px;height:100%}
|
|
19
|
+
a{color:var(--accent);text-decoration:none}
|
|
20
|
+
|
|
21
|
+
/* Layout */
|
|
22
|
+
.app{display:flex;flex-direction:column;height:100vh;max-width:1400px;margin:0 auto;padding:0 20px}
|
|
23
|
+
|
|
24
|
+
/* Header */
|
|
25
|
+
.header{display:flex;align-items:center;gap:12px;padding:14px 0;border-bottom:1px solid var(--border);flex-shrink:0}
|
|
26
|
+
.header h1{font-size:15px;font-weight:600;white-space:nowrap;display:flex;align-items:center;gap:8px}
|
|
27
|
+
.status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
|
28
|
+
.status-dot.ok{background:var(--ok);box-shadow:0 0 6px var(--ok)}
|
|
29
|
+
.status-dot.err{background:var(--err);box-shadow:0 0 6px var(--err)}
|
|
30
|
+
.status-dot.pending{background:var(--warn);animation:pulse 1.5s infinite}
|
|
31
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
|
32
|
+
.header-controls{display:flex;gap:8px;align-items:center;margin-left:auto}
|
|
33
|
+
.header-controls input{background:var(--surface);border:1px solid var(--border);color:var(--text);padding:5px 10px;border-radius:6px;font:inherit;font-size:12px;width:200px}
|
|
34
|
+
.header-controls input:focus{outline:none;border-color:var(--accent)}
|
|
35
|
+
.btn{background:var(--surface);border:1px solid var(--border);color:var(--text2);padding:5px 10px;border-radius:6px;cursor:pointer;font:inherit;font-size:12px;font-weight:500;transition:all .15s;display:inline-flex;align-items:center;gap:4px}
|
|
36
|
+
.btn:hover{border-color:var(--border-h);color:var(--text)}
|
|
37
|
+
.btn.active{border-color:var(--accent);color:var(--accent);background:var(--accent-subtle)}
|
|
38
|
+
.nav-link{font-size:12px;color:var(--text2);padding:4px 8px;border-radius:4px;transition:all .15s}
|
|
39
|
+
.nav-link:hover{color:var(--text);background:var(--surface)}
|
|
40
|
+
|
|
41
|
+
/* Context Selector */
|
|
42
|
+
.context-bar{display:flex;gap:12px;padding:8px 0;border-bottom:1px solid var(--border);flex-shrink:0;flex-wrap:wrap;align-items:center}
|
|
43
|
+
.context-bar label{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.5px;font-weight:600}
|
|
44
|
+
.ctx-select{background:var(--surface);border:1px solid var(--border);color:var(--text);font-family:var(--font);font-size:12px;padding:3px 22px 3px 8px;border-radius:12px;cursor:pointer;transition:border .15s;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%238b949e' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 6px center}
|
|
45
|
+
.ctx-select:focus{outline:none;border-color:var(--accent)}
|
|
46
|
+
.ctx-group{display:flex;align-items:center;gap:5px}
|
|
47
|
+
.ctx-sep{width:1px;height:18px;background:var(--border);margin:0 3px}
|
|
48
|
+
|
|
49
|
+
/* Stats Bar */
|
|
50
|
+
.stats-bar{display:flex;gap:24px;padding:10px 0;border-bottom:1px solid var(--border);flex-shrink:0;flex-wrap:wrap}
|
|
51
|
+
.stat{display:flex;align-items:center;gap:5px;font-size:12px;color:var(--text2)}
|
|
52
|
+
.stat-value{font-weight:600;color:var(--text);font-variant-numeric:tabular-nums}
|
|
53
|
+
.stat-value.err{color:var(--err)}
|
|
54
|
+
|
|
55
|
+
/* Filter Bar */
|
|
56
|
+
.filter-bar{display:flex;gap:8px;padding:8px 0;border-bottom:1px solid var(--border);flex-shrink:0;flex-wrap:wrap;align-items:center}
|
|
57
|
+
.filter-bar select{background:var(--surface);border:1px solid var(--border);color:var(--text);padding:4px 8px;border-radius:6px;font:inherit;font-size:12px}
|
|
58
|
+
.filter-bar select:focus{outline:none;border-color:var(--accent)}
|
|
59
|
+
.filter-bar input{background:var(--surface);border:1px solid var(--border);color:var(--text);padding:4px 10px;border-radius:6px;font:inherit;font-size:12px;flex:1;min-width:150px}
|
|
60
|
+
.filter-bar input:focus{outline:none;border-color:var(--accent)}
|
|
61
|
+
.filter-count{font-size:11px;color:var(--text3);margin-left:auto;font-family:var(--mono)}
|
|
62
|
+
|
|
63
|
+
/* Log Table */
|
|
64
|
+
.log-container{flex:1;overflow-y:auto;min-height:0}
|
|
65
|
+
.log-table{width:100%}
|
|
66
|
+
/* 5-column grid: Time | IDE | Tool | Status | Duration */
|
|
67
|
+
.log-row{display:grid;grid-template-columns:95px 80px 1fr 65px 75px;gap:10px;padding:7px 8px;border-bottom:1px solid var(--border);align-items:center;cursor:pointer;transition:background .1s}
|
|
68
|
+
.log-row:hover{background:var(--surface)}
|
|
69
|
+
.log-row.expanded{background:var(--surface);border-bottom:none}
|
|
70
|
+
.log-row.error{border-left:2px solid var(--err)}
|
|
71
|
+
.log-row.slow{border-left:2px solid var(--warn)}
|
|
72
|
+
.log-time{font-family:var(--mono);font-size:11px;color:var(--text2);white-space:nowrap}
|
|
73
|
+
.log-ide{font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
74
|
+
.log-tool{font-family:var(--mono);font-size:12px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
75
|
+
.log-status{font-size:11px;font-weight:500;text-align:center;padding:2px 6px;border-radius:10px;display:inline-block}
|
|
76
|
+
.log-status.ok{color:var(--ok);background:rgba(63,185,80,.1)}
|
|
77
|
+
.log-status.error{color:var(--err);background:rgba(248,81,73,.1)}
|
|
78
|
+
.log-duration{font-family:var(--mono);font-size:11px;color:var(--text2);text-align:right}
|
|
79
|
+
.log-duration.slow{color:var(--warn);font-weight:600}
|
|
80
|
+
|
|
81
|
+
/* IDE Badges */
|
|
82
|
+
.ide-badge{display:inline-flex;align-items:center;gap:3px;font-size:10px;font-weight:500;padding:1px 6px;border-radius:8px;border:1px solid;white-space:nowrap}
|
|
83
|
+
.ide-badge[data-ide="cursor"]{background:rgba(168,85,247,0.12);border-color:rgba(168,85,247,0.3);color:#a855f7}
|
|
84
|
+
.ide-badge[data-ide="vscode"]{background:rgba(56,139,253,0.12);border-color:rgba(56,139,253,0.3);color:#388bfd}
|
|
85
|
+
.ide-badge[data-ide="antigravity"]{background:rgba(63,185,80,0.12);border-color:rgba(63,185,80,0.3);color:#3fb950}
|
|
86
|
+
.ide-badge[data-ide="windsurf"]{background:rgba(210,153,34,0.12);border-color:rgba(210,153,34,0.3);color:#d29922}
|
|
87
|
+
.ide-badge[data-ide="claude-desktop"]{background:rgba(248,81,73,0.12);border-color:rgba(248,81,73,0.3);color:#f85149}
|
|
88
|
+
.ide-badge[data-ide="gemini-cli"]{background:rgba(56,139,253,0.12);border-color:rgba(56,139,253,0.3);color:#388bfd}
|
|
89
|
+
.ide-badge[data-ide="codex"]{background:rgba(63,185,80,0.12);border-color:rgba(63,185,80,0.3);color:#3fb950}
|
|
90
|
+
.ide-badge[data-ide="neovim"]{background:rgba(63,185,80,0.12);border-color:rgba(63,185,80,0.3);color:#3fb950}
|
|
91
|
+
.ide-badge[data-ide="zed"]{background:rgba(247,120,186,0.12);border-color:rgba(247,120,186,0.3);color:#f778ba}
|
|
92
|
+
.ide-badge[data-ide="unknown"]{background:rgba(139,148,158,0.12);border-color:rgba(139,148,158,0.3);color:#8b949e}
|
|
93
|
+
|
|
94
|
+
/* Detail Panel */
|
|
95
|
+
.log-detail{background:var(--surface2);border-bottom:1px solid var(--border);padding:12px 16px;display:none}
|
|
96
|
+
.log-detail.show{display:block}
|
|
97
|
+
.detail-section{margin-bottom:10px}
|
|
98
|
+
.detail-section:last-child{margin-bottom:0}
|
|
99
|
+
.detail-label{font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:var(--text3);margin-bottom:4px;font-weight:600}
|
|
100
|
+
.detail-content{font-family:var(--mono);font-size:11px;color:var(--text2);background:var(--bg);padding:8px 10px;border-radius:6px;border:1px solid var(--border);white-space:pre-wrap;word-break:break-all;max-height:200px;overflow-y:auto;line-height:1.5}
|
|
101
|
+
.detail-content.err{border-color:var(--err);color:var(--err)}
|
|
102
|
+
.detail-meta{display:flex;gap:14px;flex-wrap:wrap}
|
|
103
|
+
.detail-meta span{font-size:11px;color:var(--text3)}
|
|
104
|
+
.detail-meta strong{color:var(--text2);font-weight:500}
|
|
105
|
+
|
|
106
|
+
/* Empty State */
|
|
107
|
+
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 20px;color:var(--text3)}
|
|
108
|
+
.empty-state .icon{font-size:40px;margin-bottom:12px;opacity:.5}
|
|
109
|
+
.empty-state p{font-size:14px}
|
|
110
|
+
|
|
111
|
+
/* Responsive */
|
|
112
|
+
@media(max-width:768px){
|
|
113
|
+
.header{flex-wrap:wrap}
|
|
114
|
+
.header-controls{width:100%}
|
|
115
|
+
.header-controls input{flex:1}
|
|
116
|
+
.stats-bar{gap:12px}
|
|
117
|
+
/* Hide IDE column on mobile */
|
|
118
|
+
.log-row{grid-template-columns:80px 1fr 50px 60px;gap:6px;font-size:12px}
|
|
119
|
+
.log-ide{display:none}
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
122
|
+
</head>
|
|
123
|
+
<body>
|
|
124
|
+
<div class="app">
|
|
125
|
+
<!-- Header -->
|
|
126
|
+
<div class="header">
|
|
127
|
+
<div class="status-dot pending" id="statusDot"></div>
|
|
128
|
+
<h1>🔍 HSA Logs</h1>
|
|
129
|
+
<a href="/dashboard" class="nav-link">← Dashboard</a>
|
|
130
|
+
<div class="header-controls">
|
|
131
|
+
<input type="text" id="endpoint" value="http://localhost:13100" placeholder="HSA endpoint">
|
|
132
|
+
<button class="btn" id="connectBtn" onclick="connect()">Connect</button>
|
|
133
|
+
<button class="btn" id="streamBtn" onclick="toggleStream()">▶ Stream</button>
|
|
134
|
+
<button class="btn" onclick="clearLogs()">Clear</button>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<!-- Context Selector Bar -->
|
|
139
|
+
<div class="context-bar">
|
|
140
|
+
<div class="ctx-group">
|
|
141
|
+
<label>IDE</label>
|
|
142
|
+
<select class="ctx-select" id="ctxIde" onchange="applyFilters()">
|
|
143
|
+
<option value="">All IDEs</option>
|
|
144
|
+
</select>
|
|
145
|
+
</div>
|
|
146
|
+
<span class="ctx-sep"></span>
|
|
147
|
+
<div class="ctx-group">
|
|
148
|
+
<label>Project</label>
|
|
149
|
+
<select class="ctx-select" id="ctxProject" onchange="applyFilters()">
|
|
150
|
+
<option value="">All Projects</option>
|
|
151
|
+
</select>
|
|
152
|
+
</div>
|
|
153
|
+
<span class="ctx-sep"></span>
|
|
154
|
+
<div class="ctx-group">
|
|
155
|
+
<label>Session</label>
|
|
156
|
+
<select class="ctx-select" id="ctxSession" onchange="applyFilters()">
|
|
157
|
+
<option value="">All Sessions</option>
|
|
158
|
+
</select>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<!-- Stats Bar -->
|
|
163
|
+
<div class="stats-bar">
|
|
164
|
+
<div class="stat">📊 <span class="stat-value" id="statTotal">0</span> calls</div>
|
|
165
|
+
<div class="stat">❌ <span class="stat-value err" id="statErrors">0</span> errors</div>
|
|
166
|
+
<div class="stat">⚡ <span class="stat-value" id="statAvg">0</span>ms avg</div>
|
|
167
|
+
<div class="stat">📈 <span class="stat-value" id="statRate">0</span> calls/min</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<!-- Filter Bar -->
|
|
171
|
+
<div class="filter-bar">
|
|
172
|
+
<select id="filterTool" onchange="applyFilters()">
|
|
173
|
+
<option value="">All Tools</option>
|
|
174
|
+
</select>
|
|
175
|
+
<select id="filterStatus" onchange="applyFilters()">
|
|
176
|
+
<option value="">All Status</option>
|
|
177
|
+
<option value="ok">✓ OK</option>
|
|
178
|
+
<option value="error">✗ Error</option>
|
|
179
|
+
</select>
|
|
180
|
+
<select id="filterTransport" onchange="applyFilters()">
|
|
181
|
+
<option value="">All Transports</option>
|
|
182
|
+
<option value="stdio">stdio</option>
|
|
183
|
+
<option value="http">http</option>
|
|
184
|
+
</select>
|
|
185
|
+
<input type="text" id="filterSearch" placeholder="🔍 Search logs..." oninput="debounceFilter()">
|
|
186
|
+
<span class="filter-count" id="filterCount"></span>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<!-- Log Container -->
|
|
190
|
+
<div class="log-container" id="logContainer">
|
|
191
|
+
<div class="empty-state" id="emptyState">
|
|
192
|
+
<div class="icon">📋</div>
|
|
193
|
+
<p>No log entries yet. Connect to an HSA endpoint to start streaming.</p>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="log-table" id="logTable"></div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<script>
|
|
200
|
+
// ── State ─────────────────────────────────────────────────
|
|
201
|
+
let entries = [];
|
|
202
|
+
let filteredEntries = [];
|
|
203
|
+
let baseUrl = '';
|
|
204
|
+
let eventSource = null;
|
|
205
|
+
let isStreaming = false;
|
|
206
|
+
let refreshTimer = null;
|
|
207
|
+
let filterTimeout = null;
|
|
208
|
+
let allSessions = [];
|
|
209
|
+
|
|
210
|
+
// ── Init ──────────────────────────────────────────────────
|
|
211
|
+
const savedEndpoint = localStorage.getItem('hsa-log-endpoint');
|
|
212
|
+
if (savedEndpoint) document.getElementById('endpoint').value = savedEndpoint;
|
|
213
|
+
setTimeout(() => connect(), 300);
|
|
214
|
+
|
|
215
|
+
// ── Connection ────────────────────────────────────────────
|
|
216
|
+
async function connect() {
|
|
217
|
+
let url = document.getElementById('endpoint').value.trim();
|
|
218
|
+
if (!url) return;
|
|
219
|
+
if (!url.startsWith('http')) url = 'http://' + url;
|
|
220
|
+
if (url.match(/^https?:\/\/:\d+/)) url = url.replace('://', '://localhost');
|
|
221
|
+
baseUrl = url.replace(/\/$/, '');
|
|
222
|
+
localStorage.setItem('hsa-log-endpoint', document.getElementById('endpoint').value.trim());
|
|
223
|
+
|
|
224
|
+
setStatus('pending');
|
|
225
|
+
try {
|
|
226
|
+
const res = await fetch(baseUrl + '/api/logs?limit=200');
|
|
227
|
+
if (!res.ok) throw new Error(res.statusText);
|
|
228
|
+
const data = await res.json();
|
|
229
|
+
entries = data.entries || [];
|
|
230
|
+
updateToolFilter(data.tools || []);
|
|
231
|
+
updateContextSelectors(data.ides || [], data.projects || []);
|
|
232
|
+
setStatus('ok');
|
|
233
|
+
applyFilters();
|
|
234
|
+
fetchStats();
|
|
235
|
+
fetchSessions();
|
|
236
|
+
if (!isStreaming) toggleStream();
|
|
237
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
238
|
+
refreshTimer = setInterval(() => { fetchStats(); fetchSessions(); }, 5000);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
setStatus('err');
|
|
241
|
+
console.error('Connection failed:', e);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function setStatus(s) {
|
|
246
|
+
document.getElementById('statusDot').className = 'status-dot ' + s;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Sessions for Context ──────────────────────────────────
|
|
250
|
+
async function fetchSessions() {
|
|
251
|
+
if (!baseUrl) return;
|
|
252
|
+
try {
|
|
253
|
+
const res = await fetch(baseUrl + '/api/sessions', { signal: AbortSignal.timeout(5000) });
|
|
254
|
+
if (!res.ok) return;
|
|
255
|
+
const data = await res.json();
|
|
256
|
+
allSessions = data.sessions || [];
|
|
257
|
+
populateSessionDropdown(allSessions);
|
|
258
|
+
} catch {}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function populateSessionDropdown(sessions) {
|
|
262
|
+
const sel = document.getElementById('ctxSession');
|
|
263
|
+
const cur = sel.value;
|
|
264
|
+
sel.innerHTML = '<option value="">All Sessions</option>';
|
|
265
|
+
sessions.forEach(s => {
|
|
266
|
+
const opt = document.createElement('option');
|
|
267
|
+
opt.value = s.sessionId;
|
|
268
|
+
opt.textContent = `${s.projectName} (${s.ideName})`;
|
|
269
|
+
sel.appendChild(opt);
|
|
270
|
+
});
|
|
271
|
+
sel.value = cur;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function updateContextSelectors(ides, projects) {
|
|
275
|
+
const ideSelect = document.getElementById('ctxIde');
|
|
276
|
+
const projSelect = document.getElementById('ctxProject');
|
|
277
|
+
const curIde = ideSelect.value;
|
|
278
|
+
const curProj = projSelect.value;
|
|
279
|
+
|
|
280
|
+
ideSelect.innerHTML = '<option value="">All IDEs</option>';
|
|
281
|
+
ides.forEach(i => {
|
|
282
|
+
const opt = document.createElement('option');
|
|
283
|
+
opt.value = i; opt.textContent = i;
|
|
284
|
+
ideSelect.appendChild(opt);
|
|
285
|
+
});
|
|
286
|
+
ideSelect.value = curIde;
|
|
287
|
+
|
|
288
|
+
projSelect.innerHTML = '<option value="">All Projects</option>';
|
|
289
|
+
projects.forEach(p => {
|
|
290
|
+
const opt = document.createElement('option');
|
|
291
|
+
opt.value = p; opt.textContent = p;
|
|
292
|
+
projSelect.appendChild(opt);
|
|
293
|
+
});
|
|
294
|
+
projSelect.value = curProj;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── SSE Streaming ─────────────────────────────────────────
|
|
298
|
+
function toggleStream() {
|
|
299
|
+
if (isStreaming) stopStream();
|
|
300
|
+
else startStream();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function startStream() {
|
|
304
|
+
if (!baseUrl) return;
|
|
305
|
+
if (eventSource) eventSource.close();
|
|
306
|
+
try {
|
|
307
|
+
eventSource = new EventSource(baseUrl + '/api/logs/stream');
|
|
308
|
+
eventSource.onmessage = (event) => {
|
|
309
|
+
try {
|
|
310
|
+
const data = JSON.parse(event.data);
|
|
311
|
+
if (data.type === 'entry') {
|
|
312
|
+
entries.unshift(data.entry);
|
|
313
|
+
if (entries.length > 500) entries.pop();
|
|
314
|
+
applyFilters();
|
|
315
|
+
fetchStats();
|
|
316
|
+
}
|
|
317
|
+
} catch {}
|
|
318
|
+
};
|
|
319
|
+
eventSource.onerror = () => setStatus('err');
|
|
320
|
+
eventSource.onopen = () => setStatus('ok');
|
|
321
|
+
isStreaming = true;
|
|
322
|
+
document.getElementById('streamBtn').textContent = '⏸ Pause';
|
|
323
|
+
document.getElementById('streamBtn').classList.add('active');
|
|
324
|
+
} catch {}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function stopStream() {
|
|
328
|
+
if (eventSource) { eventSource.close(); eventSource = null; }
|
|
329
|
+
isStreaming = false;
|
|
330
|
+
document.getElementById('streamBtn').textContent = '▶ Stream';
|
|
331
|
+
document.getElementById('streamBtn').classList.remove('active');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Stats ─────────────────────────────────────────────────
|
|
335
|
+
async function fetchStats() {
|
|
336
|
+
if (!baseUrl) return;
|
|
337
|
+
try {
|
|
338
|
+
const res = await fetch(baseUrl + '/api/logs/stats');
|
|
339
|
+
if (!res.ok) return;
|
|
340
|
+
const s = await res.json();
|
|
341
|
+
document.getElementById('statTotal').textContent = s.totalCalls;
|
|
342
|
+
document.getElementById('statErrors').textContent = s.errorCount;
|
|
343
|
+
document.getElementById('statAvg').textContent = s.avgDurationMs;
|
|
344
|
+
document.getElementById('statRate').textContent = s.callsPerMinute;
|
|
345
|
+
} catch {}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── Filters ───────────────────────────────────────────────
|
|
349
|
+
function updateToolFilter(tools) {
|
|
350
|
+
const sel = document.getElementById('filterTool');
|
|
351
|
+
const current = sel.value;
|
|
352
|
+
sel.innerHTML = '<option value="">All Tools</option>';
|
|
353
|
+
tools.forEach(t => {
|
|
354
|
+
const opt = document.createElement('option');
|
|
355
|
+
opt.value = t; opt.textContent = t.replace('hsa_', '');
|
|
356
|
+
sel.appendChild(opt);
|
|
357
|
+
});
|
|
358
|
+
sel.value = current;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function debounceFilter() {
|
|
362
|
+
clearTimeout(filterTimeout);
|
|
363
|
+
filterTimeout = setTimeout(applyFilters, 200);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function applyFilters() {
|
|
367
|
+
const tool = document.getElementById('filterTool').value;
|
|
368
|
+
const status = document.getElementById('filterStatus').value;
|
|
369
|
+
const transport = document.getElementById('filterTransport').value;
|
|
370
|
+
const search = document.getElementById('filterSearch').value.toLowerCase();
|
|
371
|
+
const ctxIde = document.getElementById('ctxIde').value;
|
|
372
|
+
const ctxProject = document.getElementById('ctxProject').value;
|
|
373
|
+
const ctxSession = document.getElementById('ctxSession').value;
|
|
374
|
+
|
|
375
|
+
filteredEntries = entries.filter(e => {
|
|
376
|
+
if (tool && e.tool !== tool) return false;
|
|
377
|
+
if (status && e.status !== status) return false;
|
|
378
|
+
if (transport && e.transport !== transport) return false;
|
|
379
|
+
if (ctxIde && e.ideName !== ctxIde) return false;
|
|
380
|
+
if (ctxProject && e.projectName !== ctxProject) return false;
|
|
381
|
+
if (ctxSession && e.sessionId !== ctxSession) return false;
|
|
382
|
+
if (search) {
|
|
383
|
+
const haystack = `${e.tool} ${e.response?.preview || ''} ${e.error || ''} ${e.ideName || ''} ${e.projectName || ''}`.toLowerCase();
|
|
384
|
+
if (!haystack.includes(search)) return false;
|
|
385
|
+
}
|
|
386
|
+
return true;
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
document.getElementById('filterCount').textContent =
|
|
390
|
+
filteredEntries.length === entries.length
|
|
391
|
+
? `${entries.length} entries`
|
|
392
|
+
: `${filteredEntries.length} / ${entries.length}`;
|
|
393
|
+
|
|
394
|
+
renderEntries();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── Rendering ─────────────────────────────────────────────
|
|
398
|
+
function renderEntries() {
|
|
399
|
+
const table = document.getElementById('logTable');
|
|
400
|
+
const empty = document.getElementById('emptyState');
|
|
401
|
+
|
|
402
|
+
if (filteredEntries.length === 0) {
|
|
403
|
+
empty.style.display = 'flex';
|
|
404
|
+
table.innerHTML = '';
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
empty.style.display = 'none';
|
|
408
|
+
|
|
409
|
+
let html = '';
|
|
410
|
+
for (let i = 0; i < filteredEntries.length; i++) {
|
|
411
|
+
html += renderRow(filteredEntries[i], i);
|
|
412
|
+
}
|
|
413
|
+
table.innerHTML = html;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function renderRow(e, idx) {
|
|
417
|
+
const time = formatTime(e.timestamp);
|
|
418
|
+
const isErr = e.status === 'error';
|
|
419
|
+
const isSlow = e.durationMs > 500;
|
|
420
|
+
const rowClass = isErr ? 'error' : (isSlow ? 'slow' : '');
|
|
421
|
+
const statusClass = isErr ? 'error' : 'ok';
|
|
422
|
+
const statusText = isErr ? '✗ ERR' : '✓ OK';
|
|
423
|
+
const durClass = isSlow ? 'slow' : '';
|
|
424
|
+
const durText = e.durationMs < 1 ? '<1ms' : (e.durationMs > 1000 ? (e.durationMs/1000).toFixed(1)+'s' : Math.round(e.durationMs)+'ms');
|
|
425
|
+
const toolDisplay = e.tool.replace('hsa_', '');
|
|
426
|
+
const ide = e.ideName || 'unknown';
|
|
427
|
+
|
|
428
|
+
let detail = `<div class="log-detail" id="detail-${idx}">`;
|
|
429
|
+
|
|
430
|
+
// Request args
|
|
431
|
+
detail += `<div class="detail-section"><div class="detail-label">Request</div>`;
|
|
432
|
+
detail += `<div class="detail-content">${escHtml(JSON.stringify(e.request?.args || {}, null, 2))}</div></div>`;
|
|
433
|
+
|
|
434
|
+
// Response preview
|
|
435
|
+
if (e.response?.preview) {
|
|
436
|
+
detail += `<div class="detail-section"><div class="detail-label">Response (${fmtSize(e.response.size)})</div>`;
|
|
437
|
+
detail += `<div class="detail-content">${escHtml(e.response.preview)}</div></div>`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Error
|
|
441
|
+
if (e.error) {
|
|
442
|
+
detail += `<div class="detail-section"><div class="detail-label">Error</div>`;
|
|
443
|
+
detail += `<div class="detail-content err">${escHtml(e.error)}</div></div>`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Meta — now includes Session & Project
|
|
447
|
+
detail += `<div class="detail-meta">`;
|
|
448
|
+
detail += `<span>ID: <strong>${e.id}</strong></span>`;
|
|
449
|
+
detail += `<span>Transport: <strong>${e.transport}</strong></span>`;
|
|
450
|
+
if (e.sessionId) detail += `<span>Session: <strong>${e.sessionId}</strong></span>`;
|
|
451
|
+
if (e.projectName) detail += `<span>Project: <strong>${e.projectName}</strong></span>`;
|
|
452
|
+
if (e.request?.requestId) detail += `<span>Request: <strong>${e.request.requestId}</strong></span>`;
|
|
453
|
+
if (e.meta?.ip) detail += `<span>IP: <strong>${e.meta.ip}</strong></span>`;
|
|
454
|
+
detail += `</div></div>`;
|
|
455
|
+
|
|
456
|
+
return `<div class="log-row ${rowClass}" onclick="toggleDetail(${idx})">
|
|
457
|
+
<span class="log-time">${time}</span>
|
|
458
|
+
<span class="log-ide"><span class="ide-badge" data-ide="${escHtml(ide)}">${escHtml(ide)}</span></span>
|
|
459
|
+
<span class="log-tool">${toolDisplay}</span>
|
|
460
|
+
<span class="log-status ${statusClass}">${statusText}</span>
|
|
461
|
+
<span class="log-duration ${durClass}">${durText}</span>
|
|
462
|
+
</div>${detail}`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function toggleDetail(idx) {
|
|
466
|
+
const el = document.getElementById('detail-' + idx);
|
|
467
|
+
if (!el) return;
|
|
468
|
+
const row = el.previousElementSibling;
|
|
469
|
+
const isOpen = el.classList.contains('show');
|
|
470
|
+
|
|
471
|
+
document.querySelectorAll('.log-detail.show').forEach(d => {
|
|
472
|
+
d.classList.remove('show');
|
|
473
|
+
d.previousElementSibling?.classList.remove('expanded');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
if (!isOpen) {
|
|
477
|
+
el.classList.add('show');
|
|
478
|
+
row?.classList.add('expanded');
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ── Helpers ───────────────────────────────────────────────
|
|
483
|
+
function formatTime(iso) {
|
|
484
|
+
const d = new Date(iso);
|
|
485
|
+
return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}.${String(d.getMilliseconds()).padStart(3,'0')}`;
|
|
486
|
+
}
|
|
487
|
+
function fmtSize(n) { return n < 1024 ? n + ' chars' : (n/1024).toFixed(1) + 'K'; }
|
|
488
|
+
function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
489
|
+
function clearLogs() { entries = []; filteredEntries = []; applyFilters(); }
|
|
490
|
+
</script>
|
|
491
|
+
</body>
|
|
492
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nockdev/hsa",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"dist/hsa-lib.bundle.js",
|
|
16
16
|
"dist/hsa-engine.node",
|
|
17
17
|
"dist/integrity.json",
|
|
18
|
+
"dashboard.html",
|
|
19
|
+
"logs.html",
|
|
18
20
|
"README.md"
|
|
19
21
|
],
|
|
20
22
|
"publishConfig": {
|