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.
Files changed (248) hide show
  1. package/.env.example +33 -3
  2. package/LICENSE +111 -56
  3. package/README.md +8 -3
  4. package/docs/configuration.md +8 -0
  5. package/docs/getting-started.md +9 -3
  6. package/docs/index.md +4 -0
  7. package/extensions/chrome-browser/background.mjs +45 -7
  8. package/extensions/chrome-browser/icons/icon128.png +0 -0
  9. package/extensions/chrome-browser/icons/icon16.png +0 -0
  10. package/extensions/chrome-browser/icons/icon48.png +0 -0
  11. package/extensions/chrome-browser/icons/logo.svg +39 -8
  12. package/extensions/chrome-browser/manifest.json +3 -2
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +15 -2
  15. package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
  16. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  17. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  18. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  19. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  20. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  21. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  22. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  23. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  24. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  25. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  26. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  27. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  28. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  29. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  30. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  31. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  32. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  33. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  34. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  35. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  36. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  37. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  38. package/flutter_app/lib/features/location/location_service.dart +3 -0
  39. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  40. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
  41. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  42. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
  43. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
  44. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  45. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
  46. package/flutter_app/lib/main.dart +3 -0
  47. package/flutter_app/lib/main_account_settings.dart +10 -34
  48. package/flutter_app/lib/main_admin.dart +14 -1
  49. package/flutter_app/lib/main_app_shell.dart +377 -340
  50. package/flutter_app/lib/main_chat.dart +707 -227
  51. package/flutter_app/lib/main_controller.dart +338 -46
  52. package/flutter_app/lib/main_devices.dart +851 -122
  53. package/flutter_app/lib/main_integrations.dart +255 -6
  54. package/flutter_app/lib/main_launcher.dart +1 -1
  55. package/flutter_app/lib/main_model_picker.dart +685 -0
  56. package/flutter_app/lib/main_models.dart +212 -0
  57. package/flutter_app/lib/main_navigation.dart +1 -9
  58. package/flutter_app/lib/main_operations.dart +1417 -614
  59. package/flutter_app/lib/main_settings.dart +764 -887
  60. package/flutter_app/lib/main_shared.dart +971 -443
  61. package/flutter_app/lib/main_spacing.dart +4 -4
  62. package/flutter_app/lib/main_theme.dart +22 -14
  63. package/flutter_app/lib/main_unified.dart +5 -72
  64. package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
  65. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
  66. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
  67. package/flutter_app/lib/src/backend_client.dart +79 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
  70. package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
  71. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  72. package/flutter_app/lib/src/stream_renderer.dart +205 -35
  73. package/flutter_app/lib/src/theme/palette.dart +76 -34
  74. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  75. package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  81. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  82. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  83. package/flutter_app/pubspec.lock +2 -2
  84. package/flutter_app/pubspec.yaml +7 -1
  85. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  94. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  95. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  96. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  97. package/flutter_app/web/favicon.png +0 -0
  98. package/flutter_app/web/favicon.svg +40 -9
  99. package/flutter_app/web/favicon_light.svg +43 -0
  100. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-192.png +0 -0
  102. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-512.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  106. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  107. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  108. package/flutter_app/windows/runner/main.cpp +7 -1
  109. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  110. package/lib/manager.js +445 -83
  111. package/package.json +17 -3
  112. package/runtime/paths.js +3 -1
  113. package/server/admin/access.js +198 -0
  114. package/server/admin/admin.css +268 -0
  115. package/server/admin/admin.js +348 -0
  116. package/server/admin/analytics.js +128 -0
  117. package/server/admin/index.html +1015 -0
  118. package/server/admin/login.html +290 -0
  119. package/server/admin/logo.svg +43 -0
  120. package/server/admin/sql.js +134 -0
  121. package/server/admin/users.js +147 -0
  122. package/server/config/origins.js +3 -1
  123. package/server/db/database.js +92 -0
  124. package/server/db/ftsQuery.js +27 -0
  125. package/server/http/routes.js +1 -0
  126. package/server/http/static.js +23 -6
  127. package/server/index.js +1 -1
  128. package/server/middleware/adminAuth.js +48 -0
  129. package/server/middleware/auth.js +1 -40
  130. package/server/public/.last_build_id +1 -1
  131. package/server/public/app_icon.png +0 -0
  132. package/server/public/assets/AssetManifest.bin +1 -1
  133. package/server/public/assets/AssetManifest.bin.json +1 -1
  134. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  138. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  139. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  140. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  141. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  142. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  143. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  144. package/server/public/assets/web/icons/Icon-192.png +0 -0
  145. package/server/public/favicon.png +0 -0
  146. package/server/public/favicon.svg +40 -9
  147. package/server/public/favicon_light.svg +43 -0
  148. package/server/public/flutter_bootstrap.js +2 -2
  149. package/server/public/icons/Icon-192-light.png +0 -0
  150. package/server/public/icons/Icon-192.png +0 -0
  151. package/server/public/icons/Icon-512-light.png +0 -0
  152. package/server/public/icons/Icon-512.png +0 -0
  153. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-192.png +0 -0
  155. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  156. package/server/public/icons/Icon-maskable-512.png +0 -0
  157. package/server/public/main.dart.js +88944 -85720
  158. package/server/routes/admin.js +632 -0
  159. package/server/routes/agent_profiles.js +3 -0
  160. package/server/routes/agents.js +1 -1
  161. package/server/routes/auth.js +3 -1
  162. package/server/routes/browser.js +14 -0
  163. package/server/routes/browser_extension.js +21 -4
  164. package/server/routes/desktop.js +10 -0
  165. package/server/routes/mcp.js +29 -13
  166. package/server/routes/memory.js +23 -1
  167. package/server/routes/messaging.js +2 -0
  168. package/server/routes/screenHistory.js +14 -8
  169. package/server/routes/settings.js +14 -2
  170. package/server/routes/stream.js +12 -3
  171. package/server/routes/triggers.js +4 -4
  172. package/server/routes/voice_assistant.js +36 -1
  173. package/server/routes/workspace.js +86 -0
  174. package/server/services/account/admin_two_factor.js +132 -0
  175. package/server/services/account/password_policy.js +6 -1
  176. package/server/services/ai/completion.js +44 -0
  177. package/server/services/ai/engine.js +321 -500
  178. package/server/services/ai/imageAnalysis.js +9 -5
  179. package/server/services/ai/logFormat.js +46 -0
  180. package/server/services/ai/loopPolicy.js +11 -0
  181. package/server/services/ai/messagingFallback.js +228 -0
  182. package/server/services/ai/models.js +194 -55
  183. package/server/services/ai/providerRetry.js +169 -0
  184. package/server/services/ai/providers/anthropic.js +15 -4
  185. package/server/services/ai/providers/google.js +38 -11
  186. package/server/services/ai/providers/grok.js +19 -68
  187. package/server/services/ai/providers/grokOauth.js +141 -0
  188. package/server/services/ai/providers/nvidia.js +154 -0
  189. package/server/services/ai/providers/ollama.js +76 -39
  190. package/server/services/ai/providers/openai.js +20 -31
  191. package/server/services/ai/providers/openaiCodex.js +6 -2
  192. package/server/services/ai/providers/openaiCompatible.js +70 -0
  193. package/server/services/ai/providers/openrouter.js +162 -0
  194. package/server/services/ai/settings.js +30 -0
  195. package/server/services/ai/systemPrompt.js +51 -29
  196. package/server/services/ai/taskAnalysis.js +60 -1
  197. package/server/services/ai/toolEvidence.js +207 -0
  198. package/server/services/ai/toolSelector.js +8 -1
  199. package/server/services/ai/tools.js +80 -15
  200. package/server/services/android/controller.js +36 -1
  201. package/server/services/browser/controller.js +18 -0
  202. package/server/services/browser/extension/manifest.js +33 -0
  203. package/server/services/browser/extension/provider.js +12 -6
  204. package/server/services/browser/extension/registry.js +229 -18
  205. package/server/services/desktop/protocol.js +1 -0
  206. package/server/services/desktop/provider.js +4 -0
  207. package/server/services/desktop/registry.js +104 -1
  208. package/server/services/desktop/screenRecorder.js +208 -98
  209. package/server/services/desktop/screen_recorder_support.js +46 -0
  210. package/server/services/integrations/google/provider.js +13 -0
  211. package/server/services/integrations/home_assistant/constants.js +88 -0
  212. package/server/services/integrations/home_assistant/network.js +207 -0
  213. package/server/services/integrations/home_assistant/provider.js +249 -0
  214. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  215. package/server/services/integrations/home_assistant/tools.js +101 -0
  216. package/server/services/integrations/manager.js +5 -0
  217. package/server/services/integrations/registry.js +2 -0
  218. package/server/services/integrations/trello/provider.js +8 -2
  219. package/server/services/manager.js +51 -47
  220. package/server/services/mcp/client.js +208 -268
  221. package/server/services/mcp/client_support.js +172 -0
  222. package/server/services/mcp/recovery.js +116 -0
  223. package/server/services/mcp/tool_operations.js +123 -0
  224. package/server/services/memory/ingestion.js +185 -370
  225. package/server/services/memory/ingestion_coverage.js +129 -0
  226. package/server/services/memory/ingestion_documents.js +123 -0
  227. package/server/services/memory/ingestion_support.js +171 -0
  228. package/server/services/memory/intelligence.js +181 -0
  229. package/server/services/memory/manager.js +476 -32
  230. package/server/services/messaging/automation.js +71 -134
  231. package/server/services/messaging/formatting_guides.js +26 -1
  232. package/server/services/messaging/inbound_queue.js +111 -0
  233. package/server/services/messaging/manager.js +25 -8
  234. package/server/services/messaging/typing_keepalive.js +110 -0
  235. package/server/services/runtime/manager.js +7 -3
  236. package/server/services/runtime/settings.js +17 -0
  237. package/server/services/streaming/android-stream.js +296 -32
  238. package/server/services/streaming/stream-hub.js +1 -1
  239. package/server/services/tasks/integration_runtime.js +4 -1
  240. package/server/services/tasks/runtime.js +415 -56
  241. package/server/services/tasks/task_repository.js +48 -6
  242. package/server/services/websocket.js +8 -2
  243. package/server/services/widgets/service.js +17 -1
  244. package/server/services/workspace/manager.js +116 -0
  245. package/server/utils/logger.js +44 -6
  246. package/server/utils/security.js +3 -0
  247. package/server/utils/version.js +29 -19
  248. package/server/services/memory/openhuman_uplift.test.js +0 -98
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.4.1-beta.8",
3
+ "version": "2.4.1",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -38,7 +38,18 @@
38
38
  "flutter:run:web": "cd flutter_app && flutter run -d chrome --dart-define=NEOAGENT_WEB_BUILD_ID=$(node ../scripts/web_build_id.js)",
