uv-suite 0.22.0 → 0.24.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 +15 -0
- package/personas/professional.json +24 -0
- package/personas/spike.json +15 -0
- package/personas/sport.json +15 -0
- package/watchtower/dashboard.html +113 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uv-suite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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
|
@@ -67,6 +67,21 @@
|
|
|
67
67
|
]
|
|
68
68
|
}
|
|
69
69
|
],
|
|
70
|
+
"PermissionRequest": [
|
|
71
|
+
{ "matcher": "*", "hooks": [
|
|
72
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PermissionRequest", "timeout": 2, "async": true }
|
|
73
|
+
]}
|
|
74
|
+
],
|
|
75
|
+
"Notification": [
|
|
76
|
+
{ "matcher": "*", "hooks": [
|
|
77
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh Notification", "timeout": 2, "async": true }
|
|
78
|
+
]}
|
|
79
|
+
],
|
|
80
|
+
"PostToolUseFailure": [
|
|
81
|
+
{ "matcher": "*", "hooks": [
|
|
82
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PostToolUseFailure", "timeout": 2, "async": true }
|
|
83
|
+
]}
|
|
84
|
+
],
|
|
70
85
|
"PreToolUse": [
|
|
71
86
|
{
|
|
72
87
|
"matcher": "Bash",
|
|
@@ -79,6 +79,30 @@
|
|
|
79
79
|
]
|
|
80
80
|
}
|
|
81
81
|
],
|
|
82
|
+
"PermissionRequest": [
|
|
83
|
+
{
|
|
84
|
+
"matcher": "*",
|
|
85
|
+
"hooks": [
|
|
86
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PermissionRequest", "timeout": 2, "async": true }
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"Notification": [
|
|
91
|
+
{
|
|
92
|
+
"matcher": "*",
|
|
93
|
+
"hooks": [
|
|
94
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh Notification", "timeout": 2, "async": true }
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
"PostToolUseFailure": [
|
|
99
|
+
{
|
|
100
|
+
"matcher": "*",
|
|
101
|
+
"hooks": [
|
|
102
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PostToolUseFailure", "timeout": 2, "async": true }
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
],
|
|
82
106
|
"PreToolUse": [
|
|
83
107
|
{
|
|
84
108
|
"matcher": "Bash",
|
package/personas/spike.json
CHANGED
|
@@ -53,6 +53,21 @@
|
|
|
53
53
|
]
|
|
54
54
|
}
|
|
55
55
|
],
|
|
56
|
+
"PermissionRequest": [
|
|
57
|
+
{ "matcher": "*", "hooks": [
|
|
58
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PermissionRequest", "timeout": 2, "async": true }
|
|
59
|
+
]}
|
|
60
|
+
],
|
|
61
|
+
"Notification": [
|
|
62
|
+
{ "matcher": "*", "hooks": [
|
|
63
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh Notification", "timeout": 2, "async": true }
|
|
64
|
+
]}
|
|
65
|
+
],
|
|
66
|
+
"PostToolUseFailure": [
|
|
67
|
+
{ "matcher": "*", "hooks": [
|
|
68
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PostToolUseFailure", "timeout": 2, "async": true }
|
|
69
|
+
]}
|
|
70
|
+
],
|
|
56
71
|
"PostToolUse": [
|
|
57
72
|
{
|
|
58
73
|
"matcher": "*",
|
package/personas/sport.json
CHANGED
|
@@ -39,6 +39,21 @@
|
|
|
39
39
|
]
|
|
40
40
|
}
|
|
41
41
|
],
|
|
42
|
+
"PermissionRequest": [
|
|
43
|
+
{ "matcher": "*", "hooks": [
|
|
44
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PermissionRequest", "timeout": 2, "async": true }
|
|
45
|
+
]}
|
|
46
|
+
],
|
|
47
|
+
"Notification": [
|
|
48
|
+
{ "matcher": "*", "hooks": [
|
|
49
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh Notification", "timeout": 2, "async": true }
|
|
50
|
+
]}
|
|
51
|
+
],
|
|
52
|
+
"PostToolUseFailure": [
|
|
53
|
+
{ "matcher": "*", "hooks": [
|
|
54
|
+
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PostToolUseFailure", "timeout": 2, "async": true }
|
|
55
|
+
]}
|
|
56
|
+
],
|
|
42
57
|
"PostToolUse": [
|
|
43
58
|
{
|
|
44
59
|
"matcher": "*",
|
|
@@ -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,6 +143,7 @@ let autoScroll = true;
|
|
|
117
143
|
let humanOnly = false;
|
|
118
144
|
let selectedSession = '';
|
|
119
145
|
let selectedType = '';
|
|
146
|
+
let lastEventDiv = null;
|
|
120
147
|
|
|
121
148
|
// Session colors and naming
|
|
122
149
|
const palette = ['#0a84ff','#30d158','#ff9f0a','#bf5af2','#ff375f','#64d2ff','#ffd60a','#ac8ee0','#ff6961','#5e5ce6'];
|
|
@@ -132,19 +159,17 @@ function sessionColor(id) {
|
|
|
132
159
|
return sessions[id].color;
|
|
133
160
|
}
|
|
134
161
|
|
|
135
|
-
// Build a meaningful session label from the first user prompt or app name
|
|
136
162
|
function updateSessionLabel(sid, ev) {
|
|
137
163
|
if (!sessions[sid]) return;
|
|
138
|
-
// Capture source_app as fallback name
|
|
139
164
|
if (!sessions[sid].app && ev.source_app) {
|
|
140
165
|
sessions[sid].app = ev.source_app;
|
|
141
166
|
}
|
|
142
|
-
// Use first UserPromptSubmit
|
|
143
|
-
|
|
167
|
+
// Use first UserPromptSubmit as session label
|
|
168
|
+
const type = ev.event_type || ev.hook_event_name || '';
|
|
169
|
+
if (!sessions[sid].label && type === 'UserPromptSubmit') {
|
|
144
170
|
const prompt = ev.tool_input?.prompt || ev.tool_input?.content || ev.message || '';
|
|
145
171
|
if (prompt.length > 0) {
|
|
146
|
-
|
|
147
|
-
let label = prompt.slice(0, 40).replace(/\s+\S*$/, '');
|
|
172
|
+
let label = prompt.slice(0, 45).replace(/\s+\S*$/, '');
|
|
148
173
|
if (prompt.length > label.length) label += '...';
|
|
149
174
|
sessions[sid].label = label;
|
|
150
175
|
updateSessionBar();
|
|
@@ -156,7 +181,9 @@ function updateSessionLabel(sid, ev) {
|
|
|
156
181
|
function sessionDisplayName(id) {
|
|
157
182
|
const s = sessions[id];
|
|
158
183
|
if (!s) return shortId(id);
|
|
159
|
-
if (s.label)
|
|
184
|
+
if (s.label) {
|
|
185
|
+
return s.app ? s.app + ': ' + s.label : s.label;
|
|
186
|
+
}
|
|
160
187
|
if (s.app) return s.app;
|
|
161
188
|
return shortId(id);
|
|
162
189
|
}
|
|
@@ -166,32 +193,36 @@ function shortId(id) {
|
|
|
166
193
|
return id.length > 10 ? id.slice(0, 8) + '..' : id;
|
|
167
194
|
}
|
|
168
195
|
|
|
196
|
+
function shortSession(id) {
|
|
197
|
+
return sessionDisplayName(id);
|
|
198
|
+
}
|
|
199
|
+
|
|
169
200
|
function formatTime(ts) {
|
|
170
201
|
const d = new Date(ts);
|
|
171
202
|
return d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
172
203
|
}
|
|
173
204
|
|
|
174
|
-
function
|
|
175
|
-
|
|
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';
|
|
176
211
|
}
|
|
177
212
|
|
|
178
|
-
// Detect
|
|
213
|
+
// Detect human intervention needed
|
|
179
214
|
function needsHuman(ev) {
|
|
180
215
|
const type = ev.event_type || ev.hook_event_name || '';
|
|
181
|
-
// Permission requests always need human
|
|
182
216
|
if (type === 'PermissionRequest') return true;
|
|
183
|
-
// Notifications that are permission prompts
|
|
184
217
|
if (type === 'Notification' && ev.notification_type === 'permission_prompt') return true;
|
|
185
|
-
// Tool failures after multiple attempts
|
|
186
218
|
if (type === 'PostToolUseFailure') return true;
|
|
187
|
-
// Stuck indicators in messages
|
|
188
219
|
if (ev.message && /stuck|blocked|escalat|need.*help|can.?t.*find/i.test(ev.message)) return true;
|
|
189
220
|
return false;
|
|
190
221
|
}
|
|
191
222
|
|
|
192
223
|
function isFailure(ev) {
|
|
193
224
|
const type = ev.event_type || ev.hook_event_name || '';
|
|
194
|
-
return type.includes('Failure')
|
|
225
|
+
return type.includes('Failure');
|
|
195
226
|
}
|
|
196
227
|
|
|
197
228
|
function isSessionBoundary(ev) {
|
|
@@ -199,7 +230,12 @@ function isSessionBoundary(ev) {
|
|
|
199
230
|
return type === 'SessionStart' || type === 'SessionEnd' || type === 'Stop';
|
|
200
231
|
}
|
|
201
232
|
|
|
202
|
-
|
|
233
|
+
function isUserPrompt(ev) {
|
|
234
|
+
const type = ev.event_type || ev.hook_event_name || '';
|
|
235
|
+
return type === 'UserPromptSubmit';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Rich detail
|
|
203
239
|
function eventDetail(ev) {
|
|
204
240
|
const parts = [];
|
|
205
241
|
const tool = ev.tool_name || '';
|
|
@@ -214,27 +250,29 @@ function eventDetail(ev) {
|
|
|
214
250
|
if (input.content) parts.push(`(${input.content.length} chars)`);
|
|
215
251
|
} else if (tool === 'Edit' && input.file_path) {
|
|
216
252
|
parts.push(`<span class="filepath">${input.file_path}</span>`);
|
|
217
|
-
if (input.old_string) parts.push(`
|
|
253
|
+
if (input.old_string) parts.push(`"${input.old_string.slice(0, 40)}..."`);
|
|
218
254
|
} else if (tool === 'Bash' && input.command) {
|
|
219
|
-
parts.push(`<span class="cmd">$ ${input.command.slice(0,
|
|
220
|
-
if (input.timeout) parts.push(`timeout:${input.timeout}ms`);
|
|
255
|
+
parts.push(`<span class="cmd">$ ${input.command.slice(0, 100)}</span>`);
|
|
221
256
|
} else if (tool === 'Grep' && input.pattern) {
|
|
222
257
|
parts.push(`pattern: "${input.pattern}"`);
|
|
223
258
|
if (input.path) parts.push(`in <span class="filepath">${input.path}</span>`);
|
|
224
259
|
} else if (tool === 'Glob' && input.pattern) {
|
|
225
260
|
parts.push(`<span class="filepath">${input.pattern}</span>`);
|
|
226
261
|
} else if (tool === 'Agent') {
|
|
227
|
-
parts.push(input.description || input.prompt?.slice(0,
|
|
262
|
+
parts.push(input.description || input.prompt?.slice(0, 80) || 'subagent');
|
|
228
263
|
} else if (tool === 'WebSearch' && input.query) {
|
|
229
|
-
parts.push(`"${input.query.slice(0,
|
|
264
|
+
parts.push(`"${input.query.slice(0, 80)}"`);
|
|
230
265
|
} else if (tool === 'WebFetch' && input.url) {
|
|
231
|
-
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));
|
|
232
270
|
} else if (ev.message) {
|
|
233
|
-
parts.push(ev.message.slice(0,
|
|
271
|
+
parts.push(ev.message.slice(0, 100));
|
|
234
272
|
} else if (input.file_path) {
|
|
235
273
|
parts.push(`<span class="filepath">${input.file_path}</span>`);
|
|
236
274
|
} else if (input.command) {
|
|
237
|
-
parts.push(`<span class="cmd">$ ${input.command.slice(0,
|
|
275
|
+
parts.push(`<span class="cmd">$ ${input.command.slice(0, 80)}</span>`);
|
|
238
276
|
} else if (ev.source_app) {
|
|
239
277
|
parts.push(ev.source_app);
|
|
240
278
|
}
|
|
@@ -250,18 +288,22 @@ function renderEvent(ev) {
|
|
|
250
288
|
const human = needsHuman(ev);
|
|
251
289
|
const fail = isFailure(ev);
|
|
252
290
|
const boundary = isSessionBoundary(ev);
|
|
291
|
+
const prompt = isUserPrompt(ev);
|
|
253
292
|
|
|
254
293
|
const div = document.createElement('div');
|
|
255
294
|
let cls = 'event';
|
|
256
295
|
if (human) cls += ' needs-human';
|
|
257
296
|
else if (fail) cls += ' failure';
|
|
297
|
+
else if (prompt) cls += ' user-prompt';
|
|
258
298
|
else if (boundary) cls += ' session-boundary';
|
|
259
299
|
div.className = cls;
|
|
260
300
|
|
|
301
|
+
const humanBadge = human ? '<span class="human-badge">NEEDS HUMAN</span>' : '';
|
|
302
|
+
|
|
261
303
|
div.innerHTML = `
|
|
262
304
|
<span class="time">${formatTime(ev._ts)}</span>
|
|
263
|
-
<span class="type type-${type}">${type}</span>
|
|
264
|
-
<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>
|
|
265
307
|
<span class="tool">${tool}</span>
|
|
266
308
|
<span class="detail">${eventDetail(ev)}</span>
|
|
267
309
|
`;
|
|
@@ -280,12 +322,36 @@ function addEvent(ev) {
|
|
|
280
322
|
if (emptyState.parentNode) emptyState.remove();
|
|
281
323
|
const sid = ev.session_id || ev.source_app || 'unknown';
|
|
282
324
|
updateSessionLabel(sid, ev);
|
|
283
|
-
|
|
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
|
+
|
|
284
334
|
updateStats();
|
|
285
335
|
updateFilterType(ev);
|
|
286
|
-
|
|
336
|
+
updateWaitingText(ev);
|
|
337
|
+
|
|
338
|
+
if (autoScroll) {
|
|
339
|
+
document.getElementById('timelineEnd').scrollIntoView({ behavior: 'smooth' });
|
|
340
|
+
}
|
|
287
341
|
}
|
|
288
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
|
+
|
|
289
355
|
function updateStats() {
|
|
290
356
|
document.getElementById('sessionCount').textContent = Object.keys(sessions).length;
|
|
291
357
|
document.getElementById('eventCount').textContent = events.length;
|
|
@@ -305,7 +371,8 @@ function updateSessionBar() {
|
|
|
305
371
|
tag.className = 'session-tag' + (selectedSession === id ? ' active' : '');
|
|
306
372
|
tag.style.background = s.color + '22';
|
|
307
373
|
tag.style.color = s.color;
|
|
308
|
-
tag.textContent =
|
|
374
|
+
tag.textContent = sessionDisplayName(id) + ' (' + s.count + ')';
|
|
375
|
+
tag.title = id;
|
|
309
376
|
tag.onclick = () => { selectedSession = selectedSession === id ? '' : id; refilter(); updateSessionBar(); };
|
|
310
377
|
sessionBar.appendChild(tag);
|
|
311
378
|
}
|
|
@@ -326,7 +393,7 @@ function updateFilterSession() {
|
|
|
326
393
|
filterSession.innerHTML = '<option value="">All sessions</option>';
|
|
327
394
|
for (const id of Object.keys(sessions)) {
|
|
328
395
|
const opt = document.createElement('option');
|
|
329
|
-
opt.value = id; opt.textContent =
|
|
396
|
+
opt.value = id; opt.textContent = sessionDisplayName(id);
|
|
330
397
|
filterSession.appendChild(opt);
|
|
331
398
|
}
|
|
332
399
|
}
|
|
@@ -349,7 +416,7 @@ function refilter() {
|
|
|
349
416
|
filterType.onchange = refilter;
|
|
350
417
|
filterSession.onchange = refilter;
|
|
351
418
|
|
|
352
|
-
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 = ''; }
|
|
353
420
|
function toggleAutoScroll() {
|
|
354
421
|
autoScroll = !autoScroll;
|
|
355
422
|
document.getElementById('btnAutoScroll').classList.toggle('active', autoScroll);
|
|
@@ -360,7 +427,7 @@ function toggleHumanOnly() {
|
|
|
360
427
|
refilter();
|
|
361
428
|
}
|
|
362
429
|
|
|
363
|
-
// Server-Sent Events
|
|
430
|
+
// Server-Sent Events
|
|
364
431
|
function connect() {
|
|
365
432
|
const source = new EventSource('/stream');
|
|
366
433
|
|