superlocalmemory 2.3.7 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +25 -0
- package/hooks/memory-profile-skill.js +7 -18
- package/mcp_server.py +74 -12
- package/package.json +1 -1
- package/src/__pycache__/auto_backup.cpython-312.pyc +0 -0
- package/src/__pycache__/memory_store_v2.cpython-312.pyc +0 -0
- package/src/__pycache__/pattern_learner.cpython-312.pyc +0 -0
- package/src/auto_backup.py +424 -0
- package/src/graph_engine.py +126 -39
- package/src/memory-profiles.py +321 -243
- package/src/memory_store_v2.py +82 -31
- package/src/pattern_learner.py +126 -44
- package/ui/app.js +526 -17
- package/ui/index.html +182 -1
- package/ui_server.py +340 -43
package/ui/app.js
CHANGED
|
@@ -199,7 +199,7 @@ function renderGraph(data) {
|
|
|
199
199
|
var colorScale = d3.scaleOrdinal(d3.schemeCategory10);
|
|
200
200
|
var simulation = d3.forceSimulation(data.nodes).force('link', d3.forceLink(data.links).id(function(d) { return d.id; }).distance(100)).force('charge', d3.forceManyBody().strength(-200)).force('center', d3.forceCenter(width / 2, height / 2)).force('collision', d3.forceCollide().radius(20));
|
|
201
201
|
var link = svg.append('g').selectAll('line').data(data.links).enter().append('line').attr('class', 'link').attr('stroke-width', function(d) { return Math.sqrt(d.weight * 2); });
|
|
202
|
-
var node = svg.append('g').selectAll('circle').data(data.nodes).enter().append('circle').attr('class', 'node').attr('r', function(d) { return 5 + (d.importance || 5); }).attr('fill', function(d) { return colorScale(d.cluster_id || 0); }).call(d3.drag().on('start', dragStarted).on('drag', dragged).on('end', dragEnded)).on('mouseover', function(event, d) { tooltip.transition().duration(200).style('opacity', .9);
|
|
202
|
+
var node = svg.append('g').selectAll('circle').data(data.nodes).enter().append('circle').attr('class', 'node').attr('r', function(d) { return 5 + (d.importance || 5); }).attr('fill', function(d) { return colorScale(d.cluster_id || 0); }).call(d3.drag().on('start', dragStarted).on('drag', dragged).on('end', dragEnded)).on('mouseover', function(event, d) { tooltip.transition().duration(200).style('opacity', .9); var label = d.category || d.project_name || 'Memory #' + d.id; tooltip.text(label + ': ' + (d.content_preview || d.summary || 'No content')).style('left', (event.pageX + 10) + 'px').style('top', (event.pageY - 28) + 'px'); }).on('mouseout', function() { tooltip.transition().duration(500).style('opacity', 0); }).on('click', function(event, d) { openMemoryDetail(d); });
|
|
203
203
|
simulation.on('tick', function() { link.attr('x1', function(d) { return d.source.x; }).attr('y1', function(d) { return d.source.y; }).attr('x2', function(d) { return d.target.x; }).attr('y2', function(d) { return d.target.y; }); node.attr('cx', function(d) { return d.x; }).attr('cy', function(d) { return d.y; }); });
|
|
204
204
|
function dragStarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }
|
|
205
205
|
function dragged(event, d) { d.fx = event.x; d.fy = event.y; }
|
|
@@ -275,9 +275,14 @@ function renderMemoriesTable(memories, showScores) {
|
|
|
275
275
|
});
|
|
276
276
|
|
|
277
277
|
var html = '<table class="table table-hover memory-table"><thead><tr>'
|
|
278
|
-
+ '<th>ID</th
|
|
278
|
+
+ '<th class="sortable" data-sort="id">ID</th>'
|
|
279
|
+
+ '<th class="sortable" data-sort="category">Category</th>'
|
|
280
|
+
+ '<th class="sortable" data-sort="project">Project</th>'
|
|
281
|
+
+ '<th>Content</th>'
|
|
279
282
|
+ scoreHeader
|
|
280
|
-
+ '<th>Importance</th
|
|
283
|
+
+ '<th class="sortable" data-sort="importance">Importance</th>'
|
|
284
|
+
+ '<th>Cluster</th>'
|
|
285
|
+
+ '<th class="sortable" data-sort="created">Created</th>'
|
|
281
286
|
+ '</tr></thead><tbody>' + rows + '</tbody></table>';
|
|
282
287
|
|
|
283
288
|
// Safe: all interpolated values above are escaped via escapeHtml()
|
|
@@ -287,6 +292,12 @@ function renderMemoriesTable(memories, showScores) {
|
|
|
287
292
|
var table = container.querySelector('table');
|
|
288
293
|
if (table) {
|
|
289
294
|
table.addEventListener('click', function(e) {
|
|
295
|
+
// Check if clicking a sortable header
|
|
296
|
+
var th = e.target.closest('th.sortable');
|
|
297
|
+
if (th) {
|
|
298
|
+
handleSort(th);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
290
301
|
var row = e.target.closest('tr[data-mem-idx]');
|
|
291
302
|
if (row) {
|
|
292
303
|
var idx = parseInt(row.getAttribute('data-mem-idx'), 10);
|
|
@@ -565,26 +576,78 @@ function renderPatterns(patterns) {
|
|
|
565
576
|
return;
|
|
566
577
|
}
|
|
567
578
|
|
|
568
|
-
|
|
569
|
-
var
|
|
579
|
+
var typeIcons = { preference: 'heart', style: 'palette', terminology: 'code-slash' };
|
|
580
|
+
var typeLabels = { preference: 'Preferences', style: 'Coding Style', terminology: 'Terminology' };
|
|
581
|
+
|
|
582
|
+
// Build using DOM for safety
|
|
583
|
+
container.textContent = '';
|
|
584
|
+
|
|
570
585
|
for (var type in patterns) {
|
|
571
586
|
if (!patterns.hasOwnProperty(type)) continue;
|
|
572
587
|
var items = patterns[type];
|
|
573
|
-
|
|
588
|
+
|
|
589
|
+
var header = document.createElement('h6');
|
|
590
|
+
header.className = 'mt-3 mb-2';
|
|
591
|
+
var icon = document.createElement('i');
|
|
592
|
+
icon.className = 'bi bi-' + (typeIcons[type] || 'puzzle') + ' me-1';
|
|
593
|
+
header.appendChild(icon);
|
|
594
|
+
header.appendChild(document.createTextNode(typeLabels[type] || type));
|
|
595
|
+
var countBadge = document.createElement('span');
|
|
596
|
+
countBadge.className = 'badge bg-secondary ms-2';
|
|
597
|
+
countBadge.textContent = items.length;
|
|
598
|
+
header.appendChild(countBadge);
|
|
599
|
+
container.appendChild(header);
|
|
600
|
+
|
|
601
|
+
var group = document.createElement('div');
|
|
602
|
+
group.className = 'list-group mb-3';
|
|
603
|
+
|
|
574
604
|
items.forEach(function(pattern) {
|
|
575
|
-
var
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
605
|
+
var pct = Math.round(pattern.confidence * 100);
|
|
606
|
+
var barColor = pct >= 60 ? '#43e97b' : pct >= 40 ? '#f9c74f' : '#6c757d';
|
|
607
|
+
var badgeClass = pct >= 60 ? 'bg-success' : pct >= 40 ? 'bg-warning text-dark' : 'bg-secondary';
|
|
608
|
+
|
|
609
|
+
var item = document.createElement('div');
|
|
610
|
+
item.className = 'list-group-item';
|
|
611
|
+
|
|
612
|
+
var topRow = document.createElement('div');
|
|
613
|
+
topRow.className = 'd-flex justify-content-between align-items-center';
|
|
614
|
+
var keyEl = document.createElement('strong');
|
|
615
|
+
keyEl.textContent = pattern.key;
|
|
616
|
+
var badge = document.createElement('span');
|
|
617
|
+
badge.className = 'badge ' + badgeClass;
|
|
618
|
+
badge.textContent = pct + '%';
|
|
619
|
+
topRow.appendChild(keyEl);
|
|
620
|
+
topRow.appendChild(badge);
|
|
621
|
+
item.appendChild(topRow);
|
|
622
|
+
|
|
623
|
+
// Confidence bar
|
|
624
|
+
var barContainer = document.createElement('div');
|
|
625
|
+
barContainer.className = 'confidence-bar';
|
|
626
|
+
var barFill = document.createElement('div');
|
|
627
|
+
barFill.className = 'confidence-fill';
|
|
628
|
+
barFill.style.width = pct + '%';
|
|
629
|
+
barFill.style.background = barColor;
|
|
630
|
+
barContainer.appendChild(barFill);
|
|
631
|
+
item.appendChild(barContainer);
|
|
632
|
+
|
|
633
|
+
var valueEl = document.createElement('div');
|
|
634
|
+
valueEl.className = 'mt-1';
|
|
635
|
+
var valueSmall = document.createElement('small');
|
|
636
|
+
valueSmall.className = 'text-muted';
|
|
637
|
+
valueSmall.textContent = typeof pattern.value === 'string' ? pattern.value : JSON.stringify(pattern.value);
|
|
638
|
+
valueEl.appendChild(valueSmall);
|
|
639
|
+
item.appendChild(valueEl);
|
|
640
|
+
|
|
641
|
+
var evidenceEl = document.createElement('small');
|
|
642
|
+
evidenceEl.className = 'text-muted';
|
|
643
|
+
evidenceEl.textContent = 'Evidence: ' + (pattern.evidence_count || '?') + ' memories';
|
|
644
|
+
item.appendChild(evidenceEl);
|
|
645
|
+
|
|
646
|
+
group.appendChild(item);
|
|
584
647
|
});
|
|
585
|
-
|
|
648
|
+
|
|
649
|
+
container.appendChild(group);
|
|
586
650
|
}
|
|
587
|
-
container.innerHTML = html; // nosemgrep: innerHTML-xss — all values escaped
|
|
588
651
|
}
|
|
589
652
|
|
|
590
653
|
// ============================================================================
|
|
@@ -645,10 +708,456 @@ document.getElementById('memories-tab').addEventListener('shown.bs.tab', loadMem
|
|
|
645
708
|
document.getElementById('clusters-tab').addEventListener('shown.bs.tab', loadClusters);
|
|
646
709
|
document.getElementById('patterns-tab').addEventListener('shown.bs.tab', loadPatterns);
|
|
647
710
|
document.getElementById('timeline-tab').addEventListener('shown.bs.tab', loadTimeline);
|
|
711
|
+
document.getElementById('settings-tab').addEventListener('shown.bs.tab', loadSettings);
|
|
648
712
|
document.getElementById('search-query').addEventListener('keypress', function(e) { if (e.key === 'Enter') searchMemories(); });
|
|
649
713
|
|
|
714
|
+
document.getElementById('profile-select').addEventListener('change', function() {
|
|
715
|
+
switchProfile(this.value);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
document.getElementById('add-profile-btn').addEventListener('click', function() {
|
|
719
|
+
createProfile();
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
var newProfileInput = document.getElementById('new-profile-name');
|
|
723
|
+
if (newProfileInput) {
|
|
724
|
+
newProfileInput.addEventListener('keypress', function(e) {
|
|
725
|
+
if (e.key === 'Enter') createProfile();
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
650
729
|
window.addEventListener('DOMContentLoaded', function() {
|
|
651
730
|
initDarkMode();
|
|
731
|
+
loadProfiles();
|
|
652
732
|
loadStats();
|
|
653
733
|
loadGraph();
|
|
654
734
|
});
|
|
735
|
+
|
|
736
|
+
// ============================================================================
|
|
737
|
+
// Profile Management
|
|
738
|
+
// ============================================================================
|
|
739
|
+
|
|
740
|
+
async function loadProfiles() {
|
|
741
|
+
try {
|
|
742
|
+
var response = await fetch('/api/profiles');
|
|
743
|
+
var data = await response.json();
|
|
744
|
+
var select = document.getElementById('profile-select');
|
|
745
|
+
select.textContent = '';
|
|
746
|
+
var profiles = data.profiles || [];
|
|
747
|
+
var active = data.active_profile || 'default';
|
|
748
|
+
|
|
749
|
+
profiles.forEach(function(p) {
|
|
750
|
+
var opt = document.createElement('option');
|
|
751
|
+
opt.value = p.name;
|
|
752
|
+
opt.textContent = p.name + (p.memory_count ? ' (' + p.memory_count + ')' : '');
|
|
753
|
+
if (p.name === active) opt.selected = true;
|
|
754
|
+
select.appendChild(opt);
|
|
755
|
+
});
|
|
756
|
+
} catch (error) {
|
|
757
|
+
console.error('Error loading profiles:', error);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async function createProfile(nameOverride) {
|
|
762
|
+
var name = nameOverride || document.getElementById('new-profile-name').value.trim();
|
|
763
|
+
if (!name) {
|
|
764
|
+
// Prompt with a simple browser dialog if called from the "+" button
|
|
765
|
+
name = prompt('Enter new profile name:');
|
|
766
|
+
if (!name || !name.trim()) return;
|
|
767
|
+
name = name.trim();
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Validate: alphanumeric, dashes, underscores only
|
|
771
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
772
|
+
showToast('Invalid name. Use letters, numbers, dashes, underscores.');
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
var response = await fetch('/api/profiles/create', {
|
|
778
|
+
method: 'POST',
|
|
779
|
+
headers: { 'Content-Type': 'application/json' },
|
|
780
|
+
body: JSON.stringify({ profile_name: name })
|
|
781
|
+
});
|
|
782
|
+
var data = await response.json();
|
|
783
|
+
if (response.status === 409) {
|
|
784
|
+
showToast('Profile "' + name + '" already exists');
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (!response.ok) {
|
|
788
|
+
showToast(data.detail || 'Failed to create profile');
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
showToast('Profile "' + name + '" created');
|
|
792
|
+
var input = document.getElementById('new-profile-name');
|
|
793
|
+
if (input) input.value = '';
|
|
794
|
+
loadProfiles();
|
|
795
|
+
loadProfilesTable();
|
|
796
|
+
} catch (error) {
|
|
797
|
+
console.error('Error creating profile:', error);
|
|
798
|
+
showToast('Error creating profile');
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function deleteProfile(name) {
|
|
803
|
+
if (name === 'default') {
|
|
804
|
+
showToast('Cannot delete the default profile');
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
if (!confirm('Delete profile "' + name + '"?\nIts memories will be moved to the default profile.')) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
var response = await fetch('/api/profiles/' + encodeURIComponent(name), {
|
|
812
|
+
method: 'DELETE'
|
|
813
|
+
});
|
|
814
|
+
var data = await response.json();
|
|
815
|
+
if (!response.ok) {
|
|
816
|
+
showToast(data.detail || 'Failed to delete profile');
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
showToast(data.message || 'Profile deleted');
|
|
820
|
+
loadProfiles();
|
|
821
|
+
loadProfilesTable();
|
|
822
|
+
loadStats();
|
|
823
|
+
} catch (error) {
|
|
824
|
+
console.error('Error deleting profile:', error);
|
|
825
|
+
showToast('Error deleting profile');
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async function loadProfilesTable() {
|
|
830
|
+
var container = document.getElementById('profiles-table');
|
|
831
|
+
if (!container) return;
|
|
832
|
+
try {
|
|
833
|
+
var response = await fetch('/api/profiles');
|
|
834
|
+
var data = await response.json();
|
|
835
|
+
var profiles = data.profiles || [];
|
|
836
|
+
var active = data.active_profile || 'default';
|
|
837
|
+
|
|
838
|
+
if (profiles.length === 0) {
|
|
839
|
+
showEmpty('profiles-table', 'people', 'No profiles found.');
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
var table = document.createElement('table');
|
|
844
|
+
table.className = 'table table-sm mb-0';
|
|
845
|
+
var thead = document.createElement('thead');
|
|
846
|
+
var headRow = document.createElement('tr');
|
|
847
|
+
['Name', 'Memories', 'Status', 'Actions'].forEach(function(h) {
|
|
848
|
+
var th = document.createElement('th');
|
|
849
|
+
th.textContent = h;
|
|
850
|
+
headRow.appendChild(th);
|
|
851
|
+
});
|
|
852
|
+
thead.appendChild(headRow);
|
|
853
|
+
table.appendChild(thead);
|
|
854
|
+
|
|
855
|
+
var tbody = document.createElement('tbody');
|
|
856
|
+
profiles.forEach(function(p) {
|
|
857
|
+
var row = document.createElement('tr');
|
|
858
|
+
|
|
859
|
+
var nameCell = document.createElement('td');
|
|
860
|
+
var nameIcon = document.createElement('i');
|
|
861
|
+
nameIcon.className = 'bi bi-person me-1';
|
|
862
|
+
nameCell.appendChild(nameIcon);
|
|
863
|
+
nameCell.appendChild(document.createTextNode(p.name));
|
|
864
|
+
row.appendChild(nameCell);
|
|
865
|
+
|
|
866
|
+
var countCell = document.createElement('td');
|
|
867
|
+
countCell.textContent = (p.memory_count || 0) + ' memories';
|
|
868
|
+
row.appendChild(countCell);
|
|
869
|
+
|
|
870
|
+
var statusCell = document.createElement('td');
|
|
871
|
+
if (p.name === active) {
|
|
872
|
+
var badge = document.createElement('span');
|
|
873
|
+
badge.className = 'badge bg-success';
|
|
874
|
+
badge.textContent = 'Active';
|
|
875
|
+
statusCell.appendChild(badge);
|
|
876
|
+
} else {
|
|
877
|
+
var switchBtn = document.createElement('button');
|
|
878
|
+
switchBtn.className = 'btn btn-sm btn-outline-primary';
|
|
879
|
+
switchBtn.textContent = 'Switch';
|
|
880
|
+
switchBtn.addEventListener('click', (function(n) {
|
|
881
|
+
return function() { switchProfile(n); };
|
|
882
|
+
})(p.name));
|
|
883
|
+
statusCell.appendChild(switchBtn);
|
|
884
|
+
}
|
|
885
|
+
row.appendChild(statusCell);
|
|
886
|
+
|
|
887
|
+
var actionsCell = document.createElement('td');
|
|
888
|
+
if (p.name !== 'default') {
|
|
889
|
+
var delBtn = document.createElement('button');
|
|
890
|
+
delBtn.className = 'btn btn-sm btn-outline-danger btn-delete-profile';
|
|
891
|
+
delBtn.title = 'Delete profile';
|
|
892
|
+
var delIcon = document.createElement('i');
|
|
893
|
+
delIcon.className = 'bi bi-trash';
|
|
894
|
+
delBtn.appendChild(delIcon);
|
|
895
|
+
delBtn.addEventListener('click', (function(n) {
|
|
896
|
+
return function() { deleteProfile(n); };
|
|
897
|
+
})(p.name));
|
|
898
|
+
actionsCell.appendChild(delBtn);
|
|
899
|
+
} else {
|
|
900
|
+
var protectedBadge = document.createElement('span');
|
|
901
|
+
protectedBadge.className = 'badge bg-secondary';
|
|
902
|
+
protectedBadge.textContent = 'Protected';
|
|
903
|
+
actionsCell.appendChild(protectedBadge);
|
|
904
|
+
}
|
|
905
|
+
row.appendChild(actionsCell);
|
|
906
|
+
|
|
907
|
+
tbody.appendChild(row);
|
|
908
|
+
});
|
|
909
|
+
table.appendChild(tbody);
|
|
910
|
+
|
|
911
|
+
container.textContent = '';
|
|
912
|
+
container.appendChild(table);
|
|
913
|
+
} catch (error) {
|
|
914
|
+
console.error('Error loading profiles table:', error);
|
|
915
|
+
showEmpty('profiles-table', 'exclamation-triangle', 'Failed to load profiles');
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
async function switchProfile(profileName) {
|
|
920
|
+
try {
|
|
921
|
+
var response = await fetch('/api/profiles/' + encodeURIComponent(profileName) + '/switch', {
|
|
922
|
+
method: 'POST'
|
|
923
|
+
});
|
|
924
|
+
var data = await response.json();
|
|
925
|
+
if (data.success || data.active_profile) {
|
|
926
|
+
showToast('Switched to profile: ' + profileName);
|
|
927
|
+
loadProfiles();
|
|
928
|
+
loadStats();
|
|
929
|
+
loadGraph();
|
|
930
|
+
loadProfilesTable();
|
|
931
|
+
var activeTab = document.querySelector('#mainTabs .nav-link.active');
|
|
932
|
+
if (activeTab) activeTab.click();
|
|
933
|
+
} else {
|
|
934
|
+
showToast('Failed to switch profile');
|
|
935
|
+
}
|
|
936
|
+
} catch (error) {
|
|
937
|
+
console.error('Error switching profile:', error);
|
|
938
|
+
showToast('Error switching profile');
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// ============================================================================
|
|
943
|
+
// Settings & Backup
|
|
944
|
+
// ============================================================================
|
|
945
|
+
|
|
946
|
+
async function loadSettings() {
|
|
947
|
+
loadProfilesTable();
|
|
948
|
+
loadBackupStatus();
|
|
949
|
+
loadBackupList();
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
async function loadBackupStatus() {
|
|
953
|
+
try {
|
|
954
|
+
var response = await fetch('/api/backup/status');
|
|
955
|
+
var data = await response.json();
|
|
956
|
+
renderBackupStatus(data);
|
|
957
|
+
document.getElementById('backup-interval').value = data.interval_hours <= 24 ? '24' : '168';
|
|
958
|
+
document.getElementById('backup-max').value = data.max_backups || 10;
|
|
959
|
+
document.getElementById('backup-enabled').checked = data.enabled !== false;
|
|
960
|
+
} catch (error) {
|
|
961
|
+
var container = document.getElementById('backup-status');
|
|
962
|
+
var alert = document.createElement('div');
|
|
963
|
+
alert.className = 'alert alert-warning mb-0';
|
|
964
|
+
alert.textContent = 'Auto-backup not available. Update to v2.4.0+.';
|
|
965
|
+
container.textContent = '';
|
|
966
|
+
container.appendChild(alert);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function renderBackupStatus(data) {
|
|
971
|
+
var container = document.getElementById('backup-status');
|
|
972
|
+
container.textContent = '';
|
|
973
|
+
|
|
974
|
+
var lastBackup = data.last_backup ? formatDateFull(data.last_backup) : 'Never';
|
|
975
|
+
var nextBackup = data.next_backup || 'N/A';
|
|
976
|
+
if (nextBackup === 'overdue') nextBackup = 'Overdue';
|
|
977
|
+
else if (nextBackup !== 'N/A' && nextBackup !== 'unknown') nextBackup = formatDateFull(nextBackup);
|
|
978
|
+
|
|
979
|
+
var statusColor = data.enabled ? 'text-success' : 'text-secondary';
|
|
980
|
+
var statusText = data.enabled ? 'Active' : 'Disabled';
|
|
981
|
+
|
|
982
|
+
// Build DOM nodes for safety
|
|
983
|
+
var row = document.createElement('div');
|
|
984
|
+
row.className = 'row g-2 mb-2';
|
|
985
|
+
|
|
986
|
+
var stats = [
|
|
987
|
+
{ value: statusText, label: 'Status', cls: statusColor },
|
|
988
|
+
{ value: String(data.backup_count || 0), label: 'Backups', cls: '' },
|
|
989
|
+
{ value: (data.total_size_mb || 0) + ' MB', label: 'Storage', cls: '' }
|
|
990
|
+
];
|
|
991
|
+
|
|
992
|
+
stats.forEach(function(s) {
|
|
993
|
+
var col = document.createElement('div');
|
|
994
|
+
col.className = 'col-4';
|
|
995
|
+
var stat = document.createElement('div');
|
|
996
|
+
stat.className = 'backup-stat';
|
|
997
|
+
var val = document.createElement('div');
|
|
998
|
+
val.className = 'value ' + s.cls;
|
|
999
|
+
val.textContent = s.value;
|
|
1000
|
+
var lbl = document.createElement('div');
|
|
1001
|
+
lbl.className = 'label';
|
|
1002
|
+
lbl.textContent = s.label;
|
|
1003
|
+
stat.appendChild(val);
|
|
1004
|
+
stat.appendChild(lbl);
|
|
1005
|
+
col.appendChild(stat);
|
|
1006
|
+
row.appendChild(col);
|
|
1007
|
+
});
|
|
1008
|
+
container.appendChild(row);
|
|
1009
|
+
|
|
1010
|
+
var details = [
|
|
1011
|
+
{ label: 'Last backup:', value: lastBackup },
|
|
1012
|
+
{ label: 'Next backup:', value: nextBackup },
|
|
1013
|
+
{ label: 'Interval:', value: data.interval_display || '-' }
|
|
1014
|
+
];
|
|
1015
|
+
details.forEach(function(d) {
|
|
1016
|
+
var div = document.createElement('div');
|
|
1017
|
+
div.className = 'small text-muted';
|
|
1018
|
+
var strong = document.createElement('strong');
|
|
1019
|
+
strong.textContent = d.label + ' ';
|
|
1020
|
+
div.appendChild(strong);
|
|
1021
|
+
div.appendChild(document.createTextNode(d.value));
|
|
1022
|
+
container.appendChild(div);
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async function saveBackupConfig() {
|
|
1027
|
+
try {
|
|
1028
|
+
var response = await fetch('/api/backup/configure', {
|
|
1029
|
+
method: 'POST',
|
|
1030
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1031
|
+
body: JSON.stringify({
|
|
1032
|
+
interval_hours: parseInt(document.getElementById('backup-interval').value),
|
|
1033
|
+
max_backups: parseInt(document.getElementById('backup-max').value),
|
|
1034
|
+
enabled: document.getElementById('backup-enabled').checked
|
|
1035
|
+
})
|
|
1036
|
+
});
|
|
1037
|
+
var data = await response.json();
|
|
1038
|
+
renderBackupStatus(data);
|
|
1039
|
+
showToast('Backup settings saved');
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
console.error('Error saving backup config:', error);
|
|
1042
|
+
showToast('Failed to save backup settings');
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async function createBackupNow() {
|
|
1047
|
+
showToast('Creating backup...');
|
|
1048
|
+
try {
|
|
1049
|
+
var response = await fetch('/api/backup/create', { method: 'POST' });
|
|
1050
|
+
var data = await response.json();
|
|
1051
|
+
if (data.success) {
|
|
1052
|
+
showToast('Backup created: ' + data.filename);
|
|
1053
|
+
loadBackupStatus();
|
|
1054
|
+
loadBackupList();
|
|
1055
|
+
} else {
|
|
1056
|
+
showToast('Backup failed');
|
|
1057
|
+
}
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
console.error('Error creating backup:', error);
|
|
1060
|
+
showToast('Backup failed');
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async function loadBackupList() {
|
|
1065
|
+
try {
|
|
1066
|
+
var response = await fetch('/api/backup/list');
|
|
1067
|
+
var data = await response.json();
|
|
1068
|
+
renderBackupList(data.backups || []);
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
var container = document.getElementById('backup-list');
|
|
1071
|
+
container.textContent = 'Backup list unavailable';
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function renderBackupList(backups) {
|
|
1076
|
+
var container = document.getElementById('backup-list');
|
|
1077
|
+
if (!backups || backups.length === 0) {
|
|
1078
|
+
showEmpty('backup-list', 'archive', 'No backups yet. Create your first backup above.');
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Build table using DOM nodes
|
|
1083
|
+
var table = document.createElement('table');
|
|
1084
|
+
table.className = 'table table-sm';
|
|
1085
|
+
var thead = document.createElement('thead');
|
|
1086
|
+
var headRow = document.createElement('tr');
|
|
1087
|
+
['Filename', 'Size', 'Age', 'Created'].forEach(function(h) {
|
|
1088
|
+
var th = document.createElement('th');
|
|
1089
|
+
th.textContent = h;
|
|
1090
|
+
headRow.appendChild(th);
|
|
1091
|
+
});
|
|
1092
|
+
thead.appendChild(headRow);
|
|
1093
|
+
table.appendChild(thead);
|
|
1094
|
+
|
|
1095
|
+
var tbody = document.createElement('tbody');
|
|
1096
|
+
backups.forEach(function(b) {
|
|
1097
|
+
var row = document.createElement('tr');
|
|
1098
|
+
var age = b.age_hours < 48 ? Math.round(b.age_hours) + 'h ago' : Math.round(b.age_hours / 24) + 'd ago';
|
|
1099
|
+
var cells = [b.filename, b.size_mb + ' MB', age, formatDateFull(b.created)];
|
|
1100
|
+
cells.forEach(function(text) {
|
|
1101
|
+
var td = document.createElement('td');
|
|
1102
|
+
td.textContent = text;
|
|
1103
|
+
row.appendChild(td);
|
|
1104
|
+
});
|
|
1105
|
+
tbody.appendChild(row);
|
|
1106
|
+
});
|
|
1107
|
+
table.appendChild(tbody);
|
|
1108
|
+
|
|
1109
|
+
container.textContent = '';
|
|
1110
|
+
container.appendChild(table);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// ============================================================================
|
|
1114
|
+
// Column Sorting
|
|
1115
|
+
// ============================================================================
|
|
1116
|
+
|
|
1117
|
+
var currentSort = { column: null, direction: 'asc' };
|
|
1118
|
+
|
|
1119
|
+
function handleSort(th) {
|
|
1120
|
+
var col = th.getAttribute('data-sort');
|
|
1121
|
+
if (!col) return;
|
|
1122
|
+
|
|
1123
|
+
if (currentSort.column === col) {
|
|
1124
|
+
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
|
1125
|
+
} else {
|
|
1126
|
+
currentSort.column = col;
|
|
1127
|
+
currentSort.direction = 'asc';
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Update header classes
|
|
1131
|
+
document.querySelectorAll('#memories-list th.sortable').forEach(function(h) {
|
|
1132
|
+
h.classList.remove('sort-asc', 'sort-desc');
|
|
1133
|
+
});
|
|
1134
|
+
th.classList.add('sort-' + currentSort.direction);
|
|
1135
|
+
|
|
1136
|
+
// Sort the data
|
|
1137
|
+
if (!window._slmMemories) return;
|
|
1138
|
+
var memories = window._slmMemories.slice();
|
|
1139
|
+
var dir = currentSort.direction === 'asc' ? 1 : -1;
|
|
1140
|
+
|
|
1141
|
+
memories.sort(function(a, b) {
|
|
1142
|
+
var av, bv;
|
|
1143
|
+
switch (col) {
|
|
1144
|
+
case 'id': return ((a.id || 0) - (b.id || 0)) * dir;
|
|
1145
|
+
case 'importance': return ((a.importance || 0) - (b.importance || 0)) * dir;
|
|
1146
|
+
case 'category':
|
|
1147
|
+
av = (a.category || '').toLowerCase(); bv = (b.category || '').toLowerCase();
|
|
1148
|
+
return av < bv ? -dir : av > bv ? dir : 0;
|
|
1149
|
+
case 'project':
|
|
1150
|
+
av = (a.project_name || '').toLowerCase(); bv = (b.project_name || '').toLowerCase();
|
|
1151
|
+
return av < bv ? -dir : av > bv ? dir : 0;
|
|
1152
|
+
case 'created':
|
|
1153
|
+
av = a.created_at || ''; bv = b.created_at || '';
|
|
1154
|
+
return av < bv ? -dir : av > bv ? dir : 0;
|
|
1155
|
+
case 'score': return ((a.score || 0) - (b.score || 0)) * dir;
|
|
1156
|
+
default: return 0;
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
window._slmMemories = memories;
|
|
1161
|
+
var showScores = memories.length > 0 && typeof memories[0].score === 'number';
|
|
1162
|
+
renderMemoriesTable(memories, showScores);
|
|
1163
|
+
}
|