laminark 2.21.7 → 2.21.9

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.
Files changed (52) hide show
  1. package/README.md +36 -71
  2. package/package.json +9 -7
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/plugin/CLAUDE.md +10 -0
  5. package/plugin/commands/recall.md +55 -0
  6. package/plugin/commands/remember.md +34 -0
  7. package/plugin/commands/resume.md +45 -0
  8. package/plugin/commands/stash.md +34 -0
  9. package/plugin/commands/status.md +33 -0
  10. package/plugin/dist/hooks/handler.d.ts +3 -1
  11. package/plugin/dist/hooks/handler.d.ts.map +1 -1
  12. package/plugin/dist/hooks/handler.js +312 -23
  13. package/plugin/dist/hooks/handler.js.map +1 -1
  14. package/plugin/dist/index.d.ts +3 -1
  15. package/plugin/dist/index.d.ts.map +1 -1
  16. package/plugin/dist/index.js +2111 -525
  17. package/plugin/dist/index.js.map +1 -1
  18. package/plugin/dist/{observations-Ch0nc47i.d.mts → observations-CorAAc1A.d.mts} +23 -1
  19. package/plugin/dist/observations-CorAAc1A.d.mts.map +1 -0
  20. package/plugin/dist/{tool-registry-CZ3mJ4iR.mjs → tool-registry-D8un_AcG.mjs} +932 -13
  21. package/plugin/dist/tool-registry-D8un_AcG.mjs.map +1 -0
  22. package/plugin/hooks/hooks.json +6 -6
  23. package/plugin/laminark.db +0 -0
  24. package/plugin/package.json +17 -0
  25. package/plugin/scripts/README.md +19 -1
  26. package/plugin/scripts/bump-version.sh +24 -19
  27. package/plugin/scripts/dev-sync.sh +58 -0
  28. package/plugin/scripts/ensure-deps.sh +5 -2
  29. package/plugin/scripts/install.sh +115 -39
  30. package/plugin/scripts/local-install.sh +93 -58
  31. package/plugin/scripts/uninstall.sh +76 -38
  32. package/plugin/scripts/update.sh +20 -69
  33. package/plugin/scripts/verify-install.sh +69 -25
  34. package/plugin/ui/activity.js +12 -0
  35. package/plugin/ui/app.js +24 -54
  36. package/plugin/ui/graph.js +413 -186
  37. package/plugin/ui/help/activity-feed.png +0 -0
  38. package/plugin/ui/help/analysis-panel.png +0 -0
  39. package/plugin/ui/help/graph-toolbar.png +0 -0
  40. package/plugin/ui/help/graph-view.png +0 -0
  41. package/plugin/ui/help/settings.png +0 -0
  42. package/plugin/ui/help/timeline.png +0 -0
  43. package/plugin/ui/help.js +876 -172
  44. package/plugin/ui/index.html +506 -242
  45. package/plugin/ui/settings.js +781 -17
  46. package/plugin/ui/styles.css +990 -44
  47. package/plugin/ui/timeline.js +2 -2
  48. package/plugin/ui/tools.js +826 -0
  49. package/.claude-plugin/marketplace.json +0 -15
  50. package/plugin/dist/observations-Ch0nc47i.d.mts.map +0 -1
  51. package/plugin/dist/tool-registry-CZ3mJ4iR.mjs.map +0 -1
  52. package/plugin/scripts/setup-tmpdir.sh +0 -65
@@ -130,7 +130,14 @@
130
130
 
131
131
  var scope = getSelectedScope();
132
132
  var count = getAffectedCount(type);
133
- var scopeLabel = scope === 'current' ? 'current project' : 'ALL projects';
133
+ var projectName = '';
134
+ if (scope === 'current') {
135
+ var select = document.getElementById('project-selector');
136
+ if (select && select.selectedOptions && select.selectedOptions[0]) {
137
+ projectName = select.selectedOptions[0].textContent;
138
+ }
139
+ }
140
+ var scopeLabel = scope === 'current' ? 'project "' + (projectName || 'unknown') + '"' : 'ALL projects';
134
141
 
135
142
  var title = document.getElementById('confirm-title');
136
143
  var desc = document.getElementById('confirm-desc');
@@ -318,14 +325,6 @@
318
325
  }
319
326
 
