superlocalmemory 3.4.1 → 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.
- package/README.md +9 -12
- package/package.json +1 -1
- package/pyproject.toml +11 -2
- package/scripts/postinstall.js +26 -7
- package/src/superlocalmemory/cli/commands.py +71 -60
- package/src/superlocalmemory/cli/daemon.py +184 -64
- package/src/superlocalmemory/cli/main.py +25 -2
- package/src/superlocalmemory/cli/service_installer.py +367 -0
- package/src/superlocalmemory/cli/setup_wizard.py +150 -9
- package/src/superlocalmemory/core/config.py +28 -0
- package/src/superlocalmemory/core/consolidation_engine.py +38 -1
- package/src/superlocalmemory/core/engine.py +9 -0
- package/src/superlocalmemory/core/health_monitor.py +313 -0
- package/src/superlocalmemory/core/reranker_worker.py +19 -5
- package/src/superlocalmemory/ingestion/__init__.py +13 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +234 -0
- package/src/superlocalmemory/ingestion/base_adapter.py +177 -0
- package/src/superlocalmemory/ingestion/calendar_adapter.py +340 -0
- package/src/superlocalmemory/ingestion/credentials.py +118 -0
- package/src/superlocalmemory/ingestion/gmail_adapter.py +369 -0
- package/src/superlocalmemory/ingestion/parsers.py +100 -0
- package/src/superlocalmemory/ingestion/transcript_adapter.py +156 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +47 -1
- package/src/superlocalmemory/learning/entity_compiler.py +377 -0
- package/src/superlocalmemory/mcp/server.py +32 -3
- package/src/superlocalmemory/mcp/tools_mesh.py +249 -0
- package/src/superlocalmemory/mesh/__init__.py +12 -0
- package/src/superlocalmemory/mesh/broker.py +344 -0
- package/src/superlocalmemory/retrieval/entity_channel.py +12 -6
- package/src/superlocalmemory/server/api.py +6 -7
- package/src/superlocalmemory/server/routes/adapters.py +63 -0
- package/src/superlocalmemory/server/routes/entity.py +151 -0
- package/src/superlocalmemory/server/routes/ingest.py +110 -0
- package/src/superlocalmemory/server/routes/mesh.py +186 -0
- package/src/superlocalmemory/server/unified_daemon.py +693 -0
- package/src/superlocalmemory/storage/schema_v343.py +229 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +1588 -0
- package/src/superlocalmemory/ui/index.html +134 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +28 -1
- package/src/superlocalmemory/ui/js/ng-entities.js +272 -0
- package/src/superlocalmemory/ui/js/ng-health.js +208 -0
- package/src/superlocalmemory/ui/js/ng-ingestion.js +203 -0
- package/src/superlocalmemory/ui/js/ng-mesh.js +311 -0
- package/src/superlocalmemory/ui/js/ng-shell.js +471 -0
- package/src/superlocalmemory.egg-info/PKG-INFO +18 -14
- package/src/superlocalmemory.egg-info/SOURCES.txt +26 -0
- package/src/superlocalmemory.egg-info/requires.txt +9 -1
|
@@ -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
|
+
})();
|