@reactive-skills/runtime 0.4.2 → 0.4.4
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 +39 -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 +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
|
|
2
|
+
## [0.4.4] - 2026-09-14
|
|
3
|
+
|
|
4
|
+
- fix(ci): calibrate perf budget for virtualized CI runners & automate GitHub Releases (e2dd853)
|
|
5
|
+
- chore(release): bump version to 0.4.3 (2f89b7d)
|
|
6
|
+
- feat(telemetry): embed standalone live web dashboard at GET / (4ffb3a9)
|
|
7
|
+
- docs: add comprehensive job management documentation across readmes (e1a626a)
|
|
8
|
+
- docs: add practical telemetry inspection and metrics querying guide (ef37fb6)
|
|
9
|
+
|
|
10
|
+
## [0.4.3] - 2026-09-14
|
|
11
|
+
|
|
12
|
+
- feat(telemetry): embed standalone live web dashboard at GET / (4ffb3a9)
|
|
13
|
+
- docs: add comprehensive job management documentation across readmes (e1a626a)
|
|
14
|
+
- docs: add practical telemetry inspection and metrics querying guide (ef37fb6)
|
|
15
|
+
- chore: release v0.4.2 (28c16b9)
|
|
16
|
+
- feat(runtime): add in-engine telemetry, template caching, and performance budget verification (7dfae37)
|
|
17
|
+
|
|
2
18
|
## [0.4.2] - 2026-09-14
|
|
3
19
|
|
|
4
20
|
- feat(runtime): add in-engine telemetry, template caching, and performance budget verification (7dfae37)
|
package/README.md
CHANGED
|
@@ -69,7 +69,46 @@ Dual-mode event sourcing:
|
|
|
69
69
|
1. **JSONL** (`.reactive/skills/<skill>/events.jsonl`) - Human-readable append-only log
|
|
70
70
|
2. **SQLite** (`.reactive/skills/<skill>/events.db`) - Indexed relational database
|
|
71
71
|
|
|
72
|
+
## Job & Run Management
|
|
73
|
+
|
|
74
|
+
The runtime isolates execution runs through `JobManager`:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { JobManager, FSMEngine } from '@reactive-skills/runtime';
|
|
78
|
+
|
|
79
|
+
// 1. Manage isolated runs:
|
|
80
|
+
const jobManager = new JobManager();
|
|
81
|
+
jobManager.createJob('my-skill', { id: 'sprint-1', name: 'Sprint 1 Run', setActive: true });
|
|
82
|
+
|
|
83
|
+
// 2. Instantiate engine targeted to a specific run:
|
|
84
|
+
const engine = new FSMEngine({
|
|
85
|
+
skillDir: './skills/my-skill',
|
|
86
|
+
jobId: 'sprint-1',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Deliverables automatically mirror to .docs/my-skill/jobs/sprint-1/ and canonical .docs/
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Performance & Telemetry
|
|
93
|
+
|
|
94
|
+
The runtime captures execution telemetry and token estimates with zero latency penalty:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// 1. In-turn prompt slice telemetry:
|
|
98
|
+
const slice = engine.generatePromptSlice();
|
|
99
|
+
console.log(slice.metrics);
|
|
100
|
+
// => { slice_duration_ms: 0.23, slice_tokens_est: 282, allowed_tools_count: 4 }
|
|
101
|
+
|
|
102
|
+
// 2. State transition telemetry:
|
|
103
|
+
const res = await engine.handleSignal('TEST_RAN', { exit_code: 0 });
|
|
104
|
+
console.log(res.metrics);
|
|
105
|
+
// => { transition_duration_ms: 1.05, slice_duration_ms: 0.23, slice_tokens_est: 282 }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
See [PERFORMANCE-STANDARDS.md](../../PERFORMANCE-STANDARDS.md) for full latency budgets and caching architecture.
|
|
109
|
+
|
|
72
110
|
## License
|
|
73
111
|
|
|
74
112
|
MIT
|
|
75
113
|
|
|
114
|
+
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface DashboardOptions {
|
|
2
|
+
skillName?: string;
|
|
3
|
+
port: number;
|
|
4
|
+
host: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Generates a self-contained, zero-dependency HTML dashboard for the TelemetryServer.
|
|
8
|
+
* Provides live SSE event streaming, state & context inspection, and signal dispatching.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderDashboardHtml(options: DashboardOptions): string;
|
|
@@ -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
|
+
}
|
package/dist/telemetry/server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { URL } from 'node:url';
|
|
3
|
+
import { renderDashboardHtml } from './dashboard.js';
|
|
3
4
|
export class TelemetryServer {
|
|
4
5
|
server = null;
|
|
5
6
|
eventStore;
|
|
@@ -110,16 +111,21 @@ export class TelemetryServer {
|
|
|
110
111
|
const hostHeader = req.headers.host || `${this.host}:${this.port}`;
|
|
111
112
|
const parsedUrl = new URL(req.url || '/', `http://${hostHeader}`);
|
|
112
113
|
const pathname = parsedUrl.pathname;
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
const isGetOrHead = req.method === 'GET' || req.method === 'HEAD';
|
|
115
|
+
if (isGetOrHead && (pathname === '/' || pathname === '/index.html')) {
|
|
116
|
+
this.handleDashboard(req, res);
|
|
115
117
|
return;
|
|
116
118
|
}
|
|
117
|
-
if (
|
|
118
|
-
this.
|
|
119
|
+
if (isGetOrHead && (pathname === '/health' || pathname === '/status')) {
|
|
120
|
+
this.handleHealth(req, res);
|
|
119
121
|
return;
|
|
120
122
|
}
|
|
121
|
-
if (
|
|
122
|
-
this.
|
|
123
|
+
if (isGetOrHead && pathname === '/state') {
|
|
124
|
+
this.handleState(req, res);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (isGetOrHead && pathname === '/events/history') {
|
|
128
|
+
this.handleEventsHistory(req, parsedUrl, res);
|
|
123
129
|
return;
|
|
124
130
|
}
|
|
125
131
|
if (req.method === 'GET' && pathname === '/events') {
|
|
@@ -133,17 +139,43 @@ export class TelemetryServer {
|
|
|
133
139
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
134
140
|
res.end(JSON.stringify({ error: `Not found: ${pathname}` }));
|
|
135
141
|
}
|
|
136
|
-
|
|
142
|
+
handleDashboard(req, res) {
|
|
143
|
+
const html = renderDashboardHtml({
|
|
144
|
+
skillName: this.skillName,
|
|
145
|
+
port: this.getPort(),
|
|
146
|
+
host: this.host,
|
|
147
|
+
});
|
|
148
|
+
res.writeHead(200, {
|
|
149
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
150
|
+
'Content-Length': Buffer.byteLength(html),
|
|
151
|
+
});
|
|
152
|
+
if (req.method === 'HEAD') {
|
|
153
|
+
res.end();
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
res.end(html);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
handleHealth(req, res) {
|
|
137
160
|
const payload = {
|
|
138
161
|
status: 'ok',
|
|
139
162
|
skillName: this.skillName,
|
|
140
163
|
latestSeq: this.eventStore.getLatestSequence(),
|
|
141
164
|
uptimeSeconds: Math.floor((Date.now() - this.startTime) / 1000),
|
|
142
165
|
};
|
|
143
|
-
|
|
144
|
-
res.
|
|
166
|
+
const body = JSON.stringify(payload);
|
|
167
|
+
res.writeHead(200, {
|
|
168
|
+
'Content-Type': 'application/json',
|
|
169
|
+
'Content-Length': Buffer.byteLength(body),
|
|
170
|
+
});
|
|
171
|
+
if (req.method === 'HEAD') {
|
|
172
|
+
res.end();
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
res.end(body);
|
|
176
|
+
}
|
|
145
177
|
}
|
|
146
|
-
handleState(res) {
|
|
178
|
+
handleState(req, res) {
|
|
147
179
|
const latestSeq = this.eventStore.getLatestSequence();
|
|
148
180
|
const snapshot = this.eventStore.getLatestSnapshot();
|
|
149
181
|
const activeState = this.fsmEngine ? this.fsmEngine.getCurrentState() : snapshot?.state;
|
|
@@ -155,10 +187,19 @@ export class TelemetryServer {
|
|
|
155
187
|
context,
|
|
156
188
|
snapshot,
|
|
157
189
|
};
|
|
158
|
-
|
|
159
|
-
res.
|
|
190
|
+
const body = JSON.stringify(payload);
|
|
191
|
+
res.writeHead(200, {
|
|
192
|
+
'Content-Type': 'application/json',
|
|
193
|
+
'Content-Length': Buffer.byteLength(body),
|
|
194
|
+
});
|
|
195
|
+
if (req.method === 'HEAD') {
|
|
196
|
+
res.end();
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
res.end(body);
|
|
200
|
+
}
|
|
160
201
|
}
|
|
161
|
-
handleEventsHistory(url, res) {
|
|
202
|
+
handleEventsHistory(req, url, res) {
|
|
162
203
|
const sinceSeqParam = url.searchParams.get('sinceSeq');
|
|
163
204
|
const limitParam = url.searchParams.get('limit');
|
|
164
205
|
const sinceSeq = sinceSeqParam !== null ? parseInt(sinceSeqParam, 10) : undefined;
|
|
@@ -173,8 +214,21 @@ export class TelemetryServer {
|
|
|
173
214
|
if (limit !== undefined && !isNaN(limit) && limit > 0) {
|
|
174
215
|
events = events.slice(-limit);
|
|
175
216
|
}
|
|
176
|
-
|
|
177
|
-
|
|
217
|
+
const payload = {
|
|
218
|
+
count: events.length,
|
|
219
|
+
events,
|
|
220
|
+
};
|
|
221
|
+
const body = JSON.stringify(payload);
|
|
222
|
+
res.writeHead(200, {
|
|
223
|
+
'Content-Type': 'application/json',
|
|
224
|
+
'Content-Length': Buffer.byteLength(body),
|
|
225
|
+
});
|
|
226
|
+
if (req.method === 'HEAD') {
|
|
227
|
+
res.end();
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
res.end(body);
|
|
231
|
+
}
|
|
178
232
|
}
|
|
179
233
|
handleSseEvents(req, url, res) {
|
|
180
234
|
res.writeHead(200, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactive-skills/runtime",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Reactive Skills Architecture (RSA) core runtime — FSM engine, event store, guard evaluator, projection engine, MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"dist",
|
|
18
18
|
"CHANGELOG.md"
|
|
19
19
|
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
20
23
|
"keywords": [
|
|
21
24
|
"ai",
|
|
22
25
|
"agent",
|
|
@@ -37,9 +40,6 @@
|
|
|
37
40
|
"engines": {
|
|
38
41
|
"node": ">=22.5.0"
|
|
39
42
|
},
|
|
40
|
-
"publishConfig": {
|
|
41
|
-
"access": "public"
|
|
42
|
-
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
45
45
|
"handlebars": "^4.7.8",
|