superlocalmemory 3.4.3 → 3.4.5

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.
@@ -0,0 +1,311 @@
1
+ // Neural Glass — Mesh Peers Tab
2
+ // Real-time view of SLM Mesh P2P network (v3.4.3 Python broker)
3
+ // API: /mesh/peers, /mesh/status, /mesh/events, /mesh/state
4
+
5
+ (function() {
6
+ 'use strict';
7
+
8
+ var REFRESH_INTERVAL = 5000; // 5 seconds
9
+ var refreshTimer = null;
10
+
11
+ window.loadMeshPeers = function() {
12
+ fetchMeshStatus();
13
+ fetchMeshPeers();
14
+ fetchMeshEvents();
15
+ fetchMeshState();
16
+
17
+ // Auto-refresh while tab is active
18
+ clearInterval(refreshTimer);
19
+ refreshTimer = setInterval(function() {
20
+ var pane = document.getElementById('mesh-pane');
21
+ if (pane && pane.classList.contains('active')) {
22
+ fetchMeshStatus();
23
+ fetchMeshPeers();
24
+ }
25
+ }, REFRESH_INTERVAL);
26
+ };
27
+
28
+ // Try BOTH brokers: daemon (port 8765 /mesh/*) AND standalone slm-mesh (port 7899 /*)
29
+ var STANDALONE_PORT = null; // Discovered from port file or default 7899
30
+
31
+ function fetchMeshStatus() {
32
+ // Try daemon broker first, then standalone
33
+ Promise.all([
34
+ fetch('/mesh/status').then(function(r) { return r.json(); }).catch(function() { return null; }),
35
+ fetchStandaloneBroker('/health')
36
+ ]).then(function(results) {
37
+ var daemon = results[0];
38
+ var standalone = results[1];
39
+ renderMeshStatus(daemon, standalone);
40
+ });
41
+ }
42
+
43
+ function fetchStandaloneBroker(path) {
44
+ // Try port file first, then default 7899
45
+ var ports = [7899];
46
+ return fetch('http://127.0.0.1:' + ports[0] + path, { signal: AbortSignal.timeout(2000) })
47
+ .then(function(r) { STANDALONE_PORT = ports[0]; return r.json(); })
48
+ .catch(function() { return null; });
49
+ }
50
+
51
+ function fetchMeshPeers() {
52
+ // Fetch from BOTH brokers and merge
53
+ Promise.all([
54
+ fetch('/mesh/peers').then(function(r) { return r.json(); }).catch(function() { return { peers: [] }; }),
55
+ fetchStandaloneBroker('/peers')
56
+ ]).then(function(results) {
57
+ var daemonPeers = (results[0] && results[0].peers) || [];
58
+ var standalonePeers = (results[1] && (results[1].peers || results[1])) || [];
59
+ if (!Array.isArray(standalonePeers)) standalonePeers = [];
60
+ // Merge, dedup by peer_id
61
+ var seen = {};
62
+ var allPeers = [];
63
+ daemonPeers.concat(standalonePeers).forEach(function(p) {
64
+ var id = p.peer_id || p.id || JSON.stringify(p);
65
+ if (!seen[id]) { seen[id] = true; allPeers.push(p); }
66
+ });
67
+ renderMeshPeers(allPeers);
68
+ }).catch(function() {
69
+ document.getElementById('mesh-peers-list').innerHTML =
70
+ '<div class="text-center" style="padding:24px;color:var(--ng-text-tertiary)">' +
71
+ '<i class="bi bi-wifi-off" style="font-size:2rem;display:block;margin-bottom:8px"></i>' +
72
+ 'Mesh broker not reachable' +
73
+ '</div>';
74
+ });
75
+ }
76
+
77
+ function fetchMeshEvents() {
78
+ fetch('/mesh/events').then(function(r) { return r.json(); }).then(function(data) {
79
+ renderMeshEvents(data.events || data || []);
80
+ }).catch(function() {});
81
+ }
82
+
83
+ function fetchMeshState() {
84
+ fetch('/mesh/state').then(function(r) { return r.json(); }).then(function(data) {
85
+ renderMeshState(data.state || data || {});
86
+ }).catch(function() {});
87
+ }
88
+
89
+ function renderMeshStatus(daemonData, standaloneData) {
90
+ var el = document.getElementById('mesh-status-cards');
91
+ if (!el) return;
92
+
93
+ // Daemon broker (Python, integrated)
94
+ var daemonUp = daemonData && daemonData.broker_up === true;
95
+ var daemonUptime = daemonData ? (daemonData.uptime_s || 0) : 0;
96
+ var daemonPeers = daemonData ? (daemonData.peer_count || 0) : 0;
97
+
98
+ // Standalone broker (TypeScript, slm-mesh npm)
99
+ var standaloneUp = standaloneData && standaloneData.status === 'ok';
100
+ var standaloneUptime = standaloneData ? (standaloneData.uptime || 0) : 0;
101
+ var standaloneVersion = standaloneData ? (standaloneData.version || '') : '';
102
+
103
+ var anyUp = daemonUp || standaloneUp;
104
+ var statusText = anyUp ? 'Active' : 'Offline';
105
+ var statusKey = anyUp ? 'active' : 'dead';
106
+ var bestUptime = Math.max(daemonUptime, standaloneUptime);
107
+
108
+ // Combined info
109
+ var brokerInfo = [];
110
+ if (daemonUp) brokerInfo.push('Daemon (Python)');
111
+ if (standaloneUp) brokerInfo.push('slm-mesh ' + standaloneVersion);
112
+
113
+ el.innerHTML =
114
+ '<div class="row g-3">' +
115
+ statusCard('Broker', statusDot(statusKey) + ' ' + statusText, 'bi-wifi') +
116
+ statusCard('Peers', daemonPeers, 'bi-people') +
117
+ statusCard('Uptime', formatUptime(bestUptime), 'bi-clock') +
118
+ statusCard('Brokers', brokerInfo.length, 'bi-hdd-stack') +
119
+ '</div>' +
120
+ '<div style="font-size:0.75rem;color:var(--ng-text-quaternary);margin-top:8px;text-align:center">' +
121
+ (brokerInfo.length > 0 ? 'Running: ' + brokerInfo.join(' + ') : 'No brokers detected') +
122
+ (standaloneUp ? ' (port 7899)' : '') +
123
+ ' · Peers register via <code>mesh_summary</code> MCP tool and expire after 60s without heartbeat' +
124
+ '</div>';
125
+ }
126
+
127
+ function renderMeshStatusOffline() {
128
+ var el = document.getElementById('mesh-status-cards');
129
+ if (!el) return;
130
+ el.innerHTML =
131
+ '<div class="row g-3">' +
132
+ statusCard('Status', statusDot('dead') + ' Offline', 'bi-wifi-off') +
133
+ statusCard('Peers', '0', 'bi-people') +
134
+ statusCard('Messages', '0', 'bi-chat-dots') +
135
+ statusCard('State Keys', '0', 'bi-database') +
136
+ '</div>';
137
+ }
138
+
139
+ function statusCard(label, value, icon) {
140
+ return '<div class="col-md-3 col-6">' +
141
+ '<div class="ng-glass" style="padding:16px;text-align:center">' +
142
+ '<i class="bi ' + icon + '" style="font-size:1.25rem;color:var(--ng-accent);display:block;margin-bottom:8px"></i>' +
143
+ '<div class="ng-stat-value" style="font-size:1.5rem">' + value + '</div>' +
144
+ '<div class="ng-stat-label">' + label + '</div>' +
145
+ '</div>' +
146
+ '</div>';
147
+ }
148
+
149
+ function statusDot(status) {
150
+ var color = 'var(--ng-text-quaternary)';
151
+ if (status === 'active' || status === 'running' || status === 'ok') color = 'var(--ng-status-success)';
152
+ else if (status === 'stale') color = 'var(--ng-status-warning)';
153
+ else if (status === 'dead' || status === 'error') color = 'var(--ng-status-error)';
154
+ return '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + color + ';box-shadow:0 0 6px ' + color + ';margin-right:4px"></span>';
155
+ }
156
+
157
+ function renderMeshPeers(peers) {
158
+ var el = document.getElementById('mesh-peers-list');
159
+ if (!el) return;
160
+
161
+ if (!peers || peers.length === 0) {
162
+ el.innerHTML =
163
+ '<div class="text-center" style="padding:32px;color:var(--ng-text-tertiary)">' +
164
+ '<i class="bi bi-people" style="font-size:2.5rem;display:block;margin-bottom:12px;opacity:0.3"></i>' +
165
+ '<div style="font-size:0.9375rem;margin-bottom:4px">No peers connected</div>' +
166
+ '<div style="font-size:0.8125rem">Start another Claude Code session to see it appear here.<br>' +
167
+ 'Peers register via <code>mesh_summary</code> MCP tool.</div>' +
168
+ '</div>';
169
+ return;
170
+ }
171
+
172
+ var html = '<div class="row g-3">';
173
+ peers.forEach(function(peer) {
174
+ var st = peer.status || 'unknown';
175
+ var ago = timeAgo(peer.last_heartbeat);
176
+ html +=
177
+ '<div class="col-md-6 col-lg-4">' +
178
+ '<div class="ng-glass" style="padding:16px;transition:border-color 150ms">' +
179
+ '<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px">' +
180
+ '<div>' +
181
+ '<div style="font-weight:590;color:var(--ng-text-primary);font-size:0.9375rem">' +
182
+ escapeHtml(peer.session_id || peer.peer_id || 'Unknown') +
183
+ '</div>' +
184
+ '<div style="font-size:0.75rem;color:var(--ng-text-tertiary)">' +
185
+ escapeHtml(peer.peer_id || '') +
186
+ '</div>' +
187
+ '</div>' +
188
+ '<span class="ng-badge ng-badge-' + statusBadgeClass(st) + '">' +
189
+ statusDot(st) + ' ' + capitalize(st) +
190
+ '</span>' +
191
+ '</div>' +
192
+ '<div style="font-size:0.8125rem;color:var(--ng-text-secondary);margin-bottom:8px;min-height:36px;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical">' +
193
+ escapeHtml(peer.summary || 'No summary provided') +
194
+ '</div>' +
195
+ '<div style="display:flex;justify-content:space-between;font-size:0.75rem;color:var(--ng-text-quaternary)">' +
196
+ '<span><i class="bi bi-geo-alt"></i> ' + escapeHtml((peer.host || '127.0.0.1') + ':' + (peer.port || '?')) + '</span>' +
197
+ '<span><i class="bi bi-clock"></i> ' + ago + '</span>' +
198
+ '</div>' +
199
+ '</div>' +
200
+ '</div>';
201
+ });
202
+ html += '</div>';
203
+ el.innerHTML = html;
204
+ }
205
+
206
+ function renderMeshEvents(events) {
207
+ var el = document.getElementById('mesh-events-list');
208
+ if (!el) return;
209
+
210
+ if (!events || events.length === 0) {
211
+ el.innerHTML = '<div style="padding:16px;color:var(--ng-text-tertiary);text-align:center;font-size:0.8125rem">No events yet</div>';
212
+ return;
213
+ }
214
+
215
+ var html = '<div style="max-height:300px;overflow-y:auto;font-size:0.8125rem">';
216
+ events.slice(-20).reverse().forEach(function(ev) {
217
+ html +=
218
+ '<div style="padding:8px 12px;border-bottom:1px solid var(--ng-border-subtle);display:flex;gap:12px;align-items:flex-start">' +
219
+ '<span style="color:var(--ng-text-quaternary);font-size:0.75rem;white-space:nowrap;min-width:70px">' +
220
+ formatTime(ev.created_at || ev.timestamp) +
221
+ '</span>' +
222
+ '<span style="color:var(--ng-text-secondary)">' + escapeHtml(ev.content || ev.event || JSON.stringify(ev)) + '</span>' +
223
+ '</div>';
224
+ });
225
+ html += '</div>';
226
+ el.innerHTML = html;
227
+ }
228
+
229
+ function renderMeshState(state) {
230
+ var el = document.getElementById('mesh-state-list');
231
+ if (!el) return;
232
+
233
+ var keys = Object.keys(state);
234
+ if (keys.length === 0) {
235
+ el.innerHTML = '<div style="padding:16px;color:var(--ng-text-tertiary);text-align:center;font-size:0.8125rem">No shared state</div>';
236
+ return;
237
+ }
238
+
239
+ var html = '<div style="font-size:0.8125rem">';
240
+ keys.forEach(function(key) {
241
+ var entry = state[key];
242
+ var val = typeof entry === 'object' ? (entry.value || JSON.stringify(entry)) : entry;
243
+ var setBy = typeof entry === 'object' ? (entry.set_by || '') : '';
244
+ html +=
245
+ '<div style="padding:8px 12px;border-bottom:1px solid var(--ng-border-subtle);display:flex;justify-content:space-between;align-items:center">' +
246
+ '<div>' +
247
+ '<code style="color:var(--ng-accent)">' + escapeHtml(key) + '</code>' +
248
+ (setBy ? '<span style="color:var(--ng-text-quaternary);font-size:0.75rem;margin-left:8px">by ' + escapeHtml(setBy) + '</span>' : '') +
249
+ '</div>' +
250
+ '<div style="color:var(--ng-text-secondary);max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' +
251
+ escapeHtml(String(val).substring(0, 100)) +
252
+ '</div>' +
253
+ '</div>';
254
+ });
255
+ html += '</div>';
256
+ el.innerHTML = html;
257
+ }
258
+
259
+ // ── Helpers ────────────────────────────────────────────────
260
+
261
+ function escapeHtml(str) {
262
+ var div = document.createElement('div');
263
+ div.textContent = str || '';
264
+ return div.innerHTML;
265
+ }
266
+
267
+ function capitalize(s) {
268
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
269
+ }
270
+
271
+ function formatUptime(sec) {
272
+ if (!sec || sec <= 0) return 'N/A';
273
+ var h = Math.floor(sec / 3600);
274
+ var m = Math.floor((sec % 3600) / 60);
275
+ if (h > 24) return Math.floor(h / 24) + 'd ' + (h % 24) + 'h';
276
+ if (h > 0) return h + 'h ' + m + 'm';
277
+ return m + 'm';
278
+ }
279
+
280
+ function statusBadgeClass(status) {
281
+ if (status === 'active' || status === 'running') return 'success';
282
+ if (status === 'stale') return 'warning';
283
+ if (status === 'dead' || status === 'error') return 'error';
284
+ return 'neutral';
285
+ }
286
+
287
+ function timeAgo(isoStr) {
288
+ if (!isoStr) return 'unknown';
289
+ var diff = (Date.now() - new Date(isoStr).getTime()) / 1000;
290
+ if (diff < 60) return Math.floor(diff) + 's ago';
291
+ if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
292
+ if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
293
+ return Math.floor(diff / 86400) + 'd ago';
294
+ }
295
+
296
+ function formatTime(isoStr) {
297
+ if (!isoStr) return '';
298
+ try {
299
+ var d = new Date(isoStr);
300
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
301
+ } catch(e) { return ''; }
302
+ }
303
+
304
+ // Cleanup on tab switch
305
+ document.addEventListener('visibilitychange', function() {
306
+ if (document.hidden) {
307
+ clearInterval(refreshTimer);
308
+ }
309
+ });
310
+
311
+ })();