superlocalmemory 3.4.3 → 3.4.4

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,208 @@
1
+ // Neural Glass — Health Monitor Tab
2
+ // Real-time process health, RSS budget, worker heartbeat (v3.4.3)
3
+ // API: /api/v3/health/processes — returns {processes: {name: {pid, status}}, memory_mb, healthy}
4
+
5
+ (function() {
6
+ 'use strict';
7
+
8
+ var REFRESH_INTERVAL = 10000;
9
+ var refreshTimer = null;
10
+
11
+ window.loadHealthMonitor = function() {
12
+ fetchHealth();
13
+ clearInterval(refreshTimer);
14
+ refreshTimer = setInterval(function() {
15
+ var pane = document.getElementById('health-pane');
16
+ if (pane && pane.classList.contains('active')) fetchHealth();
17
+ }, REFRESH_INTERVAL);
18
+ };
19
+
20
+ function fetchHealth() {
21
+ Promise.all([
22
+ fetch('/api/v3/health/processes').then(function(r) { return r.json(); }).catch(function() { return null; }),
23
+ fetch('/api/v3/consolidation/status').then(function(r) { return r.json(); }).catch(function() { return null; }),
24
+ fetch('/api/stats').then(function(r) { return r.json(); }).catch(function() { return null; })
25
+ ]).then(function(results) {
26
+ var health = results[0];
27
+ var consolidation = results[1];
28
+ var stats = results[2];
29
+
30
+ if (!health) {
31
+ renderHealthOffline();
32
+ return;
33
+ }
34
+
35
+ renderHealthOverview(health, consolidation, stats);
36
+ renderProcessTable(health.processes || {});
37
+ renderBudgetAndInfo(health, consolidation);
38
+ });
39
+ }
40
+
41
+ function renderHealthOverview(health, consolidation, stats) {
42
+ var el = document.getElementById('health-overview');
43
+ if (!el) return;
44
+
45
+ var isHealthy = health.healthy === true;
46
+ var totalRss = health.memory_mb || 0;
47
+ var budget = 4096; // configurable via health monitor
48
+ var usagePct = budget > 0 ? Math.min(100, Math.round((totalRss / budget) * 100)) : 0;
49
+ var barClass = usagePct > 80 ? 'ng-progress-fill-error' : usagePct > 60 ? 'ng-progress-fill-warning' : '';
50
+
51
+ // Extract PID from processes
52
+ var daemonPid = 'N/A';
53
+ var procs = health.processes || {};
54
+ if (procs.parent && procs.parent.pid) daemonPid = procs.parent.pid;
55
+ else if (procs.mcp_server && procs.mcp_server.pid) daemonPid = procs.mcp_server.pid;
56
+
57
+ // Process count
58
+ var procCount = Object.keys(procs).length;
59
+
60
+ // Consolidation info
61
+ var lastConsolidation = 'Never';
62
+ if (consolidation && consolidation.last_run) {
63
+ lastConsolidation = timeAgo(consolidation.last_run);
64
+ }
65
+
66
+ // Stats info
67
+ var factCount = stats ? (stats.total_memories || stats.fact_count || 0) : 0;
68
+
69
+ el.innerHTML =
70
+ '<div class="row g-3 mb-4">' +
71
+ healthCard('Overall',
72
+ statusDotHtml(isHealthy ? 'healthy' : 'degraded') + ' ' + (isHealthy ? 'Healthy' : 'Degraded'),
73
+ 'bi-heart-pulse') +
74
+ healthCard('Daemon PID', daemonPid, 'bi-cpu') +
75
+ healthCard('Memory', totalRss.toFixed(0) + ' MB', 'bi-memory') +
76
+ healthCard('Processes', procCount + ' active', 'bi-diagram-2') +
77
+ '</div>' +
78
+ '<div class="ng-glass" style="padding:16px;margin-bottom:24px">' +
79
+ '<div style="display:flex;justify-content:space-between;margin-bottom:8px">' +
80
+ '<span style="font-size:0.8125rem;font-weight:590">Memory Budget</span>' +
81
+ '<span style="font-size:0.8125rem;color:var(--ng-text-secondary)">' + totalRss.toFixed(0) + ' / ' + budget + ' MB (' + usagePct + '%)</span>' +
82
+ '</div>' +
83
+ '<div class="ng-progress-bar">' +
84
+ '<div class="ng-progress-fill ' + barClass + '" style="width:' + usagePct + '%"></div>' +
85
+ '</div>' +
86
+ '</div>';
87
+ }
88
+
89
+ function renderProcessTable(processes) {
90
+ var el = document.getElementById('health-processes');
91
+ if (!el) return;
92
+
93
+ var keys = Object.keys(processes);
94
+ if (keys.length === 0) {
95
+ el.innerHTML = '<div style="padding:24px;color:var(--ng-text-tertiary);text-align:center">No processes reported</div>';
96
+ return;
97
+ }
98
+
99
+ var html = '<div class="table-responsive"><table class="table table-sm">' +
100
+ '<thead><tr><th>Process</th><th>PID</th><th>Status</th><th>Details</th></tr></thead><tbody>';
101
+
102
+ keys.forEach(function(name) {
103
+ var p = processes[name];
104
+ if (typeof p !== 'object') return;
105
+ var st = p.status || 'unknown';
106
+ var pid = p.pid || 'N/A';
107
+ var details = [];
108
+ if (p.rss_mb) details.push(p.rss_mb.toFixed(1) + ' MB');
109
+ if (p.cpu_percent) details.push('CPU ' + p.cpu_percent.toFixed(1) + '%');
110
+ if (p.request_count) details.push(p.request_count + ' reqs');
111
+ if (p.model) details.push(p.model);
112
+ if (p.workers) details.push(p.workers + ' workers');
113
+
114
+ html += '<tr>' +
115
+ '<td style="font-weight:510">' + escapeHtml(formatProcessName(name)) + '</td>' +
116
+ '<td><code>' + pid + '</code></td>' +
117
+ '<td>' + statusDotHtml(st) + ' ' + capitalize(st) + '</td>' +
118
+ '<td style="color:var(--ng-text-secondary);font-size:0.8125rem">' + (details.join(' · ') || '—') + '</td>' +
119
+ '</tr>';
120
+ });
121
+
122
+ html += '</tbody></table></div>';
123
+ el.innerHTML = html;
124
+ }
125
+
126
+ function renderBudgetAndInfo(health, consolidation) {
127
+ var el = document.getElementById('health-budget-rules');
128
+ if (!el) return;
129
+
130
+ var items = [
131
+ { label: 'Total Memory', value: (health.memory_mb || 0).toFixed(0) + ' MB' },
132
+ { label: 'Healthy', value: health.healthy ? 'Yes' : 'No' },
133
+ { label: 'RSS Budget', value: '4,096 MB' },
134
+ { label: 'Heartbeat', value: '30s interval' },
135
+ { label: 'Worker Recycle', value: 'After 1,000 reqs' }
136
+ ];
137
+
138
+ if (consolidation) {
139
+ items.push({ label: 'Last Consolidation', value: consolidation.last_run ? timeAgo(consolidation.last_run) : 'Never' });
140
+ if (consolidation.last_result) {
141
+ items.push({ label: 'Blocks Compiled', value: String(consolidation.last_result.blocks_compiled || 0) });
142
+ items.push({ label: 'Graph Edges', value: formatNumber(consolidation.last_result.total_edges || 0) });
143
+ }
144
+ items.push({ label: 'Store Counter', value: String(consolidation.store_count_since_last || 0) });
145
+ }
146
+
147
+ var html = '<div style="font-size:0.8125rem">';
148
+ items.forEach(function(item) {
149
+ html += '<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--ng-border-subtle)">' +
150
+ '<span style="color:var(--ng-text-secondary)">' + item.label + '</span>' +
151
+ '<span style="font-weight:590">' + item.value + '</span>' +
152
+ '</div>';
153
+ });
154
+ html += '</div>';
155
+ el.innerHTML = html;
156
+ }
157
+
158
+ function renderHealthOffline() {
159
+ var el = document.getElementById('health-overview');
160
+ if (!el) return;
161
+ el.innerHTML =
162
+ '<div class="row g-3 mb-4">' +
163
+ healthCard('Overall', statusDotHtml('dead') + ' Offline', 'bi-heart-pulse') +
164
+ healthCard('Daemon PID', 'N/A', 'bi-cpu') +
165
+ healthCard('Memory', '0 MB', 'bi-memory') +
166
+ healthCard('Processes', '0', 'bi-diagram-2') +
167
+ '</div>';
168
+ }
169
+
170
+ function healthCard(label, value, icon) {
171
+ return '<div class="col-md-3 col-6">' +
172
+ '<div class="ng-glass" style="padding:16px;text-align:center">' +
173
+ '<i class="bi ' + icon + '" style="font-size:1.25rem;color:var(--ng-accent);display:block;margin-bottom:8px"></i>' +
174
+ '<div class="ng-stat-value" style="font-size:1.5rem">' + value + '</div>' +
175
+ '<div class="ng-stat-label">' + label + '</div>' +
176
+ '</div>' +
177
+ '</div>';
178
+ }
179
+
180
+ function statusDotHtml(st) {
181
+ var c = 'var(--ng-text-quaternary)';
182
+ if (st === 'healthy' || st === 'active' || st === 'running') c = 'var(--ng-status-success)';
183
+ else if (st === 'degraded' || st === 'stale') c = 'var(--ng-status-warning)';
184
+ else if (st === 'dead' || st === 'error' || st === 'critical') c = 'var(--ng-status-error)';
185
+ return '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + c + ';box-shadow:0 0 6px ' + c + '"></span>';
186
+ }
187
+
188
+ function formatProcessName(name) {
189
+ return name.replace(/_/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
190
+ }
191
+
192
+ function capitalize(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
193
+ function escapeHtml(s) { var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
194
+ function formatNumber(n) { return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); }
195
+ function timeAgo(iso) {
196
+ if (!iso) return 'N/A';
197
+ var d = (Date.now() - new Date(iso).getTime()) / 1000;
198
+ if (d < 0) d = 0;
199
+ if (d < 60) return Math.floor(d) + 's ago';
200
+ if (d < 3600) return Math.floor(d / 60) + 'm ago';
201
+ if (d < 86400) return Math.floor(d / 3600) + 'h ago';
202
+ return Math.floor(d / 86400) + 'd ago';
203
+ }
204
+
205
+ document.addEventListener('visibilitychange', function() {
206
+ if (document.hidden) clearInterval(refreshTimer);
207
+ });
208
+ })();
@@ -0,0 +1,203 @@
1
+ // Neural Glass — Ingestion Status Tab (v3.4.4)
2
+ // Full configuration UI for non-technical users
3
+ // API: /api/adapters (GET, POST enable/disable/start/stop)
4
+
5
+ (function() {
6
+ 'use strict';
7
+
8
+ var ADAPTER_INFO = {
9
+ gmail: {
10
+ icon: 'bi-envelope-fill',
11
+ title: 'Email (Gmail)',
12
+ description: 'Automatically ingest your emails into memory. SLM extracts key facts, decisions, and action items from your inbox.',
13
+ howItWorks: 'Connects to Gmail via Google OAuth. Only reads — never sends or deletes emails. New emails are processed as they arrive.',
14
+ setup: 'Requires a Google Cloud project with Gmail API enabled. SLM will guide you through OAuth setup.',
15
+ privacy: 'All processing happens locally on your machine. Email content never leaves your device.',
16
+ color: '#ea4335'
17
+ },
18
+ calendar: {
19
+ icon: 'bi-calendar-event-fill',
20
+ title: 'Calendar Events',
21
+ description: 'Remember all your meetings, deadlines, and events. SLM builds a timeline of your schedule and links events to related memories.',
22
+ howItWorks: 'Syncs with Google Calendar via OAuth. Polls for new events hourly and processes updates in real-time via webhooks.',
23
+ setup: 'Requires Google Calendar API access. Uses the same Google Cloud project as Gmail.',
24
+ privacy: 'Event data stays local. Only event titles, times, and descriptions are stored — not attendee details.',
25
+ color: '#4285f4'
26
+ },
27
+ transcript: {
28
+ icon: 'bi-mic-fill',
29
+ title: 'Meeting Transcripts',
30
+ description: 'Turn meeting transcripts into searchable memory. Extract decisions, action items, and key discussions from recorded meetings.',
31
+ howItWorks: 'Watches a folder for .srt, .vtt, or .txt transcript files. Processes new files automatically when they appear.',
32
+ setup: 'Point SLM to your transcripts folder (e.g., where Otter.ai or Circleback saves files).',
33
+ privacy: 'Transcripts are processed locally. Speaker names and content stay on your machine.',
34
+ color: '#34a853'
35
+ }
36
+ };
37
+
38
+ window.loadIngestionStatus = function() {
39
+ fetchAdapters();
40
+ };
41
+
42
+ function fetchAdapters() {
43
+ fetch('/api/adapters')
44
+ .then(function(r) { return r.json(); })
45
+ .then(function(data) {
46
+ renderIngestionTab(data.adapters || []);
47
+ })
48
+ .catch(function() {
49
+ renderIngestionTab([]);
50
+ });
51
+ }
52
+
53
+ function renderIngestionTab(adapters) {
54
+ // Build adapter map for easy lookup
55
+ var adapterMap = {};
56
+ adapters.forEach(function(a) { adapterMap[a.name] = a; });
57
+
58
+ var el = document.getElementById('ingestion-overview');
59
+ if (!el) return;
60
+
61
+ // Count stats
62
+ var enabledCount = adapters.filter(function(a) { return a.enabled; }).length;
63
+ var runningCount = adapters.filter(function(a) { return a.running; }).length;
64
+
65
+ // Overview cards
66
+ el.innerHTML =
67
+ '<div class="row g-3 mb-4">' +
68
+ overviewCard('Available', adapters.length + ' adapters', 'bi-plug') +
69
+ overviewCard('Enabled', enabledCount + ' of ' + adapters.length, 'bi-toggle-on') +
70
+ overviewCard('Running', runningCount + ' active', 'bi-play-circle') +
71
+ overviewCard('Privacy', 'All local', 'bi-shield-lock') +
72
+ '</div>' +
73
+ '<div style="background:var(--ng-accent-muted);border:1px solid rgba(124,106,239,0.2);border-radius:var(--ng-radius-md);padding:12px 16px;margin-bottom:24px;font-size:0.8125rem">' +
74
+ '<strong>How Ingestion Works:</strong> Enable an adapter below → configure it → start it. ' +
75
+ 'SLM will automatically process new items and add them to your memory. ' +
76
+ 'All data stays on your machine. Disable anytime.' +
77
+ '</div>';
78
+
79
+ // Adapter cards
80
+ var cardsEl = document.getElementById('ingestion-adapters');
81
+ if (!cardsEl) return;
82
+
83
+ var html = '';
84
+ ['gmail', 'calendar', 'transcript'].forEach(function(name) {
85
+ var info = ADAPTER_INFO[name];
86
+ var adapter = adapterMap[name] || { name: name, enabled: false, running: false };
87
+ html += renderAdapterCard(adapter, info);
88
+ });
89
+ cardsEl.innerHTML = html;
90
+
91
+ // Ingestion log section
92
+ var logEl = document.getElementById('ingestion-log');
93
+ if (logEl) {
94
+ if (runningCount === 0 && enabledCount === 0) {
95
+ logEl.innerHTML =
96
+ '<div style="text-align:center;padding:20px;color:var(--ng-text-tertiary);font-size:0.8125rem">' +
97
+ 'No adapters active. Enable one above to start building your memory from external sources.' +
98
+ '</div>';
99
+ } else {
100
+ logEl.innerHTML =
101
+ '<div style="text-align:center;padding:20px;color:var(--ng-text-tertiary);font-size:0.8125rem">' +
102
+ 'Ingestion log will show here as items are processed.' +
103
+ '</div>';
104
+ }
105
+ }
106
+ }
107
+
108
+ function renderAdapterCard(adapter, info) {
109
+ var isEnabled = adapter.enabled;
110
+ var isRunning = adapter.running;
111
+ var statusText = isRunning ? 'Running' : (isEnabled ? 'Enabled (Stopped)' : 'Disabled');
112
+ var statusClass = isRunning ? 'ng-badge-success' : (isEnabled ? 'ng-badge-warning' : 'ng-badge-neutral');
113
+
114
+ return '<div class="ng-glass" style="padding:24px;margin-bottom:16px">' +
115
+ // Header
116
+ '<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px">' +
117
+ '<div style="display:flex;align-items:center;gap:12px">' +
118
+ '<div style="width:48px;height:48px;border-radius:var(--ng-radius-lg);background:' + info.color + '20;display:flex;align-items:center;justify-content:center">' +
119
+ '<i class="bi ' + info.icon + '" style="font-size:1.5rem;color:' + info.color + '"></i>' +
120
+ '</div>' +
121
+ '<div>' +
122
+ '<div style="font-size:1.125rem;font-weight:590">' + info.title + '</div>' +
123
+ '<span class="ng-badge ' + statusClass + '">' + statusText + '</span>' +
124
+ '</div>' +
125
+ '</div>' +
126
+ // Action buttons
127
+ '<div style="display:flex;gap:8px">' +
128
+ (isEnabled ?
129
+ (isRunning ?
130
+ '<button class="ng-btn" onclick="adapterAction(\'' + adapter.name + '\',\'stop\')">' +
131
+ '<i class="bi bi-stop-circle"></i> Stop</button>' :
132
+ '<button class="ng-btn ng-btn-accent" onclick="adapterAction(\'' + adapter.name + '\',\'start\')">' +
133
+ '<i class="bi bi-play-circle"></i> Start</button>'
134
+ ) +
135
+ '<button class="ng-btn" onclick="adapterAction(\'' + adapter.name + '\',\'disable\')" style="color:var(--ng-status-error)">' +
136
+ '<i class="bi bi-x-circle"></i> Disable</button>' :
137
+ '<button class="ng-btn ng-btn-accent" onclick="adapterAction(\'' + adapter.name + '\',\'enable\')">' +
138
+ '<i class="bi bi-power"></i> Enable</button>'
139
+ ) +
140
+ '</div>' +
141
+ '</div>' +
142
+ // Description
143
+ '<p style="color:var(--ng-text-secondary);font-size:0.875rem;margin-bottom:12px">' + info.description + '</p>' +
144
+ // Details grid
145
+ '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;font-size:0.8125rem">' +
146
+ detailBox('How it works', info.howItWorks, 'bi-gear') +
147
+ detailBox('Setup', info.setup, 'bi-tools') +
148
+ detailBox('Privacy', info.privacy, 'bi-shield-check') +
149
+ '</div>' +
150
+ // Running details
151
+ (isRunning && adapter.pid ?
152
+ '<div style="margin-top:12px;padding:8px 12px;background:var(--ng-status-success-bg);border-radius:var(--ng-radius-sm);font-size:0.8125rem">' +
153
+ '<i class="bi bi-check-circle" style="color:var(--ng-status-success)"></i> ' +
154
+ 'Running as PID ' + adapter.pid +
155
+ '</div>' : '') +
156
+ '</div>';
157
+ }
158
+
159
+ function detailBox(title, text, icon) {
160
+ return '<div style="padding:10px;background:var(--ng-bg-glass);border-radius:var(--ng-radius-sm);border:1px solid var(--ng-border-subtle)">' +
161
+ '<div style="font-weight:590;margin-bottom:4px;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.04em;color:var(--ng-text-tertiary)">' +
162
+ '<i class="bi ' + icon + '"></i> ' + title + '</div>' +
163
+ '<div style="color:var(--ng-text-secondary)">' + text + '</div>' +
164
+ '</div>';
165
+ }
166
+
167
+ // Adapter actions (called from buttons)
168
+ window.adapterAction = function(name, action) {
169
+ var btn = event.target.closest('button');
170
+ if (btn) {
171
+ btn.disabled = true;
172
+ btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Working...';
173
+ }
174
+
175
+ fetch('/api/adapters/' + action, {
176
+ method: 'POST',
177
+ headers: { 'Content-Type': 'application/json' },
178
+ body: JSON.stringify({ name: name })
179
+ })
180
+ .then(function(r) { return r.json(); })
181
+ .then(function(data) {
182
+ if (data.ok === false) {
183
+ alert(data.error || data.message || 'Action failed');
184
+ }
185
+ // Refresh the tab
186
+ fetchAdapters();
187
+ })
188
+ .catch(function(err) {
189
+ alert('Error: ' + err.message);
190
+ fetchAdapters();
191
+ });
192
+ };
193
+
194
+ function overviewCard(label, value, icon) {
195
+ return '<div class="col-md-3 col-6">' +
196
+ '<div class="ng-glass" style="padding:16px;text-align:center">' +
197
+ '<i class="bi ' + icon + '" style="font-size:1.25rem;color:var(--ng-accent);display:block;margin-bottom:8px"></i>' +
198
+ '<div class="ng-stat-value" style="font-size:1.5rem">' + value + '</div>' +
199
+ '<div class="ng-stat-label">' + label + '</div>' +
200
+ '</div>' +
201
+ '</div>';
202
+ }
203
+ })();