320
327
  function initTopicDetection() {
321
- // Collapsible section
322
- var header = document.querySelector('[data-config-toggle="topic-detection"]');
323
- if (header) {
324
- header.addEventListener('click', function () {
325
- document.getElementById('topic-detection-section').classList.toggle('collapsed');
326
- });
327
- }
328
-
329
328
  // Enabled toggle
330
329
  var enabled = document.getElementById('td-enabled');
331
330
  if (enabled) {
@@ -525,14 +524,6 @@
525
524
  }
526
525
 
527
526
  function initGraphExtraction() {
528
- // Collapsible section
529
- var header = document.querySelector('[data-config-toggle="graph-extraction"]');
530
- if (header) {
531
- header.addEventListener('click', function () {
532
- document.getElementById('graph-extraction-section').classList.toggle('collapsed');
533
- });
534
- }
535
-
536
527
  // Enabled toggle
537
528
  var enabled = document.getElementById('ge-enabled');
538
529
  if (enabled) {
@@ -599,11 +590,757 @@
599
590
  loadGraphExtractionConfig();
600
591
  }
601
592
 
593
+ // =========================================================================
594
+ // Cross-Project Access Config
595
+ // =========================================================================
596
+
597
+ var caReadable = []; // current readable project hashes
598
+
599
+ async function loadCrossAccessConfig() {
600
+ var project = getProjectHash();
601
+ if (!project) return;
602
+ try {
603
+ var res = await fetch('/api/admin/config/cross-access?project=' + encodeURIComponent(project));
604
+ if (!res.ok) throw new Error('HTTP ' + res.status);
605
+ var config = await res.json();
606
+ caReadable = config.readableProjects || [];
607
+ populateCrossAccessLists();
608
+ } catch (err) {
609
+ console.error('[laminark] Failed to load cross-access config:', err);
610
+ }
611
+ }
612
+
613
+ function getAllProjects() {
614
+ var select = document.getElementById('project-selector');
615
+ if (!select) return [];
616
+ var projects = [];
617
+ for (var i = 0; i < select.options.length; i++) {
618
+ projects.push({
619
+ hash: select.options[i].value,
620
+ name: select.options[i].textContent,
621
+ });
622
+ }
623
+ return projects;
624
+ }
625
+
626
+ function populateCrossAccessLists() {
627
+ var currentProject = getProjectHash();
628
+ var allProjects = getAllProjects();
629
+ var availableList = document.getElementById('ca-available');
630
+ var readableList = document.getElementById('ca-readable');
631
+ if (!availableList || !readableList) return;
632
+
633
+ availableList.innerHTML = '';
634
+ readableList.innerHTML = '';
635
+
636
+ var readableSet = new Set(caReadable);
637
+
638
+ allProjects.forEach(function (p) {
639
+ if (p.hash === currentProject) return; // skip self
640
+ if (readableSet.has(p.hash)) {
641
+ readableList.appendChild(createCrossAccessItem(p, 'remove'));
642
+ } else {
643
+ availableList.appendChild(createCrossAccessItem(p, 'add'));
644
+ }
645
+ });
646
+
647
+ // Update status badge
648
+ var badge = document.getElementById('ca-status');
649
+ if (badge) {
650
+ var count = caReadable.length;
651
+ badge.textContent = count + ' project' + (count !== 1 ? 's' : '');
652
+ badge.className = 'config-section-status ' + (count > 0 ? 'enabled' : 'disabled');
653
+ }
654
+ }
655
+
656
+ function createCrossAccessItem(project, action) {
657
+ var li = document.createElement('li');
658
+ li.className = 'cross-access-item';
659
+ li.setAttribute('data-hash', project.hash);
660
+
661
+ var nameSpan = document.createElement('span');
662
+ nameSpan.className = 'cross-access-item-name';
663
+ nameSpan.textContent = project.name;
664
+ nameSpan.title = project.hash;
665
+ li.appendChild(nameSpan);
666
+
667
+ var btn = document.createElement('button');
668
+ if (action === 'add') {
669
+ btn.className = 'cross-access-add-btn';
670
+ btn.innerHTML = '&#9654;'; // right arrow
671
+ btn.title = 'Add to readable projects';
672
+ btn.addEventListener('click', function () {
673
+ caReadable.push(project.hash);
674
+ populateCrossAccessLists();
675
+ });
676
+ } else {
677
+ btn.className = 'cross-access-remove-btn';
678
+ btn.innerHTML = '&times;';
679
+ btn.title = 'Remove from readable projects';
680
+ btn.addEventListener('click', function () {
681
+ caReadable = caReadable.filter(function (h) { return h !== project.hash; });
682
+ populateCrossAccessLists();
683
+ });
684
+ }
685
+ li.appendChild(btn);
686
+
687
+ return li;
688
+ }
689
+
690
+ async function saveCrossAccessConfig() {
691
+ var project = getProjectHash();
692
+ if (!project) return null;
693
+ try {
694
+ var res = await fetch('/api/admin/config/cross-access?project=' + encodeURIComponent(project), {
695
+ method: 'PUT',
696
+ headers: { 'Content-Type': 'application/json' },
697
+ body: JSON.stringify({ readableProjects: caReadable }),
698
+ });
699
+ if (!res.ok) throw new Error('HTTP ' + res.status);
700
+ var config = await res.json();
701
+ caReadable = config.readableProjects || [];
702
+ populateCrossAccessLists();
703
+ return config;
704
+ } catch (err) {
705
+ console.error('[laminark] Failed to save cross-access config:', err);
706
+ return null;
707
+ }
708
+ }
709
+
710
+ function initCrossAccess() {
711
+ // Save button
712
+ var saveBtn = document.getElementById('ca-save');
713
+ if (saveBtn) {
714
+ saveBtn.addEventListener('click', async function () {
715
+ var result = await saveCrossAccessConfig();
716
+ if (result) showSuccessMessage('Cross-project access settings saved.');
717
+ });
718
+ }
719
+
720
+ // Reset to defaults
721
+ var defaultsBtn = document.getElementById('ca-defaults');
722
+ if (defaultsBtn) {
723
+ var caResetTimer = null;
724
+ defaultsBtn.addEventListener('click', async function () {
725
+ if (defaultsBtn.classList.contains('confirming')) {
726
+ clearTimeout(caResetTimer);
727
+ defaultsBtn.classList.remove('confirming');
728
+ defaultsBtn.textContent = 'Reset to Defaults';
729
+ var project = getProjectHash();
730
+ if (!project) return;
731
+ try {
732
+ var res = await fetch('/api/admin/config/cross-access?project=' + encodeURIComponent(project), {
733
+ method: 'PUT',
734
+ headers: { 'Content-Type': 'application/json' },
735
+ body: JSON.stringify({ __reset: true }),
736
+ });
737
+ if (res.ok) {
738
+ var config = await res.json();
739
+ caReadable = config.readableProjects || [];
740
+ populateCrossAccessLists();
741
+ showSuccessMessage('Cross-project access reset to defaults.');
742
+ }
743
+ } catch (err) {
744
+ console.error('[laminark] Failed to reset cross-access config:', err);
745
+ }
746
+ } else {
747
+ defaultsBtn.classList.add('confirming');
748
+ defaultsBtn.textContent = 'Confirm?';
749
+ caResetTimer = setTimeout(function () {
750
+ defaultsBtn.classList.remove('confirming');
751
+ defaultsBtn.textContent = 'Reset to Defaults';
752
+ }, 3000);
753
+ }
754
+ });
755
+ }
756
+
757
+ loadCrossAccessConfig();
758
+
759
+ // Reload when project changes
760
+ var projectSelector = document.getElementById('project-selector');
761
+ if (projectSelector) {
762
+ projectSelector.addEventListener('change', function () {
763
+ loadCrossAccessConfig();
764
+ });
765
+ }
766
+ }
767
+
768
+ // =========================================================================
769
+ // Database Hygiene
770
+ // =========================================================================
771
+
772
+ var lastHygieneReport = null;
773
+
774
+ function getSelectedTier() {
775
+ var activeBtn = document.querySelector('#hy-tier .config-radio.active');
776
+ return activeBtn ? activeBtn.getAttribute('data-value') : 'high';
777
+ }
778
+
779
+ async function runHygieneScan() {
780
+ var project = getProjectHash();
781
+ if (!project) return;
782
+
783
+ var tier = getSelectedTier();
784
+ var params = new URLSearchParams({ project: project, tier: tier, limit: '100' });
785
+
786
+ var badge = document.getElementById('hy-status');
787
+ if (badge) { badge.textContent = 'Scanning...'; badge.className = 'config-section-status disabled'; }
788
+
789
+ try {
790
+ var res = await fetch('/api/admin/hygiene?' + params.toString());
791
+ if (!res.ok) throw new Error('HTTP ' + res.status);
792
+ var report = await res.json();
793
+ lastHygieneReport = report;
794
+ renderHygieneReport(report, tier);
795
+ } catch (err) {
796
+ console.error('[laminark] Hygiene scan failed:', err);
797
+ if (badge) { badge.textContent = 'Error'; badge.className = 'config-section-status disabled'; }
798
+ }
799
+ }
800
+
801
+ function renderHygieneReport(report, tier) {
802
+ var badge = document.getElementById('hy-status');
803
+ var total = report.summary.high + report.summary.medium;
804
+ if (badge) {
805
+ badge.textContent = total + ' candidate' + (total !== 1 ? 's' : '');
806
+ badge.className = 'config-section-status ' + (total > 0 ? 'enabled' : 'disabled');
807
+ }
808
+
809
+ var results = document.getElementById('hy-results');
810
+ if (results) results.classList.remove('hidden');
811
+
812
+ // Summary
813
+ var summary = document.getElementById('hy-summary');
814
+ if (summary) {
815
+ summary.innerHTML =
816
+ '<div class="hygiene-summary-row">' +
817
+ '<span class="hygiene-stat"><strong>' + report.totalObservations.toLocaleString() + '</strong> analyzed</span>' +
818
+ '<span class="hygiene-stat hy-high"><strong>' + report.summary.high + '</strong> high</span>' +
819
+ '<span class="hygiene-stat hy-medium"><strong>' + report.summary.medium + '</strong> medium</span>' +
820
+ '<span class="hygiene-stat"><strong>' + report.summary.orphanNodeCount + '</strong> orphan nodes</span>' +
821
+ '</div>';
822
+ }
823
+
824
+ // Table
825
+ var tbody = document.getElementById('hy-table-body');
826
+ if (!tbody) return;
827
+ tbody.innerHTML = '';
828
+
829
+ if (report.candidates.length === 0) {
830
+ var tr = document.createElement('tr');
831
+ tr.innerHTML = '<td colspan="6" style="text-align:center;opacity:0.5;">No candidates at this tier</td>';
832
+ tbody.appendChild(tr);
833
+ return;
834
+ }
835
+
836
+ report.candidates.forEach(function (c) {
837
+ var signals = [];
838
+ if (c.signals.orphaned) signals.push('orphaned');
839
+ if (c.signals.islandNode) signals.push('island');
840
+ if (c.signals.noiseClassified) signals.push('noise');
841
+ if (c.signals.shortContent) signals.push('short');
842
+ if (c.signals.autoCaptured) signals.push('auto');
843
+ if (c.signals.stale) signals.push('stale');
844
+
845
+ var tr = document.createElement('tr');
846
+ tr.className = c.tier === 'high' ? 'hy-row-high' : c.tier === 'medium' ? 'hy-row-medium' : '';
847
+ tr.innerHTML =
848
+ '<td class="mono">' + c.shortId + '</td>' +
849
+ '<td>' + c.kind + '</td>' +
850
+ '<td>' + c.source + '</td>' +
851
+ '<td>' + c.confidence.toFixed(2) + '</td>' +
852
+ '<td class="hygiene-signals">' + signals.map(function (s) { return '<span class="hy-signal">' + s + '</span>'; }).join(' ') + '</td>' +
853
+ '<td class="hygiene-preview">' + escapeHtml(c.contentPreview) + '</td>';
854
+ tbody.appendChild(tr);
855
+ });
856
+ }
857
+
858
+ function escapeHtml(text) {
859
+ var div = document.createElement('div');
860
+ div.textContent = text;
861
+ return div.innerHTML;
862
+ }
863
+
864
+ async function runHygienePurge() {
865
+ var project = getProjectHash();
866
+ if (!project) return;
867
+
868
+ var tier = getSelectedTier();
869
+ var purgeBtn = document.getElementById('hy-purge');
870
+
871
+ if (purgeBtn && !purgeBtn.classList.contains('confirming')) {
872
+ purgeBtn.classList.add('confirming');
873
+ purgeBtn.textContent = 'Confirm Purge?';
874
+ setTimeout(function () {
875
+ if (purgeBtn.classList.contains('confirming')) {
876
+ purgeBtn.classList.remove('confirming');
877
+ purgeBtn.textContent = 'Purge Selected Tier';
878
+ }
879
+ }, 3000);
880
+ return;
881
+ }
882
+
883
+ if (purgeBtn) {
884
+ purgeBtn.classList.remove('confirming');
885
+ purgeBtn.textContent = 'Purging...';
886
+ purgeBtn.disabled = true;
887
+ }
888
+
889
+ try {
890
+ var res = await fetch('/api/admin/hygiene/purge', {
891
+ method: 'POST',
892
+ headers: { 'Content-Type': 'application/json' },
893
+ body: JSON.stringify({ tier: tier }),
894
+ });
895
+ if (!res.ok) throw new Error('HTTP ' + res.status);
896
+ var result = await res.json();
897
+
898
+ showSuccessMessage(
899
+ 'Purged ' + result.observationsPurged + ' observations, ' +
900
+ result.orphanNodesRemoved + ' orphan nodes removed.'
901
+ );
902
+
903
+ // Refresh stats and re-scan
904
+ await refreshStats();
905
+ await runHygieneScan();
906
+ } catch (err) {
907
+ console.error('[laminark] Hygiene purge failed:', err);
908
+ }
909
+
910
+ if (purgeBtn) {
911
+ purgeBtn.disabled = false;
912
+ purgeBtn.textContent = 'Purge Selected Tier';
913
+ }
914
+ }
915
+
916
+ function initHygiene() {
917
+ // Tier radio buttons
918
+ var tierBtns = document.querySelectorAll('#hy-tier .config-radio');
919
+ tierBtns.forEach(function (btn) {
920
+ btn.addEventListener('click', function () {
921
+ tierBtns.forEach(function (b) { b.classList.remove('active'); });
922
+ btn.classList.add('active');
923
+ });
924
+ });
925
+
926
+ // Scan button
927
+ var scanBtn = document.getElementById('hy-scan');
928
+ if (scanBtn) {
929
+ scanBtn.addEventListener('click', runHygieneScan);
930
+ }
931
+
932
+ // Purge button
933
+ var purgeBtn = document.getElementById('hy-purge');
934
+ if (purgeBtn) {
935
+ purgeBtn.addEventListener('click', runHygienePurge);
936
+ }
937
+ }
938
+
939
+ // =========================================================================
940
+ // Hygiene Config
941
+ // =========================================================================
942
+
943
+ var HC_WEIGHT_KEYS = ['orphaned', 'islandNode', 'noiseClassified', 'shortContent', 'autoCaptured', 'stale'];
944
+
945
+ function populateHygieneConfig(config) {
946
+ HC_WEIGHT_KEYS.forEach(function (key) {
947
+ var slider = document.getElementById('hc-w-' + key);
948
+ var label = document.getElementById('hc-w-' + key + '-val');
949
+ if (slider) slider.value = config.signalWeights[key];
950
+ if (label) label.textContent = config.signalWeights[key].toFixed(2);
951
+ });
952
+
953
+ setSlider('hc-t-high', 'hc-t-high-val', config.tierThresholds.high);
954
+ setSlider('hc-t-medium', 'hc-t-medium-val', config.tierThresholds.medium);
955
+ setVal('hc-short-threshold', config.shortContentThreshold);
956
+ }
957
+
958
+ function gatherHygieneConfig() {
959
+ var weights = {};
960
+ HC_WEIGHT_KEYS.forEach(function (key) {
961
+ var slider = document.getElementById('hc-w-' + key);
962
+ weights[key] = slider ? parseFloat(slider.value) : 0;
963
+ });
964
+
965
+ return {
966
+ signalWeights: weights,
967
+ tierThresholds: {
968
+ high: parseFloat(document.getElementById('hc-t-high').value) || 0.70,
969
+ medium: parseFloat(document.getElementById('hc-t-medium').value) || 0.50,
970
+ },
971
+ shortContentThreshold: parseInt(document.getElementById('hc-short-threshold').value, 10) || 50,
972
+ };
973
+ }
974
+
975
+ async function loadHygieneConfig() {
976
+ try {
977
+ var res = await fetch('/api/admin/config/hygiene');
978
+ if (!res.ok) throw new Error('HTTP ' + res.status);
979
+ var config = await res.json();
980
+ populateHygieneConfig(config);
981
+ } catch (err) {
982
+ console.error('[laminark] Failed to load hygiene config:', err);
983
+ }
984
+ }
985
+
986
+ async function saveHygieneConfig(data) {
987
+ try {
988
+ var res = await fetch('/api/admin/config/hygiene', {
989
+ method: 'PUT',
990
+ headers: { 'Content-Type': 'application/json' },
991
+ body: JSON.stringify(data),
992
+ });
993
+ if (!res.ok) throw new Error('HTTP ' + res.status);
994
+ var config = await res.json();
995
+ populateHygieneConfig(config);
996
+ return config;
997
+ } catch (err) {
998
+ console.error('[laminark] Failed to save hygiene config:', err);
999
+ return null;
1000
+ }
1001
+ }
1002
+
1003
+ async function runFindAnalysis() {
1004
+ var project = getProjectHash();
1005
+ if (!project) return;
1006
+
1007
+ var findBtn = document.getElementById('hc-find');
1008
+ if (findBtn) { findBtn.textContent = 'Analyzing...'; findBtn.disabled = true; }
1009
+
1010
+ try {
1011
+ var res = await fetch('/api/admin/hygiene/find?project=' + encodeURIComponent(project));
1012
+ if (!res.ok) throw new Error('HTTP ' + res.status);
1013
+ var report = await res.json();
1014
+ renderFindResults(report);
1015
+ } catch (err) {
1016
+ console.error('[laminark] Find analysis failed:', err);
1017
+ }
1018
+
1019
+ if (findBtn) { findBtn.textContent = 'Find'; findBtn.disabled = false; }
1020
+ }
1021
+
1022
+ function renderFindResults(report) {
1023
+ var container = document.getElementById('hc-find-results');
1024
+ if (container) container.classList.remove('hidden');
1025
+
1026
+ // Summary
1027
+ var summary = document.getElementById('hc-find-summary');
1028
+ if (summary) {
1029
+ var signals = report.bySignal;
1030
+ summary.innerHTML =
1031
+ '<div class="hygiene-summary-row">' +
1032
+ '<span class="hygiene-stat"><strong>' + report.total.toLocaleString() + '</strong> total</span>' +
1033
+ '<span class="hygiene-stat"><strong>' + signals.orphaned + '</strong> orphaned</span>' +
1034
+ '<span class="hygiene-stat"><strong>' + signals.islandNode + '</strong> island</span>' +
1035
+ '<span class="hygiene-stat"><strong>' + signals.noiseClassified + '</strong> noise</span>' +
1036
+ '<span class="hygiene-stat"><strong>' + signals.shortContent + '</strong> short</span>' +
1037
+ '<span class="hygiene-stat"><strong>' + signals.autoCaptured + '</strong> auto</span>' +
1038
+ '<span class="hygiene-stat"><strong>' + signals.stale + '</strong> stale</span>' +
1039
+ '</div>';
1040
+ }
1041
+
1042
+ // Histogram
1043
+ var histogram = document.getElementById('hc-find-histogram');
1044
+ if (histogram) {
1045
+ var maxCount = 0;
1046
+ report.distribution.forEach(function (d) { if (d.count > maxCount) maxCount = d.count; });
1047
+
1048
+ var html = '<div class="hygiene-histogram">';
1049
+ report.distribution.forEach(function (d) {
1050
+ var pct = maxCount > 0 ? (d.count / maxCount * 100) : 0;
1051
+ html += '<div class="hygiene-histogram-bar-wrap">' +
1052
+ '<div class="hygiene-histogram-bar" style="height:' + Math.max(pct, 2) + '%;" title="' + d.range + ': ' + d.count + '"></div>' +
1053
+ '<div class="hygiene-histogram-label">' + d.range.split('-')[0] + '</div>' +
1054
+ '</div>';
1055
+ });
1056
+ html += '</div>';
1057
+ histogram.innerHTML = html;
1058
+ }
1059
+
1060
+ // Island nodes
1061
+ var islands = document.getElementById('hc-find-islands');
1062
+ if (islands && report.islandNodes) {
1063
+ var isl = report.islandNodes;
1064
+ var cap = isl.capturedAtCurrentThresholds;
1065
+ islands.innerHTML =
1066
+ '<div class="hygiene-summary-row">' +
1067
+ '<span class="hygiene-stat"><strong>' + isl.total + '</strong> island obs</span>' +
1068
+ '<span class="hygiene-stat">conf: ' + isl.minConfidence.toFixed(2) + ' &ndash; ' + isl.maxConfidence.toFixed(2) + '</span>' +
1069
+ '<span class="hygiene-stat">median: <strong>' + isl.medianConfidence.toFixed(2) + '</strong></span>' +
1070
+ '<span class="hygiene-stat hy-high">high: <strong>' + cap.high + '</strong></span>' +
1071
+ '<span class="hygiene-stat hy-medium">medium+: <strong>' + cap.medium + '</strong></span>' +
1072
+ '<span class="hygiene-stat">all: <strong>' + cap.all + '</strong></span>' +
1073
+ '</div>';
1074
+ }
1075
+ }
1076
+
1077
+ function initHygieneConfig() {
1078
+ // Bind sliders
1079
+ HC_WEIGHT_KEYS.forEach(function (key) {
1080
+ bindSlider('hc-w-' + key, 'hc-w-' + key + '-val');
1081
+ });
1082
+ bindSlider('hc-t-high', 'hc-t-high-val');
1083
+ bindSlider('hc-t-medium', 'hc-t-medium-val');
1084
+
1085
+ // Save button
1086
+ var saveBtn = document.getElementById('hc-save');
1087
+ if (saveBtn) {
1088
+ saveBtn.addEventListener('click', async function () {
1089
+ var data = gatherHygieneConfig();
1090
+ var result = await saveHygieneConfig(data);
1091
+ if (result) showSuccessMessage('Hygiene settings saved.');
1092
+ });
1093
+ }
1094
+
1095
+ // Reset to defaults
1096
+ var defaultsBtn = document.getElementById('hc-defaults');
1097
+ if (defaultsBtn) {
1098
+ var hcResetTimer = null;
1099
+ defaultsBtn.addEventListener('click', async function () {
1100
+ if (defaultsBtn.classList.contains('confirming')) {
1101
+ clearTimeout(hcResetTimer);
1102
+ defaultsBtn.classList.remove('confirming');
1103
+ defaultsBtn.textContent = 'Reset to Defaults';
1104
+ var result = await saveHygieneConfig({ __reset: true });
1105
+ if (result) showSuccessMessage('Hygiene settings reset to defaults.');
1106
+ } else {
1107
+ defaultsBtn.classList.add('confirming');
1108
+ defaultsBtn.textContent = 'Confirm?';
1109
+ hcResetTimer = setTimeout(function () {
1110
+ defaultsBtn.classList.remove('confirming');
1111
+ defaultsBtn.textContent = 'Reset to Defaults';
1112
+ }, 3000);
1113
+ }
1114
+ });
1115
+ }
1116
+
1117
+ // Find button
1118
+ var findBtn = document.getElementById('hc-find');
1119
+ if (findBtn) {
1120
+ findBtn.addEventListener('click', runFindAnalysis);
1121
+ }
1122
+
1123
+ loadHygieneConfig();
1124
+ }
1125
+
1126
+ // =========================================================================
1127
+ // Tool Response Verbosity Config
1128
+ // =========================================================================
1129
+
1130
+ var LEVEL_LABELS = { 1: 'Minimal', 2: 'Standard', 3: 'Verbose' };
1131
+
1132
+ function populateToolVerbosity(config) {
1133
+ var levelBtns = document.querySelectorAll('#tv-level .config-radio');
1134
+ levelBtns.forEach(function (btn) {
1135
+ btn.classList.toggle('active', btn.getAttribute('data-value') === String(config.level));
1136
+ });
1137
+ var badge = document.getElementById('tv-status');
1138
+ if (badge) {
1139
+ badge.textContent = LEVEL_LABELS[config.level] || 'Standard';
1140
+ badge.className = 'config-section-status enabled';
1141
+ }
1142
+ }
1143
+
1144
+ async function loadToolVerbosityConfig() {
1145
+ try {
1146
+ var res = await fetch('/api/admin/config/tool-verbosity');
1147
+ if (!res.ok) throw new Error('HTTP ' + res.status);
1148
+ var config = await res.json();
1149
+ populateToolVerbosity(config);
1150
+ } catch (err) {
1151
+ console.error('[laminark] Failed to load tool verbosity config:', err);
1152
+ }
1153
+ }
1154
+
1155
+ async function saveToolVerbosityConfig(data) {
1156
+ try {
1157
+ var res = await fetch('/api/admin/config/tool-verbosity', {
1158
+ method: 'PUT',
1159
+ headers: { 'Content-Type': 'application/json' },
1160
+ body: JSON.stringify(data),
1161
+ });
1162
+ if (!res.ok) throw new Error('HTTP ' + res.status);
1163
+ var config = await res.json();
1164
+ populateToolVerbosity(config);
1165
+ return config;
1166
+ } catch (err) {
1167
+ console.error('[laminark] Failed to save tool verbosity config:', err);
1168
+ return null;
1169
+ }
1170
+ }
1171
+
1172
+ function initToolVerbosity() {
1173
+ // Level radio buttons
1174
+ var levelBtns = document.querySelectorAll('#tv-level .config-radio');
1175
+ levelBtns.forEach(function (btn) {
1176
+ btn.addEventListener('click', function () {
1177
+ levelBtns.forEach(function (b) { b.classList.remove('active'); });
1178
+ btn.classList.add('active');
1179
+ });
1180
+ });
1181
+
1182
+ // Save button
1183
+ var saveBtn = document.getElementById('tv-save');
1184
+ if (saveBtn) {
1185
+ saveBtn.addEventListener('click', async function () {
1186
+ var activeBtn = document.querySelector('#tv-level .config-radio.active');
1187
+ var level = activeBtn ? parseInt(activeBtn.getAttribute('data-value'), 10) : 2;
1188
+ var result = await saveToolVerbosityConfig({ level: level });
1189
+ if (result) showSuccessMessage('Tool verbosity settings saved.');
1190
+ });
1191
+ }
1192
+
1193
+ // Reset to defaults
1194
+ var defaultsBtn = document.getElementById('tv-defaults');
1195
+ if (defaultsBtn) {
1196
+ var tvResetTimer = null;
1197
+ defaultsBtn.addEventListener('click', async function () {
1198
+ if (defaultsBtn.classList.contains('confirming')) {
1199
+ clearTimeout(tvResetTimer);
1200
+ defaultsBtn.classList.remove('confirming');
1201
+ defaultsBtn.textContent = 'Reset to Defaults';
1202
+ var result = await saveToolVerbosityConfig({ __reset: true });
1203
+ if (result) showSuccessMessage('Tool verbosity reset to defaults.');
1204
+ } else {
1205
+ defaultsBtn.classList.add('confirming');
1206
+ defaultsBtn.textContent = 'Confirm?';
1207
+ tvResetTimer = setTimeout(function () {
1208
+ defaultsBtn.classList.remove('confirming');
1209
+ defaultsBtn.textContent = 'Reset to Defaults';
1210
+ }, 3000);
1211
+ }
1212
+ });
1213
+ }
1214
+
1215
+ loadToolVerbosityConfig();
1216
+ }
1217
+
1218
+ // =========================================================================
1219
+ // System Info
1220
+ // =========================================================================
1221
+
1222
+ function formatBytes(bytes) {
1223
+ if (bytes === 0) return '0 B';
1224
+ var units = ['B', 'KB', 'MB', 'GB'];
1225
+ var i = Math.floor(Math.log(bytes) / Math.log(1024));
1226
+ if (i >= units.length) i = units.length - 1;
1227
+ return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + units[i];
1228
+ }
1229
+
1230
+ function formatUptime(seconds) {
1231
+ var d = Math.floor(seconds / 86400);
1232
+ var h = Math.floor((seconds % 86400) / 3600);
1233
+ var m = Math.floor((seconds % 3600) / 60);
1234
+ if (d > 0) return d + 'd ' + h + 'h';
1235
+ if (h > 0) return h + 'h ' + m + 'm';
1236
+ return m + 'm';
1237
+ }
1238
+
1239
+ function makeStatCard(id, value, label) {
1240
+ var el = document.createElement('div');
1241
+ el.className = 'stat-card';
1242
+ if (id) el.id = id;
1243
+
1244
+ var valueEl = document.createElement('div');
1245
+ valueEl.className = 'stat-value';
1246
+ valueEl.textContent = value;
1247
+
1248
+ var labelEl = document.createElement('div');
1249
+ labelEl.className = 'stat-label';
1250
+ labelEl.textContent = label;
1251
+
1252
+ el.appendChild(valueEl);
1253
+ el.appendChild(labelEl);
1254
+ return el;
1255
+ }
1256
+
1257
+ async function fetchSystemInfo() {
1258
+ try {
1259
+ var res = await fetch('/api/admin/system');
1260
+ if (!res.ok) throw new Error('HTTP ' + res.status);
1261
+ return await res.json();
1262
+ } catch (err) {
1263
+ console.error('[laminark] Failed to fetch system info:', err);
1264
+ return null;
1265
+ }
1266
+ }
1267
+
1268
+ function renderSystemInfo(info) {
1269
+ if (!info) return;
1270
+
1271
+ // System Info grid
1272
+ var sysGrid = document.getElementById('system-info-grid');
1273
+ if (sysGrid) {
1274
+ sysGrid.innerHTML = '';
1275
+ sysGrid.appendChild(makeStatCard(null, info.laminarkVersion, 'Laminark'));
1276
+ sysGrid.appendChild(makeStatCard(null, info.nodeVersion, 'Node.js'));
1277
+ sysGrid.appendChild(makeStatCard(null, info.platform + ' ' + info.arch, 'Platform'));
1278
+ sysGrid.appendChild(makeStatCard(null, formatUptime(info.uptimeSeconds), 'Uptime'));
1279
+ }
1280
+
1281
+ // Database Storage grid
1282
+ var dbGrid = document.getElementById('db-storage-grid');
1283
+ if (dbGrid) {
1284
+ dbGrid.innerHTML = '';
1285
+ dbGrid.appendChild(makeStatCard(null, formatBytes(info.database.sizeBytes), 'DB Size'));
1286
+ dbGrid.appendChild(makeStatCard(null, formatBytes(info.database.walSizeBytes), 'WAL Size'));
1287
+ dbGrid.appendChild(makeStatCard(null, info.database.pageCount.toLocaleString(), 'Page Count'));
1288
+ dbGrid.appendChild(makeStatCard(null, formatBytes(info.database.pageSize), 'Page Size'));
1289
+ }
1290
+
1291
+ // Process Memory grid
1292
+ var memGrid = document.getElementById('process-memory-grid');
1293
+ if (memGrid) {
1294
+ memGrid.innerHTML = '';
1295
+ memGrid.appendChild(makeStatCard(null, formatBytes(info.memory.rssBytes), 'RSS'));
1296
+ memGrid.appendChild(makeStatCard(null, formatBytes(info.memory.heapUsedBytes), 'Heap Used'));
1297
+ memGrid.appendChild(makeStatCard(null, formatBytes(info.memory.heapTotalBytes), 'Heap Total'));
1298
+ }
1299
+ }
1300
+
1301
+ async function refreshSystemInfo() {
1302
+ var info = await fetchSystemInfo();
1303
+ renderSystemInfo(info);
1304
+ }
1305
+
1306
+ // =========================================================================
1307
+ // Sidebar Navigation
1308
+ // =========================================================================
1309
+
1310
+ function initSettingsSidebar() {
1311
+ var items = document.querySelectorAll('.settings-sidebar-item');
1312
+ var panels = document.querySelectorAll('.settings-panel');
1313
+
1314
+ function activateTab(tabName) {
1315
+ items.forEach(function (item) {
1316
+ item.classList.toggle('active', item.getAttribute('data-settings-tab') === tabName);
1317
+ });
1318
+ panels.forEach(function (panel) {
1319
+ panel.classList.toggle('active', panel.getAttribute('data-settings-panel') === tabName);
1320
+ });
1321
+ try { localStorage.setItem('laminark-settings-tab', tabName); } catch (e) {}
1322
+ }
1323
+
1324
+ items.forEach(function (item) {
1325
+ item.addEventListener('click', function () {
1326
+ activateTab(item.getAttribute('data-settings-tab'));
1327
+ });
1328
+ });
1329
+
1330
+ // Restore last-selected tab
1331
+ var saved = null;
1332
+ try { saved = localStorage.getItem('laminark-settings-tab'); } catch (e) {}
1333
+ if (saved && document.querySelector('[data-settings-panel="' + saved + '"]')) {
1334
+ activateTab(saved);
1335
+ }
1336
+ }
1337
+
602
1338
  // =========================================================================
603
1339
  // Init
604
1340
  // =========================================================================
605
1341
 
606
1342
  function initSettings() {
1343
+ initSettingsSidebar();
607
1344
  // Reset action buttons
608
1345
  var resetBtns = document.querySelectorAll('.reset-action-btn');
609
1346
  resetBtns.forEach(function (btn) {
@@ -638,9 +1375,36 @@
638
1375
  radio.addEventListener('change', refreshStats);
639
1376
  });
640
1377
 
1378
+ // Show current project name in danger zone scope label
1379
+ updateResetScopeProjectName();
1380
+ var projectSelector = document.getElementById('project-selector');
1381
+ if (projectSelector) {
1382
+ projectSelector.addEventListener('change', updateResetScopeProjectName);
1383
+ }
1384
+
1385
+ // System info (fetched once, not re-fetched on project change)
1386
+ refreshSystemInfo();
1387
+
641
1388
  // Config sections
1389
+ initHygiene();
1390
+ initHygieneConfig();
1391
+ initToolVerbosity();
642
1392
  initTopicDetection();
643
1393
  initGraphExtraction();
1394
+ initCrossAccess();
1395
+ }
1396
+
1397
+ function updateResetScopeProjectName() {
1398
+ var el = document.getElementById('reset-scope-project-name');
1399
+ if (!el) return;
1400
+ var select = document.getElementById('project-selector');
1401
+ if (select && select.selectedOptions && select.selectedOptions[0]) {
1402
+ el.textContent = select.selectedOptions[0].textContent;
1403
+ } else if (window.laminarkState && window.laminarkState.currentProject) {
1404
+ el.textContent = window.laminarkState.currentProject.substring(0, 8) + '...';
1405
+ } else {
1406
+ el.textContent = 'unknown';
1407
+ }
644
1408
  }
645
1409
 
646
1410
  window.laminarkSettings = {