@reactive-skills/runtime 0.4.1 → 0.4.3
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/CHANGELOG.md +16 -0
- package/README.md +40 -1
- package/dist/core/fsm-engine.d.ts +12 -3
- package/dist/core/fsm-engine.js +60 -6
- package/dist/core/types.d.ts +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/telemetry/dashboard.d.ts +10 -0
- package/dist/telemetry/dashboard.js +939 -0
- package/dist/telemetry/server.d.ts +1 -0
- package/dist/telemetry/server.js +69 -15
- package/package.json +1 -1
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a self-contained, zero-dependency HTML dashboard for the TelemetryServer.
|
|
3
|
+
* Provides live SSE event streaming, state & context inspection, and signal dispatching.
|
|
4
|
+
*/
|
|
5
|
+
export function renderDashboardHtml(options) {
|
|
6
|
+
const titleSkill = options.skillName || 'Reactive Skill';
|
|
7
|
+
return `<!DOCTYPE html>
|
|
8
|
+
<html lang="en" class="dark">
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="UTF-8">
|
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
12
|
+
<title>⚡ ${escapeHtml(titleSkill)} — Reactive Skills Telemetry</title>
|
|
13
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚡</text></svg>">
|
|
14
|
+
<style>
|
|
15
|
+
:root {
|
|
16
|
+
--bg: #090d16;
|
|
17
|
+
--card-bg: #111827;
|
|
18
|
+
--card-border: #1f2937;
|
|
19
|
+
--card-hover: #1e293b;
|
|
20
|
+
--text-main: #f3f4f6;
|
|
21
|
+
--text-muted: #9ca3af;
|
|
22
|
+
--text-dim: #6b7280;
|
|
23
|
+
--primary: #38bdf8;
|
|
24
|
+
--primary-hover: #0ea5e9;
|
|
25
|
+
--accent: #818cf8;
|
|
26
|
+
--success: #34d399;
|
|
27
|
+
--warning: #fbbf24;
|
|
28
|
+
--danger: #f87171;
|
|
29
|
+
--transition-bg: #312e81;
|
|
30
|
+
--transition-text: #c7d2fe;
|
|
31
|
+
--guard-bg: #064e3b;
|
|
32
|
+
--guard-text: #a7f3d0;
|
|
33
|
+
--tool-bg: #78350f;
|
|
34
|
+
--tool-text: #fde68a;
|
|
35
|
+
--mono-font: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
36
|
+
--sans-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
40
|
+
body {
|
|
41
|
+
background-color: var(--bg);
|
|
42
|
+
color: var(--text-main);
|
|
43
|
+
font-family: var(--sans-font);
|
|
44
|
+
line-height: 1.5;
|
|
45
|
+
min-height: 100vh;
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
header {
|
|
51
|
+
background-color: var(--card-bg);
|
|
52
|
+
border-bottom: 1px solid var(--card-border);
|
|
53
|
+
padding: 0.85rem 1.5rem;
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: space-between;
|
|
57
|
+
flex-wrap: wrap;
|
|
58
|
+
gap: 1rem;
|
|
59
|
+
position: sticky;
|
|
60
|
+
top: 0;
|
|
61
|
+
z-index: 50;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.brand {
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 0.75rem;
|
|
68
|
+
}
|
|
69
|
+
.brand-logo {
|
|
70
|
+
font-size: 1.5rem;
|
|
71
|
+
line-height: 1;
|
|
72
|
+
}
|
|
73
|
+
.brand-title {
|
|
74
|
+
font-weight: 700;
|
|
75
|
+
font-size: 1.15rem;
|
|
76
|
+
letter-spacing: -0.02em;
|
|
77
|
+
}
|
|
78
|
+
.brand-skill {
|
|
79
|
+
background: linear-gradient(135deg, #0284c7 0%, #6366f1 100%);
|
|
80
|
+
color: #fff;
|
|
81
|
+
font-size: 0.75rem;
|
|
82
|
+
font-weight: 600;
|
|
83
|
+
padding: 0.2rem 0.6rem;
|
|
84
|
+
border-radius: 9999px;
|
|
85
|
+
letter-spacing: 0.05em;
|
|
86
|
+
text-transform: uppercase;
|
|
87
|
+
font-family: var(--mono-font);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.header-metrics {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 1rem;
|
|
94
|
+
font-size: 0.85rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.status-badge {
|
|
98
|
+
display: inline-flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 0.4rem;
|
|
101
|
+
padding: 0.25rem 0.65rem;
|
|
102
|
+
border-radius: 9999px;
|
|
103
|
+
font-size: 0.75rem;
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
background-color: #064e3b;
|
|
106
|
+
color: #34d399;
|
|
107
|
+
border: 1px solid #059669;
|
|
108
|
+
}
|
|
109
|
+
.status-badge.connecting {
|
|
110
|
+
background-color: #78350f;
|
|
111
|
+
color: #fbbf24;
|
|
112
|
+
border-color: #d97706;
|
|
113
|
+
}
|
|
114
|
+
.status-badge.disconnected {
|
|
115
|
+
background-color: #4c0519;
|
|
116
|
+
color: #f87171;
|
|
117
|
+
border-color: #e11d48;
|
|
118
|
+
}
|
|
119
|
+
.status-dot {
|
|
120
|
+
width: 0.5rem;
|
|
121
|
+
height: 0.5rem;
|
|
122
|
+
border-radius: 50%;
|
|
123
|
+
background-color: currentColor;
|
|
124
|
+
animation: pulse 2s infinite;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@keyframes pulse {
|
|
128
|
+
0%, 100% { opacity: 1; }
|
|
129
|
+
50% { opacity: 0.4; }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.header-links a {
|
|
133
|
+
color: var(--text-muted);
|
|
134
|
+
text-decoration: none;
|
|
135
|
+
font-size: 0.8rem;
|
|
136
|
+
padding: 0.25rem 0.5rem;
|
|
137
|
+
border-radius: 0.375rem;
|
|
138
|
+
transition: all 0.15s;
|
|
139
|
+
}
|
|
140
|
+
.header-links a:hover {
|
|
141
|
+
color: var(--primary);
|
|
142
|
+
background-color: var(--card-hover);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
main {
|
|
146
|
+
flex: 1;
|
|
147
|
+
display: grid;
|
|
148
|
+
grid-template-columns: 380px 1fr;
|
|
149
|
+
gap: 1.25rem;
|
|
150
|
+
padding: 1.25rem;
|
|
151
|
+
max-width: 1700px;
|
|
152
|
+
margin: 0 auto;
|
|
153
|
+
width: 100%;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@media (max-width: 960px) {
|
|
157
|
+
main {
|
|
158
|
+
grid-template-columns: 1fr;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.card {
|
|
163
|
+
background-color: var(--card-bg);
|
|
164
|
+
border: 1px solid var(--card-border);
|
|
165
|
+
border-radius: 0.75rem;
|
|
166
|
+
overflow: hidden;
|
|
167
|
+
display: flex;
|
|
168
|
+
flex-direction: column;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.card-header {
|
|
172
|
+
padding: 0.85rem 1.15rem;
|
|
173
|
+
border-bottom: 1px solid var(--card-border);
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
justify-content: space-between;
|
|
177
|
+
gap: 0.75rem;
|
|
178
|
+
}
|
|
179
|
+
.card-title {
|
|
180
|
+
font-size: 0.9rem;
|
|
181
|
+
font-weight: 700;
|
|
182
|
+
color: var(--text-main);
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
gap: 0.5rem;
|
|
186
|
+
text-transform: uppercase;
|
|
187
|
+
letter-spacing: 0.05em;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.state-panel {
|
|
191
|
+
padding: 1.15rem;
|
|
192
|
+
display: flex;
|
|
193
|
+
flex-direction: column;
|
|
194
|
+
gap: 1rem;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.active-state-banner {
|
|
198
|
+
background: linear-gradient(135deg, rgba(56, 189, 248, 0.1) 0%, rgba(99, 102, 241, 0.1) 100%);
|
|
199
|
+
border: 1px solid rgba(56, 189, 248, 0.3);
|
|
200
|
+
padding: 1rem;
|
|
201
|
+
border-radius: 0.625rem;
|
|
202
|
+
}
|
|
203
|
+
.active-state-label {
|
|
204
|
+
font-size: 0.75rem;
|
|
205
|
+
font-weight: 600;
|
|
206
|
+
color: var(--primary);
|
|
207
|
+
text-transform: uppercase;
|
|
208
|
+
letter-spacing: 0.05em;
|
|
209
|
+
}
|
|
210
|
+
.active-state-name {
|
|
211
|
+
font-size: 1.4rem;
|
|
212
|
+
font-weight: 800;
|
|
213
|
+
font-family: var(--mono-font);
|
|
214
|
+
color: #fff;
|
|
215
|
+
margin-top: 0.25rem;
|
|
216
|
+
word-break: break-all;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.meta-grid {
|
|
220
|
+
display: grid;
|
|
221
|
+
grid-template-columns: 1fr 1fr;
|
|
222
|
+
gap: 0.75rem;
|
|
223
|
+
}
|
|
224
|
+
.meta-box {
|
|
225
|
+
background-color: #0b1120;
|
|
226
|
+
border: 1px solid var(--card-border);
|
|
227
|
+
padding: 0.6rem 0.75rem;
|
|
228
|
+
border-radius: 0.5rem;
|
|
229
|
+
}
|
|
230
|
+
.meta-box-label {
|
|
231
|
+
font-size: 0.7rem;
|
|
232
|
+
color: var(--text-dim);
|
|
233
|
+
text-transform: uppercase;
|
|
234
|
+
}
|
|
235
|
+
.meta-box-val {
|
|
236
|
+
font-size: 0.95rem;
|
|
237
|
+
font-weight: 600;
|
|
238
|
+
font-family: var(--mono-font);
|
|
239
|
+
color: var(--text-main);
|
|
240
|
+
margin-top: 0.15rem;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.dispatch-box {
|
|
244
|
+
border-top: 1px solid var(--card-border);
|
|
245
|
+
padding: 1.15rem;
|
|
246
|
+
display: flex;
|
|
247
|
+
flex-direction: column;
|
|
248
|
+
gap: 0.75rem;
|
|
249
|
+
}
|
|
250
|
+
.form-group label {
|
|
251
|
+
display: block;
|
|
252
|
+
font-size: 0.75rem;
|
|
253
|
+
font-weight: 600;
|
|
254
|
+
color: var(--text-muted);
|
|
255
|
+
margin-bottom: 0.35rem;
|
|
256
|
+
text-transform: uppercase;
|
|
257
|
+
letter-spacing: 0.04em;
|
|
258
|
+
}
|
|
259
|
+
.input-field {
|
|
260
|
+
width: 100%;
|
|
261
|
+
background-color: #0b1120;
|
|
262
|
+
border: 1px solid var(--card-border);
|
|
263
|
+
color: var(--text-main);
|
|
264
|
+
padding: 0.55rem 0.75rem;
|
|
265
|
+
border-radius: 0.5rem;
|
|
266
|
+
font-family: var(--mono-font);
|
|
267
|
+
font-size: 0.85rem;
|
|
268
|
+
outline: none;
|
|
269
|
+
transition: border-color 0.15s;
|
|
270
|
+
}
|
|
271
|
+
.input-field:focus {
|
|
272
|
+
border-color: var(--primary);
|
|
273
|
+
}
|
|
274
|
+
textarea.input-field {
|
|
275
|
+
min-height: 70px;
|
|
276
|
+
resize: vertical;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.btn {
|
|
280
|
+
display: inline-flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
justify-content: center;
|
|
283
|
+
gap: 0.5rem;
|
|
284
|
+
padding: 0.55rem 1rem;
|
|
285
|
+
font-size: 0.85rem;
|
|
286
|
+
font-weight: 600;
|
|
287
|
+
border-radius: 0.5rem;
|
|
288
|
+
cursor: pointer;
|
|
289
|
+
border: none;
|
|
290
|
+
transition: all 0.15s;
|
|
291
|
+
}
|
|
292
|
+
.btn-primary {
|
|
293
|
+
background-color: #0284c7;
|
|
294
|
+
color: #fff;
|
|
295
|
+
}
|
|
296
|
+
.btn-primary:hover {
|
|
297
|
+
background-color: #0ea5e9;
|
|
298
|
+
}
|
|
299
|
+
.btn-secondary {
|
|
300
|
+
background-color: var(--card-hover);
|
|
301
|
+
color: var(--text-muted);
|
|
302
|
+
border: 1px solid var(--card-border);
|
|
303
|
+
}
|
|
304
|
+
.btn-secondary:hover {
|
|
305
|
+
background-color: #334155;
|
|
306
|
+
color: var(--text-main);
|
|
307
|
+
}
|
|
308
|
+
.btn:disabled {
|
|
309
|
+
opacity: 0.5;
|
|
310
|
+
cursor: not-allowed;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.btn-sm {
|
|
314
|
+
padding: 0.25rem 0.5rem;
|
|
315
|
+
font-size: 0.75rem;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.dispatch-feedback {
|
|
319
|
+
font-size: 0.8rem;
|
|
320
|
+
padding: 0.5rem 0.75rem;
|
|
321
|
+
border-radius: 0.375rem;
|
|
322
|
+
display: none;
|
|
323
|
+
font-family: var(--mono-font);
|
|
324
|
+
}
|
|
325
|
+
.dispatch-feedback.success {
|
|
326
|
+
display: block;
|
|
327
|
+
background-color: rgba(16, 185, 129, 0.15);
|
|
328
|
+
border: 1px solid rgba(16, 185, 129, 0.3);
|
|
329
|
+
color: #6ee7b7;
|
|
330
|
+
}
|
|
331
|
+
.dispatch-feedback.error {
|
|
332
|
+
display: block;
|
|
333
|
+
background-color: rgba(239, 68, 68, 0.15);
|
|
334
|
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
335
|
+
color: #fca5a5;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.context-accordion {
|
|
339
|
+
border-top: 1px solid var(--card-border);
|
|
340
|
+
padding: 1.15rem;
|
|
341
|
+
}
|
|
342
|
+
.context-accordion summary {
|
|
343
|
+
cursor: pointer;
|
|
344
|
+
font-size: 0.8rem;
|
|
345
|
+
font-weight: 600;
|
|
346
|
+
color: var(--text-muted);
|
|
347
|
+
user-select: none;
|
|
348
|
+
outline: none;
|
|
349
|
+
}
|
|
350
|
+
.context-accordion summary:hover {
|
|
351
|
+
color: var(--primary);
|
|
352
|
+
}
|
|
353
|
+
.context-json {
|
|
354
|
+
background-color: #0b1120;
|
|
355
|
+
border: 1px solid var(--card-border);
|
|
356
|
+
padding: 0.75rem;
|
|
357
|
+
border-radius: 0.5rem;
|
|
358
|
+
font-family: var(--mono-font);
|
|
359
|
+
font-size: 0.75rem;
|
|
360
|
+
color: #cbd5e1;
|
|
361
|
+
overflow-x: auto;
|
|
362
|
+
max-height: 250px;
|
|
363
|
+
margin-top: 0.6rem;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* Event Ledger View */
|
|
367
|
+
.ledger-container {
|
|
368
|
+
display: flex;
|
|
369
|
+
flex-direction: column;
|
|
370
|
+
height: 100%;
|
|
371
|
+
min-height: 600px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.ledger-controls {
|
|
375
|
+
display: flex;
|
|
376
|
+
align-items: center;
|
|
377
|
+
gap: 0.5rem;
|
|
378
|
+
flex-wrap: wrap;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.filter-select {
|
|
382
|
+
background-color: #0b1120;
|
|
383
|
+
border: 1px solid var(--card-border);
|
|
384
|
+
color: var(--text-muted);
|
|
385
|
+
font-size: 0.75rem;
|
|
386
|
+
padding: 0.25rem 0.5rem;
|
|
387
|
+
border-radius: 0.375rem;
|
|
388
|
+
outline: none;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.events-scroll {
|
|
392
|
+
flex: 1;
|
|
393
|
+
overflow-y: auto;
|
|
394
|
+
padding: 0.5rem;
|
|
395
|
+
display: flex;
|
|
396
|
+
flex-direction: column;
|
|
397
|
+
gap: 0.4rem;
|
|
398
|
+
background-color: #0b1120;
|
|
399
|
+
max-height: calc(100vh - 160px);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.event-item {
|
|
403
|
+
background-color: #111827;
|
|
404
|
+
border: 1px solid #1f2937;
|
|
405
|
+
border-radius: 0.5rem;
|
|
406
|
+
padding: 0.6rem 0.85rem;
|
|
407
|
+
font-size: 0.8rem;
|
|
408
|
+
transition: background-color 0.15s, border-color 0.15s;
|
|
409
|
+
cursor: pointer;
|
|
410
|
+
}
|
|
411
|
+
.event-item:hover {
|
|
412
|
+
background-color: #1e293b;
|
|
413
|
+
border-color: #334155;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.event-header {
|
|
417
|
+
display: flex;
|
|
418
|
+
align-items: center;
|
|
419
|
+
justify-content: space-between;
|
|
420
|
+
gap: 0.5rem;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.event-type-pill {
|
|
424
|
+
font-family: var(--mono-font);
|
|
425
|
+
font-size: 0.7rem;
|
|
426
|
+
font-weight: 700;
|
|
427
|
+
padding: 0.15rem 0.45rem;
|
|
428
|
+
border-radius: 0.25rem;
|
|
429
|
+
letter-spacing: 0.02em;
|
|
430
|
+
}
|
|
431
|
+
.type-STATE_TRANSITION {
|
|
432
|
+
background-color: var(--transition-bg);
|
|
433
|
+
color: var(--transition-text);
|
|
434
|
+
border: 1px solid #4338ca;
|
|
435
|
+
}
|
|
436
|
+
.type-GUARD_EVALUATED {
|
|
437
|
+
background-color: var(--guard-bg);
|
|
438
|
+
color: var(--guard-text);
|
|
439
|
+
border: 1px solid #047857;
|
|
440
|
+
}
|
|
441
|
+
.type-TOOL_CALL, .type-TOOL_EXECUTION {
|
|
442
|
+
background-color: var(--tool-bg);
|
|
443
|
+
color: var(--tool-text);
|
|
444
|
+
border: 1px solid #b45309;
|
|
445
|
+
}
|
|
446
|
+
.type-SKILL_INITIALIZED {
|
|
447
|
+
background-color: #082f49;
|
|
448
|
+
color: #7dd3fc;
|
|
449
|
+
border: 1px solid #0369a1;
|
|
450
|
+
}
|
|
451
|
+
.type-other {
|
|
452
|
+
background-color: #1f2937;
|
|
453
|
+
color: #9ca3af;
|
|
454
|
+
border: 1px solid #374151;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.event-seq {
|
|
458
|
+
font-family: var(--mono-font);
|
|
459
|
+
color: var(--text-dim);
|
|
460
|
+
font-size: 0.75rem;
|
|
461
|
+
}
|
|
462
|
+
.event-time {
|
|
463
|
+
font-size: 0.7rem;
|
|
464
|
+
color: var(--text-dim);
|
|
465
|
+
font-family: var(--mono-font);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.event-summary {
|
|
469
|
+
margin-top: 0.35rem;
|
|
470
|
+
font-size: 0.825rem;
|
|
471
|
+
color: #e2e8f0;
|
|
472
|
+
font-family: var(--mono-font);
|
|
473
|
+
word-break: break-word;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.event-payload-box {
|
|
477
|
+
margin-top: 0.5rem;
|
|
478
|
+
padding: 0.5rem;
|
|
479
|
+
background-color: #090d16;
|
|
480
|
+
border: 1px solid #1f2937;
|
|
481
|
+
border-radius: 0.375rem;
|
|
482
|
+
font-family: var(--mono-font);
|
|
483
|
+
font-size: 0.725rem;
|
|
484
|
+
color: #94a3b8;
|
|
485
|
+
overflow-x: auto;
|
|
486
|
+
display: none;
|
|
487
|
+
}
|
|
488
|
+
.event-item.expanded .event-payload-box {
|
|
489
|
+
display: block;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.empty-events {
|
|
493
|
+
padding: 3rem 1rem;
|
|
494
|
+
text-align: center;
|
|
495
|
+
color: var(--text-dim);
|
|
496
|
+
font-size: 0.85rem;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
footer {
|
|
500
|
+
border-top: 1px solid var(--card-border);
|
|
501
|
+
padding: 0.75rem 1.5rem;
|
|
502
|
+
font-size: 0.75rem;
|
|
503
|
+
color: var(--text-dim);
|
|
504
|
+
display: flex;
|
|
505
|
+
justify-content: space-between;
|
|
506
|
+
align-items: center;
|
|
507
|
+
background-color: var(--card-bg);
|
|
508
|
+
}
|
|
509
|
+
footer a {
|
|
510
|
+
color: var(--primary);
|
|
511
|
+
text-decoration: none;
|
|
512
|
+
}
|
|
513
|
+
footer a:hover {
|
|
514
|
+
text-decoration: underline;
|
|
515
|
+
}
|
|
516
|
+
</style>
|
|
517
|
+
</head>
|
|
518
|
+
<body>
|
|
519
|
+
|
|
520
|
+
<header>
|
|
521
|
+
<div class="brand">
|
|
522
|
+
<span class="brand-logo">⚡</span>
|
|
523
|
+
<span class="brand-title">Reactive Skills</span>
|
|
524
|
+
<span class="brand-skill" id="skillNameBadge">${escapeHtml(titleSkill)}</span>
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
<div class="header-metrics">
|
|
528
|
+
<div class="status-badge connecting" id="connectionBadge">
|
|
529
|
+
<span class="status-dot"></span>
|
|
530
|
+
<span id="connectionText">Connecting...</span>
|
|
531
|
+
</div>
|
|
532
|
+
|
|
533
|
+
<div class="header-links">
|
|
534
|
+
<a href="/state" target="_blank">/state</a>
|
|
535
|
+
<a href="/events/history" target="_blank">/history</a>
|
|
536
|
+
<a href="/health" target="_blank">/health</a>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
</header>
|
|
540
|
+
|
|
541
|
+
<main>
|
|
542
|
+
<!-- Left Column: Active State & Controls -->
|
|
543
|
+
<div style="display: flex; flex-direction: column; gap: 1.25rem;">
|
|
544
|
+
<div class="card">
|
|
545
|
+
<div class="card-header">
|
|
546
|
+
<span class="card-title">Active State & Context</span>
|
|
547
|
+
<button class="btn btn-secondary btn-sm" id="refreshStateBtn" title="Refresh state from server">↻ Refresh</button>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<div class="state-panel">
|
|
551
|
+
<div class="active-state-banner">
|
|
552
|
+
<div class="active-state-label">Current State</div>
|
|
553
|
+
<div class="active-state-name" id="activeStateDisplay">LOADING...</div>
|
|
554
|
+
</div>
|
|
555
|
+
|
|
556
|
+
<div class="meta-grid">
|
|
557
|
+
<div class="meta-box">
|
|
558
|
+
<div class="meta-box-label">Sequence</div>
|
|
559
|
+
<div class="meta-box-val" id="latestSeqDisplay">0</div>
|
|
560
|
+
</div>
|
|
561
|
+
<div class="meta-box">
|
|
562
|
+
<div class="meta-box-label">Uptime</div>
|
|
563
|
+
<div class="meta-box-val" id="uptimeDisplay">0s</div>
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
|
|
568
|
+
<!-- Signal Dispatcher -->
|
|
569
|
+
<div class="dispatch-box">
|
|
570
|
+
<div class="card-title" style="font-size: 0.8rem; margin-bottom: 0.25rem;">Dispatch Signal</div>
|
|
571
|
+
<form id="signalForm">
|
|
572
|
+
<div class="form-group">
|
|
573
|
+
<label for="signalInput">Signal Name</label>
|
|
574
|
+
<input type="text" id="signalInput" class="input-field" placeholder="e.g. RUNTIME_READY" required autocomplete="off">
|
|
575
|
+
</div>
|
|
576
|
+
|
|
577
|
+
<div class="form-group" style="margin-top: 0.5rem;">
|
|
578
|
+
<label for="payloadInput">Payload (JSON, optional)</label>
|
|
579
|
+
<textarea id="payloadInput" class="input-field" placeholder='{"key": "value"}'></textarea>
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
<div style="margin-top: 0.75rem; display: flex; gap: 0.5rem;">
|
|
583
|
+
<button type="submit" class="btn btn-primary" id="emitBtn" style="flex: 1;">⚡ Emit Signal</button>
|
|
584
|
+
</div>
|
|
585
|
+
</form>
|
|
586
|
+
<div class="dispatch-feedback" id="dispatchFeedback"></div>
|
|
587
|
+
</div>
|
|
588
|
+
|
|
589
|
+
<!-- Context Inspector -->
|
|
590
|
+
<details class="context-accordion" id="contextDetails">
|
|
591
|
+
<summary>Inspect FSM Context (<span id="contextKeyCount">0</span> keys)</summary>
|
|
592
|
+
<pre class="context-json" id="contextDisplay">{}</pre>
|
|
593
|
+
</details>
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
|
|
597
|
+
<!-- Right Column: Live Event Stream -->
|
|
598
|
+
<div class="card ledger-container">
|
|
599
|
+
<div class="card-header">
|
|
600
|
+
<div class="card-title">
|
|
601
|
+
<span>Live Event Ledger</span>
|
|
602
|
+
<span style="font-size: 0.75rem; color: var(--text-dim); font-family: var(--mono-font);" id="eventCountBadge">(0 events)</span>
|
|
603
|
+
</div>
|
|
604
|
+
<div class="ledger-controls">
|
|
605
|
+
<select class="filter-select" id="eventFilter">
|
|
606
|
+
<option value="ALL">All Events</option>
|
|
607
|
+
<option value="STATE_TRANSITION">Transitions</option>
|
|
608
|
+
<option value="GUARD_EVALUATED">Guards</option>
|
|
609
|
+
<option value="TOOL_CALL">Tools</option>
|
|
610
|
+
<option value="SKILL_INITIALIZED">Init</option>
|
|
611
|
+
</select>
|
|
612
|
+
<button class="btn btn-secondary btn-sm" id="pauseBtn">⏸ Pause</button>
|
|
613
|
+
<button class="btn btn-secondary btn-sm" id="clearBtn">Clear</button>
|
|
614
|
+
</div>
|
|
615
|
+
</div>
|
|
616
|
+
|
|
617
|
+
<div class="events-scroll" id="eventsList">
|
|
618
|
+
<div class="empty-events" id="emptyEvents">Connecting to event stream...</div>
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
</main>
|
|
622
|
+
|
|
623
|
+
<footer>
|
|
624
|
+
<div>Reactive Skills Architecture (RSA) • Telemetry Daemon v0.4.1</div>
|
|
625
|
+
<div>Running on <code>http://${escapeHtml(options.host)}:${options.port}</code></div>
|
|
626
|
+
</footer>
|
|
627
|
+
|
|
628
|
+
<script>
|
|
629
|
+
(function () {
|
|
630
|
+
let eventSource = null;
|
|
631
|
+
let isPaused = false;
|
|
632
|
+
let allEvents = [];
|
|
633
|
+
let autoScroll = true;
|
|
634
|
+
|
|
635
|
+
const connectionBadge = document.getElementById('connectionBadge');
|
|
636
|
+
const connectionText = document.getElementById('connectionText');
|
|
637
|
+
const activeStateDisplay = document.getElementById('activeStateDisplay');
|
|
638
|
+
const latestSeqDisplay = document.getElementById('latestSeqDisplay');
|
|
639
|
+
const uptimeDisplay = document.getElementById('uptimeDisplay');
|
|
640
|
+
const contextDisplay = document.getElementById('contextDisplay');
|
|
641
|
+
const contextKeyCount = document.getElementById('contextKeyCount');
|
|
642
|
+
const eventsList = document.getElementById('eventsList');
|
|
643
|
+
const emptyEvents = document.getElementById('emptyEvents');
|
|
644
|
+
const eventCountBadge = document.getElementById('eventCountBadge');
|
|
645
|
+
const eventFilter = document.getElementById('eventFilter');
|
|
646
|
+
const pauseBtn = document.getElementById('pauseBtn');
|
|
647
|
+
const clearBtn = document.getElementById('clearBtn');
|
|
648
|
+
const refreshStateBtn = document.getElementById('refreshStateBtn');
|
|
649
|
+
const signalForm = document.getElementById('signalForm');
|
|
650
|
+
const signalInput = document.getElementById('signalInput');
|
|
651
|
+
const payloadInput = document.getElementById('payloadInput');
|
|
652
|
+
const emitBtn = document.getElementById('emitBtn');
|
|
653
|
+
const dispatchFeedback = document.getElementById('dispatchFeedback');
|
|
654
|
+
const skillNameBadge = document.getElementById('skillNameBadge');
|
|
655
|
+
|
|
656
|
+
// Auto-detect scroll position
|
|
657
|
+
eventsList.addEventListener('scroll', () => {
|
|
658
|
+
const atBottom = eventsList.scrollHeight - eventsList.scrollTop - eventsList.clientHeight < 40;
|
|
659
|
+
autoScroll = atBottom;
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
function setConnectionStatus(status, text) {
|
|
663
|
+
connectionBadge.className = 'status-badge ' + status;
|
|
664
|
+
connectionText.textContent = text;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async function fetchState() {
|
|
668
|
+
try {
|
|
669
|
+
const res = await fetch('/state');
|
|
670
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
671
|
+
const data = await res.json();
|
|
672
|
+
if (data.skillName) {
|
|
673
|
+
skillNameBadge.textContent = data.skillName;
|
|
674
|
+
document.title = '⚡ ' + data.skillName + ' — Reactive Skills Telemetry';
|
|
675
|
+
}
|
|
676
|
+
activeStateDisplay.textContent = data.activeState || 'UNKNOWN';
|
|
677
|
+
latestSeqDisplay.textContent = data.latestSeq !== undefined ? data.latestSeq : '0';
|
|
678
|
+
|
|
679
|
+
const ctx = data.context || {};
|
|
680
|
+
contextDisplay.textContent = JSON.stringify(ctx, null, 2);
|
|
681
|
+
contextKeyCount.textContent = Object.keys(ctx).length;
|
|
682
|
+
} catch (err) {
|
|
683
|
+
console.warn('Failed to fetch state:', err);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async function fetchHealth() {
|
|
688
|
+
try {
|
|
689
|
+
const res = await fetch('/health');
|
|
690
|
+
if (res.ok) {
|
|
691
|
+
const data = await res.json();
|
|
692
|
+
if (data.uptimeSeconds !== undefined) {
|
|
693
|
+
const u = data.uptimeSeconds;
|
|
694
|
+
const mins = Math.floor(u / 60);
|
|
695
|
+
const secs = u % 60;
|
|
696
|
+
uptimeDisplay.textContent = mins > 0 ? (mins + 'm ' + secs + 's') : (secs + 's');
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
} catch {}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function fetchHistory() {
|
|
703
|
+
try {
|
|
704
|
+
const res = await fetch('/events/history?limit=100');
|
|
705
|
+
if (!res.ok) return;
|
|
706
|
+
const data = await res.json();
|
|
707
|
+
if (Array.isArray(data.events) && data.events.length > 0) {
|
|
708
|
+
allEvents = data.events;
|
|
709
|
+
renderEvents();
|
|
710
|
+
}
|
|
711
|
+
} catch (err) {
|
|
712
|
+
console.warn('Failed to fetch initial history:', err);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function formatSummary(evt) {
|
|
717
|
+
if (!evt) return '';
|
|
718
|
+
const p = evt.payload || {};
|
|
719
|
+
if (evt.type === 'STATE_TRANSITION') {
|
|
720
|
+
return (p.from || '?') + ' ──► ' + (p.to || '?') + (p.signal ? (' (via ' + p.signal + ')') : '');
|
|
721
|
+
}
|
|
722
|
+
if (evt.type === 'GUARD_EVALUATED') {
|
|
723
|
+
return 'Guard [' + (p.guard || '') + ']: ' + (p.result ? 'PASS ✅' : 'FAIL ❌');
|
|
724
|
+
}
|
|
725
|
+
if (evt.type === 'TOOL_CALL') {
|
|
726
|
+
return 'Tool call: ' + (p.tool || p.name || 'unnamed');
|
|
727
|
+
}
|
|
728
|
+
if (evt.type === 'SKILL_INITIALIZED') {
|
|
729
|
+
return 'Skill ' + (p.skill || '') + ' initialized at ' + (p.initial_state || 'INIT');
|
|
730
|
+
}
|
|
731
|
+
return JSON.stringify(p);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function renderEvents() {
|
|
735
|
+
const filter = eventFilter.value;
|
|
736
|
+
const filtered = filter === 'ALL'
|
|
737
|
+
? allEvents
|
|
738
|
+
: allEvents.filter(e => {
|
|
739
|
+
if (filter === 'TOOL_CALL') return e.type === 'TOOL_CALL' || e.type === 'TOOL_EXECUTION';
|
|
740
|
+
return e.type === filter;
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
eventCountBadge.textContent = '(' + filtered.length + ' events)';
|
|
744
|
+
|
|
745
|
+
if (filtered.length === 0) {
|
|
746
|
+
emptyEvents.style.display = 'block';
|
|
747
|
+
emptyEvents.textContent = allEvents.length === 0 ? 'No events recorded yet.' : 'No events match the selected filter.';
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
emptyEvents.style.display = 'none';
|
|
752
|
+
|
|
753
|
+
// Clear existing children except emptyEvents
|
|
754
|
+
Array.from(eventsList.children).forEach(child => {
|
|
755
|
+
if (child !== emptyEvents) eventsList.removeChild(child);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
filtered.forEach((evt, idx) => {
|
|
759
|
+
const item = document.createElement('div');
|
|
760
|
+
item.className = 'event-item';
|
|
761
|
+
|
|
762
|
+
const typeClass = 'type-' + (evt.type in {
|
|
763
|
+
STATE_TRANSITION: 1, GUARD_EVALUATED: 1, TOOL_CALL: 1, TOOL_EXECUTION: 1, SKILL_INITIALIZED: 1
|
|
764
|
+
} ? evt.type : 'other');
|
|
765
|
+
|
|
766
|
+
const timeStr = evt.timestamp ? new Date(evt.timestamp).toLocaleTimeString() : '';
|
|
767
|
+
const seqStr = evt.sequence !== undefined ? ('#' + evt.sequence) : ('#' + (idx + 1));
|
|
768
|
+
|
|
769
|
+
item.innerHTML =
|
|
770
|
+
'<div class="event-header">' +
|
|
771
|
+
'<span class="event-type-pill ' + typeClass + '">' + escapeHtml(evt.type || 'EVENT') + '</span>' +
|
|
772
|
+
'<div style="display:flex; gap:0.5rem; align-items:center;">' +
|
|
773
|
+
'<span class="event-time">' + escapeHtml(timeStr) + '</span>' +
|
|
774
|
+
'<span class="event-seq">' + escapeHtml(seqStr) + '</span>' +
|
|
775
|
+
'</div>' +
|
|
776
|
+
'</div>' +
|
|
777
|
+
'<div class="event-summary">' + escapeHtml(formatSummary(evt)) + '</div>' +
|
|
778
|
+
'<pre class="event-payload-box">' + escapeHtml(JSON.stringify(evt.payload || {}, null, 2)) + '</pre>';
|
|
779
|
+
|
|
780
|
+
item.addEventListener('click', () => {
|
|
781
|
+
item.classList.toggle('expanded');
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
eventsList.appendChild(item);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
if (autoScroll) {
|
|
788
|
+
eventsList.scrollTop = eventsList.scrollHeight;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function appendEvent(evt) {
|
|
793
|
+
if (!evt) return;
|
|
794
|
+
allEvents.push(evt);
|
|
795
|
+
if (allEvents.length > 500) allEvents.shift();
|
|
796
|
+
|
|
797
|
+
if (evt.type === 'STATE_TRANSITION' || evt.type === 'SKILL_INITIALIZED') {
|
|
798
|
+
fetchState();
|
|
799
|
+
}
|
|
800
|
+
if (evt.sequence !== undefined) {
|
|
801
|
+
latestSeqDisplay.textContent = evt.sequence;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (!isPaused) {
|
|
805
|
+
renderEvents();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function initSse() {
|
|
810
|
+
if (eventSource) {
|
|
811
|
+
try { eventSource.close(); } catch {}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
setConnectionStatus('connecting', 'Connecting...');
|
|
815
|
+
eventSource = new EventSource('/events');
|
|
816
|
+
|
|
817
|
+
eventSource.addEventListener('connected', () => {
|
|
818
|
+
setConnectionStatus('', 'Connected (SSE)');
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
eventSource.onopen = () => {
|
|
822
|
+
setConnectionStatus('', 'Connected (SSE)');
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
eventSource.onmessage = (e) => {
|
|
826
|
+
try {
|
|
827
|
+
const parsed = JSON.parse(e.data);
|
|
828
|
+
appendEvent(parsed);
|
|
829
|
+
} catch (err) {
|
|
830
|
+
console.warn('Failed to parse event JSON:', err);
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
eventSource.onerror = () => {
|
|
835
|
+
setConnectionStatus('disconnected', 'Reconnecting...');
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function escapeHtml(str) {
|
|
840
|
+
if (!str) return '';
|
|
841
|
+
return String(str)
|
|
842
|
+
.replace(/&/g, '&')
|
|
843
|
+
.replace(/</g, '<')
|
|
844
|
+
.replace(/>/g, '>')
|
|
845
|
+
.replace(/"/g, '"')
|
|
846
|
+
.replace(/'/g, ''');
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Signal Form Submit
|
|
850
|
+
signalForm.addEventListener('submit', async (e) => {
|
|
851
|
+
e.preventDefault();
|
|
852
|
+
const signal = signalInput.value.trim();
|
|
853
|
+
if (!signal) return;
|
|
854
|
+
|
|
855
|
+
let payload = {};
|
|
856
|
+
const rawPayload = payloadInput.value.trim();
|
|
857
|
+
if (rawPayload) {
|
|
858
|
+
try {
|
|
859
|
+
payload = JSON.parse(rawPayload);
|
|
860
|
+
} catch (err) {
|
|
861
|
+
dispatchFeedback.className = 'dispatch-feedback error';
|
|
862
|
+
dispatchFeedback.textContent = 'Invalid JSON payload: ' + err.message;
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
emitBtn.disabled = true;
|
|
868
|
+
emitBtn.textContent = 'Emitting...';
|
|
869
|
+
dispatchFeedback.style.display = 'none';
|
|
870
|
+
|
|
871
|
+
try {
|
|
872
|
+
const res = await fetch('/signal', {
|
|
873
|
+
method: 'POST',
|
|
874
|
+
headers: { 'Content-Type': 'application/json' },
|
|
875
|
+
body: JSON.stringify({ signal, payload })
|
|
876
|
+
});
|
|
877
|
+
const result = await res.json();
|
|
878
|
+
|
|
879
|
+
if (!res.ok || result.error) {
|
|
880
|
+
dispatchFeedback.className = 'dispatch-feedback error';
|
|
881
|
+
dispatchFeedback.textContent = 'Emission error: ' + (result.error || 'HTTP ' + res.status);
|
|
882
|
+
} else {
|
|
883
|
+
dispatchFeedback.className = 'dispatch-feedback success';
|
|
884
|
+
dispatchFeedback.textContent = result.transitioned
|
|
885
|
+
? '✅ Transitioned ──► ' + result.newState
|
|
886
|
+
: '⚡ Signal evaluated (no transition occurred)';
|
|
887
|
+
signalInput.value = '';
|
|
888
|
+
fetchState();
|
|
889
|
+
}
|
|
890
|
+
} catch (err) {
|
|
891
|
+
dispatchFeedback.className = 'dispatch-feedback error';
|
|
892
|
+
dispatchFeedback.textContent = 'Network error: ' + err.message;
|
|
893
|
+
} finally {
|
|
894
|
+
emitBtn.disabled = false;
|
|
895
|
+
emitBtn.textContent = '⚡ Emit Signal';
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
// Controls
|
|
900
|
+
pauseBtn.addEventListener('click', () => {
|
|
901
|
+
isPaused = !isPaused;
|
|
902
|
+
pauseBtn.textContent = isPaused ? '▶ Resume' : '⏸ Pause';
|
|
903
|
+
if (!isPaused) renderEvents();
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
clearBtn.addEventListener('click', () => {
|
|
907
|
+
allEvents = [];
|
|
908
|
+
renderEvents();
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
eventFilter.addEventListener('change', () => {
|
|
912
|
+
renderEvents();
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
refreshStateBtn.addEventListener('click', () => {
|
|
916
|
+
fetchState();
|
|
917
|
+
fetchHealth();
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
// Boot
|
|
921
|
+
fetchState();
|
|
922
|
+
fetchHealth();
|
|
923
|
+
fetchHistory().then(initSse);
|
|
924
|
+
setInterval(fetchHealth, 5000);
|
|
925
|
+
})();
|
|
926
|
+
</script>
|
|
927
|
+
</body>
|
|
928
|
+
</html>`;
|
|
929
|
+
}
|
|
930
|
+
function escapeHtml(str) {
|
|
931
|
+
if (!str)
|
|
932
|
+
return '';
|
|
933
|
+
return String(str)
|
|
934
|
+
.replace(/&/g, '&')
|
|
935
|
+
.replace(/</g, '<')
|
|
936
|
+
.replace(/>/g, '>')
|
|
937
|
+
.replace(/"/g, '"')
|
|
938
|
+
.replace(/'/g, ''');
|
|
939
|
+
}
|