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.
- package/.claude/agents/diagnosis-analyst.md +20 -10
- package/.claude/agents/evolution-reviewer.md +14 -1
- package/.claude/agents/integration-guide.md +18 -6
- package/.claude/agents/pattern-analyst.md +18 -5
- package/CHANGELOG.md +12 -4
- package/README.md +43 -35
- package/apps/local-dashboard/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +15 -0
- package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +60 -0
- package/apps/local-dashboard/dist/assets/vendor-table-B7VF2Ipl.js +26 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +346 -0
- package/apps/local-dashboard/dist/favicon.png +0 -0
- package/apps/local-dashboard/dist/index.html +17 -0
- package/apps/local-dashboard/dist/logo.png +0 -0
- package/apps/local-dashboard/dist/logo.svg +9 -0
- package/cli/selftune/badge/badge-data.ts +1 -1
- package/cli/selftune/badge/badge.ts +4 -8
- package/cli/selftune/canonical-export.ts +183 -0
- package/cli/selftune/constants.ts +28 -0
- package/cli/selftune/contribute/contribute.ts +1 -1
- package/cli/selftune/cron/setup.ts +17 -17
- package/cli/selftune/dashboard-contract.ts +202 -0
- package/cli/selftune/dashboard-server.ts +653 -186
- package/cli/selftune/dashboard.ts +41 -176
- package/cli/selftune/eval/baseline.ts +5 -4
- package/cli/selftune/eval/composability-v2.ts +273 -0
- package/cli/selftune/eval/hooks-to-evals.ts +34 -15
- package/cli/selftune/eval/unit-test-cli.ts +1 -1
- package/cli/selftune/evolution/evidence.ts +26 -0
- package/cli/selftune/evolution/evolve-body.ts +105 -11
- package/cli/selftune/evolution/evolve.ts +371 -25
- package/cli/selftune/evolution/extract-patterns.ts +87 -29
- package/cli/selftune/evolution/rollback.ts +2 -2
- package/cli/selftune/grading/auto-grade.ts +200 -0
- package/cli/selftune/grading/grade-session.ts +448 -97
- package/cli/selftune/grading/results.ts +42 -0
- package/cli/selftune/hooks/prompt-log.ts +172 -2
- package/cli/selftune/hooks/session-stop.ts +123 -3
- package/cli/selftune/hooks/skill-eval.ts +119 -3
- package/cli/selftune/index.ts +395 -116
- package/cli/selftune/ingestors/claude-replay.ts +140 -114
- package/cli/selftune/ingestors/codex-rollout.ts +345 -46
- package/cli/selftune/ingestors/codex-wrapper.ts +207 -39
- package/cli/selftune/ingestors/openclaw-ingest.ts +141 -8
- package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
- package/cli/selftune/init.ts +227 -14
- package/cli/selftune/last.ts +14 -5
- package/cli/selftune/localdb/db.ts +63 -0
- package/cli/selftune/localdb/materialize.ts +428 -0
- package/cli/selftune/localdb/queries.ts +376 -0
- package/cli/selftune/localdb/schema.ts +204 -0
- package/cli/selftune/monitoring/watch.ts +66 -15
- package/cli/selftune/normalization.ts +682 -0
- package/cli/selftune/observability.ts +19 -44
- package/cli/selftune/orchestrate.ts +1073 -0
- package/cli/selftune/quickstart.ts +203 -0
- package/cli/selftune/repair/skill-usage.ts +576 -0
- package/cli/selftune/schedule.ts +561 -0
- package/cli/selftune/status.ts +48 -26
- package/cli/selftune/sync.ts +627 -0
- package/cli/selftune/types.ts +148 -0
- package/cli/selftune/utils/canonical-log.ts +45 -0
- package/cli/selftune/utils/hooks.ts +41 -0
- package/cli/selftune/utils/html.ts +27 -0
- package/cli/selftune/utils/llm-call.ts +78 -20
- package/cli/selftune/utils/math.ts +10 -0
- package/cli/selftune/utils/query-filter.ts +139 -0
- package/cli/selftune/utils/skill-discovery.ts +340 -0
- package/cli/selftune/utils/skill-log.ts +68 -0
- package/cli/selftune/utils/skill-usage-confidence.ts +18 -0
- package/cli/selftune/utils/transcript.ts +272 -26
- package/cli/selftune/workflows/discover.ts +254 -0
- package/cli/selftune/workflows/skill-md-writer.ts +288 -0
- package/cli/selftune/workflows/workflows.ts +188 -0
- package/package.json +21 -8
- package/packages/telemetry-contract/README.md +11 -0
- package/packages/telemetry-contract/fixtures/golden.json +87 -0
- package/packages/telemetry-contract/fixtures/golden.test.ts +42 -0
- package/packages/telemetry-contract/index.ts +1 -0
- package/packages/telemetry-contract/package.json +19 -0
- package/packages/telemetry-contract/src/index.ts +2 -0
- package/packages/telemetry-contract/src/types.ts +163 -0
- package/packages/telemetry-contract/src/validators.ts +109 -0
- package/skill/SKILL.md +84 -53
- package/skill/Workflows/AutoActivation.md +17 -16
- package/skill/Workflows/Badge.md +6 -0
- package/skill/Workflows/Baseline.md +46 -23
- package/skill/Workflows/Composability.md +12 -5
- package/skill/Workflows/Contribute.md +17 -14
- package/skill/Workflows/Cron.md +56 -79
- package/skill/Workflows/Dashboard.md +45 -34
- package/skill/Workflows/Doctor.md +30 -17
- package/skill/Workflows/Evals.md +64 -40
- package/skill/Workflows/EvolutionMemory.md +2 -0
- package/skill/Workflows/Evolve.md +102 -47
- package/skill/Workflows/EvolveBody.md +6 -6
- package/skill/Workflows/Grade.md +36 -31
- package/skill/Workflows/ImportSkillsBench.md +11 -5
- package/skill/Workflows/Ingest.md +43 -36
- package/skill/Workflows/Initialize.md +44 -30
- package/skill/Workflows/Orchestrate.md +139 -0
- package/skill/Workflows/Replay.md +39 -18
- package/skill/Workflows/Rollback.md +3 -3
- package/skill/Workflows/Schedule.md +61 -0
- package/skill/Workflows/Sync.md +88 -0
- package/skill/Workflows/UnitTest.md +34 -22
- package/skill/Workflows/Watch.md +14 -4
- package/skill/Workflows/Workflows.md +129 -0
- package/skill/assets/activation-rules-default.json +26 -0
- package/skill/assets/multi-skill-settings.json +63 -0
- package/skill/assets/single-skill-settings.json +57 -0
- package/skill/references/invocation-taxonomy.md +2 -2
- package/skill/references/logs.md +164 -2
- package/skill/references/setup-patterns.md +65 -0
- package/skill/references/version-history.md +40 -0
- package/skill/settings_snippet.json +1 -1
- package/templates/multi-skill-settings.json +7 -7
- package/templates/single-skill-settings.json +6 -6
- package/dashboard/index.html +0 -1680
package/dashboard/index.html
DELETED
|
@@ -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 & drop your JSONL log files here, or click to browse.<br>
|
|
658
|
-
Files are processed locally — 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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(' · ') : '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))} · ${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>
|