selftune 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/diagnosis-analyst.md +146 -0
- package/.claude/agents/evolution-reviewer.md +167 -0
- package/.claude/agents/integration-guide.md +200 -0
- package/.claude/agents/pattern-analyst.md +147 -0
- package/CHANGELOG.md +38 -1
- package/README.md +96 -256
- package/assets/BeforeAfter.gif +0 -0
- package/assets/FeedbackLoop.gif +0 -0
- package/assets/logo.svg +9 -0
- package/assets/skill-health-badge.svg +20 -0
- package/cli/selftune/activation-rules.ts +171 -0
- package/cli/selftune/badge/badge-data.ts +108 -0
- package/cli/selftune/badge/badge-svg.ts +212 -0
- package/cli/selftune/badge/badge.ts +103 -0
- package/cli/selftune/constants.ts +75 -1
- package/cli/selftune/contribute/bundle.ts +314 -0
- package/cli/selftune/contribute/contribute.ts +214 -0
- package/cli/selftune/contribute/sanitize.ts +162 -0
- package/cli/selftune/cron/setup.ts +266 -0
- package/cli/selftune/dashboard-server.ts +582 -0
- package/cli/selftune/dashboard.ts +31 -12
- package/cli/selftune/eval/baseline.ts +247 -0
- package/cli/selftune/eval/composability.ts +117 -0
- package/cli/selftune/eval/generate-unit-tests.ts +143 -0
- package/cli/selftune/eval/hooks-to-evals.ts +68 -2
- package/cli/selftune/eval/import-skillsbench.ts +221 -0
- package/cli/selftune/eval/synthetic-evals.ts +172 -0
- package/cli/selftune/eval/unit-test-cli.ts +152 -0
- package/cli/selftune/eval/unit-test.ts +196 -0
- package/cli/selftune/evolution/deploy-proposal.ts +142 -1
- package/cli/selftune/evolution/evolve-body.ts +492 -0
- package/cli/selftune/evolution/evolve.ts +479 -104
- package/cli/selftune/evolution/extract-patterns.ts +32 -1
- package/cli/selftune/evolution/pareto.ts +314 -0
- package/cli/selftune/evolution/propose-body.ts +171 -0
- package/cli/selftune/evolution/propose-description.ts +100 -2
- package/cli/selftune/evolution/propose-routing.ts +166 -0
- package/cli/selftune/evolution/refine-body.ts +141 -0
- package/cli/selftune/evolution/rollback.ts +20 -3
- package/cli/selftune/evolution/validate-body.ts +254 -0
- package/cli/selftune/evolution/validate-proposal.ts +257 -35
- package/cli/selftune/evolution/validate-routing.ts +177 -0
- package/cli/selftune/grading/grade-session.ts +145 -19
- package/cli/selftune/grading/pre-gates.ts +104 -0
- package/cli/selftune/hooks/auto-activate.ts +185 -0
- package/cli/selftune/hooks/evolution-guard.ts +165 -0
- package/cli/selftune/hooks/skill-change-guard.ts +112 -0
- package/cli/selftune/index.ts +88 -0
- package/cli/selftune/ingestors/claude-replay.ts +351 -0
- package/cli/selftune/ingestors/codex-rollout.ts +1 -1
- package/cli/selftune/ingestors/openclaw-ingest.ts +440 -0
- package/cli/selftune/ingestors/opencode-ingest.ts +2 -2
- package/cli/selftune/init.ts +168 -5
- package/cli/selftune/last.ts +2 -2
- package/cli/selftune/memory/writer.ts +447 -0
- package/cli/selftune/monitoring/watch.ts +25 -2
- package/cli/selftune/status.ts +18 -15
- package/cli/selftune/types.ts +377 -5
- package/cli/selftune/utils/frontmatter.ts +217 -0
- package/cli/selftune/utils/llm-call.ts +29 -3
- package/cli/selftune/utils/transcript.ts +35 -0
- package/cli/selftune/utils/trigger-check.ts +89 -0
- package/cli/selftune/utils/tui.ts +156 -0
- package/dashboard/index.html +585 -19
- package/package.json +17 -6
- package/skill/SKILL.md +127 -10
- package/skill/Workflows/AutoActivation.md +144 -0
- package/skill/Workflows/Badge.md +118 -0
- package/skill/Workflows/Baseline.md +121 -0
- package/skill/Workflows/Composability.md +100 -0
- package/skill/Workflows/Contribute.md +91 -0
- package/skill/Workflows/Cron.md +155 -0
- package/skill/Workflows/Dashboard.md +203 -0
- package/skill/Workflows/Doctor.md +37 -1
- package/skill/Workflows/Evals.md +73 -5
- package/skill/Workflows/EvolutionMemory.md +152 -0
- package/skill/Workflows/Evolve.md +111 -6
- package/skill/Workflows/EvolveBody.md +159 -0
- package/skill/Workflows/ImportSkillsBench.md +111 -0
- package/skill/Workflows/Ingest.md +129 -15
- package/skill/Workflows/Initialize.md +58 -3
- package/skill/Workflows/Replay.md +70 -0
- package/skill/Workflows/Rollback.md +20 -1
- package/skill/Workflows/UnitTest.md +138 -0
- package/skill/Workflows/Watch.md +22 -0
- package/skill/settings_snippet.json +23 -0
- package/templates/activation-rules-default.json +27 -0
- package/templates/multi-skill-settings.json +64 -0
- package/templates/single-skill-settings.json +58 -0
package/dashboard/index.html
CHANGED
|
@@ -274,7 +274,10 @@
|
|
|
274
274
|
}
|
|
275
275
|
.pass-rate-fill.healthy { background: var(--green); }
|
|
276
276
|
.pass-rate-fill.drifting { background: var(--amber); }
|
|
277
|
+
.pass-rate-fill.warning { background: var(--amber); }
|
|
277
278
|
.pass-rate-fill.regressed { background: var(--red); }
|
|
279
|
+
.pass-rate-fill.critical { background: var(--red); }
|
|
280
|
+
.pass-rate-fill.unknown { background: #ccc; }
|
|
278
281
|
.pass-rate-label {
|
|
279
282
|
font-family: 'Poppins', sans-serif;
|
|
280
283
|
font-size: 0.75rem;
|
|
@@ -413,6 +416,216 @@
|
|
|
413
416
|
border-color: var(--accent);
|
|
414
417
|
color: var(--accent);
|
|
415
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; }
|
|
416
629
|
</style>
|
|
417
630
|
</head>
|
|
418
631
|
<body>
|
|
@@ -420,8 +633,17 @@
|
|
|
420
633
|
<!-- ===== Header ===== -->
|
|
421
634
|
<div class="header">
|
|
422
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>
|
|
423
645
|
<h1>self<span>tune</span></h1>
|
|
424
|
-
<span class="version">v0.
|
|
646
|
+
<span class="version">v0.1.4</span>
|
|
425
647
|
</div>
|
|
426
648
|
<div class="status" id="headerStatus">Drop log files to get started</div>
|
|
427
649
|
</div>
|
|
@@ -480,6 +702,7 @@
|
|
|
480
702
|
</div>
|
|
481
703
|
|
|
482
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;">
|
|
483
706
|
<div class="section" id="skillHealthSection" style="display:none;">
|
|
484
707
|
<div class="section-header">
|
|
485
708
|
<span>Skill Health Grid</span>
|
|
@@ -505,7 +728,15 @@
|
|
|
505
728
|
</div>
|
|
506
729
|
<div class="drill-down-content">
|
|
507
730
|
<div class="drill-down-section">
|
|
508
|
-
<
|
|
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>
|
|
509
740
|
<div class="chart-container"><canvas id="chartDrillPassRate"></canvas></div>
|
|
510
741
|
</div>
|
|
511
742
|
<div class="drill-down-section">
|
|
@@ -532,6 +763,19 @@
|
|
|
532
763
|
</table>
|
|
533
764
|
</div>
|
|
534
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>
|
|
535
779
|
</div>
|
|
536
780
|
</div>
|
|
537
781
|
|
|
@@ -556,6 +800,27 @@
|
|
|
556
800
|
</div>
|
|
557
801
|
</div>
|
|
558
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
|
+
|
|
559
824
|
</div>
|
|
560
825
|
|
|
561
826
|
<script>
|
|
@@ -572,6 +837,7 @@ const state = {
|
|
|
572
837
|
|
|
573
838
|
const charts = {};
|
|
574
839
|
let selectedSkill = null;
|
|
840
|
+
let selectedPeriodDays = 0; // 0 = All
|
|
575
841
|
|
|
576
842
|
// ========================================================================
|
|
577
843
|
// File identification
|
|
@@ -726,6 +992,8 @@ function formatDate(ts) {
|
|
|
726
992
|
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
727
993
|
}
|
|
728
994
|
|
|
995
|
+
function toDayKey(ts) { return new Date(ts).toISOString().slice(0, 10); }
|
|
996
|
+
|
|
729
997
|
function formatTimestamp(ts) {
|
|
730
998
|
const d = toDate(ts);
|
|
731
999
|
return d.toLocaleString('en-US', {
|
|
@@ -746,23 +1014,25 @@ function escapeHtml(s) {
|
|
|
746
1014
|
function groupByDay(records) {
|
|
747
1015
|
const map = {};
|
|
748
1016
|
for (const r of records) {
|
|
749
|
-
const day =
|
|
1017
|
+
const day = toDayKey(r.timestamp);
|
|
750
1018
|
map[day] = (map[day] || 0) + 1;
|
|
751
1019
|
}
|
|
752
1020
|
return map;
|
|
753
1021
|
}
|
|
754
1022
|
|
|
755
1023
|
function getSkillStatus(passRate, regressionDetected) {
|
|
756
|
-
if (
|
|
757
|
-
if (passRate < 0.
|
|
1024
|
+
if (passRate === null || passRate === undefined) return 'unknown';
|
|
1025
|
+
if (regressionDetected || passRate < 0.4) return 'critical';
|
|
1026
|
+
if (passRate < 0.7) return 'warning';
|
|
758
1027
|
return 'healthy';
|
|
759
1028
|
}
|
|
760
1029
|
|
|
761
1030
|
function getStatusBadge(status) {
|
|
762
1031
|
const map = {
|
|
763
|
-
healthy: '<span class="badge badge-
|
|
764
|
-
|
|
765
|
-
|
|
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>',
|
|
766
1036
|
};
|
|
767
1037
|
return map[status] || '';
|
|
768
1038
|
}
|
|
@@ -783,6 +1053,7 @@ function refreshAll() {
|
|
|
783
1053
|
dropZone.style.display = 'none';
|
|
784
1054
|
document.getElementById('healthSummary').style.display = 'block';
|
|
785
1055
|
document.getElementById('skillHealthSection').style.display = 'block';
|
|
1056
|
+
document.getElementById('skillSearchInput').style.display = 'block';
|
|
786
1057
|
}
|
|
787
1058
|
|
|
788
1059
|
updateHeader();
|
|
@@ -824,7 +1095,7 @@ function updateHealthSummary() {
|
|
|
824
1095
|
document.getElementById('kpi-avg-pass-rate').textContent = (avgPR * 100).toFixed(0) + '%';
|
|
825
1096
|
const status = getSkillStatus(avgPR, false);
|
|
826
1097
|
document.getElementById('kpi-pass-rate-sub').textContent =
|
|
827
|
-
status === 'healthy' ? 'system healthy' : status === '
|
|
1098
|
+
status === 'healthy' ? 'system healthy' : status === 'warning' ? 'needs monitoring' : status === 'critical' ? 'action required' : 'no data';
|
|
828
1099
|
}
|
|
829
1100
|
|
|
830
1101
|
document.getElementById('kpi-regressions').textContent = regressions.length;
|
|
@@ -906,6 +1177,16 @@ function updateSkillHealthGrid() {
|
|
|
906
1177
|
row.addEventListener('click', handler);
|
|
907
1178
|
row.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); } });
|
|
908
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
|
+
}
|
|
909
1190
|
}
|
|
910
1191
|
|
|
911
1192
|
// ========================================================================
|
|
@@ -922,6 +1203,8 @@ function openDrillDown(skillName) {
|
|
|
922
1203
|
updateDrillMissedQueries(skillName);
|
|
923
1204
|
updateDrillEvolution(skillName);
|
|
924
1205
|
updateDrillSessions(skillName);
|
|
1206
|
+
updateDrillEvalFeed(skillName);
|
|
1207
|
+
updateDrillInvocationBreakdown(skillName);
|
|
925
1208
|
}
|
|
926
1209
|
|
|
927
1210
|
document.getElementById('drillDownClose').addEventListener('click', () => {
|
|
@@ -932,27 +1215,29 @@ document.getElementById('drillDownClose').addEventListener('click', () => {
|
|
|
932
1215
|
|
|
933
1216
|
function updateDrillPassRateChart(skillName) {
|
|
934
1217
|
// Group skill records by day and compute daily pass rate
|
|
935
|
-
const
|
|
1218
|
+
const allRecords = state.skills.filter(r => r.skill_name === skillName);
|
|
1219
|
+
const records = filterByPeriod(allRecords, selectedPeriodDays);
|
|
936
1220
|
const byDay = {};
|
|
937
1221
|
for (const r of records) {
|
|
938
|
-
const day =
|
|
1222
|
+
const day = toDayKey(r.timestamp);
|
|
939
1223
|
if (!byDay[day]) byDay[day] = { triggered: 0, total: 0 };
|
|
940
1224
|
byDay[day].total++;
|
|
941
1225
|
if (r.triggered) byDay[day].triggered++;
|
|
942
1226
|
}
|
|
943
1227
|
|
|
944
|
-
const
|
|
945
|
-
const
|
|
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));
|
|
946
1231
|
|
|
947
1232
|
// Deploy events as annotations
|
|
948
1233
|
const deployDays = new Set(
|
|
949
1234
|
state.evolution
|
|
950
1235
|
.filter(e => e.action === 'deployed' && (e.details || '').toLowerCase().includes(skillName.toLowerCase()))
|
|
951
|
-
.map(e =>
|
|
1236
|
+
.map(e => toDayKey(e.timestamp))
|
|
952
1237
|
);
|
|
953
1238
|
|
|
954
|
-
const pointColors =
|
|
955
|
-
const pointSizes =
|
|
1239
|
+
const pointColors = dayKeys.map(d => deployDays.has(d) ? '#d97757' : '#788c5d');
|
|
1240
|
+
const pointSizes = dayKeys.map(d => deployDays.has(d) ? 8 : 3);
|
|
956
1241
|
|
|
957
1242
|
if (charts.drillPassRate) charts.drillPassRate.destroy();
|
|
958
1243
|
charts.drillPassRate = new Chart(document.getElementById('chartDrillPassRate'), {
|
|
@@ -1023,12 +1308,14 @@ function updateDrillSessions(skillName) {
|
|
|
1023
1308
|
const sorted = [...sessions].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
1024
1309
|
tbody.innerHTML = sorted.slice(0, 30).map(r => {
|
|
1025
1310
|
const skills = (r.skills_triggered || []).join(', ') || '\u2014';
|
|
1026
|
-
const
|
|
1027
|
-
|
|
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>`
|
|
1028
1315
|
: '<span class="badge badge-green">0</span>';
|
|
1029
1316
|
return `<tr>
|
|
1030
1317
|
<td class="mono">${escapeHtml(formatTimestamp(r.timestamp))}</td>
|
|
1031
|
-
<td>${
|
|
1318
|
+
<td>${totalToolCalls}</td>
|
|
1032
1319
|
<td>${escapeHtml(truncate(skills, 30))}</td>
|
|
1033
1320
|
<td>${errorBadge}</td>
|
|
1034
1321
|
</tr>`;
|
|
@@ -1102,12 +1389,291 @@ document.getElementById('exportCsvBtn').addEventListener('click', () => {
|
|
|
1102
1389
|
a.click();
|
|
1103
1390
|
});
|
|
1104
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
|
+
|
|
1105
1670
|
// ========================================================================
|
|
1106
1671
|
// Init: try loading embedded data
|
|
1107
1672
|
// ========================================================================
|
|
1108
1673
|
if (loadEmbeddedData()) {
|
|
1109
1674
|
refreshAll();
|
|
1110
1675
|
}
|
|
1676
|
+
initLiveMode();
|
|
1111
1677
|
</script>
|
|
1112
1678
|
|
|
1113
1679
|
</body>
|