neoagent 2.4.1-beta.8 → 2.4.1
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 +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +45 -7
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -2
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +15 -2
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +338 -46
- package/flutter_app/lib/main_devices.dart +851 -122
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +764 -887
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
- package/flutter_app/lib/src/backend_client.dart +79 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
- package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +205 -35
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88944 -85720
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/browser.js +14 -0
- package/server/routes/browser_extension.js +21 -4
- package/server/routes/desktop.js +10 -0
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +14 -2
- package/server/routes/stream.js +12 -3
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +80 -15
- package/server/services/android/controller.js +36 -1
- package/server/services/browser/controller.js +18 -0
- package/server/services/browser/extension/manifest.js +33 -0
- package/server/services/browser/extension/provider.js +12 -6
- package/server/services/browser/extension/registry.js +229 -18
- package/server/services/desktop/protocol.js +1 -0
- package/server/services/desktop/provider.js +4 -0
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -47
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/runtime/manager.js +7 -3
- package/server/services/runtime/settings.js +17 -0
- package/server/services/streaming/android-stream.js +296 -32
- package/server/services/streaming/stream-hub.js +1 -1
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- package/server/services/memory/openhuman_uplift.test.js +0 -98
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
let currentPage = 'overview';
|
|
6
|
+
let localLogs = [];
|
|
7
|
+
let logsCleared = false;
|
|
8
|
+
|
|
9
|
+
// ── Navigation ─────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
function showPage(page, btn) {
|
|
12
|
+
document.querySelectorAll('.page').forEach((el) => el.classList.remove('active'));
|
|
13
|
+
document.querySelectorAll('.nav-item[data-page]').forEach((el) => el.classList.remove('active'));
|
|
14
|
+
const section = document.getElementById('page-' + page);
|
|
15
|
+
if (section) section.classList.add('active');
|
|
16
|
+
if (btn) btn.classList.add('active');
|
|
17
|
+
currentPage = page;
|
|
18
|
+
|
|
19
|
+
const loaders = { overview: loadHealth, logs: loadLogs, updates: loadVersion, config: loadConfig, providers: loadProviders, analytics: loadAnalytics, users: loadUsers, sql: loadSql, access: loadAccess };
|
|
20
|
+
loaders[page]?.();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function signOut() {
|
|
24
|
+
await fetch('/admin/api/logout', { method: 'POST' }).catch(() => {});
|
|
25
|
+
window.location.replace('/admin/login');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── HTTP helpers ───────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
async function api(path, opts = {}) {
|
|
31
|
+
const res = await fetch(path, opts);
|
|
32
|
+
if (res.status === 401) {
|
|
33
|
+
window.location.replace('/admin/login');
|
|
34
|
+
throw new Error('unauthorized');
|
|
35
|
+
}
|
|
36
|
+
return res;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function esc(str) {
|
|
40
|
+
return String(str ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fmtTime(iso) {
|
|
44
|
+
if (!iso) return '—';
|
|
45
|
+
try { return new Date(iso).toLocaleTimeString(); } catch { return iso; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fmtUptime(s) {
|
|
49
|
+
if (!s) return '—';
|
|
50
|
+
const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60);
|
|
51
|
+
return h > 0 ? `${h}h ${m}m` : m > 0 ? `${m}m ${Math.floor(s % 60)}s` : `${Math.floor(s)}s`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function setTs(id, label) {
|
|
55
|
+
const el = document.getElementById(id);
|
|
56
|
+
if (el) el.textContent = `${label} ${new Date().toLocaleTimeString()}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Overview ───────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
async function loadHealth() {
|
|
62
|
+
const grid = document.getElementById('health-grid');
|
|
63
|
+
if (!grid) return;
|
|
64
|
+
try {
|
|
65
|
+
const data = await api('/admin/api/health').then((r) => r.json());
|
|
66
|
+
if (!data.results?.length) {
|
|
67
|
+
grid.innerHTML = '<div class="empty">No health data available</div>';
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
grid.innerHTML = data.results.map((item) => `
|
|
71
|
+
<div class="status-tile">
|
|
72
|
+
<div class="status-dot ${item.passed ? 'ok' : 'fail'}" aria-hidden="true"></div>
|
|
73
|
+
<div>
|
|
74
|
+
<div class="status-label">${esc(item.label)}</div>
|
|
75
|
+
<div class="status-detail">${esc(item.detail || '')}</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>`).join('');
|
|
78
|
+
setTs('overview-ts', 'Updated');
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err.message !== 'unauthorized') {
|
|
81
|
+
grid.innerHTML = '<div class="empty">Failed to load health data</div>';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Logs ───────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
async function loadLogs() {
|
|
89
|
+
if (logsCleared) { renderLogs(localLogs); return; }
|
|
90
|
+
try {
|
|
91
|
+
const data = await api('/admin/api/logs').then((r) => r.json());
|
|
92
|
+
localLogs = data.logs || [];
|
|
93
|
+
renderLogs(localLogs);
|
|
94
|
+
setTs('logs-ts', 'Updated');
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err.message !== 'unauthorized') {
|
|
97
|
+
document.getElementById('log-table').innerHTML = '<div class="empty">Failed to load logs</div>';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderLogs(logs) {
|
|
103
|
+
const count = document.getElementById('log-count');
|
|
104
|
+
const table = document.getElementById('log-table');
|
|
105
|
+
if (!table) return;
|
|
106
|
+
if (count) count.textContent = `${logs.length} entr${logs.length === 1 ? 'y' : 'ies'}`;
|
|
107
|
+
if (!logs.length) { table.innerHTML = '<div class="empty">No log entries</div>'; return; }
|
|
108
|
+
table.innerHTML = logs.map((e) => {
|
|
109
|
+
const level = e.type || 'log';
|
|
110
|
+
const msgClass = (level === 'error' || level === 'warn') ? `log-msg-${level}` : '';
|
|
111
|
+
return `<div class="log-row">
|
|
112
|
+
<div class="log-cell log-ts">${esc(fmtTime(e.timestamp))}</div>
|
|
113
|
+
<div class="log-cell log-level-${esc(level)}">${esc(level)}</div>
|
|
114
|
+
<div class="log-cell msg ${msgClass}">${esc(e.message || '')}</div>
|
|
115
|
+
</div>`;
|
|
116
|
+
}).join('');
|
|
117
|
+
table.scrollTop = table.scrollHeight;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function clearLogs() {
|
|
121
|
+
logsCleared = true;
|
|
122
|
+
localLogs = [];
|
|
123
|
+
renderLogs([]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function copyLogs() {
|
|
127
|
+
const text = localLogs
|
|
128
|
+
.map((e) => `[${e.timestamp || ''}] [${e.type || 'log'}] ${e.message || ''}`)
|
|
129
|
+
.join('\n');
|
|
130
|
+
navigator.clipboard.writeText(text).catch(() => {});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Updates ────────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
async function loadVersion() {
|
|
136
|
+
const vEl = document.getElementById('version-content');
|
|
137
|
+
const uEl = document.getElementById('update-content');
|
|
138
|
+
if (!vEl || !uEl) return;
|
|
139
|
+
try {
|
|
140
|
+
const d = await api('/admin/api/version').then((r) => r.json());
|
|
141
|
+
const st = d.updateStatus || {};
|
|
142
|
+
const running = st.state === 'running';
|
|
143
|
+
const canUpdate = d.allowSelfUpdate !== false;
|
|
144
|
+
const ch = d.releaseChannel || 'stable';
|
|
145
|
+
|
|
146
|
+
vEl.innerHTML = `
|
|
147
|
+
<div class="kv-row"><span class="kv-key">Version</span><span class="kv-val">${esc(d.version || d.packageVersion || '—')}</span></div>
|
|
148
|
+
<div class="kv-row"><span class="kv-key">Git SHA</span><span class="kv-val">${esc(d.gitSha ? d.gitSha.slice(0, 10) : '—')}</span></div>
|
|
149
|
+
<div class="kv-row"><span class="kv-key">Branch</span><span class="kv-val">${esc(d.gitBranch || '—')}</span></div>
|
|
150
|
+
<div class="kv-row"><span class="kv-key">Node.js</span><span class="kv-val">${esc(d.nodeVersion || '—')}</span></div>
|
|
151
|
+
<div class="kv-row"><span class="kv-key">Uptime</span><span class="kv-val">${esc(fmtUptime(d.uptime))}</span></div>
|
|
152
|
+
<div class="kv-row"><span class="kv-key">Deployment</span><span class="kv-val">${esc(d.deploymentMode || '—')}</span></div>
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
const badgeClass = st.state === 'idle' ? 'badge-idle'
|
|
156
|
+
: st.state === 'running' ? 'badge-running'
|
|
157
|
+
: st.state === 'failed' ? 'badge-err'
|
|
158
|
+
: 'badge-ok';
|
|
159
|
+
|
|
160
|
+
uEl.innerHTML = `
|
|
161
|
+
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap;">
|
|
162
|
+
<span class="badge ${badgeClass}">${esc(st.state || 'idle')}</span>
|
|
163
|
+
${st.message ? `<span style="font-size:13px;color:var(--text-muted)">${esc(st.message)}</span>` : ''}
|
|
164
|
+
</div>
|
|
165
|
+
${running ? `
|
|
166
|
+
<div class="progress-track">
|
|
167
|
+
<div class="progress-fill" style="width:${st.progress || 0}%"></div>
|
|
168
|
+
</div>
|
|
169
|
+
<p style="font-size:12px;color:var(--text-muted);margin-bottom:16px">
|
|
170
|
+
${st.progress || 0}% — ${esc(st.phase || '')}
|
|
171
|
+
</p>` : ''}
|
|
172
|
+
${canUpdate ? `
|
|
173
|
+
<div class="update-controls">
|
|
174
|
+
<div class="field">
|
|
175
|
+
<label for="channel-select">Release channel</label>
|
|
176
|
+
<select id="channel-select" onchange="saveChannel(this.value)" ${running ? 'disabled' : ''} style="width:auto;min-width:120px;">
|
|
177
|
+
<option value="stable" ${ch === 'stable' ? 'selected' : ''}>Stable</option>
|
|
178
|
+
<option value="beta" ${ch === 'beta' ? 'selected' : ''}>Beta</option>
|
|
179
|
+
</select>
|
|
180
|
+
</div>
|
|
181
|
+
<button id="update-btn" class="btn btn-primary" onclick="triggerUpdate()" ${running ? 'disabled' : ''}>
|
|
182
|
+
<svg viewBox="0 0 20 20" fill="currentColor" style="width:13px;height:13px;" aria-hidden="true">
|
|
183
|
+
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd"/>
|
|
184
|
+
</svg>
|
|
185
|
+
${running ? 'Updating…' : 'Trigger update'}
|
|
186
|
+
</button>
|
|
187
|
+
</div>` : `
|
|
188
|
+
<p style="font-size:13px;color:var(--text-muted)">Updates are managed for this deployment.</p>`}
|
|
189
|
+
`;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
if (err.message !== 'unauthorized') {
|
|
192
|
+
vEl.innerHTML = '<div class="empty">Failed to load version info</div>';
|
|
193
|
+
uEl.innerHTML = '';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function saveChannel(channel) {
|
|
199
|
+
try {
|
|
200
|
+
await api('/admin/api/update/channel', {
|
|
201
|
+
method: 'PUT',
|
|
202
|
+
headers: { 'Content-Type': 'application/json' },
|
|
203
|
+
body: JSON.stringify({ channel }),
|
|
204
|
+
});
|
|
205
|
+
} catch {}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function triggerUpdate() {
|
|
209
|
+
const btn = document.getElementById('update-btn');
|
|
210
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Starting…'; }
|
|
211
|
+
try {
|
|
212
|
+
const res = await api('/admin/api/update', { method: 'POST' });
|
|
213
|
+
if (!res.ok) {
|
|
214
|
+
const body = await res.json().catch(() => ({}));
|
|
215
|
+
alert(body.error || 'Failed to start update');
|
|
216
|
+
}
|
|
217
|
+
setTimeout(loadVersion, 1200);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
if (err.message !== 'unauthorized') alert('Failed to trigger update');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Configuration ──────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
async function loadConfig() {
|
|
226
|
+
const el = document.getElementById('config-content');
|
|
227
|
+
if (!el) return;
|
|
228
|
+
try {
|
|
229
|
+
const data = await api('/admin/api/config').then((r) => r.json());
|
|
230
|
+
const cfg = data.config || {};
|
|
231
|
+
const keys = Object.keys(cfg);
|
|
232
|
+
if (!keys.length) { el.innerHTML = '<div class="empty">No configuration data</div>'; return; }
|
|
233
|
+
el.innerHTML = `<table class="config-table"><tbody>${
|
|
234
|
+
keys.map((k) => {
|
|
235
|
+
const v = cfg[k];
|
|
236
|
+
const display = v
|
|
237
|
+
? `<span>${esc(v)}</span>`
|
|
238
|
+
: `<span class="config-empty">—</span>`;
|
|
239
|
+
return `<tr><td>${esc(k)}</td><td>${display}</td></tr>`;
|
|
240
|
+
}).join('')
|
|
241
|
+
}</tbody></table>`;
|
|
242
|
+
} catch (err) {
|
|
243
|
+
if (err.message !== 'unauthorized') {
|
|
244
|
+
el.innerHTML = '<div class="empty">Failed to load configuration</div>';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Providers ──────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
async function loadProviders() {
|
|
252
|
+
const el = document.getElementById('providers-content');
|
|
253
|
+
if (!el) return;
|
|
254
|
+
try {
|
|
255
|
+
const data = await api('/admin/api/providers').then((r) => r.json());
|
|
256
|
+
const providers = data.providers || [];
|
|
257
|
+
if (!providers.length) { el.innerHTML = '<div class="empty">No providers configured</div>'; return; }
|
|
258
|
+
el.innerHTML = providers.map((p) => `
|
|
259
|
+
<div class="provider-row" data-key="${esc(p.key)}">
|
|
260
|
+
<div class="provider-meta">
|
|
261
|
+
<span class="provider-name">${esc(p.label)}</span>
|
|
262
|
+
${p.configured
|
|
263
|
+
? `<span class="badge badge-ok">configured</span><span class="provider-hint">${esc(p.hint)}</span>`
|
|
264
|
+
: `<span class="badge badge-idle">not set</span>`}
|
|
265
|
+
</div>
|
|
266
|
+
<div class="provider-controls">
|
|
267
|
+
<input
|
|
268
|
+
type="${p.type === 'url' ? 'text' : 'password'}"
|
|
269
|
+
placeholder="${p.type === 'url' ? 'http://localhost:11434' : 'Paste new key…'}"
|
|
270
|
+
autocomplete="off"
|
|
271
|
+
spellcheck="false"
|
|
272
|
+
aria-label="${esc(p.label)} API key"
|
|
273
|
+
>
|
|
274
|
+
<button class="btn btn-ghost provider-save-btn" onclick="saveProvider('${esc(p.key)}', this)">Save</button>
|
|
275
|
+
${p.configured ? `<button class="btn btn-danger provider-save-btn" onclick="clearProvider('${esc(p.key)}', this)" title="Remove key">✕</button>` : ''}
|
|
276
|
+
</div>
|
|
277
|
+
</div>`).join('');
|
|
278
|
+
} catch (err) {
|
|
279
|
+
if (err.message !== 'unauthorized') {
|
|
280
|
+
el.innerHTML = '<div class="empty">Failed to load providers</div>';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function saveProvider(key, btn) {
|
|
286
|
+
const row = btn.closest('.provider-row');
|
|
287
|
+
const input = row?.querySelector('input');
|
|
288
|
+
const value = input?.value?.trim() || '';
|
|
289
|
+
if (!value) { input?.focus(); return; }
|
|
290
|
+
btn.disabled = true;
|
|
291
|
+
const original = btn.textContent;
|
|
292
|
+
btn.textContent = 'Saving…';
|
|
293
|
+
try {
|
|
294
|
+
const res = await api('/admin/api/providers', {
|
|
295
|
+
method: 'PUT',
|
|
296
|
+
headers: { 'Content-Type': 'application/json' },
|
|
297
|
+
body: JSON.stringify({ key, value }),
|
|
298
|
+
});
|
|
299
|
+
if (!res.ok) {
|
|
300
|
+
const body = await res.json().catch(() => ({}));
|
|
301
|
+
alert(body.error || 'Failed to save');
|
|
302
|
+
} else {
|
|
303
|
+
btn.textContent = 'Saved!';
|
|
304
|
+
setTimeout(loadProviders, 800);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
} catch (err) {
|
|
308
|
+
if (err.message !== 'unauthorized') alert('Network error');
|
|
309
|
+
}
|
|
310
|
+
btn.disabled = false;
|
|
311
|
+
btn.textContent = original;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function clearProvider(key, btn) {
|
|
315
|
+
btn.disabled = true;
|
|
316
|
+
try {
|
|
317
|
+
await api('/admin/api/providers', {
|
|
318
|
+
method: 'PUT',
|
|
319
|
+
headers: { 'Content-Type': 'application/json' },
|
|
320
|
+
body: JSON.stringify({ key, value: '' }),
|
|
321
|
+
});
|
|
322
|
+
setTimeout(loadProviders, 400);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
if (err.message !== 'unauthorized') alert('Network error');
|
|
325
|
+
btn.disabled = false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── Auto-refresh ───────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
function startPolling() {
|
|
332
|
+
setInterval(() => { if (currentPage === 'overview') loadHealth(); }, 30_000);
|
|
333
|
+
setInterval(() => { if (currentPage === 'logs' && !logsCleared) loadLogs(); }, 5_000);
|
|
334
|
+
setInterval(() => { if (currentPage === 'updates') loadVersion(); }, 10_000);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ── Init ───────────────────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
(async function init() {
|
|
340
|
+
try {
|
|
341
|
+
const res = await fetch('/admin/api/version');
|
|
342
|
+
if (res.status === 401) { window.location.replace('/admin/login'); return; }
|
|
343
|
+
} catch {
|
|
344
|
+
// server unreachable — still render; API calls will fail gracefully
|
|
345
|
+
}
|
|
346
|
+
loadHealth();
|
|
347
|
+
startPolling();
|
|
348
|
+
}());
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Analytics ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
async function loadAnalytics() {
|
|
6
|
+
const el = document.getElementById('analytics-content');
|
|
7
|
+
if (!el) return;
|
|
8
|
+
el.innerHTML = '<div class="empty"><span class="spinner"></span></div>';
|
|
9
|
+
try {
|
|
10
|
+
const data = await api('/admin/api/analytics').then((r) => r.json());
|
|
11
|
+
renderAnalytics(el, data);
|
|
12
|
+
setTs('analytics-ts', 'Updated');
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (err.message !== 'unauthorized') el.innerHTML = '<div class="empty">Failed to load analytics</div>';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function renderAnalytics(el, { stats, topUsers, recentRuns }) {
|
|
19
|
+
const s = stats || {};
|
|
20
|
+
el.innerHTML = `
|
|
21
|
+
<div class="stat-grid">
|
|
22
|
+
<div class="stat-card">
|
|
23
|
+
<div class="stat-value">${fmtNum(s.totalUsers)}</div>
|
|
24
|
+
<div class="stat-label">Total Users</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="stat-card">
|
|
27
|
+
<div class="stat-value">${fmtNum(s.activeToday)}</div>
|
|
28
|
+
<div class="stat-label">Active Today</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="stat-card">
|
|
31
|
+
<div class="stat-value">${fmtNum(s.newThisWeek)}</div>
|
|
32
|
+
<div class="stat-label">New This Week</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="stat-card">
|
|
35
|
+
<div class="stat-value">${fmtNum(s.totalRuns)}</div>
|
|
36
|
+
<div class="stat-label">Total Runs</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="stat-card">
|
|
39
|
+
<div class="stat-value">${fmtNum(s.runsToday)}</div>
|
|
40
|
+
<div class="stat-label">Runs Today</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="stat-card">
|
|
43
|
+
<div class="stat-value">${fmtTokens(s.totalTokens)}</div>
|
|
44
|
+
<div class="stat-label">Total Tokens</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="stat-card">
|
|
47
|
+
<div class="stat-value">${fmtNum(s.activeSessions)}</div>
|
|
48
|
+
<div class="stat-label">Active Sessions</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="stat-card">
|
|
51
|
+
<div class="stat-value">${fmtBytes(s.totalStorage)}</div>
|
|
52
|
+
<div class="stat-label">Artifact Storage</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
${renderTopUsers(topUsers || [])}
|
|
57
|
+
${renderRecentRuns(recentRuns || [])}
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderTopUsers(users) {
|
|
62
|
+
if (!users.length) return '';
|
|
63
|
+
const maxRuns = Math.max(...users.map((u) => u.runs), 1);
|
|
64
|
+
return `
|
|
65
|
+
<div class="card" style="margin-top:20px;">
|
|
66
|
+
<div class="card-title">Top Users by Runs</div>
|
|
67
|
+
<table class="analytics-table">
|
|
68
|
+
<thead><tr>
|
|
69
|
+
<th>User</th><th>Runs</th><th>Tokens</th><th>Storage</th><th>Activity</th>
|
|
70
|
+
</tr></thead>
|
|
71
|
+
<tbody>${users.map((u) => `
|
|
72
|
+
<tr>
|
|
73
|
+
<td><span class="user-avatar">${esc((u.display_name || u.username || '?')[0]).toUpperCase()}</span> ${esc(u.display_name || u.username)}</td>
|
|
74
|
+
<td>${fmtNum(u.runs)}</td>
|
|
75
|
+
<td>${fmtTokens(u.tokens)}</td>
|
|
76
|
+
<td>${fmtBytes(u.storage)}</td>
|
|
77
|
+
<td>
|
|
78
|
+
<div class="mini-bar"><div class="mini-bar-fill" style="width:${Math.round((u.runs / maxRuns) * 100)}%"></div></div>
|
|
79
|
+
</td>
|
|
80
|
+
</tr>`).join('')}
|
|
81
|
+
</tbody>
|
|
82
|
+
</table>
|
|
83
|
+
</div>`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderRecentRuns(runs) {
|
|
87
|
+
if (!runs.length) return '';
|
|
88
|
+
return `
|
|
89
|
+
<div class="card" style="margin-top:16px;">
|
|
90
|
+
<div class="card-title">Recent Runs</div>
|
|
91
|
+
<table class="analytics-table">
|
|
92
|
+
<thead><tr>
|
|
93
|
+
<th>User</th><th>Title</th><th>Status</th><th>Tokens</th><th>Started</th>
|
|
94
|
+
</tr></thead>
|
|
95
|
+
<tbody>${runs.map((r) => `
|
|
96
|
+
<tr>
|
|
97
|
+
<td>${esc(r.username)}</td>
|
|
98
|
+
<td class="cell-truncate">${esc(r.title || '(untitled)')}</td>
|
|
99
|
+
<td><span class="run-badge run-${esc(r.status)}">${esc(r.status)}</span></td>
|
|
100
|
+
<td>${fmtTokens(r.total_tokens)}</td>
|
|
101
|
+
<td>${fmtTime(r.created_at)}</td>
|
|
102
|
+
</tr>`).join('')}
|
|
103
|
+
</tbody>
|
|
104
|
+
</table>
|
|
105
|
+
</div>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Shared number helpers (also used by other modules) ────────────────────
|
|
109
|
+
|
|
110
|
+
function fmtNum(n) {
|
|
111
|
+
if (n == null) return '—';
|
|
112
|
+
return Number(n).toLocaleString();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function fmtTokens(n) {
|
|
116
|
+
if (!n) return '0';
|
|
117
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
|
|
118
|
+
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
|
|
119
|
+
return String(n);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function fmtBytes(n) {
|
|
123
|
+
if (!n) return '0 B';
|
|
124
|
+
if (n >= 1_073_741_824) return (n / 1_073_741_824).toFixed(1) + ' GB';
|
|
125
|
+
if (n >= 1_048_576) return (n / 1_048_576).toFixed(1) + ' MB';
|
|
126
|
+
if (n >= 1_024) return (n / 1_024).toFixed(1) + ' KB';
|
|
127
|
+
return n + ' B';
|
|
128
|
+
}
|