selftune 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.claude/agents/diagnosis-analyst.md +20 -10
  2. package/.claude/agents/evolution-reviewer.md +14 -1
  3. package/.claude/agents/integration-guide.md +18 -6
  4. package/.claude/agents/pattern-analyst.md +18 -5
  5. package/CHANGELOG.md +12 -4
  6. package/README.md +43 -35
  7. package/apps/local-dashboard/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  8. package/apps/local-dashboard/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  9. package/apps/local-dashboard/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  10. package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +15 -0
  11. package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +1 -0
  12. package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +60 -0
  13. package/apps/local-dashboard/dist/assets/vendor-table-B7VF2Ipl.js +26 -0
  14. package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +346 -0
  15. package/apps/local-dashboard/dist/favicon.png +0 -0
  16. package/apps/local-dashboard/dist/index.html +17 -0
  17. package/apps/local-dashboard/dist/logo.png +0 -0
  18. package/apps/local-dashboard/dist/logo.svg +9 -0
  19. package/cli/selftune/badge/badge-data.ts +1 -1
  20. package/cli/selftune/badge/badge.ts +4 -8
  21. package/cli/selftune/canonical-export.ts +183 -0
  22. package/cli/selftune/constants.ts +28 -0
  23. package/cli/selftune/contribute/contribute.ts +1 -1
  24. package/cli/selftune/cron/setup.ts +17 -17
  25. package/cli/selftune/dashboard-contract.ts +202 -0
  26. package/cli/selftune/dashboard-server.ts +653 -186
  27. package/cli/selftune/dashboard.ts +41 -176
  28. package/cli/selftune/eval/baseline.ts +5 -4
  29. package/cli/selftune/eval/composability-v2.ts +273 -0
  30. package/cli/selftune/eval/hooks-to-evals.ts +34 -15
  31. package/cli/selftune/eval/unit-test-cli.ts +1 -1
  32. package/cli/selftune/evolution/evidence.ts +26 -0
  33. package/cli/selftune/evolution/evolve-body.ts +105 -11
  34. package/cli/selftune/evolution/evolve.ts +371 -25
  35. package/cli/selftune/evolution/extract-patterns.ts +87 -29
  36. package/cli/selftune/evolution/rollback.ts +2 -2
  37. package/cli/selftune/grading/auto-grade.ts +200 -0
  38. package/cli/selftune/grading/grade-session.ts +448 -97
  39. package/cli/selftune/grading/results.ts +42 -0
  40. package/cli/selftune/hooks/prompt-log.ts +172 -2
  41. package/cli/selftune/hooks/session-stop.ts +123 -3
  42. package/cli/selftune/hooks/skill-eval.ts +119 -3
  43. package/cli/selftune/index.ts +395 -116
  44. package/cli/selftune/ingestors/claude-replay.ts +140 -114
  45. package/cli/selftune/ingestors/codex-rollout.ts +345 -46
  46. package/cli/selftune/ingestors/codex-wrapper.ts +207 -39
  47. package/cli/selftune/ingestors/openclaw-ingest.ts +141 -8
  48. package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
  49. package/cli/selftune/init.ts +227 -14
  50. package/cli/selftune/last.ts +14 -5
  51. package/cli/selftune/localdb/db.ts +63 -0
  52. package/cli/selftune/localdb/materialize.ts +428 -0
  53. package/cli/selftune/localdb/queries.ts +376 -0
  54. package/cli/selftune/localdb/schema.ts +204 -0
  55. package/cli/selftune/monitoring/watch.ts +66 -15
  56. package/cli/selftune/normalization.ts +682 -0
  57. package/cli/selftune/observability.ts +19 -44
  58. package/cli/selftune/orchestrate.ts +1073 -0
  59. package/cli/selftune/quickstart.ts +203 -0
  60. package/cli/selftune/repair/skill-usage.ts +576 -0
  61. package/cli/selftune/schedule.ts +561 -0
  62. package/cli/selftune/status.ts +48 -26
  63. package/cli/selftune/sync.ts +627 -0
  64. package/cli/selftune/types.ts +148 -0
  65. package/cli/selftune/utils/canonical-log.ts +45 -0
  66. package/cli/selftune/utils/hooks.ts +41 -0
  67. package/cli/selftune/utils/html.ts +27 -0
  68. package/cli/selftune/utils/llm-call.ts +78 -20
  69. package/cli/selftune/utils/math.ts +10 -0
  70. package/cli/selftune/utils/query-filter.ts +139 -0
  71. package/cli/selftune/utils/skill-discovery.ts +340 -0
  72. package/cli/selftune/utils/skill-log.ts +68 -0
  73. package/cli/selftune/utils/skill-usage-confidence.ts +18 -0
  74. package/cli/selftune/utils/transcript.ts +272 -26
  75. package/cli/selftune/workflows/discover.ts +254 -0
  76. package/cli/selftune/workflows/skill-md-writer.ts +288 -0
  77. package/cli/selftune/workflows/workflows.ts +188 -0
  78. package/package.json +21 -8
  79. package/packages/telemetry-contract/README.md +11 -0
  80. package/packages/telemetry-contract/fixtures/golden.json +87 -0
  81. package/packages/telemetry-contract/fixtures/golden.test.ts +42 -0
  82. package/packages/telemetry-contract/index.ts +1 -0
  83. package/packages/telemetry-contract/package.json +19 -0
  84. package/packages/telemetry-contract/src/index.ts +2 -0
  85. package/packages/telemetry-contract/src/types.ts +163 -0
  86. package/packages/telemetry-contract/src/validators.ts +109 -0
  87. package/skill/SKILL.md +84 -53
  88. package/skill/Workflows/AutoActivation.md +17 -16
  89. package/skill/Workflows/Badge.md +6 -0
  90. package/skill/Workflows/Baseline.md +46 -23
  91. package/skill/Workflows/Composability.md +12 -5
  92. package/skill/Workflows/Contribute.md +17 -14
  93. package/skill/Workflows/Cron.md +56 -79
  94. package/skill/Workflows/Dashboard.md +45 -34
  95. package/skill/Workflows/Doctor.md +30 -17
  96. package/skill/Workflows/Evals.md +64 -40
  97. package/skill/Workflows/EvolutionMemory.md +2 -0
  98. package/skill/Workflows/Evolve.md +102 -47
  99. package/skill/Workflows/EvolveBody.md +6 -6
  100. package/skill/Workflows/Grade.md +36 -31
  101. package/skill/Workflows/ImportSkillsBench.md +11 -5
  102. package/skill/Workflows/Ingest.md +43 -36
  103. package/skill/Workflows/Initialize.md +44 -30
  104. package/skill/Workflows/Orchestrate.md +139 -0
  105. package/skill/Workflows/Replay.md +39 -18
  106. package/skill/Workflows/Rollback.md +3 -3
  107. package/skill/Workflows/Schedule.md +61 -0
  108. package/skill/Workflows/Sync.md +88 -0
  109. package/skill/Workflows/UnitTest.md +34 -22
  110. package/skill/Workflows/Watch.md +14 -4
  111. package/skill/Workflows/Workflows.md +129 -0
  112. package/skill/assets/activation-rules-default.json +26 -0
  113. package/skill/assets/multi-skill-settings.json +63 -0
  114. package/skill/assets/single-skill-settings.json +57 -0
  115. package/skill/references/invocation-taxonomy.md +2 -2
  116. package/skill/references/logs.md +164 -2
  117. package/skill/references/setup-patterns.md +65 -0
  118. package/skill/references/version-history.md +40 -0
  119. package/skill/settings_snippet.json +1 -1
  120. package/templates/multi-skill-settings.json +7 -7
  121. package/templates/single-skill-settings.json +6 -6
  122. package/dashboard/index.html +0 -1680
