uv-suite 0.21.0 → 0.23.0
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/package.json +1 -1
- package/personas/auto.json +10 -1
- package/personas/professional.json +19 -0
- package/personas/spike.json +10 -1
- package/personas/sport.json +10 -1
- package/watchtower/dashboard.html +144 -42
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uv-suite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Portable framework for AI-assisted software development. 10 agents, 9 skills, 5 hooks, 4 personas. Works with Claude Code, Cursor, and Codex.",
|
|
5
5
|
"author": "Utsav Anand",
|
|
6
6
|
"license": "MIT",
|
package/personas/auto.json
CHANGED
|
@@ -54,7 +54,16 @@
|
|
|
54
54
|
{
|
|
55
55
|
"matcher": "*",
|
|
56
56
|
"hooks": [
|
|
57
|
-
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh", "timeout": 5 }
|
|
57
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh", "timeout": 5 },
|
|
58
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh SessionStart", "timeout": 2, "async": true }
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"UserPromptSubmit": [
|
|
63
|
+
{
|
|
64
|
+
"matcher": "*",
|
|
65
|
+
"hooks": [
|
|
66
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh UserPromptSubmit", "timeout": 2, "async": true }
|
|
58
67
|
]
|
|
59
68
|
}
|
|
60
69
|
],
|
|
@@ -56,6 +56,25 @@
|
|
|
56
56
|
"type": "command",
|
|
57
57
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh",
|
|
58
58
|
"timeout": 5
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "command",
|
|
62
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh SessionStart",
|
|
63
|
+
"timeout": 2,
|
|
64
|
+
"async": true
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"UserPromptSubmit": [
|
|
70
|
+
{
|
|
71
|
+
"matcher": "*",
|
|
72
|
+
"hooks": [
|
|
73
|
+
{
|
|
74
|
+
"type": "command",
|
|
75
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh UserPromptSubmit",
|
|
76
|
+
"timeout": 2,
|
|
77
|
+
"async": true
|
|
59
78
|
}
|
|
60
79
|
]
|
|
61
80
|
}
|
package/personas/spike.json
CHANGED
|
@@ -40,7 +40,16 @@
|
|
|
40
40
|
{
|
|
41
41
|
"matcher": "*",
|
|
42
42
|
"hooks": [
|
|
43
|
-
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh", "timeout": 5 }
|
|
43
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh", "timeout": 5 },
|
|
44
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh SessionStart", "timeout": 2, "async": true }
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"UserPromptSubmit": [
|
|
49
|
+
{
|
|
50
|
+
"matcher": "*",
|
|
51
|
+
"hooks": [
|
|
52
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh UserPromptSubmit", "timeout": 2, "async": true }
|
|
44
53
|
]
|
|
45
54
|
}
|
|
46
55
|
],
|
package/personas/sport.json
CHANGED
|
@@ -26,7 +26,16 @@
|
|
|
26
26
|
{
|
|
27
27
|
"matcher": "*",
|
|
28
28
|
"hooks": [
|
|
29
|
-
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh", "timeout": 5 }
|
|
29
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh", "timeout": 5 },
|
|
30
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh SessionStart", "timeout": 2, "async": true }
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"UserPromptSubmit": [
|
|
35
|
+
{
|
|
36
|
+
"matcher": "*",
|
|
37
|
+
"hooks": [
|
|
38
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh UserPromptSubmit", "timeout": 2, "async": true }
|
|
30
39
|
]
|
|
31
40
|
}
|
|
32
41
|
],
|
|
@@ -30,29 +30,48 @@
|
|
|
30
30
|
.session-tag:hover { border-color: #424245; }
|
|
31
31
|
.session-tag.active { border-color: #fff; }
|
|
32
32
|
|
|
33
|
-
.timeline { padding: 8px 0; overflow-y: auto; max-height: calc(100vh -
|
|
34
|
-
|
|
35
|
-
.event
|
|
36
|
-
.event
|
|
37
|
-
.event .
|
|
38
|
-
.event .
|
|
39
|
-
.event .
|
|
40
|
-
.event .
|
|
33
|
+
.timeline { padding: 8px 0; overflow-y: auto; max-height: calc(100vh - 280px); }
|
|
34
|
+
|
|
35
|
+
.event { padding: 10px 24px; display: grid; grid-template-columns: 70px 110px 140px 65px 1fr; gap: 10px; align-items: start; font-size: 13px; border-bottom: 1px solid #0d0d0d; transition: all 0.2s; opacity: 0.75; }
|
|
36
|
+
.event:hover { background: #0d0d0d; opacity: 1; }
|
|
37
|
+
.event .time { color: #6e6e73; font-variant-numeric: tabular-nums; font-family: 'SF Mono', monospace; font-size: 11px; padding-top: 2px; }
|
|
38
|
+
.event .type { font-weight: 500; font-size: 12px; }
|
|
39
|
+
.event .session { font-size: 11px; border-radius: 8px; padding: 2px 8px; display: inline-block; max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
40
|
+
.event .tool { color: #a1a1a6; font-family: 'SF Mono', monospace; font-size: 11px; padding-top: 2px; }
|
|
41
|
+
.event .detail { color: #a1a1a6; font-size: 12px; line-height: 1.5; word-break: break-all; }
|
|
41
42
|
.event .detail .filepath { color: #64d2ff; }
|
|
42
43
|
.event .detail .cmd { color: #ffd60a; }
|
|
43
44
|
|
|
44
|
-
/*
|
|
45
|
-
.event.
|
|
45
|
+
/* Latest event — full opacity, larger, highlighted */
|
|
46
|
+
.event.latest { opacity: 1; font-size: 14px; background: #0a0a0a; border-bottom: 2px solid #2d2d2f; }
|
|
47
|
+
.event.latest .type { font-size: 13px; }
|
|
48
|
+
.event.latest .detail { font-size: 13px; color: #d1d1d6; }
|
|
49
|
+
|
|
50
|
+
/* Human intervention — strong highlight */
|
|
51
|
+
.event.needs-human { background: #ff375f18; border-left: 4px solid #ff375f; opacity: 1; }
|
|
46
52
|
.event.needs-human .type { color: #ff375f; }
|
|
47
|
-
.event.needs-human
|
|
48
|
-
.event.needs-human { position: relative; }
|
|
53
|
+
.event.needs-human .human-badge { display: inline-block; background: #ff375f; color: #fff; font-size: 9px; font-weight: 700; padding: 1px 6px; border-radius: 3px; letter-spacing: 0.5px; margin-left: 8px; vertical-align: middle; }
|
|
49
54
|
|
|
50
55
|
/* Failure highlight */
|
|
51
|
-
.event.failure { background: #
|
|
56
|
+
.event.failure { background: #ff696115; border-left: 4px solid #ff6961; opacity: 1; }
|
|
52
57
|
|
|
53
|
-
/* Session start/end
|
|
58
|
+
/* Session start/end */
|
|
54
59
|
.event.session-boundary { background: #30d15808; }
|
|
55
60
|
|
|
61
|
+
/* User prompt — show what was asked */
|
|
62
|
+
.event.user-prompt { background: #ffd60a08; }
|
|
63
|
+
.event.user-prompt .detail { color: #ffd60a; font-style: italic; }
|
|
64
|
+
|
|
65
|
+
/* Bottom loader area */
|
|
66
|
+
.timeline-end { padding: 24px; text-align: center; }
|
|
67
|
+
.loader { display: inline-block; width: 40px; height: 4px; position: relative; }
|
|
68
|
+
.loader::before { content: ''; position: absolute; width: 8px; height: 4px; background: #424245; border-radius: 2px; animation: loader 1.2s ease-in-out infinite; }
|
|
69
|
+
@keyframes loader {
|
|
70
|
+
0%, 100% { left: 0; }
|
|
71
|
+
50% { left: 32px; }
|
|
72
|
+
}
|
|
73
|
+
.timeline-end .waiting { font-size: 11px; color: #424245; margin-top: 8px; }
|
|
74
|
+
|
|
56
75
|
.empty { padding: 60px 24px; text-align: center; color: #6e6e73; }
|
|
57
76
|
.empty p { margin-top: 8px; font-size: 13px; }
|
|
58
77
|
|
|
@@ -102,6 +121,12 @@
|
|
|
102
121
|
</div>
|
|
103
122
|
</div>
|
|
104
123
|
|
|
124
|
+
<!-- Bottom loader — always visible, shows system is listening -->
|
|
125
|
+
<div class="timeline-end" id="timelineEnd">
|
|
126
|
+
<div class="loader"></div>
|
|
127
|
+
<div class="waiting" id="waitingText">Listening for events...</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
105
130
|
<script>
|
|
106
131
|
const timeline = document.getElementById('timeline');
|
|
107
132
|
const emptyState = document.getElementById('emptyState');
|
|
@@ -110,6 +135,7 @@ const filterSession = document.getElementById('filterSession');
|
|
|
110
135
|
const statusDot = document.getElementById('statusDot');
|
|
111
136
|
const statusText = document.getElementById('statusText');
|
|
112
137
|
const sessionBar = document.getElementById('sessionBar');
|
|
138
|
+
const waitingText = document.getElementById('waitingText');
|
|
113
139
|
|
|
114
140
|
let events = [];
|
|
115
141
|
let sessions = {};
|
|
@@ -117,13 +143,14 @@ let autoScroll = true;
|
|
|
117
143
|
let humanOnly = false;
|
|
118
144
|
let selectedSession = '';
|
|
119
145
|
let selectedType = '';
|
|
146
|
+
let lastEventDiv = null;
|
|
120
147
|
|
|
121
|
-
// Session colors
|
|
148
|
+
// Session colors and naming
|
|
122
149
|
const palette = ['#0a84ff','#30d158','#ff9f0a','#bf5af2','#ff375f','#64d2ff','#ffd60a','#ac8ee0','#ff6961','#5e5ce6'];
|
|
123
150
|
let colorIdx = 0;
|
|
124
151
|
function sessionColor(id) {
|
|
125
152
|
if (!sessions[id]) {
|
|
126
|
-
sessions[id] = { color: palette[colorIdx++ % palette.length], count: 0, lastEvent: null };
|
|
153
|
+
sessions[id] = { color: palette[colorIdx++ % palette.length], count: 0, lastEvent: null, label: null, app: null };
|
|
127
154
|
updateSessionBar();
|
|
128
155
|
updateFilterSession();
|
|
129
156
|
}
|
|
@@ -132,33 +159,70 @@ function sessionColor(id) {
|
|
|
132
159
|
return sessions[id].color;
|
|
133
160
|
}
|
|
134
161
|
|
|
162
|
+
function updateSessionLabel(sid, ev) {
|
|
163
|
+
if (!sessions[sid]) return;
|
|
164
|
+
if (!sessions[sid].app && ev.source_app) {
|
|
165
|
+
sessions[sid].app = ev.source_app;
|
|
166
|
+
}
|
|
167
|
+
// Use first UserPromptSubmit as session label
|
|
168
|
+
const type = ev.event_type || ev.hook_event_name || '';
|
|
169
|
+
if (!sessions[sid].label && type === 'UserPromptSubmit') {
|
|
170
|
+
const prompt = ev.tool_input?.prompt || ev.tool_input?.content || ev.message || '';
|
|
171
|
+
if (prompt.length > 0) {
|
|
172
|
+
let label = prompt.slice(0, 45).replace(/\s+\S*$/, '');
|
|
173
|
+
if (prompt.length > label.length) label += '...';
|
|
174
|
+
sessions[sid].label = label;
|
|
175
|
+
updateSessionBar();
|
|
176
|
+
updateFilterSession();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function sessionDisplayName(id) {
|
|
182
|
+
const s = sessions[id];
|
|
183
|
+
if (!s) return shortId(id);
|
|
184
|
+
if (s.label) {
|
|
185
|
+
return s.app ? s.app + ': ' + s.label : s.label;
|
|
186
|
+
}
|
|
187
|
+
if (s.app) return s.app;
|
|
188
|
+
return shortId(id);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function shortId(id) {
|
|
192
|
+
if (!id) return '—';
|
|
193
|
+
return id.length > 10 ? id.slice(0, 8) + '..' : id;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function shortSession(id) {
|
|
197
|
+
return sessionDisplayName(id);
|
|
198
|
+
}
|
|
199
|
+
|
|
135
200
|
function formatTime(ts) {
|
|
136
201
|
const d = new Date(ts);
|
|
137
202
|
return d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
138
203
|
}
|
|
139
204
|
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
205
|
+
function timeSince(ts) {
|
|
206
|
+
const secs = Math.floor((Date.now() - ts) / 1000);
|
|
207
|
+
if (secs < 5) return 'just now';
|
|
208
|
+
if (secs < 60) return secs + 's ago';
|
|
209
|
+
if (secs < 3600) return Math.floor(secs / 60) + 'm ago';
|
|
210
|
+
return Math.floor(secs / 3600) + 'h ago';
|
|
143
211
|
}
|
|
144
212
|
|
|
145
|
-
// Detect
|
|
213
|
+
// Detect human intervention needed
|
|
146
214
|
function needsHuman(ev) {
|
|
147
215
|
const type = ev.event_type || ev.hook_event_name || '';
|
|
148
|
-
// Permission requests always need human
|
|
149
216
|
if (type === 'PermissionRequest') return true;
|
|
150
|
-
// Notifications that are permission prompts
|
|
151
217
|
if (type === 'Notification' && ev.notification_type === 'permission_prompt') return true;
|
|
152
|
-
// Tool failures after multiple attempts
|
|
153
218
|
if (type === 'PostToolUseFailure') return true;
|
|
154
|
-
// Stuck indicators in messages
|
|
155
219
|
if (ev.message && /stuck|blocked|escalat|need.*help|can.?t.*find/i.test(ev.message)) return true;
|
|
156
220
|
return false;
|
|
157
221
|
}
|
|
158
222
|
|
|
159
223
|
function isFailure(ev) {
|
|
160
224
|
const type = ev.event_type || ev.hook_event_name || '';
|
|
161
|
-
return type.includes('Failure')
|
|
225
|
+
return type.includes('Failure');
|
|
162
226
|
}
|
|
163
227
|
|
|
164
228
|
function isSessionBoundary(ev) {
|
|
@@ -166,7 +230,12 @@ function isSessionBoundary(ev) {
|
|
|
166
230
|
return type === 'SessionStart' || type === 'SessionEnd' || type === 'Stop';
|
|
167
231
|
}
|
|
168
232
|
|
|
169
|
-
|
|
233
|
+
function isUserPrompt(ev) {
|
|
234
|
+
const type = ev.event_type || ev.hook_event_name || '';
|
|
235
|
+
return type === 'UserPromptSubmit';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Rich detail
|
|
170
239
|
function eventDetail(ev) {
|
|
171
240
|
const parts = [];
|
|
172
241
|
const tool = ev.tool_name || '';
|
|
@@ -181,27 +250,29 @@ function eventDetail(ev) {
|
|
|
181
250
|
if (input.content) parts.push(`(${input.content.length} chars)`);
|
|
182
251
|
} else if (tool === 'Edit' && input.file_path) {
|
|
183
252
|
parts.push(`<span class="filepath">${input.file_path}</span>`);
|
|
184
|
-
if (input.old_string) parts.push(`
|
|
253
|
+
if (input.old_string) parts.push(`"${input.old_string.slice(0, 40)}..."`);
|
|
185
254
|
} else if (tool === 'Bash' && input.command) {
|
|
186
|
-
parts.push(`<span class="cmd">$ ${input.command.slice(0,
|
|
187
|
-
if (input.timeout) parts.push(`timeout:${input.timeout}ms`);
|
|
255
|
+
parts.push(`<span class="cmd">$ ${input.command.slice(0, 100)}</span>`);
|
|
188
256
|
} else if (tool === 'Grep' && input.pattern) {
|
|
189
257
|
parts.push(`pattern: "${input.pattern}"`);
|
|
190
258
|
if (input.path) parts.push(`in <span class="filepath">${input.path}</span>`);
|
|
191
259
|
} else if (tool === 'Glob' && input.pattern) {
|
|
192
260
|
parts.push(`<span class="filepath">${input.pattern}</span>`);
|
|
193
261
|
} else if (tool === 'Agent') {
|
|
194
|
-
parts.push(input.description || input.prompt?.slice(0,
|
|
262
|
+
parts.push(input.description || input.prompt?.slice(0, 80) || 'subagent');
|
|
195
263
|
} else if (tool === 'WebSearch' && input.query) {
|
|
196
|
-
parts.push(`"${input.query.slice(0,
|
|
264
|
+
parts.push(`"${input.query.slice(0, 80)}"`);
|
|
197
265
|
} else if (tool === 'WebFetch' && input.url) {
|
|
198
|
-
parts.push(`<span class="filepath">${input.url.slice(0,
|
|
266
|
+
parts.push(`<span class="filepath">${input.url.slice(0, 80)}</span>`);
|
|
267
|
+
} else if (isUserPrompt(ev)) {
|
|
268
|
+
const prompt = input.prompt || input.content || ev.message || '';
|
|
269
|
+
parts.push(prompt.slice(0, 120));
|
|
199
270
|
} else if (ev.message) {
|
|
200
|
-
parts.push(ev.message.slice(0,
|
|
271
|
+
parts.push(ev.message.slice(0, 100));
|
|
201
272
|
} else if (input.file_path) {
|
|
202
273
|
parts.push(`<span class="filepath">${input.file_path}</span>`);
|
|
203
274
|
} else if (input.command) {
|
|
204
|
-
parts.push(`<span class="cmd">$ ${input.command.slice(0,
|
|
275
|
+
parts.push(`<span class="cmd">$ ${input.command.slice(0, 80)}</span>`);
|
|
205
276
|
} else if (ev.source_app) {
|
|
206
277
|
parts.push(ev.source_app);
|
|
207
278
|
}
|
|
@@ -217,18 +288,22 @@ function renderEvent(ev) {
|
|
|
217
288
|
const human = needsHuman(ev);
|
|
218
289
|
const fail = isFailure(ev);
|
|
219
290
|
const boundary = isSessionBoundary(ev);
|
|
291
|
+
const prompt = isUserPrompt(ev);
|
|
220
292
|
|
|
221
293
|
const div = document.createElement('div');
|
|
222
294
|
let cls = 'event';
|
|
223
295
|
if (human) cls += ' needs-human';
|
|
224
296
|
else if (fail) cls += ' failure';
|
|
297
|
+
else if (prompt) cls += ' user-prompt';
|
|
225
298
|
else if (boundary) cls += ' session-boundary';
|
|
226
299
|
div.className = cls;
|
|
227
300
|
|
|
301
|
+
const humanBadge = human ? '<span class="human-badge">NEEDS HUMAN</span>' : '';
|
|
302
|
+
|
|
228
303
|
div.innerHTML = `
|
|
229
304
|
<span class="time">${formatTime(ev._ts)}</span>
|
|
230
|
-
<span class="type type-${type}">${type}</span>
|
|
231
|
-
<span class="session" style="background:${color}22;color:${color}">${shortSession(sid)}</span>
|
|
305
|
+
<span class="type type-${type}">${type}${humanBadge}</span>
|
|
306
|
+
<span class="session" style="background:${color}22;color:${color}" title="${sessionDisplayName(sid)}">${shortSession(sid)}</span>
|
|
232
307
|
<span class="tool">${tool}</span>
|
|
233
308
|
<span class="detail">${eventDetail(ev)}</span>
|
|
234
309
|
`;
|
|
@@ -245,12 +320,38 @@ function renderEvent(ev) {
|
|
|
245
320
|
function addEvent(ev) {
|
|
246
321
|
events.push(ev);
|
|
247
322
|
if (emptyState.parentNode) emptyState.remove();
|
|
248
|
-
|
|
323
|
+
const sid = ev.session_id || ev.source_app || 'unknown';
|
|
324
|
+
updateSessionLabel(sid, ev);
|
|
325
|
+
|
|
326
|
+
// Remove "latest" class from previous latest
|
|
327
|
+
if (lastEventDiv) lastEventDiv.classList.remove('latest');
|
|
328
|
+
|
|
329
|
+
const div = renderEvent(ev);
|
|
330
|
+
div.classList.add('latest');
|
|
331
|
+
lastEventDiv = div;
|
|
332
|
+
timeline.appendChild(div);
|
|
333
|
+
|
|
249
334
|
updateStats();
|
|
250
335
|
updateFilterType(ev);
|
|
251
|
-
|
|
336
|
+
updateWaitingText(ev);
|
|
337
|
+
|
|
338
|
+
if (autoScroll) {
|
|
339
|
+
document.getElementById('timelineEnd').scrollIntoView({ behavior: 'smooth' });
|
|
340
|
+
}
|
|
252
341
|
}
|
|
253
342
|
|
|
343
|
+
function updateWaitingText(ev) {
|
|
344
|
+
if (ev) {
|
|
345
|
+
const sid = ev.session_id || ev.source_app || 'unknown';
|
|
346
|
+
waitingText.textContent = `Last: ${sessionDisplayName(sid)} — ${timeSince(ev._ts)}`;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Keep "time since" updated
|
|
351
|
+
setInterval(() => {
|
|
352
|
+
if (events.length > 0) updateWaitingText(events[events.length - 1]);
|
|
353
|
+
}, 5000);
|
|
354
|
+
|
|
254
355
|
function updateStats() {
|
|
255
356
|
document.getElementById('sessionCount').textContent = Object.keys(sessions).length;
|
|
256
357
|
document.getElementById('eventCount').textContent = events.length;
|
|
@@ -270,7 +371,8 @@ function updateSessionBar() {
|
|
|
270
371
|
tag.className = 'session-tag' + (selectedSession === id ? ' active' : '');
|
|
271
372
|
tag.style.background = s.color + '22';
|
|
272
373
|
tag.style.color = s.color;
|
|
273
|
-
tag.textContent =
|
|
374
|
+
tag.textContent = sessionDisplayName(id) + ' (' + s.count + ')';
|
|
375
|
+
tag.title = id;
|
|
274
376
|
tag.onclick = () => { selectedSession = selectedSession === id ? '' : id; refilter(); updateSessionBar(); };
|
|
275
377
|
sessionBar.appendChild(tag);
|
|
276
378
|
}
|
|
@@ -291,7 +393,7 @@ function updateFilterSession() {
|
|
|
291
393
|
filterSession.innerHTML = '<option value="">All sessions</option>';
|
|
292
394
|
for (const id of Object.keys(sessions)) {
|
|
293
395
|
const opt = document.createElement('option');
|
|
294
|
-
opt.value = id; opt.textContent =
|
|
396
|
+
opt.value = id; opt.textContent = sessionDisplayName(id);
|
|
295
397
|
filterSession.appendChild(opt);
|
|
296
398
|
}
|
|
297
399
|
}
|
|
@@ -314,7 +416,7 @@ function refilter() {
|
|
|
314
416
|
filterType.onchange = refilter;
|
|
315
417
|
filterSession.onchange = refilter;
|
|
316
418
|
|
|
317
|
-
function clearEvents() { events = []; sessions = {}; colorIdx = 0; timeline.innerHTML = ''; updateStats(); sessionBar.innerHTML = ''; }
|
|
419
|
+
function clearEvents() { events = []; sessions = {}; colorIdx = 0; timeline.innerHTML = ''; lastEventDiv = null; updateStats(); sessionBar.innerHTML = ''; }
|
|
318
420
|
function toggleAutoScroll() {
|
|
319
421
|
autoScroll = !autoScroll;
|
|
320
422
|
document.getElementById('btnAutoScroll').classList.toggle('active', autoScroll);
|
|
@@ -325,7 +427,7 @@ function toggleHumanOnly() {
|
|
|
325
427
|
refilter();
|
|
326
428
|
}
|
|
327
429
|
|
|
328
|
-
// Server-Sent Events
|
|
430
|
+
// Server-Sent Events
|
|
329
431
|
function connect() {
|
|
330
432
|
const source = new EventSource('/stream');
|
|
331
433
|
|