selftune 0.1.4 → 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 +37 -0
- 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 +25 -3
- 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 +466 -103
- 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 +19 -2
- 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 +138 -18
- 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/openclaw-ingest.ts +440 -0
- package/cli/selftune/init.ts +150 -3
- package/cli/selftune/memory/writer.ts +447 -0
- package/cli/selftune/monitoring/watch.ts +25 -2
- package/cli/selftune/status.ts +17 -13
- 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 +569 -8
- package/package.json +8 -4
- package/skill/SKILL.md +124 -8
- 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 +69 -1
- 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 +117 -3
- package/skill/Workflows/Initialize.md +57 -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,6 +633,15 @@
|
|
|
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
646
|
<span class="version">v0.1.4</span>
|
|
425
647
|
</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
|
|
@@ -755,16 +1021,18 @@ function groupByDay(records) {
|
|
|
755
1021
|
}
|
|
756
1022
|
|
|
757
1023
|
function getSkillStatus(passRate, regressionDetected) {
|
|
758
|
-
if (
|
|
759
|
-
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';
|
|
760
1027
|
return 'healthy';
|
|
761
1028
|
}
|
|
762
1029
|
|
|
763
1030
|
function getStatusBadge(status) {
|
|
764
1031
|
const map = {
|
|
765
|
-
healthy: '<span class="badge badge-
|
|
766
|
-
|
|
767
|
-
|
|
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>',
|
|
768
1036
|
};
|
|
769
1037
|
return map[status] || '';
|
|
770
1038
|
}
|
|
@@ -785,6 +1053,7 @@ function refreshAll() {
|
|
|
785
1053
|
dropZone.style.display = 'none';
|
|
786
1054
|
document.getElementById('healthSummary').style.display = 'block';
|
|
787
1055
|
document.getElementById('skillHealthSection').style.display = 'block';
|
|
1056
|
+
document.getElementById('skillSearchInput').style.display = 'block';
|
|
788
1057
|
}
|
|
789
1058
|
|
|
790
1059
|
updateHeader();
|
|
@@ -826,7 +1095,7 @@ function updateHealthSummary() {
|
|
|
826
1095
|
document.getElementById('kpi-avg-pass-rate').textContent = (avgPR * 100).toFixed(0) + '%';
|
|
827
1096
|
const status = getSkillStatus(avgPR, false);
|
|
828
1097
|
document.getElementById('kpi-pass-rate-sub').textContent =
|
|
829
|
-
status === 'healthy' ? 'system healthy' : status === '
|
|
1098
|
+
status === 'healthy' ? 'system healthy' : status === 'warning' ? 'needs monitoring' : status === 'critical' ? 'action required' : 'no data';
|
|
830
1099
|
}
|
|
831
1100
|
|
|
832
1101
|
document.getElementById('kpi-regressions').textContent = regressions.length;
|
|
@@ -908,6 +1177,16 @@ function updateSkillHealthGrid() {
|
|
|
908
1177
|
row.addEventListener('click', handler);
|
|
909
1178
|
row.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); } });
|
|
910
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
|
+
}
|
|
911
1190
|
}
|
|
912
1191
|
|
|
913
1192
|
// ========================================================================
|
|
@@ -924,6 +1203,8 @@ function openDrillDown(skillName) {
|
|
|
924
1203
|
updateDrillMissedQueries(skillName);
|
|
925
1204
|
updateDrillEvolution(skillName);
|
|
926
1205
|
updateDrillSessions(skillName);
|
|
1206
|
+
updateDrillEvalFeed(skillName);
|
|
1207
|
+
updateDrillInvocationBreakdown(skillName);
|
|
927
1208
|
}
|
|
928
1209
|
|
|
929
1210
|
document.getElementById('drillDownClose').addEventListener('click', () => {
|
|
@@ -934,7 +1215,8 @@ document.getElementById('drillDownClose').addEventListener('click', () => {
|
|
|
934
1215
|
|
|
935
1216
|
function updateDrillPassRateChart(skillName) {
|
|
936
1217
|
// Group skill records by day and compute daily pass rate
|
|
937
|
-
const
|
|
1218
|
+
const allRecords = state.skills.filter(r => r.skill_name === skillName);
|
|
1219
|
+
const records = filterByPeriod(allRecords, selectedPeriodDays);
|
|
938
1220
|
const byDay = {};
|
|
939
1221
|
for (const r of records) {
|
|
940
1222
|
const day = toDayKey(r.timestamp);
|
|
@@ -1107,12 +1389,291 @@ document.getElementById('exportCsvBtn').addEventListener('click', () => {
|
|
|
1107
1389
|
a.click();
|
|
1108
1390
|
});
|
|
1109
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
|
+
|
|
1110
1670
|
// ========================================================================
|
|
1111
1671
|
// Init: try loading embedded data
|
|
1112
1672
|
// ========================================================================
|
|
1113
1673
|
if (loadEmbeddedData()) {
|
|
1114
1674
|
refreshAll();
|
|
1115
1675
|
}
|
|
1676
|
+
initLiveMode();
|
|
1116
1677
|
</script>
|
|
1117
1678
|
|
|
1118
1679
|
</body>
|