superlocalmemory 2.4.2 → 2.5.1
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 +62 -0
- package/README.md +62 -2
- package/docs/ARCHITECTURE-V2.5.md +190 -0
- package/docs/architecture-diagram.drawio +405 -0
- package/mcp_server.py +115 -14
- package/package.json +4 -1
- package/scripts/generate-thumbnails.py +220 -0
- package/src/agent_registry.py +385 -0
- package/src/db_connection_manager.py +532 -0
- package/src/event_bus.py +555 -0
- package/src/memory_store_v2.py +626 -471
- package/src/provenance_tracker.py +322 -0
- package/src/subscription_manager.py +399 -0
- package/src/trust_scorer.py +456 -0
- package/src/webhook_dispatcher.py +229 -0
- package/ui/app.js +425 -0
- package/ui/index.html +147 -1
- package/ui/js/agents.js +192 -0
- package/ui/js/clusters.js +80 -0
- package/ui/js/core.js +230 -0
- package/ui/js/events.js +178 -0
- package/ui/js/graph.js +32 -0
- package/ui/js/init.js +31 -0
- package/ui/js/memories.js +149 -0
- package/ui/js/modal.js +139 -0
- package/ui/js/patterns.js +93 -0
- package/ui/js/profiles.js +202 -0
- package/ui/js/search.js +59 -0
- package/ui/js/settings.js +167 -0
- package/ui/js/timeline.js +32 -0
- package/ui_server.py +69 -1665
- package/docs/COMPETITIVE-ANALYSIS.md +0 -210
package/ui/js/agents.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// SuperLocalMemory V2 - Connected Agents + Trust Overview (v2.5)
|
|
2
|
+
// Depends on: core.js
|
|
3
|
+
// Security: All DOM built with safe methods (createElement/textContent).
|
|
4
|
+
|
|
5
|
+
async function loadAgents() {
|
|
6
|
+
try {
|
|
7
|
+
var response = await fetch('/api/agents');
|
|
8
|
+
var data = await response.json();
|
|
9
|
+
var agents = data.agents || [];
|
|
10
|
+
var stats = data.stats || {};
|
|
11
|
+
|
|
12
|
+
var el;
|
|
13
|
+
el = document.getElementById('agent-stat-total');
|
|
14
|
+
if (el) el.textContent = (stats.total_agents || 0).toLocaleString();
|
|
15
|
+
el = document.getElementById('agent-stat-active');
|
|
16
|
+
if (el) el.textContent = (stats.active_last_24h || 0).toLocaleString();
|
|
17
|
+
el = document.getElementById('agent-stat-writes');
|
|
18
|
+
if (el) el.textContent = (stats.total_writes || 0).toLocaleString();
|
|
19
|
+
el = document.getElementById('agent-stat-recalls');
|
|
20
|
+
if (el) el.textContent = (stats.total_recalls || 0).toLocaleString();
|
|
21
|
+
|
|
22
|
+
var container = document.getElementById('agents-list');
|
|
23
|
+
if (!container) return;
|
|
24
|
+
|
|
25
|
+
if (agents.length === 0) {
|
|
26
|
+
container.textContent = '';
|
|
27
|
+
var empty = document.createElement('div');
|
|
28
|
+
empty.className = 'text-muted text-center py-4';
|
|
29
|
+
var emptyIcon = document.createElement('i');
|
|
30
|
+
emptyIcon.className = 'bi bi-robot';
|
|
31
|
+
emptyIcon.style.fontSize = '2rem';
|
|
32
|
+
empty.appendChild(emptyIcon);
|
|
33
|
+
var emptyText = document.createElement('p');
|
|
34
|
+
emptyText.className = 'mt-2';
|
|
35
|
+
emptyText.textContent = 'No agents registered yet. Agents appear automatically when they connect via MCP, CLI, or REST.';
|
|
36
|
+
empty.appendChild(emptyText);
|
|
37
|
+
container.appendChild(empty);
|
|
38
|
+
loadTrustOverview();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var table = document.createElement('table');
|
|
43
|
+
table.className = 'table table-hover table-sm';
|
|
44
|
+
var thead = document.createElement('thead');
|
|
45
|
+
var headerRow = document.createElement('tr');
|
|
46
|
+
['Agent', 'Protocol', 'Trust', 'Writes', 'Recalls', 'Last Seen'].forEach(function(h) {
|
|
47
|
+
var th = document.createElement('th');
|
|
48
|
+
th.textContent = h;
|
|
49
|
+
headerRow.appendChild(th);
|
|
50
|
+
});
|
|
51
|
+
thead.appendChild(headerRow);
|
|
52
|
+
table.appendChild(thead);
|
|
53
|
+
|
|
54
|
+
var tbody = document.createElement('tbody');
|
|
55
|
+
agents.forEach(function(agent) {
|
|
56
|
+
var tr = document.createElement('tr');
|
|
57
|
+
|
|
58
|
+
var tdName = document.createElement('td');
|
|
59
|
+
var strong = document.createElement('strong');
|
|
60
|
+
strong.textContent = agent.agent_name || agent.agent_id;
|
|
61
|
+
tdName.appendChild(strong);
|
|
62
|
+
tdName.appendChild(document.createElement('br'));
|
|
63
|
+
var smallId = document.createElement('small');
|
|
64
|
+
smallId.className = 'text-muted';
|
|
65
|
+
smallId.textContent = agent.agent_id;
|
|
66
|
+
tdName.appendChild(smallId);
|
|
67
|
+
tr.appendChild(tdName);
|
|
68
|
+
|
|
69
|
+
var tdProto = document.createElement('td');
|
|
70
|
+
var protoBadge = document.createElement('span');
|
|
71
|
+
var protocolColors = {
|
|
72
|
+
'mcp': 'bg-primary', 'cli': 'bg-success', 'rest': 'bg-info',
|
|
73
|
+
'python': 'bg-secondary', 'a2a': 'bg-warning'
|
|
74
|
+
};
|
|
75
|
+
protoBadge.className = 'badge ' + (protocolColors[agent.protocol] || 'bg-secondary');
|
|
76
|
+
protoBadge.textContent = agent.protocol;
|
|
77
|
+
tdProto.appendChild(protoBadge);
|
|
78
|
+
tr.appendChild(tdProto);
|
|
79
|
+
|
|
80
|
+
var tdTrust = document.createElement('td');
|
|
81
|
+
var trustScore = agent.trust_score != null ? agent.trust_score : 1.0;
|
|
82
|
+
tdTrust.className = trustScore < 0.7 ? 'text-danger fw-bold'
|
|
83
|
+
: trustScore < 0.9 ? 'text-warning fw-bold' : 'text-success fw-bold';
|
|
84
|
+
tdTrust.textContent = trustScore.toFixed(2);
|
|
85
|
+
tr.appendChild(tdTrust);
|
|
86
|
+
|
|
87
|
+
var tdW = document.createElement('td');
|
|
88
|
+
tdW.textContent = agent.memories_written || 0;
|
|
89
|
+
tr.appendChild(tdW);
|
|
90
|
+
|
|
91
|
+
var tdR = document.createElement('td');
|
|
92
|
+
tdR.textContent = agent.memories_recalled || 0;
|
|
93
|
+
tr.appendChild(tdR);
|
|
94
|
+
|
|
95
|
+
var tdLast = document.createElement('td');
|
|
96
|
+
var lastSmall = document.createElement('small');
|
|
97
|
+
lastSmall.textContent = agent.last_seen ? new Date(agent.last_seen).toLocaleString() : 'Never';
|
|
98
|
+
tdLast.appendChild(lastSmall);
|
|
99
|
+
tr.appendChild(tdLast);
|
|
100
|
+
|
|
101
|
+
tbody.appendChild(tr);
|
|
102
|
+
});
|
|
103
|
+
table.appendChild(tbody);
|
|
104
|
+
|
|
105
|
+
container.textContent = '';
|
|
106
|
+
container.appendChild(table);
|
|
107
|
+
|
|
108
|
+
loadTrustOverview();
|
|
109
|
+
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.log('Agents not available:', err);
|
|
112
|
+
var container = document.getElementById('agents-list');
|
|
113
|
+
if (container) {
|
|
114
|
+
container.textContent = '';
|
|
115
|
+
var msg = document.createElement('small');
|
|
116
|
+
msg.className = 'text-muted';
|
|
117
|
+
msg.textContent = 'Agent registry not available. This feature requires v2.5+.';
|
|
118
|
+
container.appendChild(msg);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function loadTrustOverview() {
|
|
124
|
+
try {
|
|
125
|
+
var response = await fetch('/api/trust/stats');
|
|
126
|
+
var stats = await response.json();
|
|
127
|
+
var container = document.getElementById('trust-overview');
|
|
128
|
+
if (!container) return;
|
|
129
|
+
|
|
130
|
+
container.textContent = '';
|
|
131
|
+
var row = document.createElement('div');
|
|
132
|
+
row.className = 'row g-3';
|
|
133
|
+
|
|
134
|
+
var cardData = [
|
|
135
|
+
{ value: (stats.total_signals || 0).toLocaleString(), label: 'Total Signals Collected', cls: '' },
|
|
136
|
+
{ value: (stats.avg_trust_score || 1.0).toFixed(3), label: 'Average Trust Score', cls: '' },
|
|
137
|
+
{ value: stats.enforcement || 'disabled', label: 'Enforcement Status', cls: 'text-info' }
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
cardData.forEach(function(c) {
|
|
141
|
+
var col = document.createElement('div');
|
|
142
|
+
col.className = 'col-md-4';
|
|
143
|
+
var card = document.createElement('div');
|
|
144
|
+
card.className = 'border rounded p-3 text-center';
|
|
145
|
+
var val = document.createElement('div');
|
|
146
|
+
val.className = 'fs-4 fw-bold ' + c.cls;
|
|
147
|
+
val.textContent = c.value;
|
|
148
|
+
card.appendChild(val);
|
|
149
|
+
var lbl = document.createElement('small');
|
|
150
|
+
lbl.className = 'text-muted';
|
|
151
|
+
lbl.textContent = c.label;
|
|
152
|
+
card.appendChild(lbl);
|
|
153
|
+
col.appendChild(card);
|
|
154
|
+
row.appendChild(col);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
container.appendChild(row);
|
|
158
|
+
|
|
159
|
+
if (stats.by_signal_type && Object.keys(stats.by_signal_type).length > 0) {
|
|
160
|
+
var breakdownDiv = document.createElement('div');
|
|
161
|
+
breakdownDiv.className = 'col-12 mt-3';
|
|
162
|
+
var h6 = document.createElement('h6');
|
|
163
|
+
h6.textContent = 'Signal Breakdown';
|
|
164
|
+
breakdownDiv.appendChild(h6);
|
|
165
|
+
var badgeWrap = document.createElement('div');
|
|
166
|
+
badgeWrap.className = 'd-flex flex-wrap gap-2';
|
|
167
|
+
Object.keys(stats.by_signal_type).forEach(function(type) {
|
|
168
|
+
var count = stats.by_signal_type[type];
|
|
169
|
+
var signalClass = (type.indexOf('high_volume') >= 0 || type.indexOf('quick_delete') >= 0)
|
|
170
|
+
? 'bg-danger' : (type.indexOf('recalled') >= 0 || type.indexOf('high_importance') >= 0)
|
|
171
|
+
? 'bg-success' : 'bg-secondary';
|
|
172
|
+
var b = document.createElement('span');
|
|
173
|
+
b.className = 'badge ' + signalClass;
|
|
174
|
+
b.textContent = type + ': ' + count;
|
|
175
|
+
badgeWrap.appendChild(b);
|
|
176
|
+
});
|
|
177
|
+
breakdownDiv.appendChild(badgeWrap);
|
|
178
|
+
container.appendChild(breakdownDiv);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.log('Trust stats not available:', err);
|
|
183
|
+
var container = document.getElementById('trust-overview');
|
|
184
|
+
if (container) {
|
|
185
|
+
container.textContent = '';
|
|
186
|
+
var msg = document.createElement('small');
|
|
187
|
+
msg.className = 'text-muted';
|
|
188
|
+
msg.textContent = 'Trust scoring data will appear here once agents interact with memory.';
|
|
189
|
+
container.appendChild(msg);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SuperLocalMemory V2 - Clusters View
|
|
2
|
+
// Depends on: core.js
|
|
3
|
+
//
|
|
4
|
+
// Security: All dynamic values escaped via escapeHtml(). Data from local DB only.
|
|
5
|
+
|
|
6
|
+
async function loadClusters() {
|
|
7
|
+
showLoading('clusters-list', 'Loading clusters...');
|
|
8
|
+
try {
|
|
9
|
+
var response = await fetch('/api/clusters');
|
|
10
|
+
var data = await response.json();
|
|
11
|
+
renderClusters(data.clusters);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error('Error loading clusters:', error);
|
|
14
|
+
showEmpty('clusters-list', 'collection', 'Failed to load clusters');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function renderClusters(clusters) {
|
|
19
|
+
var container = document.getElementById('clusters-list');
|
|
20
|
+
if (!clusters || clusters.length === 0) {
|
|
21
|
+
showEmpty('clusters-list', 'collection', 'No clusters found. Run "slm build-graph" to generate clusters.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var colors = ['#667eea', '#f093fb', '#4facfe', '#43e97b', '#fa709a'];
|
|
26
|
+
container.textContent = '';
|
|
27
|
+
|
|
28
|
+
clusters.forEach(function(cluster, idx) {
|
|
29
|
+
var color = colors[idx % colors.length];
|
|
30
|
+
|
|
31
|
+
var card = document.createElement('div');
|
|
32
|
+
card.className = 'card cluster-card';
|
|
33
|
+
card.style.borderColor = color;
|
|
34
|
+
|
|
35
|
+
var body = document.createElement('div');
|
|
36
|
+
body.className = 'card-body';
|
|
37
|
+
|
|
38
|
+
var title = document.createElement('h6');
|
|
39
|
+
title.className = 'card-title';
|
|
40
|
+
title.textContent = 'Cluster ' + cluster.cluster_id + ' ';
|
|
41
|
+
var countBadge = document.createElement('span');
|
|
42
|
+
countBadge.className = 'badge bg-secondary float-end';
|
|
43
|
+
countBadge.textContent = cluster.member_count + ' memories';
|
|
44
|
+
title.appendChild(countBadge);
|
|
45
|
+
body.appendChild(title);
|
|
46
|
+
|
|
47
|
+
var imp = document.createElement('p');
|
|
48
|
+
imp.className = 'mb-2';
|
|
49
|
+
imp.textContent = 'Avg Importance: ' + parseFloat(cluster.avg_importance).toFixed(1);
|
|
50
|
+
body.appendChild(imp);
|
|
51
|
+
|
|
52
|
+
var cats = document.createElement('p');
|
|
53
|
+
cats.className = 'mb-2';
|
|
54
|
+
cats.textContent = 'Categories: ' + (cluster.categories || 'None');
|
|
55
|
+
body.appendChild(cats);
|
|
56
|
+
|
|
57
|
+
var entLabel = document.createElement('strong');
|
|
58
|
+
entLabel.textContent = 'Top Entities:';
|
|
59
|
+
body.appendChild(entLabel);
|
|
60
|
+
body.appendChild(document.createElement('br'));
|
|
61
|
+
|
|
62
|
+
if (cluster.top_entities && cluster.top_entities.length > 0) {
|
|
63
|
+
cluster.top_entities.forEach(function(e) {
|
|
64
|
+
var badge = document.createElement('span');
|
|
65
|
+
badge.className = 'badge bg-info entity-badge';
|
|
66
|
+
badge.textContent = e.entity + ' (' + e.count + ')';
|
|
67
|
+
body.appendChild(badge);
|
|
68
|
+
body.appendChild(document.createTextNode(' '));
|
|
69
|
+
});
|
|
70
|
+
} else {
|
|
71
|
+
var none = document.createElement('span');
|
|
72
|
+
none.className = 'text-muted';
|
|
73
|
+
none.textContent = 'No entities';
|
|
74
|
+
body.appendChild(none);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
card.appendChild(body);
|
|
78
|
+
container.appendChild(card);
|
|
79
|
+
});
|
|
80
|
+
}
|
package/ui/js/core.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// SuperLocalMemory V2 - Core Utilities
|
|
2
|
+
// Shared functions used by all other modules.
|
|
3
|
+
// Security: All dynamic text MUST pass through escapeHtml() before DOM insertion.
|
|
4
|
+
// Data originates from our own trusted local SQLite database (localhost only).
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Dark Mode
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
function initDarkMode() {
|
|
11
|
+
var saved = localStorage.getItem('slm-theme');
|
|
12
|
+
var theme;
|
|
13
|
+
if (saved) {
|
|
14
|
+
theme = saved;
|
|
15
|
+
} else {
|
|
16
|
+
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
17
|
+
}
|
|
18
|
+
applyTheme(theme);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function applyTheme(theme) {
|
|
22
|
+
document.documentElement.setAttribute('data-bs-theme', theme);
|
|
23
|
+
var icon = document.getElementById('theme-icon');
|
|
24
|
+
if (icon) {
|
|
25
|
+
icon.className = theme === 'dark' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function toggleDarkMode() {
|
|
30
|
+
var current = document.documentElement.getAttribute('data-bs-theme');
|
|
31
|
+
var next = current === 'dark' ? 'light' : 'dark';
|
|
32
|
+
localStorage.setItem('slm-theme', next);
|
|
33
|
+
applyTheme(next);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Animated Counter
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
function animateCounter(elementId, target) {
|
|
41
|
+
var el = document.getElementById(elementId);
|
|
42
|
+
if (!el) return;
|
|
43
|
+
var duration = 600;
|
|
44
|
+
var startTime = null;
|
|
45
|
+
|
|
46
|
+
function step(timestamp) {
|
|
47
|
+
if (!startTime) startTime = timestamp;
|
|
48
|
+
var progress = Math.min((timestamp - startTime) / duration, 1);
|
|
49
|
+
var eased = 1 - Math.pow(1 - progress, 3);
|
|
50
|
+
el.textContent = Math.floor(eased * target).toLocaleString();
|
|
51
|
+
if (progress < 1) {
|
|
52
|
+
requestAnimationFrame(step);
|
|
53
|
+
} else {
|
|
54
|
+
el.textContent = target.toLocaleString();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (target === 0) {
|
|
59
|
+
el.textContent = '0';
|
|
60
|
+
} else {
|
|
61
|
+
requestAnimationFrame(step);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// HTML Escaping — all dynamic text MUST pass through this before DOM insertion
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
function escapeHtml(text) {
|
|
70
|
+
if (!text) return '';
|
|
71
|
+
var div = document.createElement('div');
|
|
72
|
+
div.appendChild(document.createTextNode(String(text)));
|
|
73
|
+
return div.innerHTML;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Loading / Empty State helpers
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
function showLoading(containerId, message) {
|
|
81
|
+
var el = document.getElementById(containerId);
|
|
82
|
+
if (!el) return;
|
|
83
|
+
el.textContent = '';
|
|
84
|
+
var wrapper = document.createElement('div');
|
|
85
|
+
wrapper.className = 'loading';
|
|
86
|
+
var spinner = document.createElement('div');
|
|
87
|
+
spinner.className = 'spinner-border text-primary';
|
|
88
|
+
spinner.setAttribute('role', 'status');
|
|
89
|
+
var msg = document.createElement('div');
|
|
90
|
+
msg.textContent = message || 'Loading...';
|
|
91
|
+
wrapper.appendChild(spinner);
|
|
92
|
+
wrapper.appendChild(msg);
|
|
93
|
+
el.appendChild(wrapper);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function showEmpty(containerId, icon, message) {
|
|
97
|
+
var el = document.getElementById(containerId);
|
|
98
|
+
if (!el) return;
|
|
99
|
+
el.textContent = '';
|
|
100
|
+
var wrapper = document.createElement('div');
|
|
101
|
+
wrapper.className = 'empty-state';
|
|
102
|
+
var iconEl = document.createElement('i');
|
|
103
|
+
iconEl.className = 'bi bi-' + icon + ' d-block';
|
|
104
|
+
var p = document.createElement('p');
|
|
105
|
+
p.textContent = message;
|
|
106
|
+
wrapper.appendChild(iconEl);
|
|
107
|
+
wrapper.appendChild(p);
|
|
108
|
+
el.appendChild(wrapper);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// Safe HTML builder — tagged template for escaped interpolation
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
function safeHtml(templateParts) {
|
|
116
|
+
var args = Array.prototype.slice.call(arguments, 1);
|
|
117
|
+
var result = '';
|
|
118
|
+
for (var i = 0; i < templateParts.length; i++) {
|
|
119
|
+
result += templateParts[i];
|
|
120
|
+
if (i < args.length) {
|
|
121
|
+
result += escapeHtml(String(args[i]));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// File Download helper
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
function downloadFile(filename, content, mimeType) {
|
|
132
|
+
var blob = new Blob([content], { type: mimeType });
|
|
133
|
+
var url = URL.createObjectURL(blob);
|
|
134
|
+
var a = document.createElement('a');
|
|
135
|
+
a.href = url;
|
|
136
|
+
a.download = filename;
|
|
137
|
+
document.body.appendChild(a);
|
|
138
|
+
a.click();
|
|
139
|
+
document.body.removeChild(a);
|
|
140
|
+
URL.revokeObjectURL(url);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// Toast notification
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
function showToast(message) {
|
|
148
|
+
var toast = document.createElement('div');
|
|
149
|
+
toast.style.cssText = 'position:fixed;bottom:24px;right:24px;background:#333;color:#fff;padding:10px 20px;border-radius:8px;font-size:0.9rem;z-index:9999;opacity:0;transition:opacity 0.3s;';
|
|
150
|
+
toast.textContent = message;
|
|
151
|
+
document.body.appendChild(toast);
|
|
152
|
+
requestAnimationFrame(function() { toast.style.opacity = '1'; });
|
|
153
|
+
setTimeout(function() {
|
|
154
|
+
toast.style.opacity = '0';
|
|
155
|
+
setTimeout(function() {
|
|
156
|
+
if (toast.parentNode) document.body.removeChild(toast);
|
|
157
|
+
}, 300);
|
|
158
|
+
}, 2000);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// Date Formatters
|
|
163
|
+
// ============================================================================
|
|
164
|
+
|
|
165
|
+
function formatDate(dateString) {
|
|
166
|
+
if (!dateString) return '-';
|
|
167
|
+
var date = new Date(dateString);
|
|
168
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function formatDateFull(dateString) {
|
|
172
|
+
if (!dateString) return '-';
|
|
173
|
+
var date = new Date(dateString);
|
|
174
|
+
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// Stats (loaded on startup)
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
async function loadStats() {
|
|
182
|
+
try {
|
|
183
|
+
var response = await fetch('/api/stats');
|
|
184
|
+
var data = await response.json();
|
|
185
|
+
animateCounter('stat-memories', data.overview.total_memories);
|
|
186
|
+
animateCounter('stat-clusters', data.overview.total_clusters);
|
|
187
|
+
animateCounter('stat-nodes', data.overview.graph_nodes);
|
|
188
|
+
animateCounter('stat-edges', data.overview.graph_edges);
|
|
189
|
+
populateFilters(data.categories, data.projects);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('Error loading stats:', error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function populateFilters(categories, projects) {
|
|
196
|
+
var categorySelect = document.getElementById('filter-category');
|
|
197
|
+
var projectSelect = document.getElementById('filter-project');
|
|
198
|
+
categories.forEach(function(cat) {
|
|
199
|
+
if (cat.category) {
|
|
200
|
+
var option = document.createElement('option');
|
|
201
|
+
option.value = cat.category;
|
|
202
|
+
option.textContent = cat.category + ' (' + cat.count + ')';
|
|
203
|
+
categorySelect.appendChild(option);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
projects.forEach(function(proj) {
|
|
207
|
+
if (proj.project_name) {
|
|
208
|
+
var option = document.createElement('option');
|
|
209
|
+
option.value = proj.project_name;
|
|
210
|
+
option.textContent = proj.project_name + ' (' + proj.count + ')';
|
|
211
|
+
projectSelect.appendChild(option);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Application Init (DOMContentLoaded)
|
|
218
|
+
// ============================================================================
|
|
219
|
+
|
|
220
|
+
window.addEventListener('DOMContentLoaded', function() {
|
|
221
|
+
initDarkMode();
|
|
222
|
+
loadProfiles();
|
|
223
|
+
loadStats();
|
|
224
|
+
loadGraph();
|
|
225
|
+
|
|
226
|
+
// v2.5 — Event Bus + Agent Registry (graceful if functions don't exist)
|
|
227
|
+
if (typeof initEventStream === 'function') initEventStream();
|
|
228
|
+
if (typeof loadEventStats === 'function') loadEventStats();
|
|
229
|
+
if (typeof loadAgents === 'function') loadAgents();
|
|
230
|
+
});
|
package/ui/js/events.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// SuperLocalMemory V2 - Live Events (v2.5 — SSE Stream)
|
|
2
|
+
// Depends on: core.js
|
|
3
|
+
// Security: All DOM built with safe methods (createElement/textContent).
|
|
4
|
+
|
|
5
|
+
var _eventSource = null;
|
|
6
|
+
var _eventStreamItems = [];
|
|
7
|
+
var _maxEventStreamItems = 200;
|
|
8
|
+
|
|
9
|
+
function initEventStream() {
|
|
10
|
+
try {
|
|
11
|
+
_eventSource = new EventSource('/events/stream');
|
|
12
|
+
|
|
13
|
+
_eventSource.onopen = function() {
|
|
14
|
+
var badge = document.getElementById('event-connection-status');
|
|
15
|
+
if (badge) {
|
|
16
|
+
badge.textContent = 'Connected';
|
|
17
|
+
badge.className = 'badge bg-success me-2';
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
_eventSource.onmessage = function(e) {
|
|
22
|
+
try {
|
|
23
|
+
appendEventToStream(JSON.parse(e.data));
|
|
24
|
+
} catch (err) { /* keepalive comments */ }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
_eventSource.onerror = function() {
|
|
28
|
+
var badge = document.getElementById('event-connection-status');
|
|
29
|
+
if (badge) {
|
|
30
|
+
badge.textContent = 'Reconnecting...';
|
|
31
|
+
badge.className = 'badge bg-warning me-2';
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
['memory.created', 'memory.updated', 'memory.deleted', 'memory.recalled',
|
|
36
|
+
'agent.connected', 'agent.disconnected', 'graph.updated', 'pattern.learned'
|
|
37
|
+
].forEach(function(type) {
|
|
38
|
+
_eventSource.addEventListener(type, function(e) {
|
|
39
|
+
try { appendEventToStream(JSON.parse(e.data)); } catch (err) { /* ignore */ }
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.log('SSE not available:', err);
|
|
44
|
+
var badge = document.getElementById('event-connection-status');
|
|
45
|
+
if (badge) {
|
|
46
|
+
badge.textContent = 'Unavailable';
|
|
47
|
+
badge.className = 'badge bg-secondary me-2';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function appendEventToStream(event) {
|
|
53
|
+
var container = document.getElementById('event-stream');
|
|
54
|
+
if (!container) return;
|
|
55
|
+
|
|
56
|
+
if (_eventStreamItems.length === 0) container.textContent = '';
|
|
57
|
+
|
|
58
|
+
_eventStreamItems.push(event);
|
|
59
|
+
if (_eventStreamItems.length > _maxEventStreamItems) _eventStreamItems.shift();
|
|
60
|
+
|
|
61
|
+
var filter = document.getElementById('event-type-filter');
|
|
62
|
+
var filterValue = filter ? filter.value : '';
|
|
63
|
+
if (filterValue && event.event_type !== filterValue) return;
|
|
64
|
+
|
|
65
|
+
var typeColors = {
|
|
66
|
+
'memory.created': 'text-success', 'memory.updated': 'text-info',
|
|
67
|
+
'memory.deleted': 'text-danger', 'memory.recalled': 'text-primary',
|
|
68
|
+
'agent.connected': 'text-warning', 'agent.disconnected': 'text-secondary',
|
|
69
|
+
'graph.updated': 'text-info', 'pattern.learned': 'text-success'
|
|
70
|
+
};
|
|
71
|
+
var typeIcons = {
|
|
72
|
+
'memory.created': 'bi-plus-circle', 'memory.updated': 'bi-pencil',
|
|
73
|
+
'memory.deleted': 'bi-trash', 'memory.recalled': 'bi-search',
|
|
74
|
+
'agent.connected': 'bi-plug', 'agent.disconnected': 'bi-plug',
|
|
75
|
+
'graph.updated': 'bi-diagram-3', 'pattern.learned': 'bi-lightbulb'
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
var colorClass = typeColors[event.event_type] || 'text-muted';
|
|
79
|
+
var iconClass = typeIcons[event.event_type] || 'bi-circle';
|
|
80
|
+
var ts = event.timestamp ? new Date(event.timestamp).toLocaleTimeString() : '';
|
|
81
|
+
var payload = event.payload || {};
|
|
82
|
+
var preview = payload.content_preview || payload.agent_id || payload.agent_name || '';
|
|
83
|
+
if (preview.length > 80) preview = preview.substring(0, 80) + '...';
|
|
84
|
+
|
|
85
|
+
var div = document.createElement('div');
|
|
86
|
+
div.className = 'event-line mb-1 pb-1 border-bottom border-opacity-25';
|
|
87
|
+
|
|
88
|
+
var timeSpan = document.createElement('small');
|
|
89
|
+
timeSpan.className = 'text-muted';
|
|
90
|
+
timeSpan.textContent = ts;
|
|
91
|
+
|
|
92
|
+
var icon = document.createElement('i');
|
|
93
|
+
icon.className = 'bi ' + iconClass + ' ' + colorClass;
|
|
94
|
+
icon.style.marginLeft = '6px';
|
|
95
|
+
|
|
96
|
+
var typeSpan = document.createElement('span');
|
|
97
|
+
typeSpan.className = colorClass + ' fw-bold';
|
|
98
|
+
typeSpan.style.marginLeft = '4px';
|
|
99
|
+
typeSpan.textContent = event.event_type;
|
|
100
|
+
|
|
101
|
+
div.appendChild(timeSpan);
|
|
102
|
+
div.appendChild(document.createTextNode(' '));
|
|
103
|
+
div.appendChild(icon);
|
|
104
|
+
div.appendChild(document.createTextNode(' '));
|
|
105
|
+
div.appendChild(typeSpan);
|
|
106
|
+
div.appendChild(document.createTextNode(' '));
|
|
107
|
+
|
|
108
|
+
if (event.memory_id) {
|
|
109
|
+
var badge = document.createElement('span');
|
|
110
|
+
badge.className = 'badge bg-secondary';
|
|
111
|
+
badge.textContent = '#' + event.memory_id;
|
|
112
|
+
div.appendChild(badge);
|
|
113
|
+
div.appendChild(document.createTextNode(' '));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
var previewSpan = document.createElement('span');
|
|
117
|
+
previewSpan.className = 'text-muted';
|
|
118
|
+
previewSpan.textContent = preview;
|
|
119
|
+
div.appendChild(previewSpan);
|
|
120
|
+
|
|
121
|
+
container.insertBefore(div, container.firstChild);
|
|
122
|
+
|
|
123
|
+
while (container.children.length > _maxEventStreamItems) {
|
|
124
|
+
container.removeChild(container.lastChild);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function filterEvents() {
|
|
129
|
+
var container = document.getElementById('event-stream');
|
|
130
|
+
if (!container) return;
|
|
131
|
+
container.textContent = '';
|
|
132
|
+
|
|
133
|
+
var filter = document.getElementById('event-type-filter');
|
|
134
|
+
var filterValue = filter ? filter.value : '';
|
|
135
|
+
|
|
136
|
+
var filtered = filterValue
|
|
137
|
+
? _eventStreamItems.filter(function(e) { return e.event_type === filterValue; })
|
|
138
|
+
: _eventStreamItems;
|
|
139
|
+
|
|
140
|
+
filtered.forEach(function(event) { appendEventToStream(event); });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function clearEventStream() {
|
|
144
|
+
_eventStreamItems = [];
|
|
145
|
+
var container = document.getElementById('event-stream');
|
|
146
|
+
if (container) {
|
|
147
|
+
container.textContent = '';
|
|
148
|
+
var placeholder = document.createElement('div');
|
|
149
|
+
placeholder.className = 'text-muted text-center py-4';
|
|
150
|
+
var pIcon = document.createElement('i');
|
|
151
|
+
pIcon.className = 'bi bi-broadcast';
|
|
152
|
+
pIcon.style.fontSize = '2rem';
|
|
153
|
+
placeholder.appendChild(pIcon);
|
|
154
|
+
var pText = document.createElement('p');
|
|
155
|
+
pText.className = 'mt-2';
|
|
156
|
+
pText.textContent = 'Event stream cleared. Waiting for new events...';
|
|
157
|
+
placeholder.appendChild(pText);
|
|
158
|
+
container.appendChild(placeholder);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function loadEventStats() {
|
|
163
|
+
try {
|
|
164
|
+
var response = await fetch('/api/events/stats');
|
|
165
|
+
var stats = await response.json();
|
|
166
|
+
var el;
|
|
167
|
+
el = document.getElementById('event-stat-total');
|
|
168
|
+
if (el) el.textContent = (stats.total_events || 0).toLocaleString();
|
|
169
|
+
el = document.getElementById('event-stat-24h');
|
|
170
|
+
if (el) el.textContent = (stats.events_last_24h || 0).toLocaleString();
|
|
171
|
+
el = document.getElementById('event-stat-listeners');
|
|
172
|
+
if (el) el.textContent = (stats.listener_count || 0).toLocaleString();
|
|
173
|
+
el = document.getElementById('event-stat-buffer');
|
|
174
|
+
if (el) el.textContent = (stats.buffer_size || 0).toLocaleString();
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.log('Event stats not available:', err);
|
|
177
|
+
}
|
|
178
|
+
}
|