39
39
  "flutter:build:web": "cd flutter_app && flutter build web --output ../server/public --dart-define=NEOAGENT_BACKEND_URL=${NEOAGENT_BACKEND_URL:-} --dart-define=NEOAGENT_WEB_BUILD_ID=$(node ../scripts/web_build_id.js)",
40
40
  "manage": "node bin/neoagent.js",
41
- "test": "node --test",
41
+ "test": "npm run test:backend",
42
+ "test:unit": "node --test --test-reporter=spec test/backend/unit/*.test.js",
43
+ "test:integration": "node --test --test-reporter=spec test/integration/*.test.js",
44
+ "test:security": "node --test --test-reporter=spec test/security/*.test.js",
45
+ "test:contract": "node --test --test-reporter=spec test/contract/*.test.js",
46
+ "test:e2e": "node --test --test-reporter=spec test/e2e/*.test.js",
47
+ "test:ws": "node --test --test-reporter=spec test/websocket/*.test.js",
48
+ "test:backend": "npm run test:unit && npm run test:integration && npm run test:security && npm run test:contract && npm run test:e2e && npm run test:ws",
49
+ "test:load": "node test/load/auth_load.js",
50
+ "flutter:test": "cd flutter_app && flutter test ../test/flutter/unit ../test/flutter/widget",
51
+ "flutter:test:unit": "cd flutter_app && flutter test ../test/flutter/unit",
52
+ "flutter:test:widget": "cd flutter_app && flutter test ../test/flutter/widget",
42
53
  "benchmark:tokens": "node scripts/benchmark-token-cost.js",
