neoagent 3.0.1-beta.2 → 3.0.1-beta.21
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/.env.example +19 -0
- package/README.md +20 -3
- package/docs/benchmarking.md +102 -0
- package/docs/billing.md +224 -0
- package/docs/configuration.md +22 -0
- package/docs/getting-started.md +10 -8
- package/docs/index.md +1 -0
- package/docs/operations.md +1 -1
- package/flutter_app/lib/main.dart +4 -1
- package/flutter_app/lib/main_account_settings.dart +138 -0
- package/flutter_app/lib/main_app_shell.dart +316 -0
- package/flutter_app/lib/main_billing.dart +1465 -0
- package/flutter_app/lib/main_chat.dart +1594 -224
- package/flutter_app/lib/main_controller.dart +263 -26
- package/flutter_app/lib/main_devices.dart +1 -1
- package/flutter_app/lib/main_install.dart +1147 -0
- package/flutter_app/lib/main_navigation.dart +12 -0
- package/flutter_app/lib/main_operations.dart +134 -9
- package/flutter_app/lib/main_settings.dart +148 -52
- package/flutter_app/lib/main_shared.dart +1 -0
- package/flutter_app/lib/src/backend_client.dart +86 -1
- package/landing/index.html +2 -2
- package/lib/manager.js +184 -66
- package/lib/schema_migrations.js +84 -0
- package/package.json +10 -4
- package/server/admin/access.js +12 -7
- package/server/admin/admin.css +78 -0
- package/server/admin/admin.js +511 -23
- package/server/admin/billing.js +415 -0
- package/server/admin/index.html +120 -13
- package/server/admin/users.js +15 -15
- package/server/db/database.js +13 -21
- package/server/db/sessions_db.js +8 -0
- package/server/http/middleware.js +4 -4
- package/server/http/routes.js +9 -0
- package/server/http/static.js +4 -2
- package/server/middleware/requireBilling.js +12 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +89347 -85449
- package/server/routes/account.js +53 -0
- package/server/routes/admin.js +515 -63
- package/server/routes/agents.js +203 -21
- package/server/routes/billing.js +127 -0
- package/server/routes/billing_webhook.js +43 -0
- package/server/routes/memory.js +1 -4
- package/server/routes/settings.js +1 -2
- package/server/routes/skills.js +1 -1
- package/server/routes/triggers.js +2 -2
- package/server/services/account/erasure.js +263 -0
- package/server/services/account/sessions.js +1 -9
- package/server/services/ai/loop/agent_engine_core.js +42 -0
- package/server/services/ai/loop/blank_recovery.js +36 -0
- package/server/services/ai/loop/completion_judge.js +42 -0
- package/server/services/ai/loop/conversation_loop.js +248 -34
- package/server/services/ai/loop/progress_monitor.js +6 -6
- package/server/services/ai/loopPolicy.js +37 -44
- package/server/services/ai/messagingFallback.js +25 -1
- package/server/services/ai/models.js +18 -0
- package/server/services/ai/preModelCompaction.js +25 -5
- package/server/services/ai/rate_limits.js +28 -4
- package/server/services/ai/systemPrompt.js +23 -5
- package/server/services/ai/taskAnalysis.js +2 -0
- package/server/services/ai/tools.js +231 -20
- package/server/services/android/controller.js +6 -2
- package/server/services/billing/billing_email.js +106 -0
- package/server/services/billing/config.js +17 -0
- package/server/services/billing/plans.js +121 -0
- package/server/services/billing/stripe_client.js +21 -0
- package/server/services/billing/subscriptions.js +462 -0
- package/server/services/billing/trial_guard.js +111 -0
- package/server/services/browser/contentExtractor.js +629 -0
- package/server/services/browser/controller.js +4 -8
- package/server/services/desktop/gateway.js +7 -4
- package/server/services/integrations/google/calendar.js +30 -2
- package/server/services/integrations/secrets.js +7 -4
- package/server/services/manager.js +11 -0
- package/server/services/messaging/automation.js +1 -1
- package/server/services/messaging/telnyx.js +12 -11
- package/server/services/messaging/whatsapp.js +1 -1
- package/server/services/recordings/manager.js +13 -7
- package/server/services/runtime/backends/local-vm.js +40 -22
- package/server/services/runtime/docker-vm-manager.js +157 -266
- package/server/services/runtime/guest_bootstrap.js +17 -5
- package/server/services/runtime/guest_image.js +182 -0
- package/server/services/runtime/manager.js +0 -1
- package/server/services/runtime/validation.js +3 -8
- package/server/services/tasks/runtime.js +103 -15
- package/server/services/tasks/schedule_utils.js +5 -5
- package/server/services/voice/runtimeManager.js +19 -10
- package/server/services/wearable/gateway.js +8 -5
- package/server/services/websocket.js +26 -8
- package/server/services/workspace/manager.js +37 -3
package/server/admin/admin.js
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// ── Shared UI Helpers ──────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
function showToast(message, type = 'info') {
|
|
6
|
+
// type: 'info' | 'success' | 'error'
|
|
7
|
+
let stack = document.getElementById('admin-toast-stack');
|
|
8
|
+
if (!stack) {
|
|
9
|
+
stack = document.createElement('div');
|
|
10
|
+
stack.id = 'admin-toast-stack';
|
|
11
|
+
stack.style.cssText = 'position:fixed;bottom:24px;right:24px;z-index:99999;display:flex;flex-direction:column;gap:10px;pointer-events:none;';
|
|
12
|
+
document.body.appendChild(stack);
|
|
13
|
+
}
|
|
14
|
+
const toast = document.createElement('div');
|
|
15
|
+
const colors = { info: 'var(--info,#3b82f6)', success: 'var(--success,#22c55e)', error: 'var(--danger,#ef4444)' };
|
|
16
|
+
toast.style.cssText = `background:var(--bg-primary,#1a1a1a);border:1px solid ${colors[type] || colors.info};border-radius:10px;padding:12px 18px;color:var(--text,#fff);font-size:13px;box-shadow:0 6px 24px rgba(0,0,0,0.4);pointer-events:auto;max-width:360px;line-height:1.5;opacity:0;transform:translateY(8px);transition:opacity 0.18s,transform 0.18s;`;
|
|
17
|
+
toast.textContent = message;
|
|
18
|
+
stack.appendChild(toast);
|
|
19
|
+
requestAnimationFrame(() => { toast.style.opacity = '1'; toast.style.transform = 'translateY(0)'; });
|
|
20
|
+
const remove = () => {
|
|
21
|
+
toast.style.opacity = '0';
|
|
22
|
+
toast.style.transform = 'translateY(8px)';
|
|
23
|
+
setTimeout(() => toast.remove(), 200);
|
|
24
|
+
};
|
|
25
|
+
toast.onclick = remove;
|
|
26
|
+
setTimeout(remove, 4000);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function showConfirmModal({ title, body, confirmLabel = 'Confirm', confirmClass = 'btn-danger', onConfirm }) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const overlay = document.createElement('div');
|
|
32
|
+
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:99990;backdrop-filter:blur(2px);';
|
|
33
|
+
const modal = document.createElement('div');
|
|
34
|
+
modal.setAttribute('role', 'dialog');
|
|
35
|
+
modal.setAttribute('aria-modal', 'true');
|
|
36
|
+
modal.setAttribute('aria-labelledby', 'admin-modal-title');
|
|
37
|
+
modal.style.cssText = 'width:440px;max-width:calc(100vw - 32px);background:var(--bg-primary,#1a1a1a);border:1px solid var(--border,#2a2a2a);border-radius:12px;padding:28px;box-shadow:0 16px 48px rgba(0,0,0,0.6);';
|
|
38
|
+
modal.innerHTML = `
|
|
39
|
+
<div id="admin-modal-title" style="font-size:16px;font-weight:700;color:var(--text);margin-bottom:12px;"></div>
|
|
40
|
+
<div style="font-size:13px;color:var(--text-muted);line-height:1.6;margin-bottom:24px;">${body}</div>
|
|
41
|
+
<div style="display:flex;gap:10px;justify-content:flex-end;">
|
|
42
|
+
<button class="btn btn-ghost" id="admin-modal-cancel" style="padding:8px 16px;">Cancel</button>
|
|
43
|
+
<button class="btn ${confirmClass}" id="admin-modal-confirm" style="padding:8px 16px;">${confirmLabel}</button>
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
46
|
+
modal.querySelector('#admin-modal-title').textContent = title;
|
|
47
|
+
overlay.appendChild(modal);
|
|
48
|
+
document.body.appendChild(overlay);
|
|
49
|
+
const cancel = () => { overlay.remove(); resolve(false); };
|
|
50
|
+
const confirm = () => { overlay.remove(); resolve(true); if (onConfirm) onConfirm(); };
|
|
51
|
+
modal.querySelector('#admin-modal-cancel').onclick = cancel;
|
|
52
|
+
modal.querySelector('#admin-modal-confirm').onclick = confirm;
|
|
53
|
+
overlay.addEventListener('click', (e) => { if (e.target === overlay) cancel(); });
|
|
54
|
+
document.addEventListener('keydown', function handler(e) {
|
|
55
|
+
if (e.key === 'Escape') { cancel(); document.removeEventListener('keydown', handler); }
|
|
56
|
+
});
|
|
57
|
+
modal.querySelector('#admin-modal-confirm').focus();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
3
61
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
4
62
|
|
|
5
63
|
let currentPage = 'overview';
|
|
@@ -16,29 +74,21 @@ function showPage(page, btn) {
|
|
|
16
74
|
if (btn) btn.classList.add('active');
|
|
17
75
|
currentPage = page;
|
|
18
76
|
|
|
19
|
-
const
|
|
77
|
+
const billingLoader = typeof loadBilling !== 'undefined' ? loadBilling : null;
|
|
78
|
+
const loaders = { overview: loadHealth, logs: loadLogs, issues: loadIssues, updates: loadVersion, config: loadConfig, providers: loadProviders, models: loadModels, analytics: loadAnalytics, users: loadUsers, sql: loadSql, access: loadAccess, billing: billingLoader, integrations: loadIntegrationsConfig };
|
|
20
79
|
loaders[page]?.();
|
|
21
80
|
}
|
|
22
81
|
|
|
23
82
|
// ── Theme ──────────────────────────────────────────────────────────────────
|
|
24
83
|
|
|
25
|
-
function applyTheme(
|
|
26
|
-
const isLight = theme === 'light';
|
|
84
|
+
function applyTheme(isLight) {
|
|
27
85
|
document.documentElement.setAttribute('data-theme', isLight ? 'light' : 'dark');
|
|
28
|
-
const label = document.getElementById('theme-toggle-label');
|
|
29
|
-
if (label) label.textContent = isLight ? 'Dark mode' : 'Light mode';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function toggleTheme() {
|
|
33
|
-
const next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
|
|
34
|
-
try { localStorage.setItem('admin-theme', next); } catch {}
|
|
35
|
-
applyTheme(next);
|
|
36
86
|
}
|
|
37
87
|
|
|
38
88
|
function initTheme() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
applyTheme(
|
|
89
|
+
const mq = window.matchMedia('(prefers-color-scheme: light)');
|
|
90
|
+
applyTheme(mq.matches);
|
|
91
|
+
mq.addEventListener('change', (e) => applyTheme(e.matches));
|
|
42
92
|
}
|
|
43
93
|
|
|
44
94
|
async function signOut() {
|
|
@@ -289,11 +339,11 @@ async function triggerUpdate() {
|
|
|
289
339
|
const res = await api('/admin/api/update', { method: 'POST' });
|
|
290
340
|
if (!res.ok) {
|
|
291
341
|
const body = await res.json().catch(() => ({}));
|
|
292
|
-
|
|
342
|
+
showToast(body.error || 'Failed to start update', 'error');
|
|
293
343
|
}
|
|
294
344
|
setTimeout(loadVersion, 1200);
|
|
295
345
|
} catch (err) {
|
|
296
|
-
if (err.message !== 'unauthorized')
|
|
346
|
+
if (err.message !== 'unauthorized') showToast('Failed to trigger update', 'error');
|
|
297
347
|
}
|
|
298
348
|
}
|
|
299
349
|
|
|
@@ -303,6 +353,10 @@ async function loadConfig() {
|
|
|
303
353
|
const el = document.getElementById('config-content');
|
|
304
354
|
if (!el) return;
|
|
305
355
|
loadEmailConfig();
|
|
356
|
+
loadGeneralConfig();
|
|
357
|
+
loadVmConfig();
|
|
358
|
+
loadScreenRecorderConfig();
|
|
359
|
+
loadBillingSetupConfig();
|
|
306
360
|
try {
|
|
307
361
|
const data = await api('/admin/api/config').then((r) => r.json());
|
|
308
362
|
const cfg = data.config || {};
|
|
@@ -451,12 +505,12 @@ async function persistEmailConfig(payload, btn) {
|
|
|
451
505
|
});
|
|
452
506
|
const body = await res.json().catch(() => ({}));
|
|
453
507
|
if (!res.ok) {
|
|
454
|
-
|
|
508
|
+
showToast(body.error || 'Failed to save email settings', 'error');
|
|
455
509
|
return;
|
|
456
510
|
}
|
|
457
511
|
await loadEmailConfig();
|
|
458
512
|
} catch (err) {
|
|
459
|
-
if (err.message !== 'unauthorized')
|
|
513
|
+
if (err.message !== 'unauthorized') showToast('Network error', 'error');
|
|
460
514
|
} finally {
|
|
461
515
|
if (btn?.isConnected) {
|
|
462
516
|
btn.disabled = false;
|
|
@@ -526,14 +580,14 @@ async function saveProvider(key, btn) {
|
|
|
526
580
|
});
|
|
527
581
|
if (!res.ok) {
|
|
528
582
|
const body = await res.json().catch(() => ({}));
|
|
529
|
-
|
|
583
|
+
showToast(body.error || 'Failed to save', 'error');
|
|
530
584
|
} else {
|
|
531
585
|
btn.textContent = 'Saved!';
|
|
532
586
|
setTimeout(loadProviders, 800);
|
|
533
587
|
return;
|
|
534
588
|
}
|
|
535
589
|
} catch (err) {
|
|
536
|
-
if (err.message !== 'unauthorized')
|
|
590
|
+
if (err.message !== 'unauthorized') showToast('Network error', 'error');
|
|
537
591
|
}
|
|
538
592
|
btn.disabled = false;
|
|
539
593
|
btn.textContent = original;
|
|
@@ -549,7 +603,7 @@ async function clearProvider(key, btn) {
|
|
|
549
603
|
});
|
|
550
604
|
setTimeout(loadProviders, 400);
|
|
551
605
|
} catch (err) {
|
|
552
|
-
if (err.message !== 'unauthorized')
|
|
606
|
+
if (err.message !== 'unauthorized') showToast('Network error', 'error');
|
|
553
607
|
btn.disabled = false;
|
|
554
608
|
}
|
|
555
609
|
}
|
|
@@ -685,19 +739,453 @@ async function saveEnabledModels(btn) {
|
|
|
685
739
|
});
|
|
686
740
|
if (!res.ok) {
|
|
687
741
|
const body = await res.json().catch(() => ({}));
|
|
688
|
-
|
|
742
|
+
showToast(body.error || 'Failed to save', 'error');
|
|
689
743
|
} else {
|
|
690
744
|
btn.textContent = 'Saved!';
|
|
691
745
|
setTimeout(() => { btn.textContent = original; btn.disabled = false; }, 2000);
|
|
692
746
|
return;
|
|
693
747
|
}
|
|
694
748
|
} catch (err) {
|
|
695
|
-
if (err.message !== 'unauthorized')
|
|
749
|
+
if (err.message !== 'unauthorized') showToast('Network error', 'error');
|
|
696
750
|
}
|
|
697
751
|
btn.disabled = false;
|
|
698
752
|
btn.textContent = original;
|
|
699
753
|
}
|
|
700
754
|
|
|
755
|
+
// ── General Config ─────────────────────────────────────────────────────────
|
|
756
|
+
|
|
757
|
+
async function loadGeneralConfig() {
|
|
758
|
+
const el = document.getElementById('general-config-content');
|
|
759
|
+
if (!el) return;
|
|
760
|
+
try {
|
|
761
|
+
const data = await api('/admin/api/config/general').then((r) => r.json());
|
|
762
|
+
const s = data.settings || {};
|
|
763
|
+
el.innerHTML = `
|
|
764
|
+
<form id="general-config-form" onsubmit="saveGeneralConfig(event)">
|
|
765
|
+
<div class="email-settings-grid">
|
|
766
|
+
<div class="field field-wide">
|
|
767
|
+
<label for="gc-public-url">Public URL</label>
|
|
768
|
+
<input type="text" id="gc-public-url" value="${esc(s.publicUrl)}" autocomplete="off" spellcheck="false" placeholder="https://agent.example.com">
|
|
769
|
+
</div>
|
|
770
|
+
<div class="field">
|
|
771
|
+
<label for="gc-profile">Deployment profile</label>
|
|
772
|
+
<select id="gc-profile">
|
|
773
|
+
<option value="prod" ${s.neoagentProfile === 'prod' ? 'selected' : ''}>prod (multi-user / isolated VM)</option>
|
|
774
|
+
<option value="private" ${s.neoagentProfile === 'private' ? 'selected' : ''}>private (single-user)</option>
|
|
775
|
+
</select>
|
|
776
|
+
</div>
|
|
777
|
+
<div class="field">
|
|
778
|
+
<label for="gc-allowed-origins">Allowed CORS origins</label>
|
|
779
|
+
<input type="text" id="gc-allowed-origins" value="${esc(s.allowedOrigins)}" autocomplete="off" spellcheck="false" placeholder="https://a.com,https://b.com">
|
|
780
|
+
</div>
|
|
781
|
+
<div class="field">
|
|
782
|
+
<label for="gc-memory-interval">Memory ingestion interval (ms)</label>
|
|
783
|
+
<input type="number" min="1000" step="1000" id="gc-memory-interval" value="${esc(s.memoryIngestionIntervalMs)}" autocomplete="off">
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
<div class="email-settings-checks" style="margin-top:4px;">
|
|
787
|
+
${emailConfigCheckbox('gc-secure-cookies', 'Secure cookies (required behind HTTPS / TLS proxy)', s.secureCookies)}
|
|
788
|
+
${emailConfigCheckbox('gc-meshtastic', 'Meshtastic enabled', s.meshtasticEnabled)}
|
|
789
|
+
</div>
|
|
790
|
+
<button class="btn btn-primary" type="submit">Save General Settings</button>
|
|
791
|
+
</form>`;
|
|
792
|
+
} catch (err) {
|
|
793
|
+
if (err.message !== 'unauthorized') {
|
|
794
|
+
el.innerHTML = '<div class="empty">Failed to load general configuration</div>';
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
async function saveGeneralConfig(event) {
|
|
800
|
+
event.preventDefault();
|
|
801
|
+
const btn = event.submitter;
|
|
802
|
+
const original = btn?.textContent;
|
|
803
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }
|
|
804
|
+
try {
|
|
805
|
+
const res = await api('/admin/api/config/general', {
|
|
806
|
+
method: 'PUT',
|
|
807
|
+
headers: { 'Content-Type': 'application/json' },
|
|
808
|
+
body: JSON.stringify({
|
|
809
|
+
publicUrl: document.getElementById('gc-public-url')?.value?.trim() || '',
|
|
810
|
+
secureCookies: document.getElementById('gc-secure-cookies')?.checked === true,
|
|
811
|
+
neoagentProfile: document.getElementById('gc-profile')?.value || '',
|
|
812
|
+
allowedOrigins: document.getElementById('gc-allowed-origins')?.value?.trim() || '',
|
|
813
|
+
meshtasticEnabled: document.getElementById('gc-meshtastic')?.checked === true,
|
|
814
|
+
memoryIngestionIntervalMs: parseInt(document.getElementById('gc-memory-interval')?.value || '600000', 10),
|
|
815
|
+
}),
|
|
816
|
+
});
|
|
817
|
+
const body = await res.json().catch(() => ({}));
|
|
818
|
+
if (!res.ok) {
|
|
819
|
+
showToast(body.error || 'Failed to save', 'error');
|
|
820
|
+
} else {
|
|
821
|
+
showToast('General settings saved', 'success');
|
|
822
|
+
}
|
|
823
|
+
} catch (err) {
|
|
824
|
+
if (err.message !== 'unauthorized') showToast('Network error', 'error');
|
|
825
|
+
} finally {
|
|
826
|
+
if (btn?.isConnected) { btn.disabled = false; btn.textContent = original; }
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// ── VM Runtime Config ──────────────────────────────────────────────────────
|
|
831
|
+
|
|
832
|
+
async function loadVmConfig() {
|
|
833
|
+
const el = document.getElementById('vm-config-content');
|
|
834
|
+
if (!el) return;
|
|
835
|
+
try {
|
|
836
|
+
const data = await api('/admin/api/config/vm').then((r) => r.json());
|
|
837
|
+
const s = data.settings || {};
|
|
838
|
+
el.innerHTML = `
|
|
839
|
+
<form id="vm-config-form" onsubmit="saveVmConfig(event)">
|
|
840
|
+
<div class="email-settings-grid">
|
|
841
|
+
<div class="field field-wide">
|
|
842
|
+
<label for="vm-base-image-url">Base image URL</label>
|
|
843
|
+
<input type="text" id="vm-base-image-url" value="${esc(s.vmBaseImageUrl)}" autocomplete="off" spellcheck="false"
|
|
844
|
+
placeholder="https://cloud-images.ubuntu.com/…">
|
|
845
|
+
</div>
|
|
846
|
+
<div class="field field-wide">
|
|
847
|
+
<label for="vm-base-image">Local base image path override</label>
|
|
848
|
+
<input type="text" id="vm-base-image" value="${esc(s.vmBaseImage)}" autocomplete="off" spellcheck="false"
|
|
849
|
+
placeholder="/path/to/base.img (takes precedence over URL)">
|
|
850
|
+
</div>
|
|
851
|
+
<div class="field">
|
|
852
|
+
<label for="vm-memory-mb">Memory (MB)</label>
|
|
853
|
+
<input type="number" min="512" step="256" id="vm-memory-mb" value="${esc(s.vmMemoryMb)}" autocomplete="off">
|
|
854
|
+
</div>
|
|
855
|
+
<div class="field">
|
|
856
|
+
<label for="vm-cpus">vCPUs</label>
|
|
857
|
+
<input type="number" min="1" step="1" id="vm-cpus" value="${esc(s.vmCpus)}" autocomplete="off">
|
|
858
|
+
</div>
|
|
859
|
+
</div>
|
|
860
|
+
<button class="btn btn-primary" type="submit" style="margin-top:8px;">Save VM Settings</button>
|
|
861
|
+
</form>`;
|
|
862
|
+
} catch (err) {
|
|
863
|
+
if (err.message !== 'unauthorized') {
|
|
864
|
+
el.innerHTML = '<div class="empty">Failed to load VM configuration</div>';
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async function saveVmConfig(event) {
|
|
870
|
+
event.preventDefault();
|
|
871
|
+
const btn = event.submitter;
|
|
872
|
+
const original = btn?.textContent;
|
|
873
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }
|
|
874
|
+
try {
|
|
875
|
+
const res = await api('/admin/api/config/vm', {
|
|
876
|
+
method: 'PUT',
|
|
877
|
+
headers: { 'Content-Type': 'application/json' },
|
|
878
|
+
body: JSON.stringify({
|
|
879
|
+
vmBaseImageUrl: document.getElementById('vm-base-image-url')?.value?.trim() || '',
|
|
880
|
+
vmBaseImage: document.getElementById('vm-base-image')?.value?.trim() || '',
|
|
881
|
+
vmMemoryMb: parseInt(document.getElementById('vm-memory-mb')?.value || '4096', 10),
|
|
882
|
+
vmCpus: parseInt(document.getElementById('vm-cpus')?.value || '2', 10),
|
|
883
|
+
}),
|
|
884
|
+
});
|
|
885
|
+
const body = await res.json().catch(() => ({}));
|
|
886
|
+
if (!res.ok) {
|
|
887
|
+
showToast(body.error || 'Failed to save', 'error');
|
|
888
|
+
} else {
|
|
889
|
+
showToast('VM settings saved', 'success');
|
|
890
|
+
}
|
|
891
|
+
} catch (err) {
|
|
892
|
+
if (err.message !== 'unauthorized') showToast('Network error', 'error');
|
|
893
|
+
} finally {
|
|
894
|
+
if (btn?.isConnected) { btn.disabled = false; btn.textContent = original; }
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ── Screen Recorder Config ─────────────────────────────────────────────────
|
|
899
|
+
|
|
900
|
+
async function loadScreenRecorderConfig() {
|
|
901
|
+
const el = document.getElementById('screen-recorder-config-content');
|
|
902
|
+
if (!el) return;
|
|
903
|
+
try {
|
|
904
|
+
const data = await api('/admin/api/config/screen-recorder').then((r) => r.json());
|
|
905
|
+
const s = data.settings || {};
|
|
906
|
+
el.innerHTML = `
|
|
907
|
+
<form id="screen-recorder-config-form" onsubmit="saveScreenRecorderConfig(event)">
|
|
908
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:16px;line-height:1.5;">
|
|
909
|
+
Captures the backend host's macOS desktop. Enable only when the host is the configured user's desktop.
|
|
910
|
+
</div>
|
|
911
|
+
<div class="email-settings-grid">
|
|
912
|
+
<div class="field">
|
|
913
|
+
<label for="sr-user-id">User ID (owner of screen history)</label>
|
|
914
|
+
<input type="text" id="sr-user-id" value="${esc(s.userId)}" autocomplete="off" spellcheck="false"
|
|
915
|
+
placeholder="Required when enabled">
|
|
916
|
+
</div>
|
|
917
|
+
<div class="field">
|
|
918
|
+
<label for="sr-interval-ms">Capture interval (ms)</label>
|
|
919
|
+
<input type="number" min="1000" step="1000" id="sr-interval-ms" value="${esc(s.intervalMs)}" autocomplete="off">
|
|
920
|
+
</div>
|
|
921
|
+
<div class="field">
|
|
922
|
+
<label for="sr-retention-days">Retention (days)</label>
|
|
923
|
+
<input type="number" min="1" step="1" id="sr-retention-days" value="${esc(s.retentionDays)}" autocomplete="off">
|
|
924
|
+
</div>
|
|
925
|
+
</div>
|
|
926
|
+
<div class="email-settings-checks" style="margin-top:4px;">
|
|
927
|
+
${emailConfigCheckbox('sr-enabled', 'Screen recorder enabled', s.enabled)}
|
|
928
|
+
</div>
|
|
929
|
+
<button class="btn btn-primary" type="submit">Save Screen Recorder Settings</button>
|
|
930
|
+
</form>`;
|
|
931
|
+
} catch (err) {
|
|
932
|
+
if (err.message !== 'unauthorized') {
|
|
933
|
+
el.innerHTML = '<div class="empty">Failed to load screen recorder configuration</div>';
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function saveScreenRecorderConfig(event) {
|
|
939
|
+
event.preventDefault();
|
|
940
|
+
const btn = event.submitter;
|
|
941
|
+
const original = btn?.textContent;
|
|
942
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }
|
|
943
|
+
try {
|
|
944
|
+
const res = await api('/admin/api/config/screen-recorder', {
|
|
945
|
+
method: 'PUT',
|
|
946
|
+
headers: { 'Content-Type': 'application/json' },
|
|
947
|
+
body: JSON.stringify({
|
|
948
|
+
enabled: document.getElementById('sr-enabled')?.checked === true,
|
|
949
|
+
userId: document.getElementById('sr-user-id')?.value?.trim() || '',
|
|
950
|
+
intervalMs: parseInt(document.getElementById('sr-interval-ms')?.value || '10000', 10),
|
|
951
|
+
retentionDays: parseInt(document.getElementById('sr-retention-days')?.value || '7', 10),
|
|
952
|
+
}),
|
|
953
|
+
});
|
|
954
|
+
const body = await res.json().catch(() => ({}));
|
|
955
|
+
if (!res.ok) {
|
|
956
|
+
showToast(body.error || 'Failed to save', 'error');
|
|
957
|
+
} else {
|
|
958
|
+
showToast('Screen recorder settings saved', 'success');
|
|
959
|
+
}
|
|
960
|
+
} catch (err) {
|
|
961
|
+
if (err.message !== 'unauthorized') showToast('Network error', 'error');
|
|
962
|
+
} finally {
|
|
963
|
+
if (btn?.isConnected) { btn.disabled = false; btn.textContent = original; }
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// ── Integrations Config ────────────────────────────────────────────────────
|
|
968
|
+
|
|
969
|
+
const INTEGRATION_FIELD_LABELS = {
|
|
970
|
+
clientId: 'Client ID',
|
|
971
|
+
clientSecret: 'Client secret',
|
|
972
|
+
redirectUri: 'Redirect URI',
|
|
973
|
+
tenantId: 'Tenant ID',
|
|
974
|
+
apiKey: 'API key',
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
async function loadIntegrationsConfig() {
|
|
978
|
+
const el = document.getElementById('integrations-config-content');
|
|
979
|
+
const deepEl = document.getElementById('deepgram-config-content');
|
|
980
|
+
if (!el) return;
|
|
981
|
+
try {
|
|
982
|
+
const data = await api('/admin/api/config/integrations').then((r) => r.json());
|
|
983
|
+
const integrations = data.integrations || [];
|
|
984
|
+
|
|
985
|
+
el.innerHTML = integrations.map((integration) => {
|
|
986
|
+
const fields = integration.fields.map((f) => {
|
|
987
|
+
const fieldId = `int-${integration.key}-${f.name}`;
|
|
988
|
+
const label = INTEGRATION_FIELD_LABELS[f.name] || f.name;
|
|
989
|
+
if (f.secret) {
|
|
990
|
+
return `<div class="field">
|
|
991
|
+
<label for="${fieldId}">${esc(label)}</label>
|
|
992
|
+
<input type="password" id="${fieldId}" value="" autocomplete="new-password"
|
|
993
|
+
placeholder="${f.configured ? 'Stored — paste new value to update' : 'Not set'}">
|
|
994
|
+
${f.configured ? `<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">A value is stored. Leave blank to keep it.</div>` : ''}
|
|
995
|
+
</div>`;
|
|
996
|
+
}
|
|
997
|
+
return `<div class="field">
|
|
998
|
+
<label for="${fieldId}">${esc(label)}</label>
|
|
999
|
+
<input type="text" id="${fieldId}" value="${esc(f.value || '')}" autocomplete="off" spellcheck="false">
|
|
1000
|
+
</div>`;
|
|
1001
|
+
}).join('');
|
|
1002
|
+
|
|
1003
|
+
const badge = integration.configured
|
|
1004
|
+
? '<span class="badge badge-ok">configured</span>'
|
|
1005
|
+
: '<span class="badge badge-idle">not set</span>';
|
|
1006
|
+
|
|
1007
|
+
return `<div class="integration-section" data-key="${esc(integration.key)}">
|
|
1008
|
+
<div class="integration-header">
|
|
1009
|
+
<span class="integration-name">${esc(integration.label)}</span>
|
|
1010
|
+
${badge}
|
|
1011
|
+
</div>
|
|
1012
|
+
<div class="email-settings-grid">${fields}</div>
|
|
1013
|
+
</div>`;
|
|
1014
|
+
}).join('');
|
|
1015
|
+
|
|
1016
|
+
if (deepEl) {
|
|
1017
|
+
const dg = data.deepgram || {};
|
|
1018
|
+
deepEl.innerHTML = `
|
|
1019
|
+
<form id="deepgram-config-form" onsubmit="saveDeepgramConfig(event)">
|
|
1020
|
+
<div class="email-settings-grid">
|
|
1021
|
+
<div class="field">
|
|
1022
|
+
<label for="dg-base-url">Base URL</label>
|
|
1023
|
+
<input type="text" id="dg-base-url" value="${esc(dg.baseUrl)}" autocomplete="off" spellcheck="false"
|
|
1024
|
+
placeholder="https://api.deepgram.com">
|
|
1025
|
+
</div>
|
|
1026
|
+
<div class="field">
|
|
1027
|
+
<label for="dg-model">Model</label>
|
|
1028
|
+
<input type="text" id="dg-model" value="${esc(dg.model)}" autocomplete="off" spellcheck="false"
|
|
1029
|
+
placeholder="nova-3">
|
|
1030
|
+
</div>
|
|
1031
|
+
<div class="field">
|
|
1032
|
+
<label for="dg-language">Language</label>
|
|
1033
|
+
<input type="text" id="dg-language" value="${esc(dg.language)}" autocomplete="off" spellcheck="false"
|
|
1034
|
+
placeholder="multi">
|
|
1035
|
+
</div>
|
|
1036
|
+
</div>
|
|
1037
|
+
<button class="btn btn-primary" type="submit" style="margin-top:8px;">Save Deepgram Settings</button>
|
|
1038
|
+
</form>`;
|
|
1039
|
+
}
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
if (err.message !== 'unauthorized') {
|
|
1042
|
+
el.innerHTML = '<div class="empty">Failed to load integrations configuration</div>';
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function saveIntegrationsConfig(btn) {
|
|
1048
|
+
if (btn) { btn.disabled = true; const orig = btn.textContent; btn.textContent = 'Saving…';
|
|
1049
|
+
try {
|
|
1050
|
+
const integrationSections = document.querySelectorAll('.integration-section');
|
|
1051
|
+
const integrations = {};
|
|
1052
|
+
for (const section of integrationSections) {
|
|
1053
|
+
const key = section.dataset.key;
|
|
1054
|
+
integrations[key] = {};
|
|
1055
|
+
for (const input of section.querySelectorAll('input')) {
|
|
1056
|
+
const fieldName = input.id.replace(`int-${key}-`, '');
|
|
1057
|
+
integrations[key][fieldName] = input.value;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
const res = await api('/admin/api/config/integrations', {
|
|
1061
|
+
method: 'PUT',
|
|
1062
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1063
|
+
body: JSON.stringify({ integrations }),
|
|
1064
|
+
});
|
|
1065
|
+
const body = await res.json().catch(() => ({}));
|
|
1066
|
+
if (!res.ok) {
|
|
1067
|
+
showToast(body.error || 'Failed to save', 'error');
|
|
1068
|
+
} else {
|
|
1069
|
+
showToast('Integration settings saved', 'success');
|
|
1070
|
+
setTimeout(loadIntegrationsConfig, 600);
|
|
1071
|
+
}
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
if (err.message !== 'unauthorized') showToast('Network error', 'error');
|
|
1074
|
+
} finally {
|
|
1075
|
+
if (btn.isConnected) { btn.disabled = false; btn.textContent = orig; }
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async function saveDeepgramConfig(event) {
|
|
1081
|
+
event.preventDefault();
|
|
1082
|
+
const btn = event.submitter;
|
|
1083
|
+
const original = btn?.textContent;
|
|
1084
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }
|
|
1085
|
+
try {
|
|
1086
|
+
const res = await api('/admin/api/config/integrations', {
|
|
1087
|
+
method: 'PUT',
|
|
1088
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1089
|
+
body: JSON.stringify({
|
|
1090
|
+
deepgram: {
|
|
1091
|
+
baseUrl: document.getElementById('dg-base-url')?.value?.trim() || '',
|
|
1092
|
+
model: document.getElementById('dg-model')?.value?.trim() || '',
|
|
1093
|
+
language: document.getElementById('dg-language')?.value?.trim() || '',
|
|
1094
|
+
},
|
|
1095
|
+
}),
|
|
1096
|
+
});
|
|
1097
|
+
const body = await res.json().catch(() => ({}));
|
|
1098
|
+
if (!res.ok) {
|
|
1099
|
+
showToast(body.error || 'Failed to save', 'error');
|
|
1100
|
+
} else {
|
|
1101
|
+
showToast('Deepgram settings saved', 'success');
|
|
1102
|
+
}
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
if (err.message !== 'unauthorized') showToast('Network error', 'error');
|
|
1105
|
+
} finally {
|
|
1106
|
+
if (btn?.isConnected) { btn.disabled = false; btn.textContent = original; }
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// ── Billing Setup Config ───────────────────────────────────────────────────
|
|
1111
|
+
|
|
1112
|
+
async function loadBillingSetupConfig() {
|
|
1113
|
+
const el = document.getElementById('billing-setup-content');
|
|
1114
|
+
if (!el) return;
|
|
1115
|
+
try {
|
|
1116
|
+
const data = await api('/admin/api/config/billing-setup').then((r) => r.json());
|
|
1117
|
+
const s = data.settings || {};
|
|
1118
|
+
el.innerHTML = `
|
|
1119
|
+
<form id="billing-setup-form" onsubmit="saveBillingSetupConfig(event)">
|
|
1120
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:16px;line-height:1.5;">
|
|
1121
|
+
Enable billing to expose Stripe-backed subscription flows. Requires a server restart when toggling.
|
|
1122
|
+
</div>
|
|
1123
|
+
<div class="email-settings-grid">
|
|
1124
|
+
<div class="field">
|
|
1125
|
+
<label for="bs-publishable-key">Stripe publishable key</label>
|
|
1126
|
+
<input type="text" id="bs-publishable-key" value="${esc(s.stripePublishableKey)}" autocomplete="off" spellcheck="false"
|
|
1127
|
+
placeholder="pk_test_…">
|
|
1128
|
+
</div>
|
|
1129
|
+
<div class="field">
|
|
1130
|
+
<label for="bs-secret-key">Stripe secret key</label>
|
|
1131
|
+
<input type="password" id="bs-secret-key" value="" autocomplete="new-password"
|
|
1132
|
+
placeholder="${s.stripeSecretKeyConfigured ? s.stripeSecretKeyHint + ' — paste new to update' : 'sk_test_…'}">
|
|
1133
|
+
${s.stripeSecretKeyConfigured ? `<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">Key stored. Leave blank to keep it.</div>` : ''}
|
|
1134
|
+
</div>
|
|
1135
|
+
<div class="field">
|
|
1136
|
+
<label for="bs-webhook-secret">Stripe webhook signing secret</label>
|
|
1137
|
+
<input type="password" id="bs-webhook-secret" value="" autocomplete="new-password"
|
|
1138
|
+
placeholder="${s.stripeWebhookSecretConfigured ? 'Stored — paste new to update' : 'whsec_…'}">
|
|
1139
|
+
${s.stripeWebhookSecretConfigured ? `<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">Secret stored. Leave blank to keep it.</div>` : ''}
|
|
1140
|
+
</div>
|
|
1141
|
+
<div class="field">
|
|
1142
|
+
<label for="bs-trial-days">Free trial period (days)</label>
|
|
1143
|
+
<input type="number" min="0" step="1" id="bs-trial-days" value="${esc(s.trialDays)}" autocomplete="off">
|
|
1144
|
+
</div>
|
|
1145
|
+
</div>
|
|
1146
|
+
<div class="email-settings-checks" style="margin-top:4px;">
|
|
1147
|
+
${emailConfigCheckbox('bs-billing-enabled', 'Billing enabled', s.billingEnabled)}
|
|
1148
|
+
</div>
|
|
1149
|
+
<button class="btn btn-primary" type="submit">Save Billing Configuration</button>
|
|
1150
|
+
</form>`;
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
if (err.message !== 'unauthorized') {
|
|
1153
|
+
el.innerHTML = '<div class="empty">Failed to load billing configuration</div>';
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
async function saveBillingSetupConfig(event) {
|
|
1159
|
+
event.preventDefault();
|
|
1160
|
+
const btn = event.submitter;
|
|
1161
|
+
const original = btn?.textContent;
|
|
1162
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }
|
|
1163
|
+
try {
|
|
1164
|
+
const res = await api('/admin/api/config/billing-setup', {
|
|
1165
|
+
method: 'PUT',
|
|
1166
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1167
|
+
body: JSON.stringify({
|
|
1168
|
+
billingEnabled: document.getElementById('bs-billing-enabled')?.checked === true,
|
|
1169
|
+
stripePublishableKey: document.getElementById('bs-publishable-key')?.value?.trim() || '',
|
|
1170
|
+
stripeSecretKey: document.getElementById('bs-secret-key')?.value || '',
|
|
1171
|
+
stripeWebhookSecret: document.getElementById('bs-webhook-secret')?.value || '',
|
|
1172
|
+
trialDays: parseInt(document.getElementById('bs-trial-days')?.value || '14', 10),
|
|
1173
|
+
}),
|
|
1174
|
+
});
|
|
1175
|
+
const body = await res.json().catch(() => ({}));
|
|
1176
|
+
if (!res.ok) {
|
|
1177
|
+
showToast(body.error || 'Failed to save', 'error');
|
|
1178
|
+
} else {
|
|
1179
|
+
showToast('Billing configuration saved — restart the server to apply changes', 'success');
|
|
1180
|
+
setTimeout(loadBillingSetupConfig, 600);
|
|
1181
|
+
}
|
|
1182
|
+
} catch (err) {
|
|
1183
|
+
if (err.message !== 'unauthorized') showToast('Network error', 'error');
|
|
1184
|
+
} finally {
|
|
1185
|
+
if (btn?.isConnected) { btn.disabled = false; btn.textContent = original; }
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
701
1189
|
// ── Auto-refresh ───────────────────────────────────────────────────────────
|
|
702
1190
|
|
|
703
1191
|
function startPolling() {
|