neoagent 2.4.1-beta.44 → 2.4.1-beta.46

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