43
54
  "release": "npx semantic-release"
44
55
  },
@@ -104,7 +115,10 @@
104
115
  "devDependencies": {
105
116
  "@docusaurus/core": "3.10.0",
106
117
  "@docusaurus/preset-classic": "3.10.0",
118
+ "autocannon": "^7.15.0",
107
119
  "react": "18.3.1",
108
- "react-dom": "18.3.1"
120
+ "react-dom": "18.3.1",
121
+ "socket.io-client": "^4.8.3",
122
+ "supertest": "^7.2.2"
109
123
  }
110
124
  }
package/runtime/paths.js CHANGED
@@ -246,5 +246,7 @@ module.exports = {
246
246
  ensureRuntimeDirs,
247
247
  ensureSecureRuntimeEnv,
248
248
  getDefaultVmBaseImageUrl,
249
- migrateLegacyRuntime
249
+ migrateLegacyRuntime,
250
+ upsertEnvValue,
251
+ readEnvFileRaw,
250
252
  };
@@ -0,0 +1,198 @@
1
+ 'use strict';
2
+
3
+ // ── Access Settings ────────────────────────────────────────────────────────
4
+
5
+ let _revealedKey = null; // holds the API key shown after rotation (one-time)
6
+ let _revealTimer = null;
7
+
8
+ async function loadAccess() {
9
+ const el = document.getElementById('access-content');
10
+ if (!el) return;
11
+ try {
12
+ const data = await api('/admin/api/settings').then((r) => r.json());
13
+ renderAccess(el, data);
14
+ } catch (err) {
15
+ if (err.message !== 'unauthorized') el.innerHTML = '<div class="empty">Failed to load settings</div>';
16
+ }
17
+ }
18
+
19
+ function renderAccess(el, { signupEnabled, apiKeyConfigured, apiKeyHint }) {
20
+ el.innerHTML = `
21
+ <!-- Signup -->
22
+ <div class="card">
23
+ <div class="card-title">User Signups</div>
24
+ <div class="access-row">
25
+ <div>
26
+ <div class="access-label">Allow new registrations</div>
27
+ <div class="access-desc">
28
+ When disabled, only existing users can log in. The first account can always be created regardless of this setting.
29
+ </div>
30
+ </div>
31
+ <label class="toggle" aria-label="Toggle signups">
32
+ <input type="checkbox" id="signup-toggle" ${signupEnabled ? 'checked' : ''}
33
+ onchange="setSignup(this.checked)">
34
+ <span class="toggle-track"></span>
35
+ <span class="toggle-thumb"></span>
36
+ </label>
37
+ </div>
38
+ <div id="signup-status" class="access-status"></div>
39
+ </div>
40
+
41
+ <!-- API key -->
42
+ <div class="card">
43
+ <div class="card-title">Admin API Key</div>
44
+ <div class="access-row">
45
+ <div>
46
+ <div class="access-label">Programmatic access</div>
47
+ <div class="access-desc">
48
+ Use <code>Authorization: Bearer &lt;key&gt;</code> to authenticate against any <code>/admin/api/*</code> endpoint from scripts or automation — without a browser session.
49
+ </div>
50
+ </div>
51
+ <span class="badge ${apiKeyConfigured ? 'badge-ok' : 'badge-idle'}">
52
+ ${apiKeyConfigured ? 'configured' : 'not set'}
53
+ </span>
54
+ </div>
55
+
56
+ ${apiKeyConfigured ? `
57
+ <div class="access-hint-row">
58
+ <code class="api-key-hint">${esc(apiKeyHint)}</code>
59
+ <span style="font-size:11px;color:var(--text-muted);">stored in .env — rotate to reveal</span>
60
+ </div>` : ''}
61
+
62
+ <div id="key-reveal-wrap" style="display:none;">
63
+ <div class="key-reveal-box">
64
+ <div style="font-size:11px;color:var(--text-muted);margin-bottom:6px;">
65
+ New API key — copy it now. It will not be shown again.
66
+ </div>
67
+ <div style="display:flex;align-items:center;gap:8px;">
68
+ <code id="key-reveal-value" class="api-key-full"></code>
69
+ <button class="btn btn-ghost" style="padding:5px 10px;font-size:11px;flex-shrink:0;"
70
+ onclick="copyApiKey()">Copy</button>
71
+ </div>
72
+ <div style="margin-top:8px;font-size:11px;color:var(--text-muted);" id="key-reveal-timer"></div>
73
+ </div>
74
+ </div>
75
+
76
+ <div class="access-actions">
77
+ <button class="btn btn-ghost" onclick="rotateApiKey(this)">
78
+ ${apiKeyConfigured ? 'Rotate key' : 'Generate key'}
79
+ </button>
80
+ ${apiKeyConfigured ? `
81
+ <button class="btn btn-danger" onclick="revokeApiKey(this)">Revoke key</button>` : ''}
82
+ </div>
83
+ <div id="apikey-status" class="access-status"></div>
84
+ </div>`;
85
+
86
+ // If we rotated in this page load, re-show the key
87
+ if (_revealedKey) showReveal(_revealedKey);
88
+ }
89
+
90
+ async function setSignup(enabled) {
91
+ const statusEl = document.getElementById('signup-status');
92
+ if (statusEl) statusEl.textContent = '';
93
+ try {
94
+ const res = await api('/admin/api/settings/signup', {
95
+ method: 'PUT',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({ enabled }),
98
+ });
99
+ if (res.ok) {
100
+ showStatus('signup-status', enabled ? 'Signups enabled.' : 'Signups disabled.', false);
101
+ } else {
102
+ const b = await res.json().catch(() => ({}));
103
+ showStatus('signup-status', b.error || 'Failed to update', true);
104
+ // Revert the toggle
105
+ const toggle = document.getElementById('signup-toggle');
106
+ if (toggle) toggle.checked = !enabled;
107
+ }
108
+ } catch (err) {
109
+ if (err.message !== 'unauthorized') showStatus('signup-status', 'Network error', true);
110
+ const toggle = document.getElementById('signup-toggle');
111
+ if (toggle) toggle.checked = !enabled;
112
+ }
113
+ }
114
+
115
+ async function rotateApiKey(btn) {
116
+ if (!confirm(
117
+ 'Rotate admin API key?\n\n' +
118
+ 'The current key will stop working immediately.\n' +
119
+ 'Any scripts using it must be updated.\n\n' +
120
+ 'The new key will be shown once — copy it before leaving.'
121
+ )) return;
122
+ btn.disabled = true;
123
+ btn.textContent = 'Rotating…';
124
+ try {
125
+ const res = await api('/admin/api/settings/apikey/rotate', { method: 'POST' });
126
+ const data = await res.json();
127
+ if (res.ok && data.apiKey) {
128
+ _revealedKey = data.apiKey;
129
+ await loadAccess(); // re-render to show badge + hint
130
+ } else {
131
+ showStatus('apikey-status', data.error || 'Failed to rotate', true);
132
+ btn.disabled = false;
133
+ btn.textContent = 'Rotate key';
134
+ }
135
+ } catch (err) {
136
+ if (err.message !== 'unauthorized') showStatus('apikey-status', 'Network error', true);
137
+ btn.disabled = false;
138
+ btn.textContent = 'Rotate key';
139
+ }
140
+ }
141
+
142
+ async function revokeApiKey(btn) {
143
+ if (!confirm('Revoke admin API key?\n\nAll programmatic access using this key will stop immediately.')) return;
144
+ btn.disabled = true;
145
+ btn.textContent = 'Revoking…';
146
+ _revealedKey = null;
147
+ try {
148
+ const res = await api('/admin/api/settings/apikey', { method: 'DELETE' });
149
+ if (res.ok) {
150
+ await loadAccess();
151
+ } else {
152
+ const b = await res.json().catch(() => ({}));
153
+ showStatus('apikey-status', b.error || 'Failed', true);
154
+ btn.disabled = false;
155
+ btn.textContent = 'Revoke key';
156
+ }
157
+ } catch (err) {
158
+ if (err.message !== 'unauthorized') showStatus('apikey-status', 'Network error', true);
159
+ btn.disabled = false;
160
+ btn.textContent = 'Revoke key';
161
+ }
162
+ }
163
+
164
+ function copyApiKey() {
165
+ if (_revealedKey) navigator.clipboard.writeText(_revealedKey).catch(() => {});
166
+ }
167
+
168
+ function showReveal(key) {
169
+ const wrap = document.getElementById('key-reveal-wrap');
170
+ const value = document.getElementById('key-reveal-value');
171
+ if (!wrap || !value) return;
172
+ value.textContent = key;
173
+ wrap.style.display = 'block';
174
+ // Auto-clear after 120 seconds
175
+ clearTimeout(_revealTimer);
176
+ let secs = 120;
177
+ const timerEl = document.getElementById('key-reveal-timer');
178
+ const tick = () => {
179
+ if (!timerEl) return;
180
+ if (secs <= 0) {
181
+ wrap.style.display = 'none';
182
+ _revealedKey = null;
183
+ return;
184
+ }
185
+ timerEl.textContent = `Hides in ${secs}s`;
186
+ secs--;
187
+ _revealTimer = setTimeout(tick, 1000);
188
+ };
189
+ tick();
190
+ }
191
+
192
+ function showStatus(id, msg, isError) {
193
+ const el = document.getElementById(id);
194
+ if (!el) return;
195
+ el.textContent = msg;
196
+ el.style.color = isError ? 'var(--danger)' : 'var(--success)';
197
+ setTimeout(() => { if (el) el.textContent = ''; }, 4000);
198
+ }
@@ -0,0 +1,268 @@
1
+ /* NeoAgent Admin — shared design tokens and base styles */
2
+
3
+ *, *::before, *::after {
4
+ box-sizing: border-box;
5
+ margin: 0;
6
+ padding: 0;
7
+ }
8
+
9
+ /* ── Tokens (NeoAgent dark palette) ─────────────────────────────────────── */
10
+ :root {
11
+ --bg: #0e1511;
12
+ --bg-sidebar: #0a0f0c;
13
+ --bg-card: #171f1a;
14
+ --bg-secondary: #252d28;
15
+ --bg-input: #0a0f0c;
16
+ --border: rgba(224, 240, 224, 0.10);
17
+ --border-light: rgba(224, 240, 224, 0.17);
18
+
19
+ --accent: #e1b052;
20
+ --accent-hover: #eac272;
21
+ --accent-alt: #84ba87;
22
+ --accent-muted: rgba(225, 176, 82, 0.16);
23
+
24
+ --text: #ecefe5;
25
+ --text-secondary:#aeb7a6;
26
+ --text-muted: #7e8877;
27
+
28
+ --success: #74c07c;
29
+ --warning: #d9a24b;
30
+ --danger: #de8a78;
31
+ --info: #6fb0a4;
32
+
33
+ --sidebar-w: 220px;
34
+ --radius: 10px;
35
+ --radius-sm: 7px;
36
+
37
+ --font-sans: 'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
38
+ --font-mono: 'Geist Mono', 'JetBrains Mono', 'Fira Code', 'Menlo', monospace;
39
+ }
40
+
41
+ /* ── Base ────────────────────────────────────────────────────────────────── */
42
+ html, body {
43
+ height: 100%;
44
+ }
45
+
46
+ body {
47
+ background: var(--bg);
48
+ color: var(--text);
49
+ font-family: var(--font-sans);
50
+ font-size: 14px;
51
+ line-height: 1.5;
52
+ -webkit-font-smoothing: antialiased;
53
+ -moz-osx-font-smoothing: grayscale;
54
+ }
55
+
56
+ /* ── Brand mark ──────────────────────────────────────────────────────────── */
57
+ .brand {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 10px;
61
+ text-decoration: none;
62
+ color: inherit;
63
+ }
64
+
65
+ .brand-mark {
66
+ width: 28px;
67
+ height: 28px;
68
+ border-radius: 8px;
69
+ flex-shrink: 0;
70
+ overflow: hidden;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ }
75
+
76
+ .brand-mark img {
77
+ width: 28px;
78
+ height: 28px;
79
+ display: block;
80
+ }
81
+
82
+ .brand-name {
83
+ font-size: 15px;
84
+ font-weight: 700;
85
+ color: var(--text);
86
+ line-height: 1.1;
87
+ letter-spacing: -0.2px;
88
+ }
89
+
90
+ .brand-sub {
91
+ font-size: 11px;
92
+ color: var(--text-muted);
93
+ font-weight: 500;
94
+ letter-spacing: 0.02em;
95
+ text-transform: uppercase;
96
+ }
97
+
98
+ /* ── Forms ───────────────────────────────────────────────────────────────── */
99
+ label {
100
+ display: block;
101
+ font-size: 12px;
102
+ font-weight: 600;
103
+ letter-spacing: 0.04em;
104
+ color: var(--text-muted);
105
+ text-transform: uppercase;
106
+ margin-bottom: 7px;
107
+ }
108
+
109
+ input[type="text"],
110
+ input[type="password"],
111
+ select {
112
+ width: 100%;
113
+ background: var(--bg-input);
114
+ border: 1px solid var(--border-light);
115
+ border-radius: var(--radius-sm);
116
+ padding: 10px 14px;
117
+ color: var(--text);
118
+ font-family: var(--font-sans);
119
+ font-size: 14px;
120
+ outline: none;
121
+ transition: border-color 0.15s;
122
+ -webkit-appearance: none;
123
+ }
124
+
125
+ input[type="text"]:focus,
126
+ input[type="password"]:focus,
127
+ select:focus {
128
+ border-color: var(--accent);
129
+ box-shadow: 0 0 0 3px var(--accent-muted);
130
+ }
131
+
132
+ .field {
133
+ margin-bottom: 20px;
134
+ }
135
+
136
+ /* ── Buttons ─────────────────────────────────────────────────────────────── */
137
+ button,
138
+ .btn {
139
+ display: inline-flex;
140
+ align-items: center;
141
+ justify-content: center;
142
+ gap: 7px;
143
+ padding: 8px 16px;
144
+ border-radius: var(--radius-sm);
145
+ border: none;
146
+ cursor: pointer;
147
+ font-family: var(--font-sans);
148
+ font-size: 13px;
149
+ font-weight: 600;
150
+ transition: background 0.15s, opacity 0.15s, box-shadow 0.15s;
151
+ white-space: nowrap;
152
+ }
153
+
154
+ button:disabled,
155
+ .btn:disabled {
156
+ opacity: 0.4;
157
+ cursor: default;
158
+ pointer-events: none;
159
+ }
160
+
161
+ .btn-primary {
162
+ background: var(--accent);
163
+ color: var(--bg);
164
+ }
165
+
166
+ .btn-primary:hover:not(:disabled) {
167
+ background: var(--accent-hover);
168
+ box-shadow: 0 2px 12px rgba(225, 176, 82, 0.22);
169
+ }
170
+
171
+ .btn-ghost {
172
+ background: var(--bg-secondary);
173
+ color: var(--text-secondary);
174
+ border: 1px solid var(--border-light);
175
+ }
176
+
177
+ .btn-ghost:hover:not(:disabled) {
178
+ color: var(--text);
179
+ background: rgba(224, 240, 224, 0.07);
180
+ }
181
+
182
+ .btn-danger {
183
+ background: rgba(222, 138, 120, 0.12);
184
+ color: var(--danger);
185
+ border: 1px solid rgba(222, 138, 120, 0.20);
186
+ }
187
+
188
+ .btn-danger:hover:not(:disabled) {
189
+ background: rgba(222, 138, 120, 0.20);
190
+ }
191
+
192
+ /* ── Card ────────────────────────────────────────────────────────────────── */
193
+ .card {
194
+ background: var(--bg-card);
195
+ border: 1px solid var(--border);
196
+ border-radius: var(--radius);
197
+ padding: 20px;
198
+ margin-bottom: 16px;
199
+ }
200
+
201
+ .card-title {
202
+ font-size: 11px;
203
+ font-weight: 600;
204
+ text-transform: uppercase;
205
+ letter-spacing: 0.08em;
206
+ color: var(--text-muted);
207
+ margin-bottom: 14px;
208
+ }
209
+
210
+ /* ── Badges ──────────────────────────────────────────────────────────────── */
211
+ .badge {
212
+ display: inline-block;
213
+ padding: 2px 9px;
214
+ border-radius: 999px;
215
+ font-size: 11px;
216
+ font-weight: 600;
217
+ letter-spacing: 0.03em;
218
+ }
219
+
220
+ .badge-ok { background: rgba(116,192,124,0.15); color: var(--success); }
221
+ .badge-warn { background: rgba(217,162,75,0.15); color: var(--warning); }
222
+ .badge-err { background: rgba(222,138,120,0.15); color: var(--danger); }
223
+ .badge-idle { background: rgba(126,136,119,0.15); color: var(--text-muted); }
224
+ .badge-running { background: var(--accent-muted); color: var(--accent); }
225
+
226
+ /* ── Spinner ─────────────────────────────────────────────────────────────── */
227
+ @keyframes spin { to { transform: rotate(360deg); } }
228
+
229
+ .spinner {
230
+ display: inline-block;
231
+ width: 14px;
232
+ height: 14px;
233
+ border: 2px solid var(--border-light);
234
+ border-top-color: var(--accent);
235
+ border-radius: 50%;
236
+ animation: spin 0.65s linear infinite;
237
+ flex-shrink: 0;
238
+ }
239
+
240
+ /* ── Empty state ─────────────────────────────────────────────────────────── */
241
+ .empty {
242
+ padding: 28px;
243
+ text-align: center;
244
+ color: var(--text-muted);
245
+ font-size: 13px;
246
+ }
247
+
248
+ /* ── Alert ───────────────────────────────────────────────────────────────── */
249
+ .alert {
250
+ border-radius: var(--radius-sm);
251
+ padding: 10px 14px;
252
+ font-size: 13px;
253
+ display: none;
254
+ }
255
+
256
+ .alert.visible { display: block; }
257
+
258
+ .alert-error {
259
+ background: rgba(222, 138, 120, 0.10);
260
+ border: 1px solid rgba(222, 138, 120, 0.25);
261
+ color: var(--danger);
262
+ }
263
+
264
+ .alert-success {
265
+ background: rgba(116, 192, 124, 0.10);
266
+ border: 1px solid rgba(116, 192, 124, 0.25);
267
+ color: var(--success);
268
+ }