@@ -1,1680 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>selftune — Dashboard</title>
7
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
8
- <style>
9
- :root {
10
- --bg: #faf9f5;
11
- --surface: #ffffff;
12
- --border: #e8e6dc;
13
- --text: #141413;
14
- --text-muted: #b0aea5;
15
- --text-secondary: #6b6961;
16
- --accent: #d97757;
17
- --accent-hover: #c4613f;
18
- --green: #788c5d;
19
- --green-bg: #eef2e8;
20
- --red: #c44;
21
- --red-bg: #fceaea;
22
- --blue: #4a7fd4;
23
- --blue-bg: #e8f0fa;
24
- --amber: #c49133;
25
- --amber-bg: #fdf4e3;
26
- --header-bg: #141413;
27
- --header-text: #faf9f5;
28
- --radius: 6px;
29
- --mono: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
30
- }
31
-
32
- * { box-sizing: border-box; margin: 0; padding: 0; }
33
-
34
- body {
35
- font-family: 'Lora', Georgia, serif;
36
- background: var(--bg);
37
- color: var(--text);
38
- height: 100vh;
39
- display: flex;
40
- flex-direction: column;
41
- }
42
-
43
- /* ---- Header ---- */
44
- .header {
45
- background: var(--header-bg);
46
- color: var(--header-text);
47
- padding: 1rem 2rem;
48
- display: flex;
49
- justify-content: space-between;
50
- align-items: center;
51
- flex-shrink: 0;
52
- }
53
- .header-left { display: flex; align-items: center; gap: 1rem; }
54
- .header h1 {
55
- font-family: 'Poppins', sans-serif;
56
- font-size: 1.25rem;
57
- font-weight: 600;
58
- letter-spacing: -0.01em;
59
- }
60
- .header h1 span { color: var(--accent); }
61
- .header .version {
62
- font-family: var(--mono);
63
- font-size: 0.6875rem;
64
- opacity: 0.5;
65
- padding: 0.15rem 0.5rem;
66
- border: 1px solid rgba(255,255,255,0.15);
67
- border-radius: 9999px;
68
- }
69
- .header .status {
70
- font-size: 0.8rem;
71
- opacity: 0.7;
72
- font-family: 'Poppins', sans-serif;
73
- }
74
- .header .status .count {
75
- color: var(--accent);
76
- font-weight: 600;
77
- }
78
-
79
- /* ---- Main ---- */
80
- .main {
81
- flex: 1;
82
- overflow-y: auto;
83
- padding: 1.5rem 2rem;
84
- display: flex;
85
- flex-direction: column;
86
- gap: 1.25rem;
87
- }
88
-
89
- /* ---- Drop zone ---- */
90
- .drop-zone {
91
- border: 2px dashed var(--border);
92
- border-radius: var(--radius);
93
- padding: 3rem 2rem;
94
- text-align: center;
95
- transition: all 0.2s;
96
- cursor: pointer;
97
- background: var(--surface);
98
- }
99
- .drop-zone:hover, .drop-zone.drag-over {
100
- border-color: var(--accent);
101
- background: rgba(217, 119, 87, 0.04);
102
- }
103
- .drop-zone h2 {
104
- font-family: 'Poppins', sans-serif;
105
- font-size: 1.125rem;
106
- font-weight: 600;
107
- margin-bottom: 0.5rem;
108
- }
109
- .drop-zone p {
110
- color: var(--text-muted);
111
- font-size: 0.875rem;
112
- margin-bottom: 1rem;
113
- }
114
- .drop-zone .file-types {
115
- display: flex;
116
- justify-content: center;
117
- gap: 0.5rem;
118
- flex-wrap: wrap;
119
- }
120
- .file-tag {
121
- display: inline-block;
122
- padding: 0.2rem 0.625rem;
123
- border-radius: 9999px;
124
- font-family: var(--mono);
125
- font-size: 0.6875rem;
126
- font-weight: 500;
127
- background: var(--bg);
128
- color: var(--text-secondary);
129
- border: 1px solid var(--border);
130
- }
131
- .file-tag.loaded {
132
- background: var(--green-bg);
133
- color: var(--green);
134
- border-color: var(--green);
135
- }
136
- .drop-zone input[type="file"] { display: none; }
137
-
138
- /* ---- KPI row ---- */
139
- .kpi-row {
140
- display: grid;
141
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
142
- gap: 1rem;
143
- }
144
- .kpi-card {
145
- background: var(--surface);
146
- border: 1px solid var(--border);
147
- border-radius: var(--radius);
148
- padding: 1.25rem;
149
- }
150
- .kpi-label {
151
- font-family: 'Poppins', sans-serif;
152
- font-size: 0.6875rem;
153
- font-weight: 500;
154
- text-transform: uppercase;
155
- letter-spacing: 0.05em;
156
- color: var(--text-muted);
157
- margin-bottom: 0.5rem;
158
- }
159
- .kpi-value {
160
- font-family: 'Poppins', sans-serif;
161
- font-size: 2rem;
162
- font-weight: 700;
163
- line-height: 1;
164
- }
165
- .kpi-sub {
166
- font-size: 0.75rem;
167
- color: var(--text-muted);
168
- margin-top: 0.375rem;
169
- }
170
-
171
- /* ---- Section cards ---- */
172
- .section {
173
- background: var(--surface);
174
- border: 1px solid var(--border);
175
- border-radius: var(--radius);
176
- }
177
- .section-header {
178
- font-family: 'Poppins', sans-serif;
179
- padding: 0.75rem 1rem;
180
- font-size: 0.75rem;
181
- font-weight: 500;
182
- text-transform: uppercase;
183
- letter-spacing: 0.05em;
184
- color: var(--text-muted);
185
- border-bottom: 1px solid var(--border);
186
- background: var(--bg);
187
- border-radius: var(--radius) var(--radius) 0 0;
188
- display: flex;
189
- justify-content: space-between;
190
- align-items: center;
191
- }
192
- .section-body { padding: 1rem; }
193
-
194
- /* ---- Chart containers ---- */
195
- .chart-container {
196
- position: relative;
197
- height: 280px;
198
- width: 100%;
199
- }
200
-
201
- /* ---- Badges ---- */
202
- .badge {
203
- display: inline-block;
204
- padding: 0.125rem 0.5rem;
205
- border-radius: 9999px;
206
- font-family: 'Poppins', sans-serif;
207
- font-size: 0.6875rem;
208
- font-weight: 600;
209
- }
210
- .badge-green { background: var(--green-bg); color: var(--green); }
211
- .badge-red { background: var(--red-bg); color: var(--red); }
212
- .badge-blue { background: var(--blue-bg); color: var(--blue); }
213
- .badge-amber { background: var(--amber-bg); color: var(--amber); }
214
-
215
- /* ---- Skill Health Grid ---- */
216
- .skill-health-grid {
217
- width: 100%;
218
- }
219
- .skill-health-row {
220
- display: grid;
221
- grid-template-columns: 180px 1fr 50px 80px 100px;
222
- align-items: center;
223
- gap: 0.75rem;
224
- padding: 0.625rem 1rem;
225
- border-bottom: 1px solid var(--border);
226
- cursor: pointer;
227
- transition: background 0.1s;
228
- }
229
- .skill-health-row:hover {
230
- background: var(--bg);
231
- }
232
- .skill-health-row.selected {
233
- background: var(--blue-bg);
234
- border-left: 3px solid var(--blue);
235
- }
236
- .skill-health-header {
237
- display: grid;
238
- grid-template-columns: 180px 1fr 50px 80px 100px;
239
- gap: 0.75rem;
240
- padding: 0.5rem 1rem;
241
- font-family: 'Poppins', sans-serif;
242
- font-size: 0.6875rem;
243
- font-weight: 500;
244
- text-transform: uppercase;
245
- letter-spacing: 0.04em;
246
- color: var(--text-muted);
247
- background: var(--bg);
248
- border-bottom: 1px solid var(--border);
249
- }
250
- .skill-name {
251
- font-family: var(--mono);
252
- font-size: 0.75rem;
253
- font-weight: 500;
254
- overflow: hidden;
255
- text-overflow: ellipsis;
256
- white-space: nowrap;
257
- }
258
- .pass-rate-bar {
259
- display: flex;
260
- align-items: center;
261
- gap: 0.5rem;
262
- }
263
- .pass-rate-track {
264
- flex: 1;
265
- height: 18px;
266
- background: var(--bg);
267
- border-radius: 3px;
268
- overflow: hidden;
269
- }
270
- .pass-rate-fill {
271
- height: 100%;
272
- border-radius: 3px;
273
- transition: width 0.4s ease;
274
- }
275
- .pass-rate-fill.healthy { background: var(--green); }
276
- .pass-rate-fill.drifting { background: var(--amber); }
277
- .pass-rate-fill.warning { background: var(--amber); }
278
- .pass-rate-fill.regressed { background: var(--red); }
279
- .pass-rate-fill.critical { background: var(--red); }
280
- .pass-rate-fill.unknown { background: #ccc; }
281
- .pass-rate-label {
282
- font-family: 'Poppins', sans-serif;
283
- font-size: 0.75rem;
284
- font-weight: 600;
285
- min-width: 42px;
286
- text-align: right;
287
- }
288
- .trend-arrow {
289
- font-size: 1rem;
290
- text-align: center;
291
- }
292
- .trend-up { color: var(--green); }
293
- .trend-down { color: var(--red); }
294
- .trend-flat { color: var(--text-muted); }
295
- .missed-count {
296
- font-family: 'Poppins', sans-serif;
297
- font-size: 0.75rem;
298
- text-align: center;
299
- }
300
-
301
- /* ---- Drill-down panel ---- */
302
- .drill-down-panel {
303
- display: none;
304
- background: var(--surface);
305
- border: 1px solid var(--border);
306
- border-radius: var(--radius);
307
- }
308
- .drill-down-panel.visible {
309
- display: block;
310
- }
311
- .drill-down-header {
312
- font-family: 'Poppins', sans-serif;
313
- padding: 0.75rem 1rem;
314
- font-size: 0.875rem;
315
- font-weight: 600;
316
- border-bottom: 1px solid var(--border);
317
- background: var(--bg);
318
- border-radius: var(--radius) var(--radius) 0 0;
319
- display: flex;
320
- justify-content: space-between;
321
- align-items: center;
322
- }
323
- .drill-down-close {
324
- font-family: 'Poppins', sans-serif;
325
- font-size: 0.75rem;
326
- padding: 0.25rem 0.75rem;
327
- border: 1px solid var(--border);
328
- border-radius: var(--radius);
329
- background: var(--surface);
330
- color: var(--text-secondary);
331
- cursor: pointer;
332
- }
333
- .drill-down-close:hover { border-color: var(--accent); color: var(--accent); }
334
- .drill-down-content {
335
- display: grid;
336
- grid-template-columns: 1fr 1fr;
337
- gap: 1rem;
338
- padding: 1rem;
339
- }
340
- @media (max-width: 900px) {
341
- .drill-down-content { grid-template-columns: 1fr; }
342
- }
343
- .drill-down-section { min-height: 200px; }
344
-
345
- /* ---- Table ---- */
346
- .data-table {
347
- width: 100%;
348
- border-collapse: collapse;
349
- font-size: 0.8125rem;
350
- }
351
- .data-table th, .data-table td {
352
- padding: 0.625rem 0.75rem;
353
- text-align: left;
354
- border-bottom: 1px solid var(--border);
355
- }
356
- .data-table th {
357
- font-family: 'Poppins', sans-serif;
358
- font-weight: 500;
359
- font-size: 0.6875rem;
360
- text-transform: uppercase;
361
- letter-spacing: 0.04em;
362
- color: var(--text-muted);
363
- background: var(--bg);
364
- position: sticky;
365
- top: 0;
366
- }
367
- .data-table tr:hover { background: var(--bg); }
368
- .data-table td.mono { font-family: var(--mono); font-size: 0.75rem; }
369
-
370
- /* ---- Timeline ---- */
371
- .timeline-item {
372
- display: flex;
373
- gap: 1rem;
374
- padding: 0.75rem 0;
375
- border-bottom: 1px solid var(--border);
376
- font-size: 0.8125rem;
377
- }
378
- .timeline-item:last-child { border-bottom: none; }
379
- .timeline-date {
380
- font-family: var(--mono);
381
- font-size: 0.6875rem;
382
- color: var(--text-muted);
383
- min-width: 140px;
384
- flex-shrink: 0;
385
- }
386
- .timeline-action { font-weight: 500; }
387
-
388
- /* ---- Empty state ---- */
389
- .empty-state {
390
- color: var(--text-muted);
391
- font-style: italic;
392
- padding: 2rem;
393
- text-align: center;
394
- }
395
-
396
- /* ---- Table scroll wrapper ---- */
397
- .table-scroll {
398
- max-height: 400px;
399
- overflow-y: auto;
400
- }
401
-
402
- /* ---- Export button ---- */
403
- .export-btn {
404
- font-family: 'Poppins', sans-serif;
405
- font-size: 0.75rem;
406
- font-weight: 500;
407
- padding: 0.4rem 1rem;
408
- border: 1px solid var(--border);
409
- border-radius: var(--radius);
410
- background: var(--surface);
411
- color: var(--text-secondary);
412
- cursor: pointer;
413
- transition: all 0.15s;
414
- }
415
- .export-btn:hover {
416
- border-color: var(--accent);
417
- color: var(--accent);
418
- }
419
-
420
- /* ---- Live indicator ---- */
421
- .live-indicator {
422
- display: inline-flex;
423
- align-items: center;
424
- gap: 0.375rem;
425
- font-family: var(--mono);
426
- font-size: 0.6875rem;
427
- color: var(--green);
428
- }
429
- .live-dot {
430
- width: 6px;
431
- height: 6px;
432
- border-radius: 50%;
433
- background: var(--green);
434
- animation: pulse 2s ease-in-out infinite;
435
- }
436
- @keyframes pulse {
437
- 0%, 100% { opacity: 1; }
438
- 50% { opacity: 0.4; }
439
- }
440
-
441
- /* ---- Action buttons ---- */
442
- .action-btn {
443
- font-family: 'Poppins', sans-serif;
444
- font-size: 0.6875rem;
445
- font-weight: 500;
446
- padding: 0.3rem 0.75rem;
447
- border: 1px solid var(--border);
448
- border-radius: var(--radius);
449
- background: var(--surface);
450
- color: var(--text-secondary);
451
- cursor: pointer;
452
- transition: all 0.15s;
453
- white-space: nowrap;
454
- }
455
- .action-btn:hover:not(:disabled) {
456
- border-color: var(--accent);
457
- color: var(--accent);
458
- }
459
- .action-btn:disabled {
460
- opacity: 0.5;
461
- cursor: not-allowed;
462
- }
463
- .action-btn.loading {
464
- position: relative;
465
- color: transparent;
466
- }
467
- .action-btn.loading::after {
468
- content: '';
469
- position: absolute;
470
- inset: 0;
471
- display: flex;
472
- align-items: center;
473
- justify-content: center;
474
- color: var(--accent);
475
- font-size: 0.625rem;
476
- }
477
- .action-btn-group {
478
- display: flex;
479
- gap: 0.375rem;
480
- flex-wrap: wrap;
481
- }
482
- .action-result {
483
- font-family: var(--mono);
484
- font-size: 0.6875rem;
485
- padding: 0.5rem 0.75rem;
486
- margin-top: 0.375rem;
487
- border-radius: var(--radius);
488
- max-height: 120px;
489
- overflow-y: auto;
490
- display: none;
491
- }
492
- .action-result.visible { display: block; }
493
- .action-result.success { background: var(--green-bg); color: var(--green); }
494
- .action-result.error { background: var(--red-bg); color: var(--red); }
495
-
496
- /* ---- Evolution timeline ---- */
497
- .evo-timeline {
498
- position: relative;
499
- padding-left: 1.5rem;
500
- }
501
- .evo-timeline::before {
502
- content: '';
503
- position: absolute;
504
- left: 0.375rem;
505
- top: 0;
506
- bottom: 0;
507
- width: 2px;
508
- background: var(--border);
509
- }
510
- .evo-timeline-item {
511
- position: relative;
512
- padding: 0.625rem 0;
513
- padding-left: 0.75rem;
514
- border-bottom: none;
515
- }
516
- .evo-timeline-item::before {
517
- content: '';
518
- position: absolute;
519
- left: -1.125rem;
520
- top: 1rem;
521
- width: 8px;
522
- height: 8px;
523
- border-radius: 50%;
524
- background: var(--border);
525
- border: 2px solid var(--surface);
526
- }
527
- .evo-timeline-item.action-evolved::before { background: var(--green); }
528
- .evo-timeline-item.action-rolled-back::before { background: var(--red); }
529
- .evo-timeline-item.action-watched::before { background: var(--blue); }
530
- .evo-timeline-meta {
531
- font-family: var(--mono);
532
- font-size: 0.6875rem;
533
- color: var(--text-muted);
534
- margin-bottom: 0.25rem;
535
- }
536
- .evo-timeline-body {
537
- font-size: 0.8125rem;
538
- color: var(--text);
539
- }
540
- .evo-timeline-rationale {
541
- font-size: 0.75rem;
542
- color: var(--text-secondary);
543
- margin-top: 0.125rem;
544
- }
545
-
546
- /* ---- Search/Filter ---- */
547
- .search-filter {
548
- font-family: var(--mono);
549
- font-size: 0.8125rem;
550
- padding: 0.5rem 0.75rem;
551
- border: 1px solid var(--border);
552
- border-radius: var(--radius);
553
- background: var(--surface);
554
- color: var(--text);
555
- width: 100%;
556
- max-width: 320px;
557
- outline: none;
558
- transition: border-color 0.15s;
559
- }
560
- .search-filter:focus {
561
- border-color: var(--accent);
562
- }
563
- .search-filter::placeholder {
564
- color: var(--text-muted);
565
- }
566
-
567
- /* ---- Time Period Selector ---- */
568
- .time-period-selector {
569
- display: inline-flex;
570
- gap: 0;
571
- border: 1px solid var(--border);
572
- border-radius: var(--radius);
573
- overflow: hidden;
574
- }
575
- .time-period-selector .period-btn {
576
- font-family: 'Poppins', sans-serif;
577
- font-size: 0.6875rem;
578
- font-weight: 500;
579
- padding: 0.3rem 0.75rem;
580
- border: none;
581
- background: var(--surface);
582
- color: var(--text-secondary);
583
- cursor: pointer;
584
- transition: all 0.15s;
585
- border-right: 1px solid var(--border);
586
- }
587
- .time-period-selector .period-btn:last-child {
588
- border-right: none;
589
- }
590
- .time-period-selector .period-btn:hover {
591
- background: var(--bg);
592
- color: var(--accent);
593
- }
594
- .time-period-selector .period-btn.active {
595
- background: var(--accent);
596
- color: #fff;
597
- }
598
-
599
- /* ---- Eval Feed ---- */
600
- .eval-feed {
601
- width: 100%;
602
- border-collapse: collapse;
603
- font-size: 0.8125rem;
604
- }
605
- .eval-feed th, .eval-feed td {
606
- padding: 0.5rem 0.75rem;
607
- text-align: left;
608
- border-bottom: 1px solid var(--border);
609
- }
610
- .eval-feed th {
611
- font-family: 'Poppins', sans-serif;
612
- font-weight: 500;
613
- font-size: 0.6875rem;
614
- text-transform: uppercase;
615
- letter-spacing: 0.04em;
616
- color: var(--text-muted);
617
- background: var(--bg);
618
- position: sticky;
619
- top: 0;
620
- }
621
- .eval-feed tr:hover { background: var(--bg); }
622
- .eval-feed td.mono { font-family: var(--mono); font-size: 0.75rem; }
623
-
624
- /* ---- 4-state badge colors ---- */
625
- .badge-warning { background: var(--amber-bg); color: var(--amber); }
626
- .badge-critical { background: var(--red-bg); color: var(--red); }
627
- .badge-healthy { background: var(--green-bg); color: var(--green); }
628
- .badge-unknown { background: #f0f0ee; color: #999; }
629
- </style>
630
- </head>
631
- <body>
632
-
633
- <!-- ===== Header ===== -->
634
- <div class="header">
635
- <div class="header-left">
636
- <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 250 250" fill="none" style="flex-shrink:0" aria-hidden="true">
637
- <path d="M 190.16,31.49 C 187.91,29.88 184.51,32.19 185.88,35.16 C 186.31,36.11 187.08,36.54 187.71,37.01 C 218.75,59.86 237.63,92.71 237.63,128.82 C 237.63,175.99 205.12,218.56 153.82,234.69 C 149.89,235.93 150.91,241.71 154.91,240.66 C 205.98,226.96 243.01,181.94 243,128.45 C 242.99,90.87 223.47,56.18 190.16,31.49 Z" fill="currentColor"/>
638
- <path d="M 125.19,243.91 C 138.08,243.91 147.18,236.44 151.21,225.01 C 193.72,217.79 226.98,184.02 226.98,140.81 C 226.98,121.17 219.82,103.78 209.93,87.04 C 191.42,55.45 165.15,34.72 117.71,28.65 C 112.91,28.04 113.77,34.35 117.19,34.82 C 161.67,39.33 185.84,56.71 203.76,86.42 C 213.87,103.68 220.68,119.61 220.68,140.81 C 220.68,179.96 190.81,211.95 148.71,219.16 C 147.11,219.47 146.27,220.32 145.92,221.8 C 142.95,231.11 135.72,238.02 125.19,237.66 C 64.48,237.66 11.67,191.61 11.67,127.51 C 11.67,79.61 44.82,36.38 93.89,27.77 L 94.11,27.73 L 94.38,26.64 C 97.04,16.61 104.57,11.82 114.19,11.82 C 134.12,13.36 152.91,18.15 170.48,26.08 C 171.92,26.78 173.81,27.09 174.76,25.59 C 176.05,23.72 175.31,21.07 173.01,20.34 C 154.78,11.96 137.21,7.17 114.47,6 H 113.52 C 101.91,6 93.46,12.16 89.49,21.78 C 42.36,31.26 6.17,74.76 6.17,128.08 C 6.17,190.05 57.92,243.91 125.19,243.91 Z" fill="currentColor"/>
639
- <path d="M 93.67,40.64 C 100.51,52.07 109.54,51.33 114.05,52.17 C 128.72,53.91 141.48,55.78 157.38,62.16 C 162.72,64.47 162.29,58.19 159.18,57.01 C 145.11,51.33 132.48,49.79 111.31,47.48 C 101.83,46.29 95.45,41.18 93.75,32.81 C 55.21,39.46 22.06,72.17 22.06,112.48 C 22.06,131.98 30.36,149.82 43.26,164.49 C 46.23,167.59 50.19,164.13 48.32,161.02 C 36.21,145.54 28.42,129.78 28.42,112.4 C 28.42,79.11 54.91,48.36 89.91,40.36 C 90.76,40.15 91.04,39.87 91.62,40.01 C 92.62,40.01 93.04,39.65 93.67,40.64 Z" fill="currentColor"/>
640
- <path d="M 152.72,82.77 C 126.61,82.77 113.07,99.44 103.01,119.33 C 100.56,123.36 103.74,125.03 105.61,123.92 C 107.15,123.22 107.89,121.05 108.73,119.61 C 118.22,102.16 130.33,88.56 152.72,88.56 C 181.62,88.56 201.91,116.01 201.91,147.31 C 201.91,175.12 183.47,199.96 152.51,205.75 C 151.84,205.96 151.63,206.03 151.56,205.54 C 147.74,195.37 139.36,188.15 128.07,186.48 C 113.2,184.24 101.23,182.36 83.8,176.81 C 79.3,175.48 77.91,182.36 82.41,183.09 C 97.21,187.46 108.09,189.47 126.25,192.65 C 136.78,194.31 145.41,201.71 147.11,210.95 C 147.74,213.05 149.13,213.41 150.15,213.26 C 183.75,208.61 208.26,180.93 208.26,147.24 C 208.26,115.06 186.94,82.77 152.72,82.77 Z" fill="currentColor"/>
641
- <path d="M 129.77,105.21 C 122.93,112.05 118.97,122.73 113.77,130.41 C 111.31,133.45 114.56,136.63 117.46,134.46 C 123.75,126.23 127.43,115.62 135.15,108.71 C 138.22,105.81 134.73,101.09 129.77,105.21 Z" fill="currentColor"/>
642
- <path d="M 136.78,120.31 C 127.71,136.71 120.12,154.91 93.74,154.91 C 66.07,154.91 47.76,128.53 47.76,104.78 C 47.76,84.47 58.57,66.08 77.66,56.25 C 82.23,54.21 79.85,47.76 75.34,49.93 C 54.77,59.72 42.01,80.11 42.01,104.71 C 42.01,131.77 61.86,161.31 93.67,161.31 C 114.77,161.31 128.91,147.24 139.86,124.06 C 142.76,120.45 139.15,117.73 136.78,120.31 Z" fill="currentColor"/>
643
- <path d="M 30.73,154.7 C 27.76,152.97 23.87,155.93 25.41,158.76 C 41.73,188.36 68.94,199.79 105.75,206.41 C 112.25,207.66 122.07,208.75 123.46,209.03 C 128.07,209.95 128.07,220.18 121.78,220.18 C 107.64,218.94 92.06,215.98 76.23,211.33 C 72.13,210.24 71.04,216.69 75.27,217.64 C 90.41,222.22 103.95,224.74 120.47,226.54 C 133.73,226.54 136.56,209.03 126.03,203.38 C 123.75,202.13 122.73,202.56 112.04,200.76 C 78.09,195.04 54.06,188.98 32.12,155.65 C 31.77,155.23 31.28,154.91 30.73,154.7 Z" fill="currentColor"/>
644
- </svg>
645
- <h1>self<span>tune</span></h1>
646
- <span class="version">v0.1.4</span>
647
- </div>
648
- <div class="status" id="headerStatus">Drop log files to get started</div>
649
- </div>
650
-
651
- <!-- ===== Content ===== -->
652
- <div class="main" id="mainContent">
653
-
654
- <!-- Drop zone (shown when no data) -->
655
- <div class="drop-zone" id="dropZone" role="button" tabindex="0" aria-label="Load log files by clicking or dragging">
656
- <h2>Load Your Data</h2>
657
- <p>Drag &amp; drop your JSONL log files here, or click to browse.<br>
658
- Files are processed locally &mdash; nothing leaves your machine.</p>
659
- <div class="file-types">
660
- <span class="file-tag" id="tag-telemetry">session_telemetry_log.jsonl</span>
661
- <span class="file-tag" id="tag-skill">skill_usage_log.jsonl</span>
662
- <span class="file-tag" id="tag-query">all_queries_log.jsonl</span>
663
- <span class="file-tag" id="tag-evolution">evolution_audit_log.jsonl</span>
664
- </div>
665
- <input type="file" id="fileInput" multiple accept=".jsonl,.json">
666
- </div>
667
-
668
- <!-- ===== SYSTEM HEALTH SUMMARY ===== -->
669
- <div id="healthSummary" style="display:none;">
670
- <div class="kpi-row" id="kpiRow">
671
- <div class="kpi-card">
672
- <div class="kpi-label">Skills Monitored</div>
673
- <div class="kpi-value" id="kpi-skills-monitored">0</div>
674
- <div class="kpi-sub" id="kpi-skills-sub"></div>
675
- </div>
676
- <div class="kpi-card">
677
- <div class="kpi-label">Avg Pass Rate</div>
678
- <div class="kpi-value" id="kpi-avg-pass-rate">--</div>
679
- <div class="kpi-sub" id="kpi-pass-rate-sub"></div>
680
- </div>
681
- <div class="kpi-card">
682
- <div class="kpi-label">Regressions</div>
683
- <div class="kpi-value" id="kpi-regressions">0</div>
684
- <div class="kpi-sub" id="kpi-regressions-sub"></div>
685
- </div>
686
- <div class="kpi-card">
687
- <div class="kpi-label">Unmatched Queries</div>
688
- <div class="kpi-value" id="kpi-unmatched">0</div>
689
- <div class="kpi-sub" id="kpi-unmatched-sub"></div>
690
- </div>
691
- <div class="kpi-card">
692
- <div class="kpi-label">Sessions</div>
693
- <div class="kpi-value" id="kpi-sessions">0</div>
694
- <div class="kpi-sub" id="kpi-sessions-sub"></div>
695
- </div>
696
- <div class="kpi-card">
697
- <div class="kpi-label">Pending Proposals</div>
698
- <div class="kpi-value" id="kpi-pending">0</div>
699
- <div class="kpi-sub" id="kpi-pending-sub"></div>
700
- </div>
701
- </div>
702
- </div>
703
-
704
- <!-- ===== SKILL HEALTH GRID ===== -->
705
- <input id="skillSearchInput" placeholder="Filter skills..." class="search-filter" aria-label="Filter skills by name" style="margin-bottom:0.5rem;display:none;">
706
- <div class="section" id="skillHealthSection" style="display:none;">
707
- <div class="section-header">
708
- <span>Skill Health Grid</span>
709
- <button class="export-btn" id="exportCsvBtn">Export CSV</button>
710
- </div>
711
- <div class="skill-health-header">
712
- <span>Skill</span>
713
- <span>Pass Rate</span>
714
- <span>Trend</span>
715
- <span>Missed</span>
716
- <span>Status</span>
717
- </div>
718
- <div class="section-body skill-health-grid" id="skillHealthGrid">
719
- <div class="empty-state">Load data to see skill health</div>
720
- </div>
721
- </div>
722
-
723
- <!-- ===== DRILL-DOWN PANEL ===== -->
724
- <div class="drill-down-panel" id="drillDownPanel">
725
- <div class="drill-down-header">
726
- <span id="drillDownTitle">Skill Details</span>
727
- <button class="drill-down-close" id="drillDownClose">Close</button>
728
- </div>
729
- <div class="drill-down-content">
730
- <div class="drill-down-section">
731
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem;">
732
- <h4 style="font-family:'Poppins',sans-serif;font-size:0.75rem;font-weight:500;text-transform:uppercase;letter-spacing:0.04em;color:var(--text-muted);">Pass Rate Over Time</h4>
733
- <div class="time-period-selector" id="timePeriodSelector">
734
- <button class="period-btn" data-days="7">7d</button>
735
- <button class="period-btn" data-days="30">30d</button>
736
- <button class="period-btn" data-days="90">90d</button>
737
- <button class="period-btn active" data-days="0">All</button>
738
- </div>
739
- </div>
740
- <div class="chart-container"><canvas id="chartDrillPassRate"></canvas></div>
741
- </div>
742
- <div class="drill-down-section">
743
- <h4 style="font-family:'Poppins',sans-serif;font-size:0.75rem;font-weight:500;text-transform:uppercase;letter-spacing:0.04em;color:var(--text-muted);margin-bottom:0.75rem;">Missed Queries</h4>
744
- <div class="table-scroll" style="max-height:260px;">
745
- <table class="data-table" id="drillMissedTable">
746
- <thead><tr><th>Timestamp</th><th>Session</th><th>Query</th></tr></thead>
747
- <tbody></tbody>
748
- </table>
749
- </div>
750
- </div>
751
- <div class="drill-down-section">
752
- <h4 style="font-family:'Poppins',sans-serif;font-size:0.75rem;font-weight:500;text-transform:uppercase;letter-spacing:0.04em;color:var(--text-muted);margin-bottom:0.75rem;">Evolution History</h4>
753
- <div id="drillEvoTimeline" class="table-scroll" style="max-height:260px;">
754
- <div class="empty-state">No evolution history</div>
755
- </div>
756
- </div>
757
- <div class="drill-down-section">
758
- <h4 style="font-family:'Poppins',sans-serif;font-size:0.75rem;font-weight:500;text-transform:uppercase;letter-spacing:0.04em;color:var(--text-muted);margin-bottom:0.75rem;">Sessions</h4>
759
- <div class="table-scroll" style="max-height:260px;">
760
- <table class="data-table" id="drillSessionsTable">
761
- <thead><tr><th>Timestamp</th><th>Tools</th><th>Skills</th><th>Errors</th></tr></thead>
762
- <tbody></tbody>
763
- </table>
764
- </div>
765
- </div>
766
- <div class="drill-down-section">
767
- <h4 style="font-family:'Poppins',sans-serif;font-size:0.75rem;font-weight:500;text-transform:uppercase;letter-spacing:0.04em;color:var(--text-muted);margin-bottom:0.75rem;">Evaluation Feed</h4>
768
- <div class="table-scroll" style="max-height:260px;">
769
- <table class="eval-feed" id="drillEvalFeed">
770
- <thead><tr><th>Time</th><th>Query</th><th>Triggered</th><th>Type</th></tr></thead>
771
- <tbody></tbody>
772
- </table>
773
- </div>
774
- </div>
775
- <div class="drill-down-section">
776
- <h4 style="font-family:'Poppins',sans-serif;font-size:0.75rem;font-weight:500;text-transform:uppercase;letter-spacing:0.04em;color:var(--text-muted);margin-bottom:0.75rem;">Invocation Breakdown</h4>
777
- <div class="chart-container"><canvas id="chartInvocationBreakdown"></canvas></div>
778
- </div>
779
- </div>
780
- </div>
781
-
782
- <!-- ===== UNMATCHED QUERIES ===== -->
783
- <div class="section" id="unmatchedSection" style="display:none;">
784
- <div class="section-header">Unmatched Queries</div>
785
- <div class="section-body">
786
- <div class="table-scroll">
787
- <table class="data-table" id="unmatchedTable">
788
- <thead><tr><th>Timestamp</th><th>Session</th><th>Query</th></tr></thead>
789
- <tbody></tbody>
790
- </table>
791
- </div>
792
- </div>
793
- </div>
794
-
795
- <!-- ===== PENDING PROPOSALS ===== -->
796
- <div class="section" id="pendingSection" style="display:none;">
797
- <div class="section-header">Pending Proposals</div>
798
- <div class="section-body" id="pendingProposals">
799
- <div class="empty-state">No pending proposals</div>
800
- </div>
801
- </div>
802
-
803
- <!-- ===== SKILL ACTIONS (live mode only) ===== -->
804
- <div class="section" id="actionsSection" style="display:none;">
805
- <div class="section-header">
806
- <span>Skill Actions</span>
807
- <span class="live-indicator" id="liveIndicator"><span class="live-dot"></span> LIVE</span>
808
- </div>
809
- <div class="section-body" id="actionsBody">
810
- <div class="empty-state">Select a skill from the health grid to see actions</div>
811
- </div>
812
- </div>
813
-
814
- <!-- ===== EVOLUTION TIMELINE (live mode only) ===== -->
815
- <div class="section" id="evoTimelineSection" style="display:none;">
816
- <div class="section-header">Evolution Timeline</div>
817
- <div class="section-body">
818
- <div class="evo-timeline" id="evoTimeline">
819
- <div class="empty-state">No evolution decisions recorded</div>
820
- </div>
821
- </div>
822
- </div>
823
-
824
- </div>
825
-
826
- <script>
827
- // ========================================================================
828
- // State
829
- // ========================================================================
830
- const state = {
831
- telemetry: [], // SessionTelemetryRecord[]
832
- skills: [], // SkillUsageRecord[]
833
- queries: [], // QueryLogRecord[]
834
- evolution: [], // EvolutionAuditEntry[]
835
- computed: null, // Pre-computed monitoring data (from CLI)
836
- };
837
-
838
- const charts = {};
839
- let selectedSkill = null;
840
- let selectedPeriodDays = 0; // 0 = All
841
-
842
- // ========================================================================
843
- // File identification
844
- // ========================================================================
845
- function identifyFile(name, firstLine) {
846
- if (name.includes('session_telemetry')) return 'telemetry';
847
- if (name.includes('skill_usage')) return 'skills';
848
- if (name.includes('all_queries')) return 'queries';
849
- if (name.includes('evolution_audit')) return 'evolution';
850
- if (firstLine) {
851
- try {
852
- const obj = JSON.parse(firstLine);
853
- if ('total_tool_calls' in obj || 'transcript_path' in obj) return 'telemetry';
854
- if ('skill_name' in obj && 'triggered' in obj) return 'skills';
855
- if ('query' in obj && !('skill_name' in obj)) return 'queries';
856
- if ('proposal_id' in obj && 'action' in obj) return 'evolution';
857
- } catch {}
858
- }
859
- return null;
860
- }
861
-
862
- function parseJSONL(text) {
863
- return text.trim().split('\n').filter(Boolean).map(line => {
864
- try { return JSON.parse(line); }
865
- catch { return null; }
866
- }).filter(Boolean);
867
- }
868
-
869
- // ========================================================================
870
- // Client-side computed data generation (for drag-drop mode)
871
- // ========================================================================
872
- const REGRESSION_THRESHOLD = 0.4;
873
- const DEFAULT_BASELINE_PASS_RATE = 0.5;
874
-
875
- function computeClientSide() {
876
- const skillNames = [...new Set(state.skills.map(r => r.skill_name))];
877
- const triggeredQueries = new Set(
878
- state.skills.filter(r => r.triggered).map(r => r.query.toLowerCase().trim())
879
- );
880
-
881
- // Per-skill snapshots
882
- const snapshots = {};
883
- for (const name of skillNames) {
884
- const skillRecords = state.skills.filter(r => r.skill_name === name);
885
- const triggered = skillRecords.filter(r => r.triggered).length;
886
- const total = state.queries.length;
887
- const passRate = total === 0 ? 1.0 : triggered / total;
888
- const falseNegatives = skillRecords.filter(r => !r.triggered).length;
889
- const fnRate = skillRecords.length === 0 ? 0 : falseNegatives / skillRecords.length;
890
- snapshots[name] = {
891
- timestamp: new Date().toISOString(),
892
- skill_name: name,
893
- window_sessions: state.telemetry.length,
894
- pass_rate: passRate,
895
- false_negative_rate: fnRate,
896
- regression_detected: passRate < REGRESSION_THRESHOLD,
897
- baseline_pass_rate: DEFAULT_BASELINE_PASS_RATE,
898
- };
899
- }
900
-
901
- // Unmatched queries
902
- const unmatched = state.queries.filter(q =>
903
- !triggeredQueries.has(q.query.toLowerCase().trim())
904
- ).map(q => ({ timestamp: q.timestamp, session_id: q.session_id, query: q.query }));
905
-
906
- // Pending proposals
907
- const proposalStatus = {};
908
- for (const e of state.evolution) {
909
- if (!proposalStatus[e.proposal_id]) proposalStatus[e.proposal_id] = [];
910
- proposalStatus[e.proposal_id].push(e.action);
911
- }
912
- const seenProposals = new Set();
913
- const pendingProposals = state.evolution.filter(e => {
914
- if (e.action !== 'created' && e.action !== 'validated') return false;
915
- const actions = proposalStatus[e.proposal_id] || [];
916
- if (actions.includes('deployed') || actions.includes('rejected') || actions.includes('rolled_back')) return false;
917
- if (seenProposals.has(e.proposal_id)) return false;
918
- seenProposals.add(e.proposal_id);
919
- return true;
920
- });
921
-
922
- return { snapshots, unmatched, pendingProposals };
923
- }
924
-
925
- // ========================================================================
926
- // File loading
927
- // ========================================================================
928
- async function handleFiles(files) {
929
- for (const file of files) {
930
- const text = await file.text();
931
- const lines = text.trim().split('\n').filter(Boolean);
932
- if (!lines.length) continue;
933
- const type = identifyFile(file.name, lines[0]);
934
- if (!type) { console.warn('Unknown file type:', file.name); continue; }
935
- state[type] = parseJSONL(text);
936
- const tag = document.getElementById(`tag-${type === 'skills' ? 'skill' : type}`);
937
- if (tag) tag.classList.add('loaded');
938
- }
939
- state.computed = computeClientSide();
940
- refreshAll();
941
- }
942
-
943
- // ========================================================================
944
- // Drag & drop + click
945
- // ========================================================================
946
- const dropZone = document.getElementById('dropZone');
947
- const fileInput = document.getElementById('fileInput');
948
-
949
- dropZone.addEventListener('click', () => fileInput.click());
950
- dropZone.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); fileInput.click(); } });
951
- fileInput.addEventListener('change', e => handleFiles(e.target.files));
952
-
953
- dropZone.addEventListener('dragover', e => {
954
- e.preventDefault();
955
- dropZone.classList.add('drag-over');
956
- });
957
- dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
958
- dropZone.addEventListener('drop', e => {
959
- e.preventDefault();
960
- dropZone.classList.remove('drag-over');
961
- handleFiles(e.dataTransfer.files);
962
- });
963
-
964
- // ========================================================================
965
- // Data loading from embedded JSON (when served by CLI)
966
- // ========================================================================
967
- function loadEmbeddedData() {
968
- const el = document.getElementById('embedded-data');
969
- if (!el) return false;
970
- try {
971
- const data = JSON.parse(el.textContent);
972
- if (data.telemetry) state.telemetry = data.telemetry;
973
- if (data.skills) state.skills = data.skills;
974
- if (data.queries) state.queries = data.queries;
975
- if (data.evolution) state.evolution = data.evolution;
976
- if (data.computed) {
977
- state.computed = data.computed;
978
- } else {
979
- state.computed = computeClientSide();
980
- }
981
- return state.telemetry.length || state.skills.length || state.queries.length || state.evolution.length;
982
- } catch { return false; }
983
- }
984
-
985
- // ========================================================================
986
- // Helpers
987
- // ========================================================================
988
- function toDate(ts) { return new Date(ts); }
989
-
990
- function formatDate(ts) {
991
- const d = toDate(ts);
992
- return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
993
- }
994
-
995
- function toDayKey(ts) { return new Date(ts).toISOString().slice(0, 10); }
996
-
997
- function formatTimestamp(ts) {
998
- const d = toDate(ts);
999
- return d.toLocaleString('en-US', {
1000
- month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
1001
- });
1002
- }
1003
-
1004
- function truncate(s, max = 60) {
1005
- if (!s) return '\u2014';
1006
- return s.length > max ? s.slice(0, max) + '...' : s;
1007
- }
1008
-
1009
- function escapeHtml(s) {
1010
- if (!s) return '';
1011
- return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1012
- }
1013
-
1014
- function groupByDay(records) {
1015
- const map = {};
1016
- for (const r of records) {
1017
- const day = toDayKey(r.timestamp);
1018
- map[day] = (map[day] || 0) + 1;
1019
- }
1020
- return map;
1021
- }
1022
-
1023
- function getSkillStatus(passRate, regressionDetected) {
1024
- if (passRate === null || passRate === undefined) return 'unknown';
1025
- if (regressionDetected || passRate < 0.4) return 'critical';
1026
- if (passRate < 0.7) return 'warning';
1027
- return 'healthy';
1028
- }
1029
-
1030
- function getStatusBadge(status) {
1031
- const map = {
1032
- healthy: '<span class="badge badge-healthy">Healthy</span>',
1033
- warning: '<span class="badge badge-warning">Warning</span>',
1034
- critical: '<span class="badge badge-critical">Critical</span>',
1035
- unknown: '<span class="badge badge-unknown">Unknown</span>',
1036
- };
1037
- return map[status] || '';
1038
- }
1039
-
1040
- const CHART_COLORS = [
1041
- '#d97757', '#788c5d', '#4a7fd4', '#c49133', '#9b6bb0',
1042
- '#5ba3a3', '#d46a9f', '#7b8fa1', '#c47a5a', '#6b9b6b'
1043
- ];
1044
-
1045
- // ========================================================================
1046
- // Refresh all views
1047
- // ========================================================================
1048
- function refreshAll() {
1049
- const hasData = state.telemetry.length || state.skills.length ||
1050
- state.queries.length || state.evolution.length;
1051
-
1052
- if (hasData) {
1053
- dropZone.style.display = 'none';
1054
- document.getElementById('healthSummary').style.display = 'block';
1055
- document.getElementById('skillHealthSection').style.display = 'block';
1056
- document.getElementById('skillSearchInput').style.display = 'block';
1057
- }
1058
-
1059
- updateHeader();
1060
- updateHealthSummary();
1061
- updateSkillHealthGrid();
1062
- updateUnmatched();
1063
- updatePending();
1064
- }
1065
-
1066
- // ========================================================================
1067
- // Header
1068
- // ========================================================================
1069
- function updateHeader() {
1070
- const parts = [];
1071
- if (state.telemetry.length) parts.push(`<span class="count">${state.telemetry.length}</span> sessions`);
1072
- if (state.skills.length) parts.push(`<span class="count">${state.skills.length}</span> skill events`);
1073
- if (state.queries.length) parts.push(`<span class="count">${state.queries.length}</span> queries`);
1074
- if (state.evolution.length) parts.push(`<span class="count">${state.evolution.length}</span> evolution actions`);
1075
- document.getElementById('headerStatus').innerHTML = parts.length ? parts.join(' &middot; ') : 'Drop log files to get started';
1076
- }
1077
-
1078
- // ========================================================================
1079
- // Health Summary KPIs
1080
- // ========================================================================
1081
- function updateHealthSummary() {
1082
- const computed = state.computed;
1083
- if (!computed) return;
1084
-
1085
- const snapshots = computed.snapshots || {};
1086
- const skillNames = Object.keys(snapshots);
1087
- const regressions = skillNames.filter(n => snapshots[n].regression_detected);
1088
-
1089
- document.getElementById('kpi-skills-monitored').textContent = skillNames.length;
1090
- document.getElementById('kpi-skills-sub').textContent =
1091
- regressions.length ? `${regressions.length} need attention` : 'all stable';
1092
-
1093
- if (skillNames.length > 0) {
1094
- const avgPR = skillNames.reduce((sum, n) => sum + snapshots[n].pass_rate, 0) / skillNames.length;
1095
- document.getElementById('kpi-avg-pass-rate').textContent = (avgPR * 100).toFixed(0) + '%';
1096
- const status = getSkillStatus(avgPR, false);
1097
- document.getElementById('kpi-pass-rate-sub').textContent =
1098
- status === 'healthy' ? 'system healthy' : status === 'warning' ? 'needs monitoring' : status === 'critical' ? 'action required' : 'no data';
1099
- }
1100
-
1101
- document.getElementById('kpi-regressions').textContent = regressions.length;
1102
- document.getElementById('kpi-regressions-sub').textContent =
1103
- regressions.length ? regressions.join(', ') : 'none detected';
1104
-
1105
- const unmatched = computed.unmatched || [];
1106
- document.getElementById('kpi-unmatched').textContent = unmatched.length;
1107
- document.getElementById('kpi-unmatched-sub').textContent =
1108
- unmatched.length ? 'queries not matched to any skill' : 'all queries matched';
1109
-
1110
- document.getElementById('kpi-sessions').textContent = state.telemetry.length;
1111
- if (state.telemetry.length) {
1112
- const sorted = [...state.telemetry].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
1113
- const first = formatDate(sorted[0].timestamp);
1114
- const last = formatDate(sorted[sorted.length - 1].timestamp);
1115
- document.getElementById('kpi-sessions-sub').textContent = first + ' \u2014 ' + last;
1116
- }
1117
-
1118
- const pending = computed.pendingProposals || [];
1119
- document.getElementById('kpi-pending').textContent = pending.length;
1120
- document.getElementById('kpi-pending-sub').textContent =
1121
- pending.length ? 'awaiting deployment' : 'none pending';
1122
- }
1123
-
1124
- // ========================================================================
1125
- // Skill Health Grid
1126
- // ========================================================================
1127
- function updateSkillHealthGrid() {
1128
- const computed = state.computed;
1129
- if (!computed) return;
1130
-
1131
- const snapshots = computed.snapshots || {};
1132
- const skillNames = Object.keys(snapshots);
1133
-
1134
- if (!skillNames.length) return;
1135
-
1136
- // Sort worst-first
1137
- const sorted = skillNames.sort((a, b) => {
1138
- const sa = snapshots[a];
1139
- const sb = snapshots[b];
1140
- if (sa.regression_detected && !sb.regression_detected) return -1;
1141
- if (!sa.regression_detected && sb.regression_detected) return 1;
1142
- return sa.pass_rate - sb.pass_rate;
1143
- });
1144
-
1145
- const grid = document.getElementById('skillHealthGrid');
1146
- grid.innerHTML = sorted.map(name => {
1147
- const snap = snapshots[name];
1148
- const status = getSkillStatus(snap.pass_rate, snap.regression_detected);
1149
- const pct = (snap.pass_rate * 100).toFixed(0);
1150
- const missed = state.skills.filter(r => r.skill_name === name && !r.triggered).length;
1151
- const trend = snap.regression_detected ? '\u2193' : (snap.pass_rate >= snap.baseline_pass_rate ? '\u2191' : '\u2192');
1152
- const trendClass = snap.regression_detected ? 'trend-down' : (snap.pass_rate >= snap.baseline_pass_rate ? 'trend-up' : 'trend-flat');
1153
-
1154
- const safeName = escapeHtml(name);
1155
- return `<div class="skill-health-row" data-skill="${safeName}" role="button" tabindex="0" aria-label="View details for skill ${safeName}">
1156
- <div class="skill-name" title="${safeName}">${safeName}</div>
1157
- <div class="pass-rate-bar">
1158
- <div class="pass-rate-track">
1159
- <div class="pass-rate-fill ${status}" style="width:${pct}%"></div>
1160
- </div>
1161
- <div class="pass-rate-label">${pct}%</div>
1162
- </div>
1163
- <div class="trend-arrow ${trendClass}">${trend}</div>
1164
- <div class="missed-count">${missed}</div>
1165
- <div>${getStatusBadge(status)}</div>
1166
- </div>`;
1167
- }).join('');
1168
-
1169
- // Click + keyboard handlers for drill-down
1170
- grid.querySelectorAll('.skill-health-row').forEach(row => {
1171
- const handler = () => {
1172
- const skillName = row.dataset.skill;
1173
- grid.querySelectorAll('.skill-health-row').forEach(r => r.classList.remove('selected'));
1174
- row.classList.add('selected');
1175
- openDrillDown(skillName);
1176
- };
1177
- row.addEventListener('click', handler);
1178
- row.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); } });
1179
- });
1180
-
1181
- // Reapply search filter after grid rebuild
1182
- const searchInput = document.getElementById('skillSearchInput');
1183
- if (searchInput && searchInput.value) {
1184
- const query = searchInput.value.toLowerCase();
1185
- grid.querySelectorAll('.skill-health-row').forEach(row => {
1186
- const name = (row.dataset.skill || '').toLowerCase();
1187
- row.style.display = name.includes(query) ? '' : 'none';
1188
- });
1189
- }
1190
- }
1191
-
1192
- // ========================================================================
1193
- // Drill-down panel
1194
- // ========================================================================
1195
- function openDrillDown(skillName) {
1196
- selectedSkill = skillName;
1197
- const panel = document.getElementById('drillDownPanel');
1198
- panel.classList.add('visible');
1199
- document.getElementById('drillDownTitle').textContent = `Skill: ${skillName}`;
1200
-
1201
- // Pass rate over time chart
1202
- updateDrillPassRateChart(skillName);
1203
- updateDrillMissedQueries(skillName);
1204
- updateDrillEvolution(skillName);
1205
- updateDrillSessions(skillName);
1206
- updateDrillEvalFeed(skillName);
1207
- updateDrillInvocationBreakdown(skillName);
1208
- }
1209
-
1210
- document.getElementById('drillDownClose').addEventListener('click', () => {
1211
- document.getElementById('drillDownPanel').classList.remove('visible');
1212
- document.getElementById('skillHealthGrid').querySelectorAll('.skill-health-row').forEach(r => r.classList.remove('selected'));
1213
- selectedSkill = null;
1214
- });
1215
-
1216
- function updateDrillPassRateChart(skillName) {
1217
- // Group skill records by day and compute daily pass rate
1218
- const allRecords = state.skills.filter(r => r.skill_name === skillName);
1219
- const records = filterByPeriod(allRecords, selectedPeriodDays);
1220
- const byDay = {};
1221
- for (const r of records) {
1222
- const day = toDayKey(r.timestamp);
1223
- if (!byDay[day]) byDay[day] = { triggered: 0, total: 0 };
1224
- byDay[day].total++;
1225
- if (r.triggered) byDay[day].triggered++;
1226
- }
1227
-
1228
- const dayKeys = Object.keys(byDay).sort();
1229
- const labels = dayKeys.map(d => formatDate(d + "T00:00:00Z"));
1230
- const data = dayKeys.map(d => ((byDay[d].triggered / byDay[d].total) * 100).toFixed(1));
1231
-
1232
- // Deploy events as annotations
1233
- const deployDays = new Set(
1234
- state.evolution
1235
- .filter(e => e.action === 'deployed' && (e.details || '').toLowerCase().includes(skillName.toLowerCase()))
1236
- .map(e => toDayKey(e.timestamp))
1237
- );
1238
-
1239
- const pointColors = dayKeys.map(d => deployDays.has(d) ? '#d97757' : '#788c5d');
1240
- const pointSizes = dayKeys.map(d => deployDays.has(d) ? 8 : 3);
1241
-
1242
- if (charts.drillPassRate) charts.drillPassRate.destroy();
1243
- charts.drillPassRate = new Chart(document.getElementById('chartDrillPassRate'), {
1244
- type: 'line',
1245
- data: {
1246
- labels,
1247
- datasets: [{
1248
- label: 'Pass Rate %',
1249
- data,
1250
- borderColor: '#788c5d',
1251
- backgroundColor: 'rgba(120, 140, 93, 0.1)',
1252
- fill: true,
1253
- tension: 0.3,
1254
- pointRadius: pointSizes,
1255
- pointBackgroundColor: pointColors,
1256
- }]
1257
- },
1258
- options: {
1259
- responsive: true,
1260
- maintainAspectRatio: false,
1261
- plugins: { legend: { display: false } },
1262
- scales: {
1263
- y: { min: 0, max: 100, ticks: { callback: v => v + '%' } },
1264
- x: { grid: { display: false } }
1265
- }
1266
- }
1267
- });
1268
- }
1269
-
1270
- function updateDrillMissedQueries(skillName) {
1271
- const missed = state.skills.filter(r => r.skill_name === skillName && !r.triggered);
1272
- const tbody = document.querySelector('#drillMissedTable tbody');
1273
- tbody.innerHTML = missed.slice(0, 50).map(r => `<tr>
1274
- <td class="mono">${escapeHtml(formatTimestamp(r.timestamp))}</td>
1275
- <td class="mono">${escapeHtml((r.session_id || '').slice(0, 8))}</td>
1276
- <td>${escapeHtml(truncate(r.query, 50))}</td>
1277
- </tr>`).join('') || '<tr><td colspan="3" class="empty-state">No missed queries</td></tr>';
1278
- }
1279
-
1280
- function updateDrillEvolution(skillName) {
1281
- const needle = skillName.toLowerCase();
1282
- const entries = state.evolution.filter(e => (e.details || '').toLowerCase().includes(needle));
1283
- const container = document.getElementById('drillEvoTimeline');
1284
- const actionBadge = {
1285
- created: 'badge-blue', validated: 'badge-amber', deployed: 'badge-green',
1286
- rolled_back: 'badge-red', rejected: 'badge-red',
1287
- };
1288
- if (entries.length) {
1289
- const sorted = [...entries].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
1290
- container.innerHTML = sorted.map(r => `<div class="timeline-item">
1291
- <div class="timeline-date">${escapeHtml(formatTimestamp(r.timestamp))}</div>
1292
- <div><span class="badge ${actionBadge[r.action] || 'badge-blue'}">${escapeHtml(r.action)}</span>
1293
- <span class="timeline-action" style="margin-left:0.5rem">${escapeHtml(r.proposal_id.slice(0, 8))}</span>
1294
- </div>
1295
- <div style="flex:1;color:var(--text-secondary);font-size:0.8125rem;">${escapeHtml(truncate(r.details, 60))}</div>
1296
- </div>`).join('');
1297
- } else {
1298
- container.innerHTML = '<div class="empty-state">No evolution history for this skill</div>';
1299
- }
1300
- }
1301
-
1302
- function updateDrillSessions(skillName) {
1303
- const sessionIds = new Set(
1304
- state.skills.filter(r => r.skill_name === skillName).map(r => r.session_id)
1305
- );
1306
- const sessions = state.telemetry.filter(r => sessionIds.has(r.session_id));
1307
- const tbody = document.querySelector('#drillSessionsTable tbody');
1308
- const sorted = [...sessions].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
1309
- tbody.innerHTML = sorted.slice(0, 30).map(r => {
1310
- const skills = (r.skills_triggered || []).join(', ') || '\u2014';
1311
- const errorCount = Number.isFinite(Number(r.errors_encountered)) ? Number(r.errors_encountered) : 0;
1312
- const totalToolCalls = Number.isFinite(Number(r.total_tool_calls)) ? Number(r.total_tool_calls) : 0;
1313
- const errorBadge = errorCount > 0
1314
- ? `<span class="badge badge-red">${errorCount}</span>`
1315
- : '<span class="badge badge-green">0</span>';
1316
- return `<tr>
1317
- <td class="mono">${escapeHtml(formatTimestamp(r.timestamp))}</td>
1318
- <td>${totalToolCalls}</td>
1319
- <td>${escapeHtml(truncate(skills, 30))}</td>
1320
- <td>${errorBadge}</td>
1321
- </tr>`;
1322
- }).join('') || '<tr><td colspan="4" class="empty-state">No sessions</td></tr>';
1323
- }
1324
-
1325
- // ========================================================================
1326
- // Unmatched queries section
1327
- // ========================================================================
1328
- function updateUnmatched() {
1329
- const computed = state.computed;
1330
- if (!computed) return;
1331
- const unmatched = computed.unmatched || [];
1332
- if (!unmatched.length) return;
1333
-
1334
- document.getElementById('unmatchedSection').style.display = 'block';
1335
- const tbody = document.querySelector('#unmatchedTable tbody');
1336
- tbody.innerHTML = unmatched.slice(0, 100).map(q => `<tr>
1337
- <td class="mono">${escapeHtml(formatTimestamp(q.timestamp))}</td>
1338
- <td class="mono">${escapeHtml((q.session_id || '').slice(0, 8))}</td>
1339
- <td>${escapeHtml(truncate(q.query, 60))}</td>
1340
- </tr>`).join('');
1341
- }
1342
-
1343
- // ========================================================================
1344
- // Pending proposals section
1345
- // ========================================================================
1346
- function updatePending() {
1347
- const computed = state.computed;
1348
- if (!computed) return;
1349
- const pending = computed.pendingProposals || [];
1350
- if (!pending.length) return;
1351
-
1352
- document.getElementById('pendingSection').style.display = 'block';
1353
- const container = document.getElementById('pendingProposals');
1354
- const actionBadge = {
1355
- created: 'badge-blue', validated: 'badge-amber',
1356
- };
1357
- container.innerHTML = pending.map(r => `<div class="timeline-item">
1358
- <div class="timeline-date">${escapeHtml(formatTimestamp(r.timestamp))}</div>
1359
- <div><span class="badge ${actionBadge[r.action] || 'badge-blue'}">${escapeHtml(r.action)}</span>
1360
- <span class="timeline-action" style="margin-left:0.5rem">${escapeHtml(r.proposal_id.slice(0, 8))}</span>
1361
- </div>
1362
- <div style="flex:1;color:var(--text-secondary);font-size:0.8125rem;">${escapeHtml(truncate(r.details, 80))}</div>
1363
- </div>`).join('');
1364
- }
1365
-
1366
- // ========================================================================
1367
- // CSV export
1368
- // ========================================================================
1369
- document.getElementById('exportCsvBtn').addEventListener('click', () => {
1370
- if (!state.computed || !state.computed.snapshots) return;
1371
- const snapshots = state.computed.snapshots;
1372
- const headers = ['skill_name','pass_rate','regression_detected','baseline_pass_rate','window_sessions','false_negative_rate'];
1373
- const rows = Object.keys(snapshots).map(name => {
1374
- const s = snapshots[name];
1375
- return headers.map(h => {
1376
- const v = s[h];
1377
- if (typeof v === 'boolean') return v ? 'true' : 'false';
1378
- if (typeof v === 'number') return v.toFixed(4);
1379
- if (typeof v === 'string' && (v.includes(',') || v.includes('"')))
1380
- return '"' + v.replace(/"/g, '""') + '"';
1381
- return v ?? '';
1382
- }).join(',');
1383
- });
1384
- const csv = [headers.join(','), ...rows].join('\n');
1385
- const blob = new Blob([csv], { type: 'text/csv' });
1386
- const a = document.createElement('a');
1387
- a.href = URL.createObjectURL(blob);
1388
- a.download = 'selftune-skill-health.csv';
1389
- a.click();
1390
- });
1391
-
1392
- // ========================================================================
1393
- // Live mode: SSE client + action buttons + evolution timeline
1394
- // ========================================================================
1395
- let sseSource = null;
1396
-
1397
- function isLiveMode() {
1398
- return window.__SELFTUNE_LIVE__ === true;
1399
- }
1400
-
1401
- function startSSE() {
1402
- if (!isLiveMode()) return;
1403
- if (sseSource) { sseSource.close(); sseSource = null; }
1404
-
1405
- sseSource = new EventSource('/api/events');
1406
- sseSource.addEventListener('data', (e) => {
1407
- try {
1408
- const data = JSON.parse(e.data);
1409
- if (data.telemetry) state.telemetry = data.telemetry;
1410
- if (data.skills) state.skills = data.skills;
1411
- if (data.queries) state.queries = data.queries;
1412
- if (data.evolution) state.evolution = data.evolution;
1413
- if (data.decisions) state.decisions = data.decisions;
1414
- if (data.computed) {
1415
- state.computed = data.computed;
1416
- } else {
1417
- state.computed = computeClientSide();
1418
- }
1419
- refreshAll();
1420
- updateEvolutionTimeline();
1421
- } catch (err) { console.warn('[selftune] SSE parse error:', err); }
1422
- });
1423
- sseSource.onerror = () => {
1424
- // Reconnect after 3 seconds on error
1425
- setTimeout(() => { if (isLiveMode()) startSSE(); }, 3000);
1426
- };
1427
- }
1428
-
1429
- // Decisions state (populated from server in live mode)
1430
- if (!state.decisions) state.decisions = [];
1431
-
1432
- function updateEvolutionTimeline() {
1433
- if (!isLiveMode()) return;
1434
- const decisions = state.decisions || [];
1435
- const section = document.getElementById('evoTimelineSection');
1436
- if (!section) return;
1437
-
1438
- section.style.display = 'block';
1439
- const container = document.getElementById('evoTimeline');
1440
- if (!decisions.length) {
1441
- container.innerHTML = '<div class="empty-state">No evolution decisions recorded</div>';
1442
- return;
1443
- }
1444
-
1445
- // Show most recent first
1446
- const sorted = [...decisions].reverse();
1447
- container.innerHTML = sorted.slice(0, 50).map(d => {
1448
- const actionClass = 'action-' + escapeHtml(d.action || '');
1449
- return `<div class="evo-timeline-item ${actionClass}">
1450
- <div class="evo-timeline-meta">${escapeHtml(formatTimestamp(d.timestamp))} &middot; ${escapeHtml(d.skillName)}</div>
1451
- <div class="evo-timeline-body">
1452
- <span class="badge ${d.action === 'evolved' ? 'badge-green' : d.action === 'rolled-back' ? 'badge-red' : 'badge-blue'}">${escapeHtml(d.action)}</span>
1453
- <span style="margin-left:0.375rem;">${escapeHtml(d.actionType)}</span>
1454
- </div>
1455
- <div class="evo-timeline-rationale">${escapeHtml(truncate(d.rationale, 100))}</div>
1456
- </div>`;
1457
- }).join('');
1458
- }
1459
-
1460
- function showActionButtons(skillName) {
1461
- if (!isLiveMode()) return;
1462
- const section = document.getElementById('actionsSection');
1463
- const body = document.getElementById('actionsBody');
1464
- if (!section || !body) return;
1465
-
1466
- section.style.display = 'block';
1467
-
1468
- // Find skill path from skill records
1469
- const skillRecord = state.skills.find(r => r.skill_name === skillName);
1470
- const skillPath = skillRecord ? skillRecord.skill_path : '';
1471
- const safeSkill = escapeHtml(skillName);
1472
- const safeSkillPath = escapeHtml(skillPath);
1473
-
1474
- body.innerHTML = `
1475
- <div style="margin-bottom:0.5rem;font-family:'Poppins',sans-serif;font-size:0.8125rem;font-weight:600;">${safeSkill}</div>
1476
- <div class="action-btn-group">
1477
- <button class="action-btn" id="btn-watch" data-skill="${safeSkill}" data-path="${safeSkillPath}">Watch</button>
1478
- <button class="action-btn" id="btn-evolve" data-skill="${safeSkill}" data-path="${safeSkillPath}">Evolve</button>
1479
- <button class="action-btn" id="btn-rollback" data-skill="${safeSkill}" data-path="${safeSkillPath}">Rollback</button>
1480
- </div>
1481
- <div class="action-result" id="action-result"></div>
1482
- `;
1483
-
1484
- // Bind action handlers
1485
- document.getElementById('btn-watch').addEventListener('click', () => runSkillAction('watch', skillName, skillPath));
1486
- document.getElementById('btn-evolve').addEventListener('click', () => runSkillAction('evolve', skillName, skillPath));
1487
- document.getElementById('btn-rollback').addEventListener('click', () => runSkillAction('rollback', skillName, skillPath));
1488
- }
1489
-
1490
- async function runSkillAction(action, skill, skillPath) {
1491
- const btn = document.getElementById('btn-' + action);
1492
- const resultEl = document.getElementById('action-result');
1493
- if (!btn || !resultEl) return;
1494
-
1495
- // Set loading state
1496
- btn.classList.add('loading');
1497
- btn.disabled = true;
1498
- btn.textContent = '...';
1499
- resultEl.className = 'action-result';
1500
- resultEl.style.display = 'none';
1501
-
1502
- try {
1503
- const payload = { skill, skillPath };
1504
- if (action === 'rollback') {
1505
- // For rollback, find the latest pending proposal
1506
- const pending = (state.computed && state.computed.pendingProposals) || [];
1507
- const needle = skill.toLowerCase();
1508
- const match = pending.find(p => (p.details || '').toLowerCase().includes(needle));
1509
- if (match) payload.proposalId = match.proposal_id;
1510
- }
1511
-
1512
- const res = await fetch('/api/actions/' + action, {
1513
- method: 'POST',
1514
- headers: { 'Content-Type': 'application/json' },
1515
- body: JSON.stringify(payload),
1516
- });
1517
- const data = await res.json();
1518
-
1519
- resultEl.style.display = 'block';
1520
- if (data.success) {
1521
- resultEl.className = 'action-result visible success';
1522
- resultEl.textContent = data.output || 'Action completed successfully';
1523
- } else {
1524
- resultEl.className = 'action-result visible error';
1525
- resultEl.textContent = data.error || data.output || 'Action failed';
1526
- }
1527
- } catch (err) {
1528
- resultEl.style.display = 'block';
1529
- resultEl.className = 'action-result visible error';
1530
- resultEl.textContent = 'Network error: ' + (err.message || err);
1531
- } finally {
1532
- btn.classList.remove('loading');
1533
- btn.disabled = false;
1534
- btn.textContent = action.charAt(0).toUpperCase() + action.slice(1);
1535
- }
1536
- }
1537
-
1538
- // ========================================================================
1539
- // Search filter
1540
- // ========================================================================
1541
- document.getElementById('skillSearchInput').addEventListener('input', function() {
1542
- const query = this.value.toLowerCase();
1543
- document.querySelectorAll('.skill-health-row').forEach(row => {
1544
- const name = (row.dataset.skill || '').toLowerCase();
1545
- row.style.display = name.includes(query) ? '' : 'none';
1546
- });
1547
- });
1548
-
1549
- // ========================================================================
1550
- // Evaluation Feed
1551
- // ========================================================================
1552
- function updateDrillEvalFeed(skillName) {
1553
- const records = state.skills.filter(r => r.skill_name === skillName);
1554
- const sorted = [...records].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
1555
- const tbody = document.querySelector('#drillEvalFeed tbody');
1556
- tbody.innerHTML = sorted.slice(0, 50).map(r => {
1557
- const triggeredBadge = r.triggered
1558
- ? '<span class="badge badge-healthy">Yes</span>'
1559
- : '<span class="badge badge-critical">No</span>';
1560
- const sourceType = escapeHtml(r.source || r.type || 'implicit');
1561
- return `<tr>
1562
- <td class="mono">${escapeHtml(formatTimestamp(r.timestamp))}</td>
1563
- <td>${escapeHtml(truncate(r.query, 50))}</td>
1564
- <td>${triggeredBadge}</td>
1565
- <td class="mono">${sourceType}</td>
1566
- </tr>`;
1567
- }).join('') || '<tr><td colspan="4" class="empty-state">No evaluations</td></tr>';
1568
- }
1569
-
1570
- // ========================================================================
1571
- // Invocation Breakdown
1572
- // ========================================================================
1573
- function updateDrillInvocationBreakdown(skillName) {
1574
- const computed = state.computed;
1575
- const snapshot = computed && computed.snapshots ? computed.snapshots[skillName] : null;
1576
- const byType = (snapshot && snapshot.by_invocation_type) || {};
1577
-
1578
- // If no invocation type data, compute from skill records
1579
- let labels, values;
1580
- if (Object.keys(byType).length > 0) {
1581
- labels = Object.keys(byType);
1582
- values = Object.values(byType);
1583
- } else {
1584
- // Fallback: count source/type fields from skill records
1585
- const records = state.skills.filter(r => r.skill_name === skillName);
1586
- const counts = {};
1587
- for (const r of records) {
1588
- const t = r.source || r.type || 'implicit';
1589
- counts[t] = (counts[t] || 0) + 1;
1590
- }
1591
- labels = Object.keys(counts);
1592
- values = Object.values(counts);
1593
- }
1594
-
1595
- if (charts.invocationBreakdown) charts.invocationBreakdown.destroy();
1596
-
1597
- if (!labels.length) return;
1598
-
1599
- charts.invocationBreakdown = new Chart(document.getElementById('chartInvocationBreakdown'), {
1600
- type: 'doughnut',
1601
- data: {
1602
- labels,
1603
- datasets: [{
1604
- data: values,
1605
- backgroundColor: CHART_COLORS.slice(0, labels.length),
1606
- borderWidth: 1,
1607
- borderColor: '#fff',
1608
- }]
1609
- },
1610
- options: {
1611
- responsive: true,
1612
- maintainAspectRatio: false,
1613
- plugins: {
1614
- legend: {
1615
- position: 'right',
1616
- labels: {
1617
- font: { family: "'Poppins', sans-serif", size: 11 },
1618
- padding: 12,
1619
- }
1620
- }
1621
- }
1622
- }
1623
- });
1624
- }
1625
-
1626
- // ========================================================================
1627
- // Time period filtering
1628
- // ========================================================================
1629
- function filterByPeriod(records, days) {
1630
- if (!days || days === 0) return records;
1631
- // Anchor cutoff to latest timestamp in dataset, not viewer's clock,
1632
- // so archived/historical datasets filter correctly.
1633
- const latest = records.reduce((max, r) => {
1634
- const t = new Date(r.timestamp).getTime();
1635
- return t > max ? t : max;
1636
- }, 0);
1637
- if (!latest) return records;
1638
- const cutoff = new Date(latest);
1639
- cutoff.setDate(cutoff.getDate() - days);
1640
- return records.filter(r => new Date(r.timestamp) >= cutoff);
1641
- }
1642
-
1643
- document.getElementById('timePeriodSelector').addEventListener('click', function(e) {
1644
- const btn = e.target.closest('.period-btn');
1645
- if (!btn) return;
1646
- this.querySelectorAll('.period-btn').forEach(b => b.classList.remove('active'));
1647
- btn.classList.add('active');
1648
- selectedPeriodDays = parseInt(btn.dataset.days, 10);
1649
- if (selectedSkill) {
1650
- updateDrillPassRateChart(selectedSkill);
1651
- }
1652
- });
1653
-
1654
- // Hook into drill-down to show action buttons in live mode
1655
- const origOpenDrillDown = typeof openDrillDown === 'function' ? openDrillDown : null;
1656
- openDrillDown = function(skillName) {
1657
- if (origOpenDrillDown) origOpenDrillDown(skillName);
1658
- showActionButtons(skillName);
1659
- };
1660
-
1661
- function initLiveMode() {
1662
- if (!isLiveMode()) return;
1663
- // Show live sections
1664
- document.getElementById('actionsSection').style.display = 'block';
1665
-
1666
- startSSE();
1667
- updateEvolutionTimeline();
1668
- }
1669
-
1670
- // ========================================================================
1671
- // Init: try loading embedded data
1672
- // ========================================================================
1673
- if (loadEmbeddedData()) {
1674
- refreshAll();
1675
- }
1676
- initLiveMode();
1677
- </script>
1678
-
1679
- </body>
1680
- </html>