neoagent 3.0.1-beta.8 → 3.1.0
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 +2 -10
- package/README.md +12 -3
- package/docs/automation.md +37 -0
- package/docs/benchmarking.md +102 -0
- package/docs/billing.md +34 -8
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +10 -8
- package/docs/operations.md +1 -1
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +138 -0
- package/flutter_app/lib/main_app_shell.dart +38 -13
- package/flutter_app/lib/main_billing.dart +1465 -0
- package/flutter_app/lib/main_chat.dart +1612 -214
- package/flutter_app/lib/main_controller.dart +398 -26
- package/flutter_app/lib/main_devices.dart +293 -207
- package/flutter_app/lib/main_models.dart +142 -0
- package/flutter_app/lib/main_navigation.dart +19 -1
- package/flutter_app/lib/main_operations.dart +288 -9
- package/flutter_app/lib/main_settings.dart +510 -300
- package/flutter_app/lib/main_shared.dart +2 -0
- package/flutter_app/lib/main_timeline.dart +1378 -0
- package/flutter_app/lib/src/backend_client.dart +149 -19
- package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
- package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
- package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
- package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
- package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
- package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
- package/flutter_app/lib/src/recording_payloads.dart +9 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
- package/flutter_app/windows/runner/flutter_window.cpp +75 -0
- package/landing/index.html +3 -3
- package/lib/manager.js +184 -66
- package/package.json +4 -1
- package/server/admin/access.js +12 -7
- package/server/admin/admin.css +78 -0
- package/server/admin/admin.js +436 -10
- package/server/admin/billing.js +165 -42
- package/server/admin/index.html +80 -5
- package/server/admin/users.js +15 -15
- package/server/db/database.js +125 -20
- package/server/http/routes.js +1 -0
- package/server/http/static.js +4 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/canvaskit/wimp.js.symbols +8475 -8467
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +91077 -87037
- package/server/routes/account.js +53 -0
- package/server/routes/admin.js +345 -64
- package/server/routes/agents.js +203 -21
- package/server/routes/billing.js +5 -0
- package/server/routes/browser.js +8 -1
- package/server/routes/recordings.js +96 -6
- package/server/routes/screenHistory.js +140 -2
- package/server/routes/timeline.js +43 -0
- package/server/services/account/erasure.js +263 -0
- package/server/services/ai/hooks.js +4 -1
- package/server/services/ai/loop/agent_engine_core.js +8 -1
- package/server/services/ai/loop/blank_recovery.js +36 -0
- package/server/services/ai/loop/conversation_loop.js +166 -33
- package/server/services/ai/messagingFallback.js +22 -0
- package/server/services/ai/rate_limits.js +28 -5
- package/server/services/ai/systemPrompt.js +6 -5
- package/server/services/ai/taskAnalysis.js +2 -0
- package/server/services/ai/toolEvidence.js +15 -0
- package/server/services/ai/toolResult.js +40 -0
- package/server/services/ai/tools.js +163 -4
- package/server/services/android/controller.js +6 -2
- package/server/services/billing/plans.js +2 -1
- package/server/services/browser/anti_detection.js +192 -0
- package/server/services/browser/controller.js +180 -54
- package/server/services/desktop/auth.js +3 -0
- package/server/services/desktop/registry.js +50 -2
- package/server/services/integrations/google/calendar.js +22 -14
- package/server/services/manager.js +12 -42
- package/server/services/memory/ingestion_chunking.js +268 -0
- package/server/services/messaging/telnyx.js +9 -8
- package/server/services/recordings/manager.js +60 -27
- 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 +188 -0
- package/server/services/runtime/manager.js +0 -1
- package/server/services/runtime/validation.js +3 -8
- package/server/services/social_video/service.js +60 -10
- package/server/services/tasks/runtime.js +234 -9
- package/server/services/tasks/schedule_utils.js +5 -5
- package/server/services/tasks/task_repository.js +13 -0
- package/server/services/timeline/service.js +558 -0
- package/server/services/wearable/gateway.js +1 -1
- package/server/services/websocket.js +21 -3
- package/server/services/desktop/screenRecorder.js +0 -292
- package/server/services/desktop/screen_recorder_support.js +0 -46
package/server/admin/billing.js
CHANGED
|
@@ -12,7 +12,6 @@ async function loadBilling() {
|
|
|
12
12
|
document.getElementById('billing-plans-content').innerHTML =
|
|
13
13
|
'<div class="empty">Billing is not enabled on this server. Set <code>NEOAGENT_BILLING_ENABLED=1</code> to enable it.</div>';
|
|
14
14
|
document.getElementById('billing-subs-content').innerHTML = '';
|
|
15
|
-
document.getElementById('nav-billing').style.display = '';
|
|
16
15
|
return;
|
|
17
16
|
}
|
|
18
17
|
const data = await r.json();
|
|
@@ -38,18 +37,18 @@ function planRowHtml(plan) {
|
|
|
38
37
|
const id = escAttr(plan.id);
|
|
39
38
|
const intervalLabel = plan.interval ? `/ ${plan.interval}` : '';
|
|
40
39
|
const price = plan.price_cents === 0 ? 'Free' : `${(plan.price_cents / 100).toFixed(2)} ${(plan.currency || 'usd').toUpperCase()} ${intervalLabel}`;
|
|
41
|
-
const status = plan.is_active ? '<span style="color:var(--success)">Active</span>' : '<span style="color:var(--muted)">Inactive</span>';
|
|
40
|
+
const status = plan.is_active ? '<span style="color:var(--success)">Active</span>' : '<span style="color:var(--text-muted)">Inactive</span>';
|
|
42
41
|
return `
|
|
43
42
|
<tr data-plan-id="${id}">
|
|
44
|
-
<td><strong>${esc(plan.name)}</strong><br><small style="color:var(--muted)">${esc(plan.id)}</small></td>
|
|
43
|
+
<td><strong>${esc(plan.name)}</strong><br><small style="color:var(--text-muted)">${esc(plan.id)}</small></td>
|
|
45
44
|
<td>${esc(price)}</td>
|
|
46
|
-
<td>${plan.token_limit_4h != null ? fmtTokens(plan.token_limit_4h) : '<span style="color:var(--muted)">Default</span>'}</td>
|
|
47
|
-
<td>${plan.token_limit_weekly != null ? fmtTokens(plan.token_limit_weekly) : '<span style="color:var(--muted)">Default</span>'}</td>
|
|
45
|
+
<td>${plan.token_limit_4h != null ? fmtTokens(plan.token_limit_4h) : '<span style="color:var(--text-muted)">Default</span>'}</td>
|
|
46
|
+
<td>${plan.token_limit_weekly != null ? fmtTokens(plan.token_limit_weekly) : '<span style="color:var(--text-muted)">Default</span>'}</td>
|
|
48
47
|
<td><code style="font-size:11px">${esc(plan.stripe_price_id || '—')}</code></td>
|
|
49
48
|
<td>${status}</td>
|
|
50
49
|
<td>
|
|
51
|
-
<button class="btn btn-sm"
|
|
52
|
-
<button class="btn btn-sm btn-danger"
|
|
50
|
+
<button class="btn btn-sm" data-action="edit" data-plan-id="${id}">Edit</button>
|
|
51
|
+
<button class="btn btn-sm btn-danger" data-action="delete" data-plan-id="${id}">Delete</button>
|
|
53
52
|
</td>
|
|
54
53
|
</tr>`;
|
|
55
54
|
}
|
|
@@ -67,6 +66,13 @@ function renderPlansTable(plans) {
|
|
|
67
66
|
</tr></thead>
|
|
68
67
|
<tbody>${plans.map(planRowHtml).join('')}</tbody>
|
|
69
68
|
</table>`;
|
|
69
|
+
el.querySelector('tbody').addEventListener('click', (e) => {
|
|
70
|
+
const btn = e.target.closest('[data-action]');
|
|
71
|
+
if (!btn) return;
|
|
72
|
+
const planId = btn.dataset.planId;
|
|
73
|
+
if (btn.dataset.action === 'edit') billingEditPlan(planId);
|
|
74
|
+
else if (btn.dataset.action === 'delete') billingDeletePlan(planId);
|
|
75
|
+
});
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
function billingShowNewPlanForm() {
|
|
@@ -78,32 +84,88 @@ function billingEditPlan(planId) {
|
|
|
78
84
|
billingOpenPlanModal(plan);
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
function billingOpenPlanModal(plan) {
|
|
87
|
+
async function billingOpenPlanModal(plan) {
|
|
82
88
|
const isNew = !plan;
|
|
83
89
|
const title = isNew ? 'New Plan' : `Edit Plan: ${plan.name}`;
|
|
90
|
+
const allowedSet = new Set(plan?.allowed_models || []);
|
|
91
|
+
|
|
92
|
+
// Fetch available models to render the picker
|
|
93
|
+
let modelPickerHtml = '<div style="color:var(--text-muted);font-size:12px;padding:8px 0;">Loading models…</div>';
|
|
94
|
+
let fetchedModels = [];
|
|
95
|
+
try {
|
|
96
|
+
const r = await fetch('/admin/api/models');
|
|
97
|
+
if (r.ok) {
|
|
98
|
+
const data = await r.json();
|
|
99
|
+
fetchedModels = (data.models || []).sort((a, b) => {
|
|
100
|
+
if (a.provider !== b.provider) return a.provider.localeCompare(b.provider);
|
|
101
|
+
return (a.label || a.id).localeCompare(b.label || b.id);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
} catch {}
|
|
105
|
+
|
|
106
|
+
if (fetchedModels.length) {
|
|
107
|
+
const rows = fetchedModels.map((m) => {
|
|
108
|
+
const checked = allowedSet.has(m.id) ? 'checked' : '';
|
|
109
|
+
return `
|
|
110
|
+
<tr class="bp-model-row" style="opacity:${checked ? '1' : '0.55'}" onclick="billingModelRowClick(this)">
|
|
111
|
+
<td style="width:36px;text-align:center;pointer-events:none;">
|
|
112
|
+
<input type="checkbox" class="bp-model-cb" value="${escAttr(m.id)}" ${checked}
|
|
113
|
+
onchange="this.closest('tr').style.opacity=this.checked?'1':'0.55'" onclick="event.stopPropagation()">
|
|
114
|
+
</td>
|
|
115
|
+
<td style="pointer-events:none;">
|
|
116
|
+
<div style="font-weight:600;color:var(--text);font-size:13px;">${esc(m.label || m.id)}</div>
|
|
117
|
+
<div style="font-size:11px;color:var(--text-muted);font-family:var(--font-mono);margin-top:1px;">${esc(m.id)}</div>
|
|
118
|
+
</td>
|
|
119
|
+
<td style="font-size:12px;text-transform:capitalize;color:var(--text-secondary);pointer-events:none;">${esc(m.provider)}</td>
|
|
120
|
+
</tr>`;
|
|
121
|
+
}).join('');
|
|
122
|
+
|
|
123
|
+
modelPickerHtml = `
|
|
124
|
+
<div style="border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden;max-height:200px;overflow-y:auto;">
|
|
125
|
+
<table class="data-table" style="margin:0;">
|
|
126
|
+
<thead><tr>
|
|
127
|
+
<th style="width:36px;position:sticky;top:0;background:var(--bg-secondary);z-index:1;"></th>
|
|
128
|
+
<th style="position:sticky;top:0;background:var(--bg-secondary);z-index:1;">Model</th>
|
|
129
|
+
<th style="position:sticky;top:0;background:var(--bg-secondary);z-index:1;">Provider</th>
|
|
130
|
+
</tr></thead>
|
|
131
|
+
<tbody>${rows}</tbody>
|
|
132
|
+
</table>
|
|
133
|
+
</div>
|
|
134
|
+
<div style="font-size:11px;color:var(--text-muted);margin-top:5px;">Check models to restrict this plan. Leave all unchecked to allow every model.</div>`;
|
|
135
|
+
} else {
|
|
136
|
+
modelPickerHtml = `
|
|
137
|
+
<input class="form-input" id="bp-models-fallback" value="${escAttr((plan?.allowed_models || []).join(', '))}" placeholder="claude-opus-4-8, gpt-4o (comma-separated, or blank for all)">
|
|
138
|
+
<div style="font-size:11px;color:var(--text-muted);margin-top:5px;">Configure providers first to use the model picker.</div>`;
|
|
139
|
+
}
|
|
140
|
+
|
|
84
141
|
const html = `
|
|
85
|
-
<div id="billing-plan-modal" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000">
|
|
86
|
-
<div style="background:var(--card
|
|
87
|
-
<h2 style="margin:0 0 20px">${esc(title)}</h2>
|
|
142
|
+
<div id="billing-plan-modal" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000" onclick="if(event.target===this)billingClosePlanModal()">
|
|
143
|
+
<div style="background:var(--bg-card);border-radius:12px;padding:28px;width:560px;max-height:90vh;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,0.3)">
|
|
144
|
+
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700;color:var(--text);">${esc(title)}</h2>
|
|
88
145
|
<form id="billing-plan-form" onsubmit="billingPlanFormSubmit(event)">
|
|
89
|
-
<input type="hidden" id="bp-id" value="${
|
|
146
|
+
<input type="hidden" id="bp-id" value="${escAttr(plan?.id || '')}">
|
|
147
|
+
|
|
90
148
|
<label class="form-label">Plan ID <small>(set once, used as identifier)</small></label>
|
|
91
|
-
<input class="form-input" id="bp-slug" value="${
|
|
149
|
+
<input class="form-input" id="bp-slug" value="${escAttr(plan?.id || '')}" ${isNew ? '' : 'disabled'} placeholder="plan_pro" style="margin-bottom:14px">
|
|
150
|
+
|
|
92
151
|
<label class="form-label">Name</label>
|
|
93
|
-
<input class="form-input" id="bp-name" value="${
|
|
152
|
+
<input class="form-input" id="bp-name" value="${escAttr(plan?.name || '')}" required placeholder="Pro" style="margin-bottom:14px">
|
|
153
|
+
|
|
94
154
|
<label class="form-label">Description</label>
|
|
95
|
-
<input class="form-input" id="bp-desc" value="${
|
|
96
|
-
|
|
155
|
+
<input class="form-input" id="bp-desc" value="${escAttr(plan?.description || '')}" placeholder="Optional description" style="margin-bottom:14px">
|
|
156
|
+
|
|
157
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
|
|
97
158
|
<div>
|
|
98
159
|
<label class="form-label">Price (cents)</label>
|
|
99
160
|
<input class="form-input" id="bp-price" type="number" min="0" value="${plan?.price_cents ?? 0}" required>
|
|
100
161
|
</div>
|
|
101
162
|
<div>
|
|
102
163
|
<label class="form-label">Currency</label>
|
|
103
|
-
<input class="form-input" id="bp-currency" value="${
|
|
164
|
+
<input class="form-input" id="bp-currency" value="${escAttr(plan?.currency || 'usd')}" placeholder="usd">
|
|
104
165
|
</div>
|
|
105
166
|
</div>
|
|
106
|
-
|
|
167
|
+
|
|
168
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
|
|
107
169
|
<div>
|
|
108
170
|
<label class="form-label">Billing interval</label>
|
|
109
171
|
<select class="form-input" id="bp-interval">
|
|
@@ -117,9 +179,11 @@ function billingOpenPlanModal(plan) {
|
|
|
117
179
|
<input class="form-input" id="bp-sort" type="number" value="${plan?.sort_order ?? 0}">
|
|
118
180
|
</div>
|
|
119
181
|
</div>
|
|
182
|
+
|
|
120
183
|
<label class="form-label">Stripe Price ID</label>
|
|
121
|
-
<input class="form-input" id="bp-stripe-price" value="${
|
|
122
|
-
|
|
184
|
+
<input class="form-input" id="bp-stripe-price" value="${escAttr(plan?.stripe_price_id || '')}" placeholder="price_..." style="margin-bottom:14px">
|
|
185
|
+
|
|
186
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
|
|
123
187
|
<div>
|
|
124
188
|
<label class="form-label">4h token limit <small>(blank = default)</small></label>
|
|
125
189
|
<input class="form-input" id="bp-tok-4h" type="number" min="0" value="${plan?.token_limit_4h ?? ''}" placeholder="2500000">
|
|
@@ -129,16 +193,20 @@ function billingOpenPlanModal(plan) {
|
|
|
129
193
|
<input class="form-input" id="bp-tok-weekly" type="number" min="0" value="${plan?.token_limit_weekly ?? ''}" placeholder="10000000">
|
|
130
194
|
</div>
|
|
131
195
|
</div>
|
|
132
|
-
|
|
133
|
-
<
|
|
196
|
+
|
|
197
|
+
<label class="form-label" style="margin-bottom:8px;">Allowed models <small>(unchecked = not allowed on this plan)</small></label>
|
|
198
|
+
<div id="bp-models-wrap" style="margin-bottom:14px">${modelPickerHtml}</div>
|
|
199
|
+
|
|
134
200
|
<label class="form-label">Features <small>(comma-separated, shown on pricing page)</small></label>
|
|
135
|
-
<input class="form-input" id="bp-features" value="${
|
|
136
|
-
|
|
201
|
+
<input class="form-input" id="bp-features" value="${escAttr((plan?.features || []).join(', '))}" placeholder="Unlimited agents, Priority support" style="margin-bottom:14px">
|
|
202
|
+
|
|
203
|
+
<label style="display:flex;align-items:center;gap:8px;margin-bottom:20px;cursor:pointer;font-size:13px;color:var(--text-secondary);text-transform:none;letter-spacing:0;font-weight:500;">
|
|
137
204
|
<input type="checkbox" id="bp-active" ${!plan || plan.is_active ? 'checked' : ''}> Active
|
|
138
205
|
</label>
|
|
206
|
+
|
|
139
207
|
<div style="display:flex;gap:8px;justify-content:flex-end">
|
|
140
|
-
<button type="button" class="btn" onclick="billingClosePlanModal()">Cancel</button>
|
|
141
|
-
<button type="submit" class="btn btn-primary">${isNew ? 'Create' : 'Save'}</button>
|
|
208
|
+
<button type="button" class="btn btn-ghost" onclick="billingClosePlanModal()">Cancel</button>
|
|
209
|
+
<button type="submit" class="btn btn-primary">${isNew ? 'Create plan' : 'Save changes'}</button>
|
|
142
210
|
</div>
|
|
143
211
|
</form>
|
|
144
212
|
</div>
|
|
@@ -146,6 +214,12 @@ function billingOpenPlanModal(plan) {
|
|
|
146
214
|
document.body.insertAdjacentHTML('beforeend', html);
|
|
147
215
|
}
|
|
148
216
|
|
|
217
|
+
function billingModelRowClick(row) {
|
|
218
|
+
const cb = row.querySelector('.bp-model-cb');
|
|
219
|
+
cb.checked = !cb.checked;
|
|
220
|
+
row.style.opacity = cb.checked ? '1' : '0.55';
|
|
221
|
+
}
|
|
222
|
+
|
|
149
223
|
function billingClosePlanModal() {
|
|
150
224
|
document.getElementById('billing-plan-modal')?.remove();
|
|
151
225
|
}
|
|
@@ -160,7 +234,12 @@ async function billingPlanFormSubmit(e) {
|
|
|
160
234
|
const n = parseInt(val, 10);
|
|
161
235
|
return isNaN(n) || val === '' ? null : n;
|
|
162
236
|
};
|
|
163
|
-
const parseModels = (
|
|
237
|
+
const parseModels = () => {
|
|
238
|
+
const cbs = document.querySelectorAll('.bp-model-cb');
|
|
239
|
+
if (cbs.length) return Array.from(cbs).filter(cb => cb.checked).map(cb => cb.value);
|
|
240
|
+
const fallback = document.getElementById('bp-models-fallback');
|
|
241
|
+
return fallback ? fallback.value.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
242
|
+
};
|
|
164
243
|
const parseFeatures = (val) => val.split(',').map((s) => s.trim()).filter(Boolean);
|
|
165
244
|
|
|
166
245
|
const body = {
|
|
@@ -173,7 +252,7 @@ async function billingPlanFormSubmit(e) {
|
|
|
173
252
|
stripe_price_id: document.getElementById('bp-stripe-price').value.trim() || null,
|
|
174
253
|
token_limit_4h: parseTokenLimit(document.getElementById('bp-tok-4h').value),
|
|
175
254
|
token_limit_weekly: parseTokenLimit(document.getElementById('bp-tok-weekly').value),
|
|
176
|
-
allowed_models: parseModels(
|
|
255
|
+
allowed_models: parseModels(),
|
|
177
256
|
features: parseFeatures(document.getElementById('bp-features').value),
|
|
178
257
|
sort_order: parseInt(document.getElementById('bp-sort').value, 10) || 0,
|
|
179
258
|
is_active: document.getElementById('bp-active').checked,
|
|
@@ -184,22 +263,27 @@ async function billingPlanFormSubmit(e) {
|
|
|
184
263
|
const method = isNew ? 'POST' : 'PUT';
|
|
185
264
|
const r = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
186
265
|
const data = await r.json();
|
|
187
|
-
if (!r.ok) {
|
|
266
|
+
if (!r.ok) { showToast(data.error || 'Failed to save plan.', 'error'); return; }
|
|
188
267
|
billingClosePlanModal();
|
|
189
268
|
await loadBilling();
|
|
190
269
|
} catch (err) {
|
|
191
|
-
|
|
270
|
+
showToast('Error: ' + err.message, 'error');
|
|
192
271
|
}
|
|
193
272
|
}
|
|
194
273
|
|
|
195
274
|
async function billingDeletePlan(planId) {
|
|
196
|
-
if (!
|
|
275
|
+
if (!await showConfirmModal({
|
|
276
|
+
title: `Delete plan "${esc(planId)}"?`,
|
|
277
|
+
body: 'The plan will be deactivated (not hard-deleted). Existing subscribers will keep access until their period ends.',
|
|
278
|
+
confirmLabel: 'Delete Plan',
|
|
279
|
+
confirmClass: 'btn-danger',
|
|
280
|
+
})) return;
|
|
197
281
|
try {
|
|
198
282
|
const r = await fetch(`/admin/api/billing/plans/${encodeURIComponent(planId)}`, { method: 'DELETE' });
|
|
199
|
-
if (!r.ok) { const d = await r.json();
|
|
283
|
+
if (!r.ok) { const d = await r.json(); showToast(d.error || 'Failed.', 'error'); return; }
|
|
200
284
|
await loadBilling();
|
|
201
285
|
} catch (err) {
|
|
202
|
-
|
|
286
|
+
showToast('Error: ' + err.message, 'error');
|
|
203
287
|
}
|
|
204
288
|
}
|
|
205
289
|
|
|
@@ -234,15 +318,15 @@ function renderSubsTable(rows, total) {
|
|
|
234
318
|
const hasPrev = _billingSubsOffset > 0;
|
|
235
319
|
const hasNext = _billingSubsOffset + BILLING_SUBS_LIMIT < total;
|
|
236
320
|
pag.innerHTML = `
|
|
237
|
-
<span style="color:var(--muted);font-size:13px">${total} total</span>
|
|
321
|
+
<span style="color:var(--text-muted);font-size:13px">${total} total</span>
|
|
238
322
|
${hasPrev ? `<button class="btn btn-sm" onclick="_billingSubsOffset-=${BILLING_SUBS_LIMIT};loadBillingSubscriptions()">← Prev</button>` : ''}
|
|
239
323
|
${hasNext ? `<button class="btn btn-sm" onclick="_billingSubsOffset+=${BILLING_SUBS_LIMIT};loadBillingSubscriptions()">Next →</button>` : ''}`;
|
|
240
324
|
}
|
|
241
325
|
|
|
242
326
|
function subRowHtml(sub) {
|
|
243
327
|
const userLabel = esc(sub.display_name || sub.username || String(sub.user_id));
|
|
244
|
-
const email = sub.email ? `<br><small style="color:var(--muted)">${esc(sub.email)}</small>` : '';
|
|
245
|
-
const statusColor = { active: 'var(--success)', trialing: 'var(--warning
|
|
328
|
+
const email = sub.email ? `<br><small style="color:var(--text-muted)">${esc(sub.email)}</small>` : '';
|
|
329
|
+
const statusColor = { active: 'var(--success)', trialing: 'var(--warning)', past_due: 'var(--danger)', canceled: 'var(--text-muted)' }[sub.status] || 'var(--text-muted)';
|
|
246
330
|
const periodEnd = sub.current_period_end ? sub.current_period_end.slice(0, 10) : '—';
|
|
247
331
|
return `
|
|
248
332
|
<tr>
|
|
@@ -255,9 +339,9 @@ function subRowHtml(sub) {
|
|
|
255
339
|
}
|
|
256
340
|
|
|
257
341
|
async function billingOverrideSub(userId) {
|
|
258
|
-
if (!_billingPlans.length) {
|
|
259
|
-
const
|
|
260
|
-
const planId =
|
|
342
|
+
if (!_billingPlans.length) { showToast('No plans available.', 'error'); return; }
|
|
343
|
+
const activePlans = _billingPlans.filter((p) => p.is_active);
|
|
344
|
+
const planId = await billingPromptPlan(activePlans);
|
|
261
345
|
if (!planId) return;
|
|
262
346
|
try {
|
|
263
347
|
const r = await fetch(`/admin/api/billing/users/${userId}/subscription`, {
|
|
@@ -266,13 +350,50 @@ async function billingOverrideSub(userId) {
|
|
|
266
350
|
body: JSON.stringify({ planId: planId.trim() }),
|
|
267
351
|
});
|
|
268
352
|
const data = await r.json();
|
|
269
|
-
if (!r.ok) {
|
|
353
|
+
if (!r.ok) { showToast(data.error || 'Failed.', 'error'); return; }
|
|
270
354
|
await loadBillingSubscriptions();
|
|
271
355
|
} catch (err) {
|
|
272
|
-
|
|
356
|
+
showToast('Error: ' + err.message, 'error');
|
|
273
357
|
}
|
|
274
358
|
}
|
|
275
359
|
|
|
360
|
+
function billingPromptPlan(plans) {
|
|
361
|
+
return new Promise((resolve) => {
|
|
362
|
+
const overlay = document.createElement('div');
|
|
363
|
+
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);';
|
|
364
|
+
const optionsHtml = plans.map((p) => `<option value="${escAttr(p.id)}">${esc(p.id)} — ${esc(p.name)}</option>`).join('');
|
|
365
|
+
const modal = document.createElement('div');
|
|
366
|
+
modal.style.cssText = 'width:400px;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);';
|
|
367
|
+
modal.innerHTML = `
|
|
368
|
+
<div style="font-size:16px;font-weight:700;color:var(--text);margin-bottom:12px;">Assign plan</div>
|
|
369
|
+
<div style="margin-bottom:16px;">
|
|
370
|
+
<select id="billing-plan-select" style="width:100%;padding:8px 10px;background:var(--bg-input,#111);border:1px solid var(--border,#2a2a2a);border-radius:6px;color:var(--text,#fff);font-size:13px;">
|
|
371
|
+
${optionsHtml}
|
|
372
|
+
</select>
|
|
373
|
+
</div>
|
|
374
|
+
<div style="display:flex;gap:10px;justify-content:flex-end;">
|
|
375
|
+
<button class="btn btn-ghost" id="billing-plan-cancel" style="padding:8px 16px;">Cancel</button>
|
|
376
|
+
<button class="btn btn-primary" id="billing-plan-confirm" style="padding:8px 16px;">Assign</button>
|
|
377
|
+
</div>
|
|
378
|
+
`;
|
|
379
|
+
overlay.appendChild(modal);
|
|
380
|
+
document.body.appendChild(overlay);
|
|
381
|
+
const cancel = () => { overlay.remove(); resolve(null); };
|
|
382
|
+
const confirm = () => {
|
|
383
|
+
const val = modal.querySelector('#billing-plan-select').value;
|
|
384
|
+
overlay.remove();
|
|
385
|
+
resolve(val || null);
|
|
386
|
+
};
|
|
387
|
+
modal.querySelector('#billing-plan-cancel').onclick = cancel;
|
|
388
|
+
modal.querySelector('#billing-plan-confirm').onclick = confirm;
|
|
389
|
+
overlay.addEventListener('click', (e) => { if (e.target === overlay) cancel(); });
|
|
390
|
+
document.addEventListener('keydown', function handler(e) {
|
|
391
|
+
if (e.key === 'Escape') { cancel(); document.removeEventListener('keydown', handler); }
|
|
392
|
+
});
|
|
393
|
+
modal.querySelector('#billing-plan-confirm').focus();
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
276
397
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
277
398
|
|
|
278
399
|
function esc(str) {
|
|
@@ -282,7 +403,9 @@ function esc(str) {
|
|
|
282
403
|
}
|
|
283
404
|
|
|
284
405
|
function escAttr(str) {
|
|
285
|
-
|
|
406
|
+
// JSON.stringify gives us a properly quoted JS string; strip the outer quotes
|
|
407
|
+
// so it can be embedded in an HTML attribute value (already inside quotes).
|
|
408
|
+
return JSON.stringify(String(str ?? '')).slice(1, -1).replace(/"/g, '"');
|
|
286
409
|
}
|
|
287
410
|
|
|
288
411
|
function fmtTokens(n) {
|
package/server/admin/index.html
CHANGED
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
width: 100%;
|
|
64
64
|
text-align: left;
|
|
65
65
|
text-decoration: none;
|
|
66
|
+
position: relative;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
.nav-item:hover { background: var(--row-hover); color: var(--text); }
|
|
@@ -82,7 +83,10 @@
|
|
|
82
83
|
.nav-item.active svg { opacity: 1; }
|
|
83
84
|
|
|
84
85
|
.nav-badge {
|
|
85
|
-
|
|
86
|
+
position: absolute;
|
|
87
|
+
right: 10px;
|
|
88
|
+
top: 50%;
|
|
89
|
+
transform: translateY(-50%);
|
|
86
90
|
min-width: 18px;
|
|
87
91
|
padding: 1px 6px;
|
|
88
92
|
border-radius: 999px;
|
|
@@ -835,6 +839,27 @@
|
|
|
835
839
|
font-size: 12px;
|
|
836
840
|
flex-shrink: 0;
|
|
837
841
|
}
|
|
842
|
+
|
|
843
|
+
/* ── Integration sections ──────────────────────────────────────── */
|
|
844
|
+
.integration-section {
|
|
845
|
+
border-bottom: 1px solid var(--border);
|
|
846
|
+
padding: 16px 0;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.integration-section:last-child { border-bottom: none; }
|
|
850
|
+
|
|
851
|
+
.integration-header {
|
|
852
|
+
display: flex;
|
|
853
|
+
align-items: center;
|
|
854
|
+
gap: 10px;
|
|
855
|
+
margin-bottom: 14px;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.integration-name {
|
|
859
|
+
font-size: 13px;
|
|
860
|
+
font-weight: 700;
|
|
861
|
+
color: var(--text);
|
|
862
|
+
}
|
|
838
863
|
</style>
|
|
839
864
|
</head>
|
|
840
865
|
<body>
|
|
@@ -871,7 +896,7 @@
|
|
|
871
896
|
|
|
872
897
|
<button class="nav-item" data-page="issues" onclick="showPage('issues',this)">
|
|
873
898
|
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
874
|
-
<path fill-rule="evenodd" d="
|
|
899
|
+
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
|
875
900
|
</svg>
|
|
876
901
|
Issues
|
|
877
902
|
<span class="nav-badge" id="issues-badge" hidden>0</span>
|
|
@@ -938,6 +963,13 @@
|
|
|
938
963
|
Models
|
|
939
964
|
</button>
|
|
940
965
|
|
|
966
|
+
<button class="nav-item" data-page="integrations" onclick="showPage('integrations',this)">
|
|
967
|
+
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
968
|
+
<path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd"/>
|
|
969
|
+
</svg>
|
|
970
|
+
Integrations
|
|
971
|
+
</button>
|
|
972
|
+
|
|
941
973
|
<button class="nav-item" data-page="config" onclick="showPage('config',this)">
|
|
942
974
|
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
943
975
|
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"/>
|
|
@@ -1177,15 +1209,53 @@
|
|
|
1177
1209
|
</div>
|
|
1178
1210
|
</section>
|
|
1179
1211
|
|
|
1212
|
+
<!-- Integrations -->
|
|
1213
|
+
<section id="page-integrations" class="page" aria-label="Integrations">
|
|
1214
|
+
<div class="page-header">
|
|
1215
|
+
<div>
|
|
1216
|
+
<h1 class="page-title">Integrations</h1>
|
|
1217
|
+
<p class="page-subtitle">OAuth client credentials for official integrations — saved to .env, applied immediately</p>
|
|
1218
|
+
</div>
|
|
1219
|
+
</div>
|
|
1220
|
+
<div class="content">
|
|
1221
|
+
<div class="card" style="margin-bottom:20px;">
|
|
1222
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
|
|
1223
|
+
<div class="card-title" style="margin-bottom:0;">OAuth Integrations</div>
|
|
1224
|
+
<button class="btn btn-primary" onclick="saveIntegrationsConfig(this)">Save All</button>
|
|
1225
|
+
</div>
|
|
1226
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:18px;line-height:1.5;">
|
|
1227
|
+
Redirect URIs should match <code style="font-family:var(--font-mono);font-size:11px;background:var(--bg-secondary);padding:1px 5px;border-radius:4px;color:var(--accent);"><PUBLIC_URL>/api/integrations/oauth/callback</code>
|
|
1228
|
+
</div>
|
|
1229
|
+
<div id="integrations-config-content"><div class="empty"><span class="spinner"></span></div></div>
|
|
1230
|
+
</div>
|
|
1231
|
+
<div class="card">
|
|
1232
|
+
<div class="card-title">Deepgram Configuration</div>
|
|
1233
|
+
<div id="deepgram-config-content"><div class="empty"><span class="spinner"></span></div></div>
|
|
1234
|
+
</div>
|
|
1235
|
+
</div>
|
|
1236
|
+
</section>
|
|
1237
|
+
|
|
1180
1238
|
<!-- Configuration -->
|
|
1181
1239
|
<section id="page-config" class="page" aria-label="Configuration">
|
|
1182
1240
|
<div class="page-header">
|
|
1183
1241
|
<div>
|
|
1184
1242
|
<h1 class="page-title">Configuration</h1>
|
|
1185
|
-
<p class="page-subtitle">
|
|
1243
|
+
<p class="page-subtitle">Server and runtime settings — saved to .env, applied immediately</p>
|
|
1186
1244
|
</div>
|
|
1187
1245
|
</div>
|
|
1188
1246
|
<div class="content">
|
|
1247
|
+
<div class="card" style="margin-bottom:20px;">
|
|
1248
|
+
<div class="card-title">General Settings</div>
|
|
1249
|
+
<div id="general-config-content"><div class="empty"><span class="spinner"></span></div></div>
|
|
1250
|
+
</div>
|
|
1251
|
+
<div class="card" style="margin-bottom:20px;">
|
|
1252
|
+
<div class="card-title">VM Runtime</div>
|
|
1253
|
+
<div id="vm-config-content"><div class="empty"><span class="spinner"></span></div></div>
|
|
1254
|
+
</div>
|
|
1255
|
+
<div class="card" style="margin-bottom:20px;">
|
|
1256
|
+
<div class="card-title">Stripe / Billing</div>
|
|
1257
|
+
<div id="billing-setup-content"><div class="empty"><span class="spinner"></span></div></div>
|
|
1258
|
+
</div>
|
|
1189
1259
|
<div class="card" style="margin-bottom:20px;">
|
|
1190
1260
|
<div class="card-title">Service Email</div>
|
|
1191
1261
|
<div id="email-config-content"><div class="empty"><span class="spinner"></span></div></div>
|
|
@@ -1198,8 +1268,13 @@
|
|
|
1198
1268
|
</section>
|
|
1199
1269
|
|
|
1200
1270
|
<section id="page-billing" class="page" aria-label="Billing">
|
|
1201
|
-
<div class="page-header"
|
|
1202
|
-
|
|
1271
|
+
<div class="page-header">
|
|
1272
|
+
<div>
|
|
1273
|
+
<h1 class="page-title">Billing</h1>
|
|
1274
|
+
<p class="page-subtitle">Subscription plans and user billing management</p>
|
|
1275
|
+
</div>
|
|
1276
|
+
</div>
|
|
1277
|
+
<div class="content">
|
|
1203
1278
|
<div class="card">
|
|
1204
1279
|
<div class="card-title">Subscription Plans</div>
|
|
1205
1280
|
<div style="margin-bottom:12px">
|
package/server/admin/users.js
CHANGED
|
@@ -31,9 +31,9 @@ async function saveDefaultRateLimits(btn) {
|
|
|
31
31
|
headers: { 'Content-Type': 'application/json' },
|
|
32
32
|
body: JSON.stringify({ rate_limit_4h: parse(v4h), rate_limit_weekly: parse(vw) }),
|
|
33
33
|
});
|
|
34
|
-
if (!res.ok) { const b = await res.json().catch(() => ({}));
|
|
34
|
+
if (!res.ok) { const b = await res.json().catch(() => ({})); showToast(b.error || 'Failed', 'error'); }
|
|
35
35
|
else { btn.textContent = 'Saved!'; setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 2000); return; }
|
|
36
|
-
} catch (_) {
|
|
36
|
+
} catch (_) { showToast('Network error', 'error'); }
|
|
37
37
|
btn.disabled = false;
|
|
38
38
|
btn.textContent = orig;
|
|
39
39
|
}
|
|
@@ -132,7 +132,7 @@ function renderUsersTable(el, users) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
async function forceLogout(id, username, btn) {
|
|
135
|
-
if (!
|
|
135
|
+
if (!await showConfirmModal({ title: `Force logout @${esc(username)}?`, body: 'This will revoke all active sessions. They can log back in.', confirmLabel: 'Force Logout', confirmClass: 'btn-danger' })) return;
|
|
136
136
|
btn.disabled = true;
|
|
137
137
|
try {
|
|
138
138
|
const res = await api(`/admin/api/users/${id}/sessions`, { method: 'DELETE' });
|
|
@@ -141,22 +141,22 @@ async function forceLogout(id, username, btn) {
|
|
|
141
141
|
setTimeout(loadUsers, 800);
|
|
142
142
|
} else {
|
|
143
143
|
const b = await res.json().catch(() => ({}));
|
|
144
|
-
|
|
144
|
+
showToast(b.error || 'Failed', 'error');
|
|
145
145
|
btn.disabled = false;
|
|
146
146
|
}
|
|
147
147
|
} catch (err) {
|
|
148
|
-
if (err.message !== 'unauthorized')
|
|
148
|
+
if (err.message !== 'unauthorized') showToast('Network error', 'error');
|
|
149
149
|
btn.disabled = false;
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
async function deleteUser(id, displayName, btn) {
|
|
154
|
-
if (!
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
'
|
|
158
|
-
|
|
159
|
-
)) return;
|
|
154
|
+
if (!await showConfirmModal({
|
|
155
|
+
title: `Delete "${esc(displayName)}"?`,
|
|
156
|
+
body: `<strong>GDPR Erasure</strong> — permanently deletes the account and <strong>all</strong> associated data:<br><br>• Agent runs, steps, and messages<br>• Memories and conversations<br>• Integrations and platform connections<br>• Artifacts and files on disk<br>• All sessions<br><br><span style="color:var(--danger)">This action cannot be undone.</span>`,
|
|
157
|
+
confirmLabel: 'Delete User',
|
|
158
|
+
confirmClass: 'btn-danger',
|
|
159
|
+
})) return;
|
|
160
160
|
btn.disabled = true;
|
|
161
161
|
btn.textContent = 'Deleting…';
|
|
162
162
|
try {
|
|
@@ -170,12 +170,12 @@ async function deleteUser(id, displayName, btn) {
|
|
|
170
170
|
}
|
|
171
171
|
} else {
|
|
172
172
|
const b = await res.json().catch(() => ({}));
|
|
173
|
-
|
|
173
|
+
showToast(b.error || 'Failed to delete user', 'error');
|
|
174
174
|
btn.disabled = false;
|
|
175
175
|
btn.textContent = 'Delete';
|
|
176
176
|
}
|
|
177
177
|
} catch (err) {
|
|
178
|
-
if (err.message !== 'unauthorized')
|
|
178
|
+
if (err.message !== 'unauthorized') showToast('Network error', 'error');
|
|
179
179
|
btn.disabled = false;
|
|
180
180
|
btn.textContent = 'Delete';
|
|
181
181
|
}
|
|
@@ -266,12 +266,12 @@ async function editRateLimits(id, username) {
|
|
|
266
266
|
overlay.remove();
|
|
267
267
|
await fetchUsers(document.getElementById('user-search')?.value?.trim() || '');
|
|
268
268
|
} catch (_) {
|
|
269
|
-
|
|
269
|
+
showToast('Failed to save rate limits', 'error');
|
|
270
270
|
bSave.disabled = false;
|
|
271
271
|
bSave.textContent = 'Save';
|
|
272
272
|
}
|
|
273
273
|
};
|
|
274
274
|
} catch (_) {
|
|
275
|
-
|
|
275
|
+
showToast('Failed to fetch rate limits', 'error');
|
|
276
276
|
}
|
|
277
277
|
}
|