@plumpslabs/kuma 2.4.9 → 2.4.11
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/README.md +7 -4
- package/dist/index.js +2486 -1518
- package/package.json +3 -3
- package/packages/ide/studio/dist/index.js +184 -4
- package/packages/ide/studio/public/index.html +1075 -149
- package/plugin.json +1 -1
- package/skills/kuma/SKILL.md +80 -0
|
@@ -322,10 +322,47 @@ header {
|
|
|
322
322
|
color: var(--muted);
|
|
323
323
|
border-radius: 4px;
|
|
324
324
|
font-size: 10px;
|
|
325
|
-
padding:
|
|
325
|
+
padding: 3px 8px;
|
|
326
326
|
cursor: pointer;
|
|
327
|
+
display: inline-flex;
|
|
328
|
+
align-items: center;
|
|
329
|
+
gap: 4px;
|
|
330
|
+
transition: all 0.2s ease;
|
|
327
331
|
}
|
|
328
332
|
.btn-copy-workaround:hover { color: #fff; border-color: var(--amber); }
|
|
333
|
+
.btn-copy-workaround.copied {
|
|
334
|
+
background: rgba(16, 185, 129, 0.15);
|
|
335
|
+
border-color: var(--emerald);
|
|
336
|
+
color: #34d399;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#kuma-toast {
|
|
340
|
+
position: fixed;
|
|
341
|
+
bottom: 24px;
|
|
342
|
+
right: 24px;
|
|
343
|
+
background: #1e293b;
|
|
344
|
+
color: #f8fafc;
|
|
345
|
+
border: 1px solid #334155;
|
|
346
|
+
border-left: 4px solid var(--emerald);
|
|
347
|
+
padding: 10px 16px;
|
|
348
|
+
border-radius: 6px;
|
|
349
|
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
|
|
350
|
+
display: flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
gap: 8px;
|
|
353
|
+
font-size: 12px;
|
|
354
|
+
font-weight: 500;
|
|
355
|
+
z-index: 9999;
|
|
356
|
+
opacity: 0;
|
|
357
|
+
transform: translateY(12px);
|
|
358
|
+
transition: opacity 0.25s ease, transform 0.25s ease;
|
|
359
|
+
pointer-events: none;
|
|
360
|
+
}
|
|
361
|
+
#kuma-toast.show {
|
|
362
|
+
opacity: 1;
|
|
363
|
+
transform: translateY(0);
|
|
364
|
+
pointer-events: auto;
|
|
365
|
+
}
|
|
329
366
|
|
|
330
367
|
.gotcha-footer {
|
|
331
368
|
font-size: 11px;
|
|
@@ -468,32 +505,280 @@ table.clean-table tr:hover td {
|
|
|
468
505
|
#graph-canvas {
|
|
469
506
|
width: 100%;
|
|
470
507
|
height: 100%;
|
|
471
|
-
background:
|
|
508
|
+
background-color: #060911;
|
|
509
|
+
background-image:
|
|
510
|
+
radial-gradient(circle at 50% 50%, rgba(30, 41, 59, 0.45) 0%, transparent 85%),
|
|
511
|
+
radial-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px);
|
|
512
|
+
background-size: 100% 100%, 28px 28px;
|
|
472
513
|
}
|
|
473
514
|
.graph-toolbar {
|
|
474
515
|
position: absolute;
|
|
475
516
|
top: 16px;
|
|
476
517
|
left: 16px;
|
|
477
518
|
display: flex;
|
|
519
|
+
align-items: center;
|
|
478
520
|
gap: 8px;
|
|
479
|
-
background: rgba(
|
|
480
|
-
padding: 6px
|
|
481
|
-
border-radius:
|
|
482
|
-
border: 1px solid
|
|
483
|
-
|
|
521
|
+
background: rgba(15, 23, 42, 0.88);
|
|
522
|
+
padding: 6px 12px;
|
|
523
|
+
border-radius: 10px;
|
|
524
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
525
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
|
526
|
+
backdrop-filter: blur(12px);
|
|
484
527
|
z-index: 10;
|
|
485
528
|
}
|
|
486
529
|
.graph-toolbar button {
|
|
487
|
-
background:
|
|
488
|
-
border: 1px solid
|
|
489
|
-
color: #
|
|
490
|
-
padding:
|
|
491
|
-
border-radius:
|
|
530
|
+
background: rgba(255, 255, 255, 0.06);
|
|
531
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
532
|
+
color: #f1f5f9;
|
|
533
|
+
padding: 5px 10px;
|
|
534
|
+
border-radius: 6px;
|
|
492
535
|
font-size: 11px;
|
|
536
|
+
font-weight: 500;
|
|
493
537
|
cursor: pointer;
|
|
538
|
+
transition: all 0.15s ease;
|
|
539
|
+
display: inline-flex;
|
|
540
|
+
align-items: center;
|
|
541
|
+
gap: 4px;
|
|
542
|
+
}
|
|
543
|
+
.graph-toolbar button:hover {
|
|
544
|
+
background: rgba(255, 255, 255, 0.12);
|
|
545
|
+
border-color: rgba(255, 255, 255, 0.2);
|
|
494
546
|
}
|
|
495
547
|
.graph-toolbar button.active {
|
|
496
|
-
background:
|
|
548
|
+
background: #d97706;
|
|
549
|
+
border-color: #f59e0b;
|
|
550
|
+
color: #fff;
|
|
551
|
+
}
|
|
552
|
+
.graph-toolbar select, .graph-toolbar input {
|
|
553
|
+
background: rgba(11, 15, 25, 0.85);
|
|
554
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
555
|
+
color: #f8fafc;
|
|
556
|
+
padding: 5px 10px;
|
|
557
|
+
border-radius: 6px;
|
|
558
|
+
font-size: 11px;
|
|
559
|
+
}
|
|
560
|
+
.graph-toolbar input:focus, .graph-toolbar select:focus {
|
|
561
|
+
outline: none;
|
|
562
|
+
border-color: var(--amber);
|
|
563
|
+
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2);
|
|
564
|
+
}
|
|
565
|
+
.btn-primary {
|
|
566
|
+
background: var(--amber-dark) !important;
|
|
567
|
+
color: #fff !important;
|
|
568
|
+
font-weight: 600 !important;
|
|
569
|
+
}
|
|
570
|
+
.btn-primary:hover {
|
|
571
|
+
background: var(--amber) !important;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/* Slide-over Inspector Sidebar */
|
|
575
|
+
#graph-inspector {
|
|
576
|
+
position: absolute;
|
|
577
|
+
top: 0;
|
|
578
|
+
right: 0;
|
|
579
|
+
width: 420px;
|
|
580
|
+
max-width: 90vw;
|
|
581
|
+
height: 100%;
|
|
582
|
+
background: var(--surface);
|
|
583
|
+
border-left: 1px solid var(--border);
|
|
584
|
+
box-shadow: -8px 0 24px rgba(0,0,0,0.5);
|
|
585
|
+
transform: translateX(100%);
|
|
586
|
+
transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
|
587
|
+
z-index: 25;
|
|
588
|
+
display: flex;
|
|
589
|
+
flex-direction: column;
|
|
590
|
+
}
|
|
591
|
+
#graph-inspector.open {
|
|
592
|
+
transform: translateX(0);
|
|
593
|
+
}
|
|
594
|
+
.inspector-header {
|
|
595
|
+
padding: 14px 18px;
|
|
596
|
+
border-bottom: 1px solid var(--border);
|
|
597
|
+
display: flex;
|
|
598
|
+
align-items: center;
|
|
599
|
+
justify-content: space-between;
|
|
600
|
+
gap: 12px;
|
|
601
|
+
background: var(--card-bg);
|
|
602
|
+
}
|
|
603
|
+
.inspector-title {
|
|
604
|
+
font-size: 15px;
|
|
605
|
+
font-weight: 700;
|
|
606
|
+
color: #fff;
|
|
607
|
+
overflow: hidden;
|
|
608
|
+
text-overflow: ellipsis;
|
|
609
|
+
white-space: nowrap;
|
|
610
|
+
}
|
|
611
|
+
.inspector-close {
|
|
612
|
+
background: transparent;
|
|
613
|
+
border: none;
|
|
614
|
+
color: var(--muted);
|
|
615
|
+
font-size: 20px;
|
|
616
|
+
cursor: pointer;
|
|
617
|
+
line-height: 1;
|
|
618
|
+
padding: 4px;
|
|
619
|
+
border-radius: 4px;
|
|
620
|
+
}
|
|
621
|
+
.inspector-close:hover {
|
|
622
|
+
color: #fff;
|
|
623
|
+
background: var(--surface-hover);
|
|
624
|
+
}
|
|
625
|
+
.inspector-body {
|
|
626
|
+
flex: 1;
|
|
627
|
+
overflow-y: auto;
|
|
628
|
+
padding: 16px 18px;
|
|
629
|
+
display: flex;
|
|
630
|
+
flex-direction: column;
|
|
631
|
+
gap: 16px;
|
|
632
|
+
}
|
|
633
|
+
.inspector-footer {
|
|
634
|
+
padding: 12px 18px;
|
|
635
|
+
border-top: 1px solid var(--border);
|
|
636
|
+
background: var(--card-bg);
|
|
637
|
+
display: flex;
|
|
638
|
+
gap: 8px;
|
|
639
|
+
justify-content: flex-end;
|
|
640
|
+
}
|
|
641
|
+
.inspector-section {
|
|
642
|
+
display: flex;
|
|
643
|
+
flex-direction: column;
|
|
644
|
+
gap: 6px;
|
|
645
|
+
}
|
|
646
|
+
.inspector-label {
|
|
647
|
+
font-size: 11px;
|
|
648
|
+
font-weight: 700;
|
|
649
|
+
text-transform: uppercase;
|
|
650
|
+
letter-spacing: 0.05em;
|
|
651
|
+
color: var(--muted);
|
|
652
|
+
display: flex;
|
|
653
|
+
align-items: center;
|
|
654
|
+
gap: 6px;
|
|
655
|
+
}
|
|
656
|
+
.inspector-code {
|
|
657
|
+
font-family: var(--mono);
|
|
658
|
+
font-size: 12px;
|
|
659
|
+
background: #090d16;
|
|
660
|
+
border: 1px solid var(--border);
|
|
661
|
+
border-radius: 6px;
|
|
662
|
+
padding: 10px 12px;
|
|
663
|
+
color: #38bdf8;
|
|
664
|
+
white-space: pre-wrap;
|
|
665
|
+
word-break: break-all;
|
|
666
|
+
max-height: 160px;
|
|
667
|
+
overflow-y: auto;
|
|
668
|
+
}
|
|
669
|
+
.inspector-desc {
|
|
670
|
+
font-size: 13px;
|
|
671
|
+
line-height: 1.5;
|
|
672
|
+
color: #e2e8f0;
|
|
673
|
+
background: rgba(245, 158, 11, 0.05);
|
|
674
|
+
border-left: 3px solid var(--amber);
|
|
675
|
+
border-radius: 0 6px 6px 0;
|
|
676
|
+
padding: 10px 12px;
|
|
677
|
+
}
|
|
678
|
+
.inspector-chip {
|
|
679
|
+
display: inline-flex;
|
|
680
|
+
align-items: center;
|
|
681
|
+
gap: 6px;
|
|
682
|
+
background: var(--card-bg);
|
|
683
|
+
border: 1px solid var(--border);
|
|
684
|
+
padding: 6px 10px;
|
|
685
|
+
border-radius: 6px;
|
|
686
|
+
font-size: 12px;
|
|
687
|
+
color: #cbd5e1;
|
|
688
|
+
cursor: pointer;
|
|
689
|
+
transition: all 0.15s;
|
|
690
|
+
text-decoration: none;
|
|
691
|
+
}
|
|
692
|
+
.inspector-chip:hover {
|
|
693
|
+
background: var(--surface-hover);
|
|
694
|
+
border-color: var(--amber);
|
|
695
|
+
color: #fff;
|
|
696
|
+
}
|
|
697
|
+
.inspector-chip .chip-badge {
|
|
698
|
+
font-size: 10px;
|
|
699
|
+
font-family: var(--mono);
|
|
700
|
+
background: rgba(255,255,255,0.08);
|
|
701
|
+
padding: 1px 5px;
|
|
702
|
+
border-radius: 4px;
|
|
703
|
+
}
|
|
704
|
+
.chip-list {
|
|
705
|
+
display: flex;
|
|
706
|
+
flex-direction: column;
|
|
707
|
+
gap: 6px;
|
|
708
|
+
max-height: 160px;
|
|
709
|
+
overflow-y: auto;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/* Modals */
|
|
713
|
+
.kuma-modal-backdrop {
|
|
714
|
+
position: fixed;
|
|
715
|
+
inset: 0;
|
|
716
|
+
background: rgba(0, 0, 0, 0.75);
|
|
717
|
+
backdrop-filter: blur(4px);
|
|
718
|
+
z-index: 100;
|
|
719
|
+
display: none;
|
|
720
|
+
align-items: center;
|
|
721
|
+
justify-content: center;
|
|
722
|
+
padding: 20px;
|
|
723
|
+
}
|
|
724
|
+
.kuma-modal-backdrop.open {
|
|
725
|
+
display: flex;
|
|
726
|
+
}
|
|
727
|
+
.kuma-modal {
|
|
728
|
+
background: var(--surface);
|
|
729
|
+
border: 1px solid var(--border);
|
|
730
|
+
border-radius: 12px;
|
|
731
|
+
width: 100%;
|
|
732
|
+
max-width: 520px;
|
|
733
|
+
box-shadow: 0 20px 40px rgba(0,0,0,0.6);
|
|
734
|
+
display: flex;
|
|
735
|
+
flex-direction: column;
|
|
736
|
+
overflow: hidden;
|
|
737
|
+
}
|
|
738
|
+
.kuma-modal-header {
|
|
739
|
+
padding: 14px 18px;
|
|
740
|
+
background: var(--card-bg);
|
|
741
|
+
border-bottom: 1px solid var(--border);
|
|
742
|
+
display: flex;
|
|
743
|
+
align-items: center;
|
|
744
|
+
justify-content: space-between;
|
|
745
|
+
}
|
|
746
|
+
.kuma-modal-body {
|
|
747
|
+
padding: 18px;
|
|
748
|
+
display: flex;
|
|
749
|
+
flex-direction: column;
|
|
750
|
+
gap: 14px;
|
|
751
|
+
}
|
|
752
|
+
.kuma-modal-footer {
|
|
753
|
+
padding: 14px 18px;
|
|
754
|
+
background: var(--card-bg);
|
|
755
|
+
border-top: 1px solid var(--border);
|
|
756
|
+
display: flex;
|
|
757
|
+
justify-content: flex-end;
|
|
758
|
+
gap: 10px;
|
|
759
|
+
}
|
|
760
|
+
.form-group {
|
|
761
|
+
display: flex;
|
|
762
|
+
flex-direction: column;
|
|
763
|
+
gap: 6px;
|
|
764
|
+
}
|
|
765
|
+
.form-group label {
|
|
766
|
+
font-size: 12px;
|
|
767
|
+
font-weight: 600;
|
|
768
|
+
color: var(--muted);
|
|
769
|
+
}
|
|
770
|
+
.form-control {
|
|
771
|
+
background: var(--bg);
|
|
772
|
+
border: 1px solid var(--border);
|
|
773
|
+
border-radius: 6px;
|
|
774
|
+
padding: 8px 12px;
|
|
775
|
+
color: #fff;
|
|
776
|
+
font-size: 13px;
|
|
777
|
+
font-family: inherit;
|
|
778
|
+
}
|
|
779
|
+
.form-control:focus {
|
|
780
|
+
outline: none;
|
|
781
|
+
border-color: var(--amber);
|
|
497
782
|
}
|
|
498
783
|
|
|
499
784
|
.empty-placeholder {
|
|
@@ -525,27 +810,24 @@ table.clean-table tr:hover td {
|
|
|
525
810
|
</div>
|
|
526
811
|
|
|
527
812
|
<nav class="nav-tabs">
|
|
528
|
-
<button class="nav-tab active" data-view="
|
|
529
|
-
<i data-lucide="
|
|
530
|
-
|
|
531
|
-
<span class="tab-badge" id="badge-
|
|
813
|
+
<button class="nav-tab active" data-view="graph">
|
|
814
|
+
<i data-lucide="network" style="width:14px;height:14px"></i>
|
|
815
|
+
Knowledge Graph
|
|
816
|
+
<span class="tab-badge" id="badge-graph-count">0</span>
|
|
532
817
|
</button>
|
|
533
818
|
<button class="nav-tab" data-view="flows">
|
|
534
819
|
<i data-lucide="git-merge" style="width:14px;height:14px"></i>
|
|
535
|
-
|
|
820
|
+
Architecture & Flows
|
|
536
821
|
<span class="tab-badge" id="badge-flow-count">0</span>
|
|
537
822
|
</button>
|
|
538
|
-
<button class="nav-tab" data-view="
|
|
539
|
-
<i data-lucide="
|
|
540
|
-
|
|
823
|
+
<button class="nav-tab" data-view="gotchas">
|
|
824
|
+
<i data-lucide="shield-alert" style="width:14px;height:14px"></i>
|
|
825
|
+
Known Gotchas
|
|
826
|
+
<span class="tab-badge" id="badge-gotcha-count">0</span>
|
|
541
827
|
</button>
|
|
542
828
|
<button class="nav-tab" data-view="health">
|
|
543
829
|
<i data-lucide="activity" style="width:14px;height:14px"></i>
|
|
544
|
-
|
|
545
|
-
</button>
|
|
546
|
-
<button class="nav-tab" data-view="graph">
|
|
547
|
-
<i data-lucide="network" style="width:14px;height:14px"></i>
|
|
548
|
-
Knowledge Graph
|
|
830
|
+
Health & Audit
|
|
549
831
|
</button>
|
|
550
832
|
</nav>
|
|
551
833
|
|
|
@@ -556,29 +838,51 @@ table.clean-table tr:hover td {
|
|
|
556
838
|
</header>
|
|
557
839
|
|
|
558
840
|
<main id="viewport">
|
|
559
|
-
<!-- VIEW 1:
|
|
560
|
-
<section class="view-panel active" id="view-
|
|
561
|
-
<div
|
|
562
|
-
<div>
|
|
563
|
-
<
|
|
564
|
-
<
|
|
841
|
+
<!-- VIEW 1: KNOWLEDGE GRAPH (PRIMARY HERO) -->
|
|
842
|
+
<section class="view-panel active" id="view-graph" style="padding:0">
|
|
843
|
+
<div id="graph-viewport">
|
|
844
|
+
<div class="graph-toolbar">
|
|
845
|
+
<button onclick="fitGraph()"><i data-lucide="maximize" style="width:11px;height:11px"></i> Fit</button>
|
|
846
|
+
<button id="btn-toggle-physics" onclick="togglePhysics()">Physics: Off</button>
|
|
847
|
+
<select id="graph-filter-type" onchange="onGraphFilterChange(this.value)">
|
|
848
|
+
<option value="all">All Types</option>
|
|
849
|
+
<option value="file">Files</option>
|
|
850
|
+
<option value="function">Functions</option>
|
|
851
|
+
<option value="class">Classes</option>
|
|
852
|
+
<option value="interface">Interfaces</option>
|
|
853
|
+
<option value="feature_domain">Domains & Architecture</option>
|
|
854
|
+
<option value="gotcha">Gotchas</option>
|
|
855
|
+
<option value="decision">Decisions</option>
|
|
856
|
+
</select>
|
|
857
|
+
<input id="graph-search-input" type="text" placeholder="Jump to node..." oninput="onGraphSearch(this.value)" />
|
|
858
|
+
<button class="btn-primary" onclick="openCreateNodeModal()">+ New Node</button>
|
|
859
|
+
<button onclick="openConnectEdgeModal()">+ Connect Edge</button>
|
|
860
|
+
<span style="color:var(--muted);font-size:11px;align-self:center;margin-left:4px" id="graph-stats-label">0 nodes</span>
|
|
565
861
|
</div>
|
|
566
|
-
<
|
|
567
|
-
</div>
|
|
862
|
+
<div id="graph-canvas"></div>
|
|
568
863
|
|
|
569
|
-
|
|
570
|
-
<
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
864
|
+
<!-- Slide-over Inspector Sidebar -->
|
|
865
|
+
<aside id="graph-inspector">
|
|
866
|
+
<div class="inspector-header">
|
|
867
|
+
<div style="display:flex;align-items:center;gap:8px;overflow:hidden">
|
|
868
|
+
<span id="inspector-badge" class="badge-project">NODE</span>
|
|
869
|
+
<span id="inspector-name" class="inspector-title">Node Name</span>
|
|
870
|
+
</div>
|
|
871
|
+
<button class="inspector-close" onclick="closeNodeInspector()">×</button>
|
|
872
|
+
</div>
|
|
873
|
+
<div class="inspector-body" id="inspector-body">
|
|
874
|
+
<div style="color:var(--muted);font-size:12px;text-align:center;padding:20px">Select a node in the graph to inspect details.</div>
|
|
875
|
+
</div>
|
|
876
|
+
<div class="inspector-footer" id="inspector-footer" style="display:none">
|
|
877
|
+
<button class="btn-action" id="inspector-connect-btn">+ Connect</button>
|
|
878
|
+
<button class="btn-action" id="inspector-edit-btn">Edit</button>
|
|
879
|
+
<button class="btn-action" style="color:var(--rose);border-color:rgba(244,63,94,0.3)" id="inspector-delete-btn">Delete</button>
|
|
880
|
+
</div>
|
|
881
|
+
</aside>
|
|
576
882
|
</div>
|
|
577
|
-
|
|
578
|
-
<div class="gotcha-grid" id="gotcha-cards-container"></div>
|
|
579
883
|
</section>
|
|
580
884
|
|
|
581
|
-
<!-- VIEW 2:
|
|
885
|
+
<!-- VIEW 2: ARCHITECTURE & FLOWS -->
|
|
582
886
|
<section class="view-panel" id="view-flows">
|
|
583
887
|
<div class="view-header">
|
|
584
888
|
<div>
|
|
@@ -589,37 +893,34 @@ table.clean-table tr:hover td {
|
|
|
589
893
|
<div class="flow-list" id="flow-cards-container"></div>
|
|
590
894
|
</section>
|
|
591
895
|
|
|
592
|
-
<!-- VIEW 3:
|
|
593
|
-
<section class="view-panel" id="view-
|
|
896
|
+
<!-- VIEW 3: GOTCHA SHIELD -->
|
|
897
|
+
<section class="view-panel" id="view-gotchas">
|
|
594
898
|
<div class="view-header">
|
|
595
899
|
<div>
|
|
596
|
-
<div class="view-title"><i data-lucide="
|
|
597
|
-
<div class="view-desc">
|
|
900
|
+
<div class="view-title"><i data-lucide="shield-alert" style="color:var(--amber)"></i> Known Code Gotchas</div>
|
|
901
|
+
<div class="view-desc">Catalog of fragile legacy contracts, edge cases, and unexpected quirks injected right before edits.</div>
|
|
598
902
|
</div>
|
|
903
|
+
<input type="text" id="gotcha-search" class="search-input" placeholder="Search file, issue, or workaround..." oninput="filterGotchas()" />
|
|
599
904
|
</div>
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
</div>
|
|
605
|
-
<div class="
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
<div style="display:flex;gap:8px;margin-bottom:12px">
|
|
609
|
-
<input type="text" id="blast-input" class="search-input" placeholder="e.g. src/auth.ts or package/core" style="flex:1" />
|
|
610
|
-
<button class="btn-action" onclick="simulateBlastRadius()">Check</button>
|
|
611
|
-
</div>
|
|
612
|
-
<div id="blast-result" style="font-size:12px;color:#cbd5e1;background:var(--bg);padding:12px;border-radius:6px;border:1px solid var(--border)">Enter a target path to inspect blast radius.</div>
|
|
613
|
-
</div>
|
|
905
|
+
|
|
906
|
+
<div class="filter-pills" id="status-pills">
|
|
907
|
+
<div class="pill active" data-status="all" onclick="setStatusFilter('all')">All Statuses</div>
|
|
908
|
+
<div class="pill" data-status="active" onclick="setStatusFilter('active')">Active</div>
|
|
909
|
+
<div class="pill" data-status="verified" onclick="setStatusFilter('verified')">Verified</div>
|
|
910
|
+
<div class="pill" data-status="candidate" onclick="setStatusFilter('candidate')">Candidate</div>
|
|
911
|
+
<div class="pill" data-status="resolved" onclick="setStatusFilter('resolved')">Resolved</div>
|
|
912
|
+
<div class="pill" data-status="deprecated" onclick="setStatusFilter('deprecated')">Deprecated</div>
|
|
614
913
|
</div>
|
|
914
|
+
|
|
915
|
+
<div class="gotcha-grid" id="gotcha-cards-container"></div>
|
|
615
916
|
</section>
|
|
616
917
|
|
|
617
|
-
<!-- VIEW 4: HEALTH &
|
|
918
|
+
<!-- VIEW 4: HEALTH & AUDIT -->
|
|
618
919
|
<section class="view-panel" id="view-health">
|
|
619
920
|
<div class="view-header">
|
|
620
921
|
<div>
|
|
621
922
|
<div class="view-title"><i data-lucide="activity" style="color:var(--emerald)"></i> Efficiency & Health Metrics</div>
|
|
622
|
-
<div class="view-desc">
|
|
923
|
+
<div class="view-desc">Project metrics and stale reference detection ensuring memory reliability.</div>
|
|
623
924
|
</div>
|
|
624
925
|
</div>
|
|
625
926
|
|
|
@@ -647,19 +948,94 @@ table.clean-table tr:hover td {
|
|
|
647
948
|
<div id="staleness-container" style="padding:16px;font-size:12px;color:var(--muted)">Checking staleness...</div>
|
|
648
949
|
</div>
|
|
649
950
|
</section>
|
|
951
|
+
</main>
|
|
650
952
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
953
|
+
<!-- Modal: Create / Edit Node -->
|
|
954
|
+
<div class="kuma-modal-backdrop" id="modal-node">
|
|
955
|
+
<div class="kuma-modal">
|
|
956
|
+
<div class="kuma-modal-header">
|
|
957
|
+
<span style="font-weight:700;font-size:14px" id="modal-node-title">Create Knowledge Node</span>
|
|
958
|
+
<button class="inspector-close" onclick="closeModal('modal-node')">×</button>
|
|
959
|
+
</div>
|
|
960
|
+
<div class="kuma-modal-body">
|
|
961
|
+
<input type="hidden" id="node-form-id" />
|
|
962
|
+
<div class="form-group">
|
|
963
|
+
<label>Type</label>
|
|
964
|
+
<select id="node-form-type" class="form-control">
|
|
965
|
+
<option value="feature_domain">feature_domain (Domain / Architecture Boundary)</option>
|
|
966
|
+
<option value="arch_flow">arch_flow (Architecture Flow / Sequence)</option>
|
|
967
|
+
<option value="decision">decision (Architecture Decision)</option>
|
|
968
|
+
<option value="workflow">workflow (Business / Code Workflow)</option>
|
|
969
|
+
<option value="function">function</option>
|
|
970
|
+
<option value="class">class</option>
|
|
971
|
+
<option value="interface">interface</option>
|
|
972
|
+
<option value="file">file</option>
|
|
973
|
+
<option value="api_route">api_route</option>
|
|
974
|
+
<option value="gotcha">gotcha</option>
|
|
975
|
+
</select>
|
|
976
|
+
</div>
|
|
977
|
+
<div class="form-group">
|
|
978
|
+
<label>Name</label>
|
|
979
|
+
<input type="text" id="node-form-name" class="form-control" placeholder="e.g. AuthEngine, calculateMetrics, UserStore" />
|
|
980
|
+
</div>
|
|
981
|
+
<div class="form-group">
|
|
982
|
+
<label>File Path (optional)</label>
|
|
983
|
+
<input type="text" id="node-form-filepath" class="form-control" placeholder="e.g. src/engine/kumaDb.ts" />
|
|
984
|
+
</div>
|
|
985
|
+
<div class="form-group">
|
|
986
|
+
<label>Purpose / Description (for AI Agents and Developers)</label>
|
|
987
|
+
<textarea id="node-form-desc" class="form-control" rows="3" placeholder="Explain what this component or function does..."></textarea>
|
|
658
988
|
</div>
|
|
659
|
-
<div id="graph-canvas"></div>
|
|
660
989
|
</div>
|
|
661
|
-
|
|
662
|
-
</
|
|
990
|
+
<div class="kuma-modal-footer">
|
|
991
|
+
<button class="btn-action" onclick="closeModal('modal-node')">Cancel</button>
|
|
992
|
+
<button class="btn-action btn-primary" onclick="submitNodeForm()">Save Node</button>
|
|
993
|
+
</div>
|
|
994
|
+
</div>
|
|
995
|
+
</div>
|
|
996
|
+
|
|
997
|
+
<!-- Modal: Connect Edge -->
|
|
998
|
+
<div class="kuma-modal-backdrop" id="modal-edge">
|
|
999
|
+
<div class="kuma-modal">
|
|
1000
|
+
<div class="kuma-modal-header">
|
|
1001
|
+
<span style="font-weight:700;font-size:14px">Connect Nodes (Create Edge)</span>
|
|
1002
|
+
<button class="inspector-close" onclick="closeModal('modal-edge')">×</button>
|
|
1003
|
+
</div>
|
|
1004
|
+
<div class="kuma-modal-body">
|
|
1005
|
+
<div class="form-group">
|
|
1006
|
+
<label>Source Node</label>
|
|
1007
|
+
<select id="edge-form-source" class="form-control"></select>
|
|
1008
|
+
</div>
|
|
1009
|
+
<div class="form-group">
|
|
1010
|
+
<label>Relationship Type</label>
|
|
1011
|
+
<select id="edge-form-type" class="form-control">
|
|
1012
|
+
<option value="calls">calls (Function A calls Function B)</option>
|
|
1013
|
+
<option value="imports">imports (Module A imports Module B)</option>
|
|
1014
|
+
<option value="defines">defines (File defines Function/Class)</option>
|
|
1015
|
+
<option value="tests">tests (Test verifies Code)</option>
|
|
1016
|
+
<option value="depends_on">depends_on (Component depends on Target)</option>
|
|
1017
|
+
<option value="affects">affects (Change here impacts Target)</option>
|
|
1018
|
+
<option value="implements">implements (Class implements Interface)</option>
|
|
1019
|
+
<option value="extends">extends (Class extends Superclass)</option>
|
|
1020
|
+
<option value="flows_through">flows_through (Architecture Flow)</option>
|
|
1021
|
+
<option value="uses">uses (Subsystem uses Component/Service)</option>
|
|
1022
|
+
<option value="produces">produces (Subsystem produces Artifact/Event)</option>
|
|
1023
|
+
<option value="validates">validates (Subsystem validates Contract)</option>
|
|
1024
|
+
<option value="routes_to">routes_to (Router routes to Subsystem)</option>
|
|
1025
|
+
<option value="configures">configures (Subsystem configures Target)</option>
|
|
1026
|
+
</select>
|
|
1027
|
+
</div>
|
|
1028
|
+
<div class="form-group">
|
|
1029
|
+
<label>Target Node</label>
|
|
1030
|
+
<select id="edge-form-target" class="form-control"></select>
|
|
1031
|
+
</div>
|
|
1032
|
+
</div>
|
|
1033
|
+
<div class="kuma-modal-footer">
|
|
1034
|
+
<button class="btn-action" onclick="closeModal('modal-edge')">Cancel</button>
|
|
1035
|
+
<button class="btn-action btn-primary" onclick="submitEdgeForm()">Create Connection</button>
|
|
1036
|
+
</div>
|
|
1037
|
+
</div>
|
|
1038
|
+
</div>
|
|
663
1039
|
|
|
664
1040
|
<script>
|
|
665
1041
|
var API = window.location.origin + "/api";
|
|
@@ -699,11 +1075,21 @@ async function fetchDashboard() {
|
|
|
699
1075
|
}
|
|
700
1076
|
|
|
701
1077
|
function renderAll(data) {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1078
|
+
if (!data) return;
|
|
1079
|
+
if (document.getElementById("badge-graph-count")) {
|
|
1080
|
+
document.getElementById("badge-graph-count").textContent = data.nodes ? data.nodes.length : 0;
|
|
1081
|
+
}
|
|
1082
|
+
try { renderGotchas(data.gotchas || []); } catch (e) { console.error("renderGotchas error:", e); }
|
|
1083
|
+
try { renderFlows(data.flows || []); } catch (e) { console.error("renderFlows error:", e); }
|
|
1084
|
+
try { renderHealth(data); } catch (e) { console.error("renderHealth error:", e); }
|
|
1085
|
+
|
|
1086
|
+
var graphTab = document.querySelector('.nav-tab[data-view="graph"]');
|
|
1087
|
+
if (graphTab && graphTab.classList.contains("active")) {
|
|
1088
|
+
setTimeout(initOrRefreshGraph, 20);
|
|
1089
|
+
}
|
|
1090
|
+
if (window.lucide && window.lucide.createIcons) {
|
|
1091
|
+
try { window.lucide.createIcons(); } catch {}
|
|
1092
|
+
}
|
|
707
1093
|
}
|
|
708
1094
|
|
|
709
1095
|
function setStatusFilter(status) {
|
|
@@ -756,8 +1142,10 @@ function renderGotchaCards(list) {
|
|
|
756
1142
|
${g.workaround ? `
|
|
757
1143
|
<div class="gotcha-workaround">
|
|
758
1144
|
<strong>Workaround:</strong>
|
|
759
|
-
|
|
760
|
-
<button class="btn-copy-workaround" onclick="
|
|
1145
|
+
<div class="workaround-text" style="margin-top:4px">${escapeHtml(g.workaround)}</div>
|
|
1146
|
+
<button class="btn-copy-workaround" onclick="copyWorkaround(this)" title="Copy workaround to clipboard">
|
|
1147
|
+
<i data-lucide="copy" style="width:11px;height:11px"></i> Copy
|
|
1148
|
+
</button>
|
|
761
1149
|
</div>
|
|
762
1150
|
` : ''}
|
|
763
1151
|
<div class="gotcha-footer">
|
|
@@ -771,23 +1159,25 @@ function renderGotchaCards(list) {
|
|
|
771
1159
|
}
|
|
772
1160
|
|
|
773
1161
|
function renderFlows(flows) {
|
|
774
|
-
document.getElementById("badge-flow-count")
|
|
1162
|
+
var countBadge = document.getElementById("badge-flow-count");
|
|
1163
|
+
if (countBadge) countBadge.textContent = flows ? flows.length : 0;
|
|
775
1164
|
var container = document.getElementById("flow-cards-container");
|
|
776
|
-
if (!
|
|
1165
|
+
if (!container) return;
|
|
1166
|
+
if (!flows || !flows.length) {
|
|
777
1167
|
container.innerHTML = '<div class="empty-placeholder"><i data-lucide="git-branch"></i><div>No domain sequence flows recorded yet.</div></div>';
|
|
778
1168
|
if (window.lucide && window.lucide.createIcons) window.lucide.createIcons();
|
|
779
1169
|
return;
|
|
780
1170
|
}
|
|
781
1171
|
container.innerHTML = flows.map(function(f) {
|
|
782
1172
|
var meta = {};
|
|
783
|
-
try { meta = typeof f.metadata === "string" ? JSON.parse(f.metadata) : f.metadata; } catch {}
|
|
784
|
-
var hops = meta.hops
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1173
|
+
try { meta = typeof f.metadata === "string" ? JSON.parse(f.metadata) : (f.metadata || {}); } catch {}
|
|
1174
|
+
var hops = Array.isArray(meta.hops) ? meta.hops : [];
|
|
1175
|
+
var hopsCount = hops.length || (typeof meta.hops === "number" ? meta.hops : (meta.hopsCount || 0));
|
|
1176
|
+
var gotchas = Array.isArray(meta.gotchas) ? meta.gotchas : [];
|
|
1177
|
+
|
|
1178
|
+
var hopsChainHtml = '';
|
|
1179
|
+
if (hops.length > 0) {
|
|
1180
|
+
hopsChainHtml = `
|
|
791
1181
|
<div class="flow-hops-chain">
|
|
792
1182
|
${hops.map(function(h, idx) {
|
|
793
1183
|
var toLabel = typeof h === "object" ? (h.to || "") : String(h);
|
|
@@ -799,45 +1189,27 @@ function renderFlows(flows) {
|
|
|
799
1189
|
return `${firstHopPrefix}<span class="hop-node">${escapeHtml(toLabel)}${rel}</span>${idx < hops.length - 1 ? '<span class="hop-arrow">→</span>' : ''}`;
|
|
800
1190
|
}).join("")}
|
|
801
1191
|
</div>
|
|
802
|
-
|
|
1192
|
+
`;
|
|
1193
|
+
} else if (hopsCount > 0) {
|
|
1194
|
+
hopsChainHtml = `<div style="font-size:12px;color:var(--muted);padding:4px 0">${hopsCount} sequence hop(s) recorded.</div>`;
|
|
1195
|
+
} else {
|
|
1196
|
+
hopsChainHtml = `<div style="font-size:12px;color:var(--muted);padding:4px 0">No hops recorded for this flow.</div>`;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
return `
|
|
1200
|
+
<div class="flow-card">
|
|
1201
|
+
<div class="flow-card-header">
|
|
1202
|
+
<div class="flow-anchor"><i data-lucide="anchor" style="width:16px;height:16px"></i> ${escapeHtml(f.name || "Flow")}</div>
|
|
1203
|
+
<span style="font-size:11px;color:var(--muted)">${hopsCount} hop(s)</span>
|
|
1204
|
+
</div>
|
|
1205
|
+
${hopsChainHtml}
|
|
1206
|
+
${gotchas.length ? `<div style="font-size:11px;color:var(--amber);margin-top:6px">⚠️ Linked Gotchas: ${gotchas.map(escapeHtml).join(", ")}</div>` : ''}
|
|
803
1207
|
</div>
|
|
804
1208
|
`;
|
|
805
1209
|
}).join("");
|
|
806
1210
|
if (window.lucide && window.lucide.createIcons) window.lucide.createIcons();
|
|
807
1211
|
}
|
|
808
1212
|
|
|
809
|
-
function renderWorkspace(ws, nodes) {
|
|
810
|
-
var container = document.getElementById("ws-topology-info");
|
|
811
|
-
if (!ws) {
|
|
812
|
-
container.innerHTML = "<div>Single package workspace.</div>";
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
container.innerHTML = `
|
|
816
|
-
<div style="margin-bottom:8px"><strong>Workspace Type:</strong> <span class="badge-project">${escapeHtml(ws.type || "standard")}</span></div>
|
|
817
|
-
<div style="margin-bottom:8px"><strong>Project Root:</strong> <code>${escapeHtml(ws.name || "root")}</code> (v${escapeHtml(ws.version || "1.0.0")})</div>
|
|
818
|
-
<div style="margin-top:12px;color:var(--muted)">Total indexed knowledge entities: <strong>${nodes.length}</strong></div>
|
|
819
|
-
`;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
function simulateBlastRadius() {
|
|
823
|
-
var val = (document.getElementById("blast-input").value || "").trim();
|
|
824
|
-
var out = document.getElementById("blast-result");
|
|
825
|
-
if (!val) {
|
|
826
|
-
out.innerHTML = "⚠️ Please enter a target path or symbol.";
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
if (!currentData || !currentData.edges) {
|
|
830
|
-
out.innerHTML = "No graph data available.";
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
var matchingEdges = currentData.edges.filter(e => String(e.source).includes(val) || String(e.target).includes(val));
|
|
834
|
-
out.innerHTML = `
|
|
835
|
-
<div style="font-weight:700;color:var(--amber-light);margin-bottom:6px">Blast Radius for "${escapeHtml(val)}":</div>
|
|
836
|
-
<div>Found <strong>${matchingEdges.length}</strong> directly connected relationship(s).</div>
|
|
837
|
-
<div style="margin-top:8px">${matchingEdges.slice(0, 5).map(e => `• <code>${escapeHtml(e.source)}</code> <strong>${escapeHtml(e.relation)}</strong> <code>${escapeHtml(e.target)}</code>`).join("<br>")}</div>
|
|
838
|
-
`;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
1213
|
function renderHealth(data) {
|
|
842
1214
|
var eff = data.efficiency || {};
|
|
843
1215
|
var kpiContainer = document.getElementById("kpi-container");
|
|
@@ -850,8 +1222,8 @@ function renderHealth(data) {
|
|
|
850
1222
|
<div class="kpi-label">Known Gotchas Tracked</div>
|
|
851
1223
|
</div>
|
|
852
1224
|
<div class="kpi-card">
|
|
853
|
-
<div class="kpi-num" style="color:var(--emerald)">${
|
|
854
|
-
<div class="kpi-label">
|
|
1225
|
+
<div class="kpi-num" style="color:var(--emerald)">${data.nodes ? data.nodes.length : 0}</div>
|
|
1226
|
+
<div class="kpi-label">Topology Nodes Mapped</div>
|
|
855
1227
|
</div>
|
|
856
1228
|
<div class="kpi-card">
|
|
857
1229
|
<div class="kpi-num" style="color:var(--amber)">~${savedMinutes}</div>
|
|
@@ -865,13 +1237,16 @@ function renderHealth(data) {
|
|
|
865
1237
|
|
|
866
1238
|
var tbody = document.querySelector("#sessions-table tbody");
|
|
867
1239
|
var sessions = eff.recentSessions || [];
|
|
1240
|
+
if (!tbody) return;
|
|
868
1241
|
if (!sessions.length) {
|
|
869
1242
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--muted)">No session records logged yet.</td></tr>';
|
|
870
1243
|
} else {
|
|
871
1244
|
tbody.innerHTML = sessions.map(function(s) {
|
|
1245
|
+
var dateObj = s.startedAt ? new Date(s.startedAt < 1e11 ? s.startedAt * 1000 : s.startedAt) : null;
|
|
1246
|
+
var timeStr = dateObj && !isNaN(dateObj.getTime()) ? dateObj.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : "Recent";
|
|
872
1247
|
return `
|
|
873
1248
|
<tr>
|
|
874
|
-
<td>${
|
|
1249
|
+
<td>${timeStr}</td>
|
|
875
1250
|
<td>${escapeHtml(s.goal || "General development")}</td>
|
|
876
1251
|
<td>${s.toolCalls}</td>
|
|
877
1252
|
<td>${s.edits}</td>
|
|
@@ -883,6 +1258,7 @@ function renderHealth(data) {
|
|
|
883
1258
|
}
|
|
884
1259
|
|
|
885
1260
|
var staleBox = document.getElementById("staleness-container");
|
|
1261
|
+
if (!staleBox) return;
|
|
886
1262
|
if (data.staleness && data.staleness.missing && data.staleness.missing.length) {
|
|
887
1263
|
staleBox.innerHTML = `
|
|
888
1264
|
<div style="color:var(--rose);margin-bottom:8px">⚠️ Detected ${data.staleness.missing.length} reference(s) to deleted files:</div>
|
|
@@ -893,50 +1269,534 @@ function renderHealth(data) {
|
|
|
893
1269
|
}
|
|
894
1270
|
}
|
|
895
1271
|
|
|
1272
|
+
var selectedNodeId = null;
|
|
1273
|
+
var visNodesDataSet = null;
|
|
1274
|
+
var visEdgesDataSet = null;
|
|
1275
|
+
|
|
1276
|
+
var NODE_COLORS = {
|
|
1277
|
+
file: "#06b6d4",
|
|
1278
|
+
function: "#3b82f6",
|
|
1279
|
+
class: "#ec4899",
|
|
1280
|
+
interface: "#a855f7",
|
|
1281
|
+
type: "#8b5cf6",
|
|
1282
|
+
component: "#0ea5e9",
|
|
1283
|
+
api_route: "#14b8a6",
|
|
1284
|
+
test: "#f59e0b",
|
|
1285
|
+
arch_flow: "#6366f1",
|
|
1286
|
+
feature_domain: "#6366f1",
|
|
1287
|
+
feature: "#4f46e5",
|
|
1288
|
+
decision: "#10b981",
|
|
1289
|
+
gotcha: "#f97316",
|
|
1290
|
+
workflow: "#818cf8",
|
|
1291
|
+
cross_service_link: "#818cf8",
|
|
1292
|
+
research: "#22c55e",
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
function getNodeColor(type) {
|
|
1296
|
+
return NODE_COLORS[type] || "#64748b";
|
|
1297
|
+
}
|
|
1298
|
+
|
|
896
1299
|
function initOrRefreshGraph() {
|
|
897
1300
|
if (!currentData || !currentData.nodes) return;
|
|
898
1301
|
var container = document.getElementById("graph-canvas");
|
|
899
1302
|
document.getElementById("graph-stats-label").textContent = `${currentData.nodes.length} nodes, ${currentData.edges ? currentData.edges.length : 0} edges`;
|
|
900
1303
|
|
|
901
|
-
var
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
else
|
|
907
|
-
|
|
1304
|
+
var filterType = document.getElementById("graph-filter-type") ? document.getElementById("graph-filter-type").value : "all";
|
|
1305
|
+
var displayNodes = currentData.nodes;
|
|
1306
|
+
if (filterType !== "all") {
|
|
1307
|
+
if (filterType === "feature_domain") {
|
|
1308
|
+
displayNodes = displayNodes.filter(n => n.type === "feature_domain" || n.type === "arch_flow" || n.type === "decision");
|
|
1309
|
+
} else {
|
|
1310
|
+
displayNodes = displayNodes.filter(n => n.type === filterType);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
visNodesDataSet = new vis.DataSet(displayNodes.slice(0, 400).map(function(n) {
|
|
1315
|
+
var color = getNodeColor(n.type);
|
|
1316
|
+
var label = n.name || n.id.split("::").pop();
|
|
1317
|
+
|
|
1318
|
+
let meta = {};
|
|
1319
|
+
try { meta = typeof n.metadata === "string" ? JSON.parse(n.metadata) : (n.metadata || {}); } catch {}
|
|
1320
|
+
|
|
1321
|
+
var title = `${label} (${n.type})${n.file_path ? '\nFile: ' + n.file_path : ''}`;
|
|
1322
|
+
if (meta.description) title += `\nPurpose: ${meta.description.split('\n')[0]}`;
|
|
1323
|
+
if (meta.signature) title += `\nSignature: ${meta.signature}`;
|
|
1324
|
+
|
|
1325
|
+
// Size hierarchy for clear visual scanning
|
|
1326
|
+
var size = 11;
|
|
1327
|
+
if (n.type === "file") size = 17;
|
|
1328
|
+
else if (n.type === "feature_domain" || n.type === "arch_flow") size = 15;
|
|
1329
|
+
else if (n.type === "class" || n.type === "interface") size = 13;
|
|
1330
|
+
else if (n.type === "function" || n.type === "component") size = 11;
|
|
1331
|
+
else if (n.type === "decision") size = 12;
|
|
1332
|
+
else if (n.type === "gotcha") size = 12;
|
|
1333
|
+
|
|
908
1334
|
return {
|
|
909
1335
|
id: n.id,
|
|
910
|
-
label:
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1336
|
+
label: label,
|
|
1337
|
+
title: title,
|
|
1338
|
+
shape: "dot",
|
|
1339
|
+
size: size,
|
|
1340
|
+
color: {
|
|
1341
|
+
background: color,
|
|
1342
|
+
border: "#0f172a",
|
|
1343
|
+
highlight: {
|
|
1344
|
+
background: color,
|
|
1345
|
+
border: "#ffffff",
|
|
1346
|
+
},
|
|
1347
|
+
hover: {
|
|
1348
|
+
background: color,
|
|
1349
|
+
border: "#e2e8f0",
|
|
1350
|
+
},
|
|
1351
|
+
},
|
|
1352
|
+
borderWidth: 2,
|
|
1353
|
+
borderWidthSelected: 3,
|
|
1354
|
+
font: {
|
|
1355
|
+
color: "#cbd5e1",
|
|
1356
|
+
size: 11,
|
|
1357
|
+
face: "Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
1358
|
+
strokeWidth: 2,
|
|
1359
|
+
strokeColor: "#060911",
|
|
1360
|
+
vadjust: 3, // Pushes title directly underneath the circular node!
|
|
1361
|
+
},
|
|
915
1362
|
};
|
|
916
1363
|
}));
|
|
917
1364
|
|
|
918
|
-
var
|
|
1365
|
+
var displayedNodeIds = new Set(visNodesDataSet.getIds());
|
|
1366
|
+
var validEdges = (currentData.edges || []).filter(e => displayedNodeIds.has(e.source) && displayedNodeIds.has(e.target));
|
|
1367
|
+
|
|
1368
|
+
visEdgesDataSet = new vis.DataSet(validEdges.slice(0, 700).map(function(e) {
|
|
1369
|
+
const isConceptVerb = ["uses", "produces", "validates", "routes_to", "configures", "flows_through"].includes(e.type);
|
|
919
1370
|
return {
|
|
920
1371
|
from: e.source,
|
|
921
1372
|
to: e.target,
|
|
922
|
-
arrows:
|
|
923
|
-
|
|
924
|
-
|
|
1373
|
+
arrows: {
|
|
1374
|
+
to: { enabled: true, scaleFactor: 0.5 }
|
|
1375
|
+
},
|
|
1376
|
+
title: e.relation || e.type,
|
|
1377
|
+
label: isConceptVerb ? e.type : undefined,
|
|
1378
|
+
font: isConceptVerb ? { size: 9, color: "#94a3b8", strokeWidth: 2, strokeColor: "#060911", align: "middle" } : undefined,
|
|
1379
|
+
color: {
|
|
1380
|
+
color: isConceptVerb ? "rgba(99, 102, 241, 0.45)" : "rgba(148, 163, 184, 0.22)",
|
|
1381
|
+
highlight: "#f59e0b",
|
|
1382
|
+
hover: "#38bdf8",
|
|
1383
|
+
},
|
|
1384
|
+
smooth: {
|
|
1385
|
+
type: "continuous",
|
|
1386
|
+
roundness: 0.15,
|
|
1387
|
+
},
|
|
1388
|
+
width: isConceptVerb ? 1.8 : 1.2,
|
|
925
1389
|
};
|
|
926
1390
|
}));
|
|
927
1391
|
|
|
928
1392
|
var options = {
|
|
929
1393
|
physics: {
|
|
930
1394
|
enabled: physicsEnabled,
|
|
931
|
-
barnesHut: {
|
|
1395
|
+
barnesHut: {
|
|
1396
|
+
gravitationalConstant: -2800,
|
|
1397
|
+
centralGravity: 0.2,
|
|
1398
|
+
springLength: 90,
|
|
1399
|
+
springConstant: 0.04,
|
|
1400
|
+
damping: 0.09,
|
|
1401
|
+
},
|
|
1402
|
+
stabilization: {
|
|
1403
|
+
iterations: 150,
|
|
1404
|
+
updateInterval: 25,
|
|
1405
|
+
},
|
|
1406
|
+
},
|
|
1407
|
+
interaction: {
|
|
1408
|
+
hover: true,
|
|
1409
|
+
tooltipDelay: 120,
|
|
1410
|
+
zoomView: true,
|
|
1411
|
+
dragView: true,
|
|
1412
|
+
selectConnectedEdges: true,
|
|
932
1413
|
},
|
|
933
|
-
interaction: { hover: true, tooltipDelay: 100 },
|
|
934
1414
|
};
|
|
935
1415
|
|
|
936
1416
|
if (graphNetwork) {
|
|
937
1417
|
graphNetwork.destroy();
|
|
938
1418
|
}
|
|
939
|
-
graphNetwork = new vis.Network(container, { nodes:
|
|
1419
|
+
graphNetwork = new vis.Network(container, { nodes: visNodesDataSet, edges: visEdgesDataSet }, options);
|
|
1420
|
+
|
|
1421
|
+
graphNetwork.on("selectNode", function(params) {
|
|
1422
|
+
if (params.nodes && params.nodes.length > 0) {
|
|
1423
|
+
openNodeInspector(params.nodes[0]);
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
graphNetwork.on("click", function(params) {
|
|
1428
|
+
if (!params.nodes || params.nodes.length === 0) {
|
|
1429
|
+
// Clicked on empty space
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function onGraphFilterChange(val) {
|
|
1435
|
+
initOrRefreshGraph();
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function onGraphSearch(query) {
|
|
1439
|
+
if (!query || !graphNetwork || !visNodesDataSet) return;
|
|
1440
|
+
var q = query.trim().toLowerCase();
|
|
1441
|
+
var foundId = null;
|
|
1442
|
+
var allItems = visNodesDataSet.get();
|
|
1443
|
+
for (var i = 0; i < allItems.length; i++) {
|
|
1444
|
+
var item = allItems[i];
|
|
1445
|
+
if (item.label.toLowerCase().includes(q) || item.id.toLowerCase().includes(q)) {
|
|
1446
|
+
foundId = item.id;
|
|
1447
|
+
break;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (foundId) {
|
|
1451
|
+
selectAndFocusNode(foundId);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
async function openNodeInspector(nodeId) {
|
|
1456
|
+
selectedNodeId = nodeId;
|
|
1457
|
+
var inspector = document.getElementById("graph-inspector");
|
|
1458
|
+
var badge = document.getElementById("inspector-badge");
|
|
1459
|
+
var nameEl = document.getElementById("inspector-name");
|
|
1460
|
+
var body = document.getElementById("inspector-body");
|
|
1461
|
+
var footer = document.getElementById("inspector-footer");
|
|
1462
|
+
|
|
1463
|
+
inspector.classList.add("open");
|
|
1464
|
+
badge.textContent = "...";
|
|
1465
|
+
badge.style.background = "rgba(255,255,255,0.1)";
|
|
1466
|
+
nameEl.textContent = "Loading node details...";
|
|
1467
|
+
body.innerHTML = '<div style="color:var(--muted);text-align:center;padding:30px">Fetching metadata & topology...</div>';
|
|
1468
|
+
footer.style.display = "none";
|
|
1469
|
+
|
|
1470
|
+
try {
|
|
1471
|
+
var res = await fetch(API + "/node/" + encodeURIComponent(nodeId)).then(r => r.json());
|
|
1472
|
+
if (!res || !res.node) {
|
|
1473
|
+
body.innerHTML = '<div style="color:var(--rose);padding:20px">Node not found.</div>';
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
var node = res.node;
|
|
1478
|
+
var outgoing = res.outgoing || [];
|
|
1479
|
+
var incoming = res.incoming || [];
|
|
1480
|
+
var gotchas = res.gotchas || [];
|
|
1481
|
+
|
|
1482
|
+
var meta = {};
|
|
1483
|
+
try { meta = typeof node.metadata === "string" ? JSON.parse(node.metadata) : (node.metadata || {}); } catch {}
|
|
1484
|
+
|
|
1485
|
+
var typeColor = getNodeColor(node.type);
|
|
1486
|
+
badge.textContent = node.type;
|
|
1487
|
+
badge.style.background = typeColor;
|
|
1488
|
+
badge.style.color = "#fff";
|
|
1489
|
+
nameEl.textContent = node.name || node.id;
|
|
1490
|
+
nameEl.title = node.name || node.id;
|
|
1491
|
+
|
|
1492
|
+
var html = [];
|
|
1493
|
+
|
|
1494
|
+
// 1. Identity & File Path
|
|
1495
|
+
html.push(`
|
|
1496
|
+
<div class="inspector-section">
|
|
1497
|
+
<div class="inspector-label">Location</div>
|
|
1498
|
+
<div style="display:flex;align-items:center;gap:6px;background:var(--card-bg);padding:8px 10px;border-radius:6px;border:1px solid var(--border)">
|
|
1499
|
+
<span style="font-family:var(--mono);font-size:12px;color:var(--text);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(node.file_path || 'Virtual / In-memory entity')}</span>
|
|
1500
|
+
${node.file_path ? `<button class="btn-action" style="padding:2px 6px;font-size:10px" onclick="copyText('${escapeAttr(node.file_path)}')">Copy</button>` : ''}
|
|
1501
|
+
</div>
|
|
1502
|
+
</div>
|
|
1503
|
+
`);
|
|
1504
|
+
|
|
1505
|
+
// 1b. Centrality & Blast Radius
|
|
1506
|
+
var centrality = res.centrality || { inDegree: incoming.length, outDegree: outgoing.length, isHub: incoming.length >= 4 };
|
|
1507
|
+
var hubTag = centrality.isHub
|
|
1508
|
+
? '<span style="background:rgba(239,68,68,0.2);color:#f87171;padding:2px 8px;border-radius:4px;font-weight:700;font-size:11px">🔥 ARCHITECTURAL HUB</span>'
|
|
1509
|
+
: '<span style="background:rgba(59,130,246,0.15);color:#60a5fa;padding:2px 8px;border-radius:4px;font-size:11px">Modular Node</span>';
|
|
1510
|
+
html.push(`
|
|
1511
|
+
<div class="inspector-section">
|
|
1512
|
+
<div class="inspector-label">Centrality & Impact Rank</div>
|
|
1513
|
+
<div style="display:flex;align-items:center;justify-content:space-between;background:var(--card-bg);padding:8px 10px;border-radius:6px;border:1px solid var(--border)">
|
|
1514
|
+
<div>${hubTag}</div>
|
|
1515
|
+
<div style="font-size:11px;color:var(--muted)"><b>${centrality.inDegree}</b> incoming · <b>${centrality.outDegree}</b> dependencies</div>
|
|
1516
|
+
</div>
|
|
1517
|
+
</div>
|
|
1518
|
+
`);
|
|
1519
|
+
|
|
1520
|
+
// 2. Description / Purpose
|
|
1521
|
+
if (meta.description) {
|
|
1522
|
+
html.push(`
|
|
1523
|
+
<div class="inspector-section">
|
|
1524
|
+
<div class="inspector-label"><i data-lucide="book-open" style="width:12px;height:12px"></i> Purpose / Explanation</div>
|
|
1525
|
+
<div class="inspector-desc">${escapeHtml(meta.description)}</div>
|
|
1526
|
+
</div>
|
|
1527
|
+
`);
|
|
1528
|
+
} else {
|
|
1529
|
+
html.push(`
|
|
1530
|
+
<div class="inspector-section">
|
|
1531
|
+
<div class="inspector-label"><i data-lucide="book-open" style="width:12px;height:12px"></i> Purpose / Explanation</div>
|
|
1532
|
+
<div style="font-size:12px;color:var(--muted);font-style:italic">No description recorded. Click 'Edit' below to add one.</div>
|
|
1533
|
+
</div>
|
|
1534
|
+
`);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// 3. Signature & Parameters
|
|
1538
|
+
if (meta.signature) {
|
|
1539
|
+
html.push(`
|
|
1540
|
+
<div class="inspector-section">
|
|
1541
|
+
<div class="inspector-label"><i data-lucide="code" style="width:12px;height:12px"></i> Type Signature</div>
|
|
1542
|
+
<pre class="inspector-code"><code>${escapeHtml(meta.signature)}</code></pre>
|
|
1543
|
+
</div>
|
|
1544
|
+
`);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// 4. Methods / Members
|
|
1548
|
+
if (meta.methods && Array.isArray(meta.methods) && meta.methods.length > 0) {
|
|
1549
|
+
html.push(`
|
|
1550
|
+
<div class="inspector-section">
|
|
1551
|
+
<div class="inspector-label">Public Methods (${meta.methods.length})</div>
|
|
1552
|
+
<div class="chip-list">
|
|
1553
|
+
${meta.methods.map(m => `<div class="inspector-chip" style="cursor:default"><span class="chip-badge">method</span> ${escapeHtml(m)}</div>`).join("")}
|
|
1554
|
+
</div>
|
|
1555
|
+
</div>
|
|
1556
|
+
`);
|
|
1557
|
+
} else if (meta.members && Array.isArray(meta.members) && meta.members.length > 0) {
|
|
1558
|
+
html.push(`
|
|
1559
|
+
<div class="inspector-section">
|
|
1560
|
+
<div class="inspector-label">Members (${meta.members.length})</div>
|
|
1561
|
+
<div class="chip-list">
|
|
1562
|
+
${meta.members.map(m => `<div class="inspector-chip" style="cursor:default"><span class="chip-badge">member</span> ${escapeHtml(m)}</div>`).join("")}
|
|
1563
|
+
</div>
|
|
1564
|
+
</div>
|
|
1565
|
+
`);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// 5. Incoming Connections (who references/calls/imports this)
|
|
1569
|
+
html.push(`
|
|
1570
|
+
<div class="inspector-section">
|
|
1571
|
+
<div class="inspector-label">Incoming Connections (${incoming.length})</div>
|
|
1572
|
+
<div class="chip-list">
|
|
1573
|
+
${incoming.length === 0 ? '<div style="color:var(--muted);font-size:12px">None</div>' : ''}
|
|
1574
|
+
${incoming.map(e => `
|
|
1575
|
+
<div class="inspector-chip" onclick="selectAndFocusNode('${escapeAttr(e.source)}')">
|
|
1576
|
+
<span class="chip-badge" style="color:var(--amber-light)">${escapeHtml(e.relation)}</span>
|
|
1577
|
+
<span>${escapeHtml(e.source_name || e.source)}</span>
|
|
1578
|
+
</div>
|
|
1579
|
+
`).join("")}
|
|
1580
|
+
</div>
|
|
1581
|
+
</div>
|
|
1582
|
+
`);
|
|
1583
|
+
|
|
1584
|
+
// 6. Outgoing Connections (what this calls/imports/defines)
|
|
1585
|
+
html.push(`
|
|
1586
|
+
<div class="inspector-section">
|
|
1587
|
+
<div class="inspector-label">Outgoing Connections (${outgoing.length})</div>
|
|
1588
|
+
<div class="chip-list">
|
|
1589
|
+
${outgoing.length === 0 ? '<div style="color:var(--muted);font-size:12px">None</div>' : ''}
|
|
1590
|
+
${outgoing.map(e => `
|
|
1591
|
+
<div class="inspector-chip" onclick="selectAndFocusNode('${escapeAttr(e.target)}')">
|
|
1592
|
+
<span class="chip-badge" style="color:var(--cyan)">${escapeHtml(e.relation)}</span>
|
|
1593
|
+
<span>${escapeHtml(e.target_name || e.target)}</span>
|
|
1594
|
+
</div>
|
|
1595
|
+
`).join("")}
|
|
1596
|
+
</div>
|
|
1597
|
+
</div>
|
|
1598
|
+
`);
|
|
1599
|
+
|
|
1600
|
+
// 7. Associated Gotchas
|
|
1601
|
+
if (gotchas.length > 0) {
|
|
1602
|
+
html.push(`
|
|
1603
|
+
<div class="inspector-section">
|
|
1604
|
+
<div class="inspector-label" style="color:var(--rose)">Associated Gotchas (${gotchas.length})</div>
|
|
1605
|
+
<div style="display:flex;flex-direction:column;gap:8px">
|
|
1606
|
+
${gotchas.map(g => `
|
|
1607
|
+
<div style="background:rgba(244,63,94,0.08);border:1px solid rgba(244,63,94,0.25);border-radius:6px;padding:10px;font-size:12px">
|
|
1608
|
+
<div style="font-weight:700;color:var(--rose);margin-bottom:4px">⚠️ ${escapeHtml(g.description)}</div>
|
|
1609
|
+
${g.workaround ? `<div style="color:var(--muted);font-size:11px">Workaround: ${escapeHtml(g.workaround)}</div>` : ''}
|
|
1610
|
+
</div>
|
|
1611
|
+
`).join("")}
|
|
1612
|
+
</div>
|
|
1613
|
+
</div>
|
|
1614
|
+
`);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
body.innerHTML = html.join("");
|
|
1618
|
+
footer.style.display = "flex";
|
|
1619
|
+
|
|
1620
|
+
// Setup action buttons
|
|
1621
|
+
document.getElementById("inspector-connect-btn").onclick = function() {
|
|
1622
|
+
openConnectEdgeModal(node.id);
|
|
1623
|
+
};
|
|
1624
|
+
document.getElementById("inspector-edit-btn").onclick = function() {
|
|
1625
|
+
openEditNodeModal(node);
|
|
1626
|
+
};
|
|
1627
|
+
document.getElementById("inspector-delete-btn").onclick = function() {
|
|
1628
|
+
deleteNode(node.id);
|
|
1629
|
+
};
|
|
1630
|
+
|
|
1631
|
+
if (window.lucide && window.lucide.createIcons) {
|
|
1632
|
+
try { window.lucide.createIcons(); } catch {}
|
|
1633
|
+
}
|
|
1634
|
+
} catch (err) {
|
|
1635
|
+
body.innerHTML = `<div style="color:var(--rose);padding:20px">Failed to load node: ${escapeHtml(String(err))}</div>`;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function closeNodeInspector() {
|
|
1640
|
+
document.getElementById("graph-inspector").classList.remove("open");
|
|
1641
|
+
selectedNodeId = null;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function selectAndFocusNode(nodeId) {
|
|
1645
|
+
if (graphNetwork) {
|
|
1646
|
+
graphNetwork.selectNodes([nodeId]);
|
|
1647
|
+
graphNetwork.focus(nodeId, {
|
|
1648
|
+
scale: 1.2,
|
|
1649
|
+
animation: { duration: 400, easingFunction: "easeInOutQuad" },
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
openNodeInspector(nodeId);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function closeModal(modalId) {
|
|
1656
|
+
var el = document.getElementById(modalId);
|
|
1657
|
+
if (el) el.classList.remove("open");
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function openCreateNodeModal() {
|
|
1661
|
+
document.getElementById("modal-node-title").textContent = "Create Knowledge Node";
|
|
1662
|
+
document.getElementById("node-form-id").value = "";
|
|
1663
|
+
document.getElementById("node-form-type").value = "feature_domain";
|
|
1664
|
+
document.getElementById("node-form-name").value = "";
|
|
1665
|
+
document.getElementById("node-form-filepath").value = "";
|
|
1666
|
+
document.getElementById("node-form-desc").value = "";
|
|
1667
|
+
document.getElementById("modal-node").classList.add("open");
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function openEditNodeModal(node) {
|
|
1671
|
+
document.getElementById("modal-node-title").textContent = "Edit Knowledge Node";
|
|
1672
|
+
document.getElementById("node-form-id").value = node.id;
|
|
1673
|
+
document.getElementById("node-form-type").value = node.type;
|
|
1674
|
+
document.getElementById("node-form-name").value = node.name || "";
|
|
1675
|
+
document.getElementById("node-form-filepath").value = node.file_path || "";
|
|
1676
|
+
var meta = {};
|
|
1677
|
+
try { meta = typeof node.metadata === "string" ? JSON.parse(node.metadata) : (node.metadata || {}); } catch {}
|
|
1678
|
+
document.getElementById("node-form-desc").value = meta.description || "";
|
|
1679
|
+
document.getElementById("modal-node").classList.add("open");
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
async function submitNodeForm() {
|
|
1683
|
+
var id = document.getElementById("node-form-id").value.trim();
|
|
1684
|
+
var type = document.getElementById("node-form-type").value;
|
|
1685
|
+
var name = document.getElementById("node-form-name").value.trim();
|
|
1686
|
+
var filePath = document.getElementById("node-form-filepath").value.trim();
|
|
1687
|
+
var desc = document.getElementById("node-form-desc").value.trim();
|
|
1688
|
+
|
|
1689
|
+
if (!name) {
|
|
1690
|
+
alert("Node name is required.");
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
try {
|
|
1695
|
+
var res = await fetch(API + "/node", {
|
|
1696
|
+
method: "POST",
|
|
1697
|
+
headers: { "Content-Type": "application/json" },
|
|
1698
|
+
body: JSON.stringify({
|
|
1699
|
+
id: id || undefined,
|
|
1700
|
+
type: type,
|
|
1701
|
+
name: name,
|
|
1702
|
+
file_path: filePath || undefined,
|
|
1703
|
+
description: desc || undefined,
|
|
1704
|
+
}),
|
|
1705
|
+
}).then(r => r.json());
|
|
1706
|
+
|
|
1707
|
+
if (res.ok) {
|
|
1708
|
+
showToast("Knowledge node saved successfully!");
|
|
1709
|
+
closeModal("modal-node");
|
|
1710
|
+
await fetchDashboard();
|
|
1711
|
+
if (res.node && res.node.id) {
|
|
1712
|
+
selectAndFocusNode(res.node.id);
|
|
1713
|
+
}
|
|
1714
|
+
} else {
|
|
1715
|
+
alert("Failed to save node: " + (res.error || "Unknown error"));
|
|
1716
|
+
}
|
|
1717
|
+
} catch (err) {
|
|
1718
|
+
alert("Error saving node: " + err);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
function openConnectEdgeModal(prefillSource) {
|
|
1723
|
+
var sourceSelect = document.getElementById("edge-form-source");
|
|
1724
|
+
var targetSelect = document.getElementById("edge-form-target");
|
|
1725
|
+
sourceSelect.innerHTML = "";
|
|
1726
|
+
targetSelect.innerHTML = "";
|
|
1727
|
+
|
|
1728
|
+
if (currentData && currentData.nodes) {
|
|
1729
|
+
currentData.nodes.forEach(function(n) {
|
|
1730
|
+
var opt1 = document.createElement("option");
|
|
1731
|
+
opt1.value = n.id;
|
|
1732
|
+
opt1.textContent = `[${n.type}] ${n.name || n.id}`;
|
|
1733
|
+
sourceSelect.appendChild(opt1);
|
|
1734
|
+
|
|
1735
|
+
var opt2 = document.createElement("option");
|
|
1736
|
+
opt2.value = n.id;
|
|
1737
|
+
opt2.textContent = `[${n.type}] ${n.name || n.id}`;
|
|
1738
|
+
targetSelect.appendChild(opt2);
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
if (prefillSource) {
|
|
1743
|
+
sourceSelect.value = prefillSource;
|
|
1744
|
+
}
|
|
1745
|
+
document.getElementById("modal-edge").classList.add("open");
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
async function submitEdgeForm() {
|
|
1749
|
+
var source = document.getElementById("edge-form-source").value;
|
|
1750
|
+
var target = document.getElementById("edge-form-target").value;
|
|
1751
|
+
var type = document.getElementById("edge-form-type").value;
|
|
1752
|
+
|
|
1753
|
+
if (!source || !target) {
|
|
1754
|
+
alert("Please select both source and target nodes.");
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
if (source === target) {
|
|
1758
|
+
alert("Source and target cannot be the same node.");
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
try {
|
|
1763
|
+
var res = await fetch(API + "/edge", {
|
|
1764
|
+
method: "POST",
|
|
1765
|
+
headers: { "Content-Type": "application/json" },
|
|
1766
|
+
body: JSON.stringify({ source: source, target: target, type: type }),
|
|
1767
|
+
}).then(r => r.json());
|
|
1768
|
+
|
|
1769
|
+
if (res.ok) {
|
|
1770
|
+
showToast("Connection created successfully!");
|
|
1771
|
+
closeModal("modal-edge");
|
|
1772
|
+
await fetchDashboard();
|
|
1773
|
+
selectAndFocusNode(source);
|
|
1774
|
+
} else {
|
|
1775
|
+
alert("Failed to create connection: " + (res.error || "Unknown error"));
|
|
1776
|
+
}
|
|
1777
|
+
} catch (err) {
|
|
1778
|
+
alert("Error creating connection: " + err);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
async function deleteNode(nodeId) {
|
|
1783
|
+
if (!confirm(`Are you sure you want to delete node "${nodeId}" and its connections?`)) return;
|
|
1784
|
+
|
|
1785
|
+
try {
|
|
1786
|
+
var res = await fetch(API + "/node/" + encodeURIComponent(nodeId), {
|
|
1787
|
+
method: "DELETE",
|
|
1788
|
+
}).then(r => r.json());
|
|
1789
|
+
|
|
1790
|
+
if (res.ok) {
|
|
1791
|
+
showToast("Node deleted successfully!");
|
|
1792
|
+
closeNodeInspector();
|
|
1793
|
+
await fetchDashboard();
|
|
1794
|
+
} else {
|
|
1795
|
+
alert("Failed to delete node: " + (res.error || "Unknown error"));
|
|
1796
|
+
}
|
|
1797
|
+
} catch (err) {
|
|
1798
|
+
alert("Error deleting node: " + err);
|
|
1799
|
+
}
|
|
940
1800
|
}
|
|
941
1801
|
|
|
942
1802
|
function togglePhysics() {
|
|
@@ -961,10 +1821,76 @@ async function resetDb() {
|
|
|
961
1821
|
}
|
|
962
1822
|
}
|
|
963
1823
|
|
|
964
|
-
function
|
|
965
|
-
|
|
966
|
-
|
|
1824
|
+
function showToast(msg) {
|
|
1825
|
+
var toast = document.getElementById("kuma-toast");
|
|
1826
|
+
if (!toast) {
|
|
1827
|
+
toast = document.createElement("div");
|
|
1828
|
+
toast.id = "kuma-toast";
|
|
1829
|
+
document.body.appendChild(toast);
|
|
967
1830
|
}
|
|
1831
|
+
toast.innerHTML = `<span style="color:var(--emerald);font-size:14px">✓</span> <span>${escapeHtml(msg || "Copied to clipboard!")}</span>`;
|
|
1832
|
+
toast.classList.add("show");
|
|
1833
|
+
clearTimeout(window.__kumaToastTimer);
|
|
1834
|
+
window.__kumaToastTimer = setTimeout(function() {
|
|
1835
|
+
toast.classList.remove("show");
|
|
1836
|
+
}, 2500);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function fallbackCopy(text) {
|
|
1840
|
+
var textarea = document.createElement("textarea");
|
|
1841
|
+
textarea.value = text;
|
|
1842
|
+
textarea.style.position = "fixed";
|
|
1843
|
+
textarea.style.top = "-9999px";
|
|
1844
|
+
textarea.style.left = "-9999px";
|
|
1845
|
+
document.body.appendChild(textarea);
|
|
1846
|
+
textarea.focus();
|
|
1847
|
+
textarea.select();
|
|
1848
|
+
try {
|
|
1849
|
+
document.execCommand("copy");
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
console.error("execCommand copy error:", err);
|
|
1852
|
+
}
|
|
1853
|
+
document.body.removeChild(textarea);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
async function copyText(str, btn, toastMsg) {
|
|
1857
|
+
if (!str) return;
|
|
1858
|
+
var success = false;
|
|
1859
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
1860
|
+
try {
|
|
1861
|
+
await navigator.clipboard.writeText(str);
|
|
1862
|
+
success = true;
|
|
1863
|
+
} catch {
|
|
1864
|
+
fallbackCopy(str);
|
|
1865
|
+
success = true;
|
|
1866
|
+
}
|
|
1867
|
+
} else {
|
|
1868
|
+
fallbackCopy(str);
|
|
1869
|
+
success = true;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
if (success) {
|
|
1873
|
+
showToast(toastMsg || "Copied to clipboard!");
|
|
1874
|
+
if (btn) {
|
|
1875
|
+
var origHtml = btn.innerHTML;
|
|
1876
|
+
btn.classList.add("copied");
|
|
1877
|
+
btn.innerHTML = '<span>Copied! ✓</span>';
|
|
1878
|
+
setTimeout(function() {
|
|
1879
|
+
btn.classList.remove("copied");
|
|
1880
|
+
btn.innerHTML = origHtml;
|
|
1881
|
+
}, 2000);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
function copyWorkaround(btn) {
|
|
1887
|
+
if (!btn) return;
|
|
1888
|
+
var parent = btn.closest(".gotcha-workaround");
|
|
1889
|
+
if (!parent) return;
|
|
1890
|
+
var textEl = parent.querySelector(".workaround-text");
|
|
1891
|
+
var text = textEl ? textEl.innerText.trim() : "";
|
|
1892
|
+
if (!text) return;
|
|
1893
|
+
copyText(text, btn, "Workaround copied to clipboard!");
|
|
968
1894
|
}
|
|
969
1895
|
|
|
970
1896
|
function escapeHtml(str) {
|