@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.
@@ -322,10 +322,47 @@ header {
322
322
  color: var(--muted);
323
323
  border-radius: 4px;
324
324
  font-size: 10px;
325
- padding: 2px 6px;
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: radial-gradient(ellipse at center, #161e31 0%, var(--bg) 75%);
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(17,24,39,0.9);
480
- padding: 6px 10px;
481
- border-radius: 8px;
482
- border: 1px solid var(--border);
483
- backdrop-filter: blur(8px);
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: var(--surface-hover);
488
- border: 1px solid var(--border);
489
- color: #fff;
490
- padding: 4px 8px;
491
- border-radius: 5px;
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: var(--amber-dark);
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="gotchas">
529
- <i data-lucide="shield-alert" style="width:14px;height:14px"></i>
530
- Gotcha Shield
531
- <span class="tab-badge" id="badge-gotcha-count">0</span>
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
- Sequence Flows
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="workspace">
539
- <i data-lucide="box" style="width:14px;height:14px"></i>
540
- Workspace & Impact
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
- Efficiency & Health
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: GOTCHA SHIELD -->
560
- <section class="view-panel active" id="view-gotchas">
561
- <div class="view-header">
562
- <div>
563
- <div class="view-title"><i data-lucide="shield-alert" style="color:var(--amber)"></i> Known Code Gotchas</div>
564
- <div class="view-desc">Catalog of fragile legacy contracts, edge cases, and unexpected quirks injected right before edits.</div>
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
- <input type="text" id="gotcha-search" class="search-input" placeholder="Search file, issue, or workaround..." oninput="filterGotchas()" />
567
- </div>
862
+ <div id="graph-canvas"></div>
568
863
 
569
- <div class="filter-pills" id="status-pills">
570
- <div class="pill active" data-status="all" onclick="setStatusFilter('all')">All Statuses</div>
571
- <div class="pill" data-status="active" onclick="setStatusFilter('active')">Active</div>
572
- <div class="pill" data-status="verified" onclick="setStatusFilter('verified')">Verified</div>
573
- <div class="pill" data-status="candidate" onclick="setStatusFilter('candidate')">Candidate</div>
574
- <div class="pill" data-status="resolved" onclick="setStatusFilter('resolved')">Resolved</div>
575
- <div class="pill" data-status="deprecated" onclick="setStatusFilter('deprecated')">Deprecated</div>
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()">&times;</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: SEQUENCE FLOWS -->
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: WORKSPACE & IMPACT -->
593
- <section class="view-panel" id="view-workspace">
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="box" style="color:var(--cyan)"></i> Workspace & Blast Radius Topology</div>
597
- <div class="view-desc">Monorepo packages, package graph, and blast radius simulation.</div>
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
- <div class="workspace-grid">
601
- <div class="ws-card">
602
- <h3><i data-lucide="layers" style="width:16px;height:16px"></i> Repository Topology</h3>
603
- <div id="ws-topology-info" style="font-size:13px;color:#cbd5e1;line-height:1.6">Loading workspace information...</div>
604
- </div>
605
- <div class="ws-card">
606
- <h3><i data-lucide="target" style="width:16px;height:16px"></i> Blast Radius Checker</h3>
607
- <p style="font-size:12px;color:var(--muted);margin-bottom:12px">Check downstream impact and consumer packages for a target file:</p>
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 & EFFICIENCY -->
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">Proving that the more coding agents use Kuma, the more efficient and regression-free development becomes.</div>
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
- <!-- VIEW 5: KNOWLEDGE GRAPH -->
652
- <section class="view-panel" id="view-graph" style="padding:0">
653
- <div id="graph-viewport">
654
- <div class="graph-toolbar">
655
- <button onclick="fitGraph()"><i data-lucide="maximize" style="width:11px;height:11px"></i> Fit</button>
656
- <button id="btn-toggle-physics" onclick="togglePhysics()">Physics: Off</button>
657
- <span style="color:var(--muted);font-size:11px;align-self:center;margin-left:4px" id="graph-stats-label">0 nodes</span>
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')">&times;</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
- </section>
662
- </main>
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')">&times;</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
- renderGotchas(data.gotchas || []);
703
- renderFlows(data.flows || []);
704
- renderWorkspace(data.workspace, data.nodes || []);
705
- renderHealth(data);
706
- if (window.lucide && window.lucide.createIcons) window.lucide.createIcons();
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
- ${escapeHtml(g.workaround)}
760
- <button class="btn-copy-workaround" onclick="copyText('${escapeAttr(g.workaround)}')">Copy</button>
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").textContent = flows.length;
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 (!flows.length) {
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
- return `
786
- <div class="flow-card">
787
- <div class="flow-card-header">
788
- <div class="flow-anchor"><i data-lucide="anchor" style="width:16px;height:16px"></i> ${escapeHtml(f.name || "Flow")}</div>
789
- <span style="font-size:11px;color:var(--muted)">${hops.length} hop(s)</span>
790
- </div>
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
- ${meta.gotchas && meta.gotchas.length ? `<div style="font-size:11px;color:var(--amber)">⚠️ Linked Gotchas: ${meta.gotchas.map(escapeHtml).join(", ")}</div>` : ''}
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)">${passedRate}</div>
854
- <div class="kpi-label">Auto-Verification Pass Rate</div>
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>${s.startedAt ? new Date(s.startedAt).toLocaleTimeString() : "Recent"}</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 visNodes = new vis.DataSet(currentData.nodes.slice(0, 300).map(function(n) {
902
- var color = "#64748b";
903
- if (n.type === "file") color = "#06b6d4";
904
- else if (n.type === "gotcha") color = "#f59e0b";
905
- else if (n.type === "arch_flow" || n.type === "feature_domain") color = "#6366f1";
906
- else if (n.type === "cross_service_link" || n.type === "workflow") color = "#818cf8";
907
- else if (n.type === "decision") color = "#10b981";
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: n.name || n.id,
911
- shape: "box",
912
- color: { background: color, border: "#243049", highlight: "#fde68a" },
913
- font: { color: "#fff", size: 11, face: "ui-monospace, monospace" },
914
- margin: 8,
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 visEdges = new vis.DataSet((currentData.edges || []).slice(0, 500).map(function(e) {
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: "to",
923
- color: { color: "#334155", highlight: "#f59e0b" },
924
- width: 1,
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: { gravitationalConstant: -3000, springLength: 80 },
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: visNodes, edges: visEdges }, options);
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 copyText(str) {
965
- if (navigator.clipboard) {
966
- navigator.clipboard.writeText(str);
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) {