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
@@ -0,0 +1,1015 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NeoAgent Admin</title>
7
+ <link rel="icon" href="/admin/logo.svg" type="image/svg+xml">
8
+ <link rel="stylesheet" href="/admin/admin.css">
9
+ <style>
10
+ /* ── Layout ────────────────────────────────────────────────────── */
11
+ body {
12
+ display: flex;
13
+ min-height: 100vh;
14
+ }
15
+
16
+ .sidebar {
17
+ width: var(--sidebar-w);
18
+ background: var(--bg-sidebar);
19
+ border-right: 1px solid var(--border);
20
+ display: flex;
21
+ flex-direction: column;
22
+ position: fixed;
23
+ inset: 0 auto 0 0;
24
+ z-index: 10;
25
+ }
26
+
27
+ .sidebar-header {
28
+ padding: 20px 18px 16px;
29
+ border-bottom: 1px solid var(--border);
30
+ }
31
+
32
+ nav {
33
+ flex: 1;
34
+ padding: 10px 10px;
35
+ overflow-y: auto;
36
+ }
37
+
38
+ .nav-section-label {
39
+ font-size: 10px;
40
+ font-weight: 600;
41
+ text-transform: uppercase;
42
+ letter-spacing: 0.10em;
43
+ color: var(--text-muted);
44
+ padding: 10px 10px 4px;
45
+ }
46
+
47
+ .nav-item {
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 9px;
51
+ padding: 8px 10px;
52
+ border-radius: var(--radius-sm);
53
+ color: var(--text-secondary);
54
+ font-size: 13px;
55
+ font-weight: 500;
56
+ cursor: pointer;
57
+ transition: background 0.12s, color 0.12s;
58
+ border: none;
59
+ background: none;
60
+ width: 100%;
61
+ text-align: left;
62
+ text-decoration: none;
63
+ }
64
+
65
+ .nav-item:hover { background: rgba(224,240,224,0.05); color: var(--text); }
66
+
67
+ .nav-item.active {
68
+ background: rgba(225, 176, 82, 0.12);
69
+ color: var(--accent);
70
+ }
71
+
72
+ .nav-item svg {
73
+ width: 15px;
74
+ height: 15px;
75
+ flex-shrink: 0;
76
+ opacity: 0.75;
77
+ }
78
+
79
+ .nav-item.active svg { opacity: 1; }
80
+
81
+ .sidebar-footer {
82
+ padding: 10px;
83
+ border-top: 1px solid var(--border);
84
+ }
85
+
86
+ .sidebar-legal {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 6px;
90
+ padding: 4px 8px 8px;
91
+ font-size: 11px;
92
+ color: var(--text-muted, #666);
93
+ }
94
+ .sidebar-legal a {
95
+ color: var(--text-muted, #666);
96
+ text-decoration: none;
97
+ transition: color 0.15s;
98
+ }
99
+ .sidebar-legal a:hover { color: var(--text, #ccc); }
100
+ .sidebar-legal span { opacity: 0.4; }
101
+
102
+ .main {
103
+ margin-left: var(--sidebar-w);
104
+ flex: 1;
105
+ display: flex;
106
+ flex-direction: column;
107
+ min-height: 100vh;
108
+ }
109
+
110
+ /* ── Page header ───────────────────────────────────────────────── */
111
+ .page-header {
112
+ padding: 28px 28px 0;
113
+ display: flex;
114
+ align-items: flex-start;
115
+ justify-content: space-between;
116
+ gap: 16px;
117
+ }
118
+
119
+ .page-title {
120
+ font-size: 20px;
121
+ font-weight: 700;
122
+ letter-spacing: -0.3px;
123
+ color: var(--text);
124
+ }
125
+
126
+ .page-subtitle {
127
+ color: var(--text-muted);
128
+ font-size: 13px;
129
+ margin-top: 3px;
130
+ }
131
+
132
+ .refresh-hint {
133
+ font-size: 12px;
134
+ color: var(--text-muted);
135
+ white-space: nowrap;
136
+ padding-top: 6px;
137
+ }
138
+
139
+ .content {
140
+ padding: 20px 28px 48px;
141
+ }
142
+
143
+ /* ── Pages ─────────────────────────────────────────────────────── */
144
+ .page { display: none; }
145
+ .page.active { display: block; }
146
+
147
+ /* ── Status grid ───────────────────────────────────────────────── */
148
+ .status-grid {
149
+ display: grid;
150
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
151
+ gap: 10px;
152
+ }
153
+
154
+ .status-tile {
155
+ background: var(--bg-secondary);
156
+ border: 1px solid var(--border);
157
+ border-radius: 8px;
158
+ padding: 14px 16px;
159
+ display: flex;
160
+ align-items: flex-start;
161
+ gap: 12px;
162
+ }
163
+
164
+ .status-dot {
165
+ width: 8px;
166
+ height: 8px;
167
+ border-radius: 50%;
168
+ margin-top: 5px;
169
+ flex-shrink: 0;
170
+ }
171
+
172
+ .status-dot.ok { background: var(--success); box-shadow: 0 0 6px rgba(116,192,124,0.5); }
173
+ .status-dot.fail { background: var(--danger); box-shadow: 0 0 6px rgba(222,138,120,0.5); }
174
+
175
+ .status-label { font-size: 13px; font-weight: 600; color: var(--text); }
176
+ .status-detail { font-size: 12px; color: var(--text-muted); margin-top: 3px; line-height: 1.4; }
177
+
178
+ /* ── Logs ──────────────────────────────────────────────────────── */
179
+ .toolbar {
180
+ display: flex;
181
+ align-items: center;
182
+ gap: 8px;
183
+ margin-bottom: 12px;
184
+ }
185
+
186
+ .toolbar-count {
187
+ font-size: 13px;
188
+ color: var(--text-muted);
189
+ }
190
+
191
+ .spacer { flex: 1; }
192
+
193
+ .log-table {
194
+ background: var(--bg-sidebar);
195
+ border: 1px solid var(--border);
196
+ border-radius: 8px;
197
+ overflow: hidden;
198
+ max-height: 540px;
199
+ overflow-y: auto;
200
+ }
201
+
202
+ .log-row {
203
+ display: grid;
204
+ grid-template-columns: 80px 46px 1fr;
205
+ border-bottom: 1px solid rgba(224,240,224,0.04);
206
+ }
207
+
208
+ .log-row:last-child { border-bottom: none; }
209
+
210
+ .log-row:hover { background: rgba(224,240,224,0.03); }
211
+
212
+ .log-cell {
213
+ padding: 6px 10px;
214
+ font-family: var(--font-mono);
215
+ font-size: 11.5px;
216
+ overflow: hidden;
217
+ text-overflow: ellipsis;
218
+ white-space: nowrap;
219
+ }
220
+
221
+ .log-cell.msg {
222
+ white-space: pre-wrap;
223
+ word-break: break-all;
224
+ color: var(--text-secondary);
225
+ }
226
+
227
+ .log-ts { color: var(--text-muted); }
228
+
229
+ .log-level-log { color: var(--text-muted); }
230
+ .log-level-info { color: var(--info); }
231
+ .log-level-warn { color: var(--warning); }
232
+ .log-level-error { color: var(--danger); }
233
+
234
+ .log-msg-warn { color: #e4ca8a; }
235
+ .log-msg-error { color: var(--danger); }
236
+
237
+ /* ── Version / updates ─────────────────────────────────────────── */
238
+ .kv-row {
239
+ display: flex;
240
+ align-items: baseline;
241
+ justify-content: space-between;
242
+ gap: 12px;
243
+ padding: 9px 0;
244
+ border-bottom: 1px solid var(--border);
245
+ }
246
+
247
+ .kv-row:last-child { border-bottom: none; }
248
+
249
+ .kv-key {
250
+ font-size: 13px;
251
+ color: var(--text-muted);
252
+ flex-shrink: 0;
253
+ }
254
+
255
+ .kv-val {
256
+ font-size: 13px;
257
+ font-family: var(--font-mono);
258
+ color: var(--text);
259
+ text-align: right;
260
+ word-break: break-all;
261
+ }
262
+
263
+ .progress-track {
264
+ height: 5px;
265
+ background: var(--bg-secondary);
266
+ border-radius: 999px;
267
+ overflow: hidden;
268
+ margin: 8px 0 4px;
269
+ }
270
+
271
+ .progress-fill {
272
+ height: 100%;
273
+ background: var(--accent);
274
+ border-radius: 999px;
275
+ transition: width 0.5s ease;
276
+ }
277
+
278
+ .update-controls {
279
+ display: flex;
280
+ align-items: flex-end;
281
+ gap: 14px;
282
+ flex-wrap: wrap;
283
+ margin-top: 16px;
284
+ }
285
+
286
+ .update-controls .field {
287
+ margin-bottom: 0;
288
+ }
289
+
290
+ .update-controls label {
291
+ text-transform: uppercase;
292
+ }
293
+
294
+ /* ── Config table ──────────────────────────────────────────────── */
295
+ .config-table {
296
+ width: 100%;
297
+ border-collapse: collapse;
298
+ }
299
+
300
+ .config-table tr {
301
+ border-bottom: 1px solid var(--border);
302
+ }
303
+
304
+ .config-table tr:last-child { border-bottom: none; }
305
+
306
+ .config-table td {
307
+ padding: 9px 4px;
308
+ font-size: 13px;
309
+ vertical-align: baseline;
310
+ }
311
+
312
+ .config-table td:first-child {
313
+ font-family: var(--font-mono);
314
+ color: var(--text-muted);
315
+ width: 220px;
316
+ padding-right: 16px;
317
+ }
318
+
319
+ .config-table td:last-child {
320
+ font-family: var(--font-mono);
321
+ color: var(--text);
322
+ }
323
+
324
+ .config-empty {
325
+ font-style: italic;
326
+ color: var(--text-muted) !important;
327
+ }
328
+
329
+ /* ── Stats grid ────────────────────────────────────────────── */
330
+ .stat-grid {
331
+ display: grid;
332
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
333
+ gap: 10px;
334
+ }
335
+
336
+ .stat-card {
337
+ background: var(--bg-secondary);
338
+ border: 1px solid var(--border);
339
+ border-radius: 8px;
340
+ padding: 18px 16px 14px;
341
+ }
342
+
343
+ .stat-value {
344
+ font-size: 28px;
345
+ font-weight: 700;
346
+ letter-spacing: -0.5px;
347
+ color: var(--text);
348
+ line-height: 1;
349
+ font-variant-numeric: tabular-nums;
350
+ }
351
+
352
+ .stat-label {
353
+ font-size: 11px;
354
+ font-weight: 600;
355
+ text-transform: uppercase;
356
+ letter-spacing: 0.06em;
357
+ color: var(--text-muted);
358
+ margin-top: 8px;
359
+ }
360
+
361
+ /* ── Analytics tables ──────────────────────────────────────── */
362
+ .analytics-table {
363
+ width: 100%;
364
+ border-collapse: collapse;
365
+ font-size: 13px;
366
+ }
367
+
368
+ .analytics-table th {
369
+ text-align: left;
370
+ font-size: 10px;
371
+ font-weight: 600;
372
+ text-transform: uppercase;
373
+ letter-spacing: 0.07em;
374
+ color: var(--text-muted);
375
+ padding: 6px 8px;
376
+ border-bottom: 1px solid var(--border);
377
+ }
378
+
379
+ .analytics-table td {
380
+ padding: 9px 8px;
381
+ border-bottom: 1px solid rgba(224,240,224,0.04);
382
+ color: var(--text-secondary);
383
+ }
384
+
385
+ .analytics-table tr:last-child td { border-bottom: none; }
386
+ .analytics-table tr:hover td { background: rgba(224,240,224,0.025); }
387
+
388
+ .cell-truncate {
389
+ max-width: 280px;
390
+ overflow: hidden;
391
+ text-overflow: ellipsis;
392
+ white-space: nowrap;
393
+ }
394
+
395
+ .run-badge {
396
+ display: inline-block;
397
+ padding: 2px 8px;
398
+ border-radius: 999px;
399
+ font-size: 10px;
400
+ font-weight: 600;
401
+ }
402
+
403
+ .run-completed { background: rgba(116,192,124,0.15); color: var(--success); }
404
+ .run-failed { background: rgba(222,138,120,0.15); color: var(--danger); }
405
+ .run-running { background: var(--accent-muted); color: var(--accent); }
406
+ .run-pending { background: rgba(126,136,119,0.15); color: var(--text-muted); }
407
+
408
+ /* ── User avatar ────────────────────────────────────────────── */
409
+ .user-avatar {
410
+ display: inline-flex;
411
+ align-items: center;
412
+ justify-content: center;
413
+ width: 28px;
414
+ height: 28px;
415
+ min-width: 28px;
416
+ border-radius: 50%;
417
+ background: var(--accent-muted);
418
+ color: var(--accent);
419
+ font-size: 12px;
420
+ font-weight: 700;
421
+ }
422
+
423
+ /* ── Mini bar ───────────────────────────────────────────────── */
424
+ .mini-bar {
425
+ height: 4px;
426
+ background: var(--bg-secondary);
427
+ border-radius: 999px;
428
+ width: 80px;
429
+ overflow: hidden;
430
+ }
431
+
432
+ .mini-bar-fill {
433
+ height: 100%;
434
+ background: var(--accent);
435
+ border-radius: 999px;
436
+ }
437
+
438
+ /* ── Users table ────────────────────────────────────────────── */
439
+ .users-table {
440
+ width: 100%;
441
+ border-collapse: collapse;
442
+ font-size: 13px;
443
+ }
444
+
445
+ .users-table th {
446
+ text-align: left;
447
+ font-size: 10px;
448
+ font-weight: 600;
449
+ text-transform: uppercase;
450
+ letter-spacing: 0.07em;
451
+ color: var(--text-muted);
452
+ padding: 6px 8px;
453
+ border-bottom: 1px solid var(--border);
454
+ white-space: nowrap;
455
+ }
456
+
457
+ .users-table td {
458
+ padding: 10px 8px;
459
+ border-bottom: 1px solid rgba(224,240,224,0.04);
460
+ vertical-align: middle;
461
+ }
462
+
463
+ .users-table tr:last-child td { border-bottom: none; }
464
+ .users-table tr:hover td { background: rgba(224,240,224,0.025); }
465
+
466
+ .gdpr-note {
467
+ font-size: 11px;
468
+ color: var(--text-muted);
469
+ margin-top: 14px;
470
+ padding: 10px 14px;
471
+ background: rgba(222, 138, 120, 0.05);
472
+ border: 1px solid rgba(222, 138, 120, 0.15);
473
+ border-radius: var(--radius-sm);
474
+ line-height: 1.5;
475
+ }
476
+
477
+ /* ── SQL editor ─────────────────────────────────────────────── */
478
+ .sql-toolbar {
479
+ display: flex;
480
+ align-items: center;
481
+ gap: 8px;
482
+ margin-bottom: 10px;
483
+ }
484
+
485
+ .sql-textarea {
486
+ width: 100%;
487
+ min-height: 140px;
488
+ background: var(--bg-input);
489
+ border: 1px solid var(--border-light);
490
+ border-radius: var(--radius-sm);
491
+ padding: 12px 14px;
492
+ color: var(--text);
493
+ font-family: var(--font-mono);
494
+ font-size: 13px;
495
+ line-height: 1.6;
496
+ resize: vertical;
497
+ outline: none;
498
+ tab-size: 2;
499
+ }
500
+
501
+ .sql-textarea:focus {
502
+ border-color: var(--accent);
503
+ box-shadow: 0 0 0 3px var(--accent-muted);
504
+ }
505
+
506
+ .sql-meta {
507
+ display: flex;
508
+ align-items: center;
509
+ gap: 8px;
510
+ font-size: 12px;
511
+ color: var(--text-muted);
512
+ margin: 12px 0 8px;
513
+ }
514
+
515
+ .sql-result-wrap {
516
+ overflow-x: auto;
517
+ border: 1px solid var(--border);
518
+ border-radius: var(--radius-sm);
519
+ max-height: 420px;
520
+ overflow-y: auto;
521
+ }
522
+
523
+ .sql-result-table {
524
+ width: 100%;
525
+ border-collapse: collapse;
526
+ font-size: 12px;
527
+ font-family: var(--font-mono);
528
+ }
529
+
530
+ .sql-result-table th {
531
+ background: var(--bg-secondary);
532
+ padding: 7px 12px;
533
+ text-align: left;
534
+ font-size: 10px;
535
+ font-weight: 700;
536
+ text-transform: uppercase;
537
+ letter-spacing: 0.05em;
538
+ color: var(--text-muted);
539
+ white-space: nowrap;
540
+ position: sticky;
541
+ top: 0;
542
+ z-index: 1;
543
+ border-bottom: 1px solid var(--border-light);
544
+ }
545
+
546
+ .sql-result-table td {
547
+ padding: 6px 12px;
548
+ border-bottom: 1px solid rgba(224,240,224,0.04);
549
+ color: var(--text-secondary);
550
+ white-space: nowrap;
551
+ max-width: 320px;
552
+ overflow: hidden;
553
+ text-overflow: ellipsis;
554
+ }
555
+
556
+ .sql-result-table tr:last-child td { border-bottom: none; }
557
+ .sql-result-table tr:hover td { background: rgba(224,240,224,0.03); }
558
+
559
+ .sql-error {
560
+ padding: 12px 14px;
561
+ margin-top: 10px;
562
+ background: rgba(222,138,120,0.08);
563
+ border: 1px solid rgba(222,138,120,0.2);
564
+ border-radius: var(--radius-sm);
565
+ color: var(--danger);
566
+ font-family: var(--font-mono);
567
+ font-size: 12px;
568
+ }
569
+
570
+ /* ── Access / toggle ───────────────────────────────────────── */
571
+ .access-row {
572
+ display: flex;
573
+ align-items: flex-start;
574
+ justify-content: space-between;
575
+ gap: 20px;
576
+ }
577
+
578
+ .access-label {
579
+ font-size: 14px;
580
+ font-weight: 600;
581
+ color: var(--text);
582
+ margin-bottom: 4px;
583
+ }
584
+
585
+ .access-desc {
586
+ font-size: 12px;
587
+ color: var(--text-muted);
588
+ line-height: 1.5;
589
+ max-width: 480px;
590
+ }
591
+
592
+ .access-desc code {
593
+ font-family: var(--font-mono);
594
+ font-size: 11px;
595
+ background: var(--bg-secondary);
596
+ padding: 1px 5px;
597
+ border-radius: 4px;
598
+ color: var(--accent);
599
+ }
600
+
601
+ .access-hint-row {
602
+ display: flex;
603
+ align-items: center;
604
+ gap: 12px;
605
+ margin-top: 12px;
606
+ }
607
+
608
+ .access-actions {
609
+ display: flex;
610
+ gap: 8px;
611
+ margin-top: 16px;
612
+ }
613
+
614
+ .access-status {
615
+ font-size: 12px;
616
+ margin-top: 8px;
617
+ min-height: 16px;
618
+ }
619
+
620
+ /* Toggle switch */
621
+ .toggle {
622
+ position: relative;
623
+ width: 40px;
624
+ height: 22px;
625
+ flex-shrink: 0;
626
+ cursor: pointer;
627
+ }
628
+
629
+ .toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
630
+
631
+ .toggle-track {
632
+ position: absolute;
633
+ inset: 0;
634
+ background: var(--bg-secondary);
635
+ border: 1px solid var(--border-light);
636
+ border-radius: 999px;
637
+ transition: background 0.18s, border-color 0.18s;
638
+ }
639
+
640
+ .toggle input:checked ~ .toggle-track {
641
+ background: var(--accent);
642
+ border-color: var(--accent);
643
+ }
644
+
645
+ .toggle-thumb {
646
+ position: absolute;
647
+ top: 3px;
648
+ left: 3px;
649
+ width: 16px;
650
+ height: 16px;
651
+ background: #fff;
652
+ border-radius: 50%;
653
+ transition: transform 0.18s;
654
+ pointer-events: none;
655
+ }
656
+
657
+ .toggle input:checked ~ .toggle-thumb { transform: translateX(18px); }
658
+
659
+ /* API key display */
660
+ .api-key-hint {
661
+ font-family: var(--font-mono);
662
+ font-size: 13px;
663
+ color: var(--text-secondary);
664
+ background: var(--bg-secondary);
665
+ padding: 4px 10px;
666
+ border-radius: 6px;
667
+ letter-spacing: 0.04em;
668
+ }
669
+
670
+ .api-key-full {
671
+ font-family: var(--font-mono);
672
+ font-size: 12px;
673
+ color: var(--text);
674
+ background: var(--bg-secondary);
675
+ padding: 6px 10px;
676
+ border-radius: 6px;
677
+ word-break: break-all;
678
+ flex: 1;
679
+ user-select: all;
680
+ }
681
+
682
+ .key-reveal-box {
683
+ background: rgba(225, 176, 82, 0.07);
684
+ border: 1px solid rgba(225, 176, 82, 0.25);
685
+ border-radius: var(--radius-sm);
686
+ padding: 12px 14px;
687
+ margin-top: 14px;
688
+ }
689
+
690
+ /* ── Provider rows ─────────────────────────────────────────── */
691
+ .provider-row {
692
+ display: grid;
693
+ grid-template-columns: 1fr auto;
694
+ align-items: center;
695
+ gap: 16px;
696
+ padding: 12px 0;
697
+ border-bottom: 1px solid var(--border);
698
+ }
699
+
700
+ .provider-row:last-child { border-bottom: none; }
701
+
702
+ .provider-meta {
703
+ display: flex;
704
+ align-items: center;
705
+ gap: 10px;
706
+ min-width: 0;
707
+ }
708
+
709
+ .provider-name {
710
+ font-size: 13px;
711
+ font-weight: 600;
712
+ color: var(--text);
713
+ flex-shrink: 0;
714
+ }
715
+
716
+ .provider-hint {
717
+ font-family: var(--font-mono);
718
+ font-size: 11px;
719
+ color: var(--text-muted);
720
+ overflow: hidden;
721
+ text-overflow: ellipsis;
722
+ white-space: nowrap;
723
+ }
724
+
725
+ .provider-controls {
726
+ display: flex;
727
+ align-items: center;
728
+ gap: 8px;
729
+ }
730
+
731
+ .provider-controls input {
732
+ width: 220px;
733
+ padding: 7px 12px;
734
+ font-size: 13px;
735
+ }
736
+
737
+ .provider-save-btn {
738
+ padding: 7px 14px;
739
+ font-size: 12px;
740
+ flex-shrink: 0;
741
+ }
742
+ </style>
743
+ </head>
744
+ <body>
745
+ <!-- ── Sidebar ─────────────────────────────────────────────────────── -->
746
+ <aside class="sidebar">
747
+ <div class="sidebar-header">
748
+ <a class="brand" href="/admin">
749
+ <div class="brand-mark">
750
+ <img src="/admin/logo.svg" width="28" height="28" alt="">
751
+ </div>
752
+ <div>
753
+ <div class="brand-name">NeoAgent</div>
754
+ <div class="brand-sub">Admin</div>
755
+ </div>
756
+ </a>
757
+ </div>
758
+
759
+ <nav aria-label="Admin navigation">
760
+ <div class="nav-section-label">Monitor</div>
761
+
762
+ <button class="nav-item active" data-page="overview" onclick="showPage('overview',this)">
763
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
764
+ <path d="M3 4a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 8a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H4a1 1 0 01-1-1v-4zm8-8a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V4zm0 8a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"/>
765
+ </svg>
766
+ Overview
767
+ </button>
768
+
769
+ <button class="nav-item" data-page="logs" onclick="showPage('logs',this)">
770
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
771
+ <path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 5a1 1 0 011-1h6a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/>
772
+ </svg>
773
+ Logs
774
+ </button>
775
+
776
+ <button class="nav-item" data-page="updates" onclick="showPage('updates',this)">
777
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
778
+ <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"/>
779
+ </svg>
780
+ Updates
781
+ </button>
782
+
783
+ <div class="nav-section-label" style="margin-top:8px;">Manage</div>
784
+
785
+ <button class="nav-item" data-page="analytics" onclick="showPage('analytics',this)">
786
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
787
+ <path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z"/>
788
+ </svg>
789
+ Analytics
790
+ </button>
791
+
792
+ <button class="nav-item" data-page="users" onclick="showPage('users',this)">
793
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
794
+ <path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"/>
795
+ </svg>
796
+ Users
797
+ </button>
798
+
799
+ <button class="nav-item" data-page="sql" onclick="showPage('sql',this)">
800
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
801
+ <path fill-rule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V5zm11 1H6v8l4-2 4 2V6z" clip-rule="evenodd"/>
802
+ </svg>
803
+ SQL Editor
804
+ </button>
805
+
806
+ <div class="nav-section-label" style="margin-top:8px;">Settings</div>
807
+
808
+ <button class="nav-item" data-page="access" onclick="showPage('access',this)">
809
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
810
+ <path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
811
+ </svg>
812
+ Access
813
+ </button>
814
+
815
+ <button class="nav-item" data-page="providers" onclick="showPage('providers',this)">
816
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
817
+ <path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clip-rule="evenodd"/>
818
+ </svg>
819
+ Providers
820
+ </button>
821
+
822
+ <button class="nav-item" data-page="config" onclick="showPage('config',this)">
823
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
824
+ <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"/>
825
+ </svg>
826
+ Configuration
827
+ </button>
828
+ </nav>
829
+
830
+ <div class="sidebar-footer">
831
+ <div class="sidebar-legal">
832
+ <a href="/legal.html#overview" target="_blank" rel="noopener">Privacy Policy</a>
833
+ <span>·</span>
834
+ <a href="/legal.html#legal-notice" target="_blank" rel="noopener">Legal Notice</a>
835
+ </div>
836
+ <button class="nav-item btn-danger" onclick="signOut()">
837
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
838
+ <path fill-rule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clip-rule="evenodd"/>
839
+ </svg>
840
+ Sign out
841
+ </button>
842
+ </div>
843
+ </aside>
844
+
845
+ <!-- ── Main ──────────────────────────────────────────────────────────── -->
846
+ <main class="main">
847
+
848
+ <!-- Overview -->
849
+ <section id="page-overview" class="page active" aria-label="Overview">
850
+ <div class="page-header">
851
+ <div>
852
+ <h1 class="page-title">Overview</h1>
853
+ <p class="page-subtitle">Connected services and system status</p>
854
+ </div>
855
+ <span class="refresh-hint" id="overview-ts" aria-live="polite"></span>
856
+ </div>
857
+ <div class="content">
858
+ <div class="card">
859
+ <div class="card-title">Service Health</div>
860
+ <div class="status-grid" id="health-grid">
861
+ <div class="empty"><span class="spinner"></span></div>
862
+ </div>
863
+ </div>
864
+ </div>
865
+ </section>
866
+
867
+ <!-- Logs -->
868
+ <section id="page-logs" class="page" aria-label="Logs">
869
+ <div class="page-header">
870
+ <div>
871
+ <h1 class="page-title">Server Logs</h1>
872
+ <p class="page-subtitle">Live server log stream — last 1,000 entries, persisted across restarts</p>
873
+ </div>
874
+ <span class="refresh-hint" id="logs-ts" aria-live="polite"></span>
875
+ </div>
876
+ <div class="content">
877
+ <div class="card">
878
+ <div class="toolbar">
879
+ <span class="toolbar-count" id="log-count" aria-live="polite">0 entries</span>
880
+ <span class="spacer"></span>
881
+ <button class="btn btn-ghost" onclick="copyLogs()">Copy all</button>
882
+ <button class="btn btn-ghost" onclick="clearLogs()">Clear</button>
883
+ </div>
884
+ <div class="log-table" id="log-table" role="log" aria-label="Server logs">
885
+ <div class="empty">No log entries yet</div>
886
+ </div>
887
+ </div>
888
+ </div>
889
+ </section>
890
+
891
+ <!-- Updates -->
892
+ <section id="page-updates" class="page" aria-label="Updates">
893
+ <div class="page-header">
894
+ <div>
895
+ <h1 class="page-title">Updates</h1>
896
+ <p class="page-subtitle">Server version and runtime update controls</p>
897
+ </div>
898
+ </div>
899
+ <div class="content">
900
+ <div class="card">
901
+ <div class="card-title">Version Info</div>
902
+ <div id="version-content"><div class="empty"><span class="spinner"></span></div></div>
903
+ </div>
904
+ <div class="card">
905
+ <div class="card-title">Runtime Update</div>
906
+ <div id="update-content"><div class="empty"><span class="spinner"></span></div></div>
907
+ </div>
908
+ </div>
909
+ </section>
910
+
911
+ <!-- Analytics -->
912
+ <section id="page-analytics" class="page" aria-label="Analytics">
913
+ <div class="page-header">
914
+ <div>
915
+ <h1 class="page-title">Analytics</h1>
916
+ <p class="page-subtitle">Usage statistics, top users, and recent activity</p>
917
+ </div>
918
+ <span class="refresh-hint" id="analytics-ts" aria-live="polite"></span>
919
+ </div>
920
+ <div class="content">
921
+ <div id="analytics-content"><div class="empty"><span class="spinner"></span></div></div>
922
+ </div>
923
+ </section>
924
+
925
+ <!-- Users -->
926
+ <section id="page-users" class="page" aria-label="User Management">
927
+ <div class="page-header">
928
+ <div>
929
+ <h1 class="page-title">Users</h1>
930
+ <p class="page-subtitle">Account management — force logout, GDPR data erasure</p>
931
+ </div>
932
+ <span class="toolbar-count" id="users-count" aria-live="polite" style="padding-top:6px;"></span>
933
+ </div>
934
+ <div class="content">
935
+ <div class="card">
936
+ <div class="toolbar" style="margin-bottom:14px;">
937
+ <input type="search" id="user-search" placeholder="Search by username or email…"
938
+ oninput="onUserSearch()" style="width:280px;padding:7px 12px;font-size:13px;" aria-label="Search users">
939
+ <span class="spacer"></span>
940
+ <button class="btn btn-ghost" onclick="loadUsers()">Refresh</button>
941
+ </div>
942
+ <div id="users-table-wrap"><div class="empty">Loading…</div></div>
943
+ </div>
944
+ </div>
945
+ </section>
946
+
947
+ <!-- SQL Editor -->
948
+ <section id="page-sql" class="page" aria-label="SQL Editor">
949
+ <div class="page-header">
950
+ <div>
951
+ <h1 class="page-title">SQL Editor</h1>
952
+ <p class="page-subtitle">Read-only SELECT queries against the live database — results capped at 500 rows</p>
953
+ </div>
954
+ </div>
955
+ <div class="content">
956
+ <div class="card">
957
+ <div id="sql-content"><div class="empty"><span class="spinner"></span></div></div>
958
+ </div>
959
+ </div>
960
+ </section>
961
+
962
+ <!-- Access -->
963
+ <section id="page-access" class="page" aria-label="Access Settings">
964
+ <div class="page-header">
965
+ <div>
966
+ <h1 class="page-title">Access</h1>
967
+ <p class="page-subtitle">Signup policy and programmatic API key management</p>
968
+ </div>
969
+ </div>
970
+ <div class="content">
971
+ <div id="access-content"><div class="empty"><span class="spinner"></span></div></div>
972
+ </div>
973
+ </section>
974
+
975
+ <!-- Providers -->
976
+ <section id="page-providers" class="page" aria-label="Providers">
977
+ <div class="page-header">
978
+ <div>
979
+ <h1 class="page-title">AI Providers</h1>
980
+ <p class="page-subtitle">API keys for LLM and voice providers — saved to .env, applied immediately</p>
981
+ </div>
982
+ </div>
983
+ <div class="content">
984
+ <div class="card">
985
+ <div class="card-title">Provider Keys</div>
986
+ <div id="providers-content"><div class="empty"><span class="spinner"></span></div></div>
987
+ </div>
988
+ </div>
989
+ </section>
990
+
991
+ <!-- Configuration -->
992
+ <section id="page-config" class="page" aria-label="Configuration">
993
+ <div class="page-header">
994
+ <div>
995
+ <h1 class="page-title">Configuration</h1>
996
+ <p class="page-subtitle">Current environment settings (non-secret values only)</p>
997
+ </div>
998
+ </div>
999
+ <div class="content">
1000
+ <div class="card">
1001
+ <div class="card-title">Environment</div>
1002
+ <div id="config-content"><div class="empty"><span class="spinner"></span></div></div>
1003
+ </div>
1004
+ </div>
1005
+ </section>
1006
+
1007
+ </main>
1008
+
1009
+ <script src="/admin/admin.js"></script>
1010
+ <script src="/admin/analytics.js"></script>
1011
+ <script src="/admin/users.js"></script>
1012
+ <script src="/admin/sql.js"></script>
1013
+ <script src="/admin/access.js"></script>
1014
+ </body>
1015
+ </html>