@bhooai/nexus-cli 2.0.5 → 2.0.7

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 (62) hide show
  1. package/package.json +1 -1
  2. package/src/commands/add.ts +8 -3
  3. package/src/commands/dev.ts +18 -2
  4. package/src/commands/init.ts +3 -0
  5. package/src/devPanel.ts +138 -11
  6. package/src/devServiceManager.ts +210 -11
  7. package/src/index.ts +19 -0
  8. package/templates/base/apps/admin/package.json.ejs +1 -0
  9. package/templates/base/apps/admin/src/App.tsx +38 -4127
  10. package/templates/base/apps/admin/src/alertCenter.tsx +3 -2
  11. package/templates/base/apps/admin/src/api.ts +11 -11
  12. package/templates/base/apps/admin/src/components/ThemeCentre.tsx +201 -0
  13. package/templates/base/apps/admin/src/components/shell/AppearancePanel.tsx +37 -0
  14. package/templates/base/apps/admin/src/components/shell/Dashboard.tsx +441 -0
  15. package/templates/base/apps/admin/src/components/shell/Login.tsx +69 -0
  16. package/templates/base/apps/admin/src/components/shell/NotifyPanel.tsx +102 -0
  17. package/templates/base/apps/admin/src/components/shell/RailPanel.tsx +133 -0
  18. package/templates/base/apps/admin/src/components/shell/SettingsDialog.tsx +217 -0
  19. package/templates/base/apps/admin/src/components/ui/ConfirmDialog.tsx +38 -0
  20. package/templates/base/apps/admin/src/components/ui/LintChecks.tsx +54 -0
  21. package/templates/base/apps/admin/src/components/ui/PageHead.tsx +13 -0
  22. package/templates/base/apps/admin/src/components/ui/PageHero.tsx +15 -0
  23. package/templates/base/apps/admin/src/icons.tsx +104 -0
  24. package/templates/base/apps/admin/src/index.css +77 -19
  25. package/templates/base/apps/admin/src/lib/constants.ts +94 -0
  26. package/templates/base/apps/admin/src/lib/theme.ts +80 -0
  27. package/templates/base/apps/admin/src/lib/types.ts +39 -0
  28. package/templates/base/apps/admin/src/lib/utils.ts +147 -0
  29. package/templates/base/apps/admin/src/main.tsx.ejs +3 -2
  30. package/templates/base/apps/admin/src/pages/Config.tsx +108 -0
  31. package/templates/base/apps/admin/src/pages/Databases.tsx +122 -0
  32. package/templates/base/apps/admin/src/pages/Environment.tsx +69 -0
  33. package/templates/base/apps/admin/src/pages/Logs.tsx +270 -0
  34. package/templates/base/apps/admin/src/pages/Monitoring.tsx +88 -0
  35. package/templates/base/apps/admin/src/pages/Overview.tsx +108 -0
  36. package/templates/base/apps/admin/src/pages/Payments.tsx +68 -0
  37. package/templates/base/apps/admin/src/pages/Plugins.tsx +36 -0
  38. package/templates/base/apps/admin/src/pages/Processes.tsx +67 -0
  39. package/templates/base/apps/admin/src/pages/Schema.tsx +130 -0
  40. package/templates/base/apps/admin/src/pages/Users.tsx +144 -0
  41. package/templates/base/apps/admin/src/pages/ai/AddProviderDialog.tsx +150 -0
  42. package/templates/base/apps/admin/src/pages/ai/AiAgents.tsx +31 -0
  43. package/templates/base/apps/admin/src/pages/ai/AiChatPlayground.tsx +208 -0
  44. package/templates/base/apps/admin/src/pages/ai/AiProviders.tsx +276 -0
  45. package/templates/base/apps/admin/src/pages/ai/AiSettings.tsx +84 -0
  46. package/templates/base/apps/admin/src/pages/ai/ProviderCard.tsx +145 -0
  47. package/templates/base/apps/admin/src/pages/ai/ProviderGroup.tsx +50 -0
  48. package/templates/base/apps/admin/src/pages/ai/ProviderKeyDialog.tsx +66 -0
  49. package/templates/base/apps/admin/src/pages/monitoring/RequestSeriesChart.tsx +70 -0
  50. package/templates/base/apps/admin/src/pages/overview/TrafficChart.tsx +32 -0
  51. package/templates/base/apps/admin/src/pages/payments/OrdersTable.tsx +27 -0
  52. package/templates/base/apps/admin/src/pages/payments/PaymentKeysDialog.tsx +74 -0
  53. package/templates/base/apps/admin/src/pages/payments/TestConsole.tsx +173 -0
  54. package/templates/base/apps/admin/src/pages/payments/TransactionsTable.tsx +26 -0
  55. package/templates/base/apps/admin/src/style.css +6517 -0
  56. package/templates/base/apps/admin/vite.config.ts.ejs +13 -8
  57. package/templates/base/apps/ai-server/main.py.ejs +2 -2
  58. package/templates/base/apps/backend/src/routes/index.ts.ejs +1 -1
  59. package/templates/base/apps/frontend/src/App.tsx.ejs +10 -0
  60. package/templates/base/apps/frontend/src/main.tsx.ejs +2 -10
  61. package/templates/base/apps/frontend/vite.config.ts.ejs +9 -4
  62. package/templates/base/docker-compose.yml.ejs +2 -2
@@ -747,6 +747,7 @@ pointer-events: none;
747
747
  background: transparent;
748
748
  text-align: left;
749
749
  font-size: 0.7rem;
750
+ line-height: 1;
750
751
  transition: 160ms ease;
751
752
  }
752
753
 
@@ -759,12 +760,24 @@ pointer-events: none;
759
760
  }
760
761
 
761
762
  .admin-nav-icon {
762
- display: grid;
763
- width: 1.15rem;
763
+ display: inline-flex;
764
+ align-items: center;
765
+ justify-content: center;
766
+ width: 1.25rem;
767
+ height: 1.25rem;
768
+ flex: 0 0 1.25rem;
764
769
  place-items: center;
765
770
  color: #62bfff;
766
- font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
767
- font-size: 0.74rem;
771
+ line-height: 0;
772
+ }
773
+
774
+ .admin-search-icon {
775
+ display: inline-flex;
776
+ align-items: center;
777
+ justify-content: center;
778
+ flex: 0 0 auto;
779
+ line-height: 0;
780
+ color: #6e87a5;
768
781
  }
769
782
 
770
783
  .admin-nav button b {
@@ -981,14 +994,17 @@ pointer-events: none;
981
994
  }
982
995
 
983
996
  .admin-search-result-icon {
984
- display: grid;
985
- place-items: center;
997
+ display: inline-flex;
998
+ align-items: center;
999
+ justify-content: center;
986
1000
  width: 1.4rem;
987
1001
  height: 1.4rem;
1002
+ flex: 0 0 1.4rem;
1003
+ place-items: center;
988
1004
  border-radius: 0.35rem;
989
1005
  color: #8fb8e4;
990
1006
  background: rgba(84, 183, 255, 0.1);
991
- font-size: 0.6rem;
1007
+ line-height: 0;
992
1008
  }
993
1009
 
994
1010
  .admin-search-empty {
@@ -1032,12 +1048,14 @@ pointer-events: none;
1032
1048
  .admin-icon-button,
1033
1049
  .admin-user-avatar {
1034
1050
  position: relative;
1035
- display: grid;
1036
- place-items: center;
1051
+ display: inline-flex;
1052
+ align-items: center;
1053
+ justify-content: center;
1037
1054
  border: 1px solid rgba(112, 168, 228, 0.2);
1038
1055
  border-radius: 0.5rem;
1039
1056
  color: #9bb4d0;
1040
1057
  background: rgba(25, 57, 95, 0.34);
1058
+ line-height: 0;
1041
1059
  }
1042
1060
 
1043
1061
  .admin-icon-button {
@@ -1424,7 +1442,7 @@ pointer-events: none;
1424
1442
  border-radius: 50%;
1425
1443
  }
1426
1444
 
1427
- /* bars — three rising service bars */
1445
+ /* bars three rising service bars */
1428
1446
  .hero-art--bars {
1429
1447
  color: #5ef0b3;
1430
1448
  border: 1px solid rgba(94, 240, 179, 0.32);
@@ -1435,7 +1453,7 @@ pointer-events: none;
1435
1453
  .hero-art--bars .ha-b { left: 43%; bottom: 30%; width: 6px; height: 46%; background: rgba(94, 240, 179, 0.7); border-radius: 3px 3px 0 0; }
1436
1454
  .hero-art--bars .ha-c { left: 58%; bottom: 30%; width: 6px; height: 26%; background: rgba(94, 240, 179, 0.45); border-radius: 3px 3px 0 0; }
1437
1455
 
1438
- /* stack — stacked plugin tiles */
1456
+ /* stack stacked plugin tiles */
1439
1457
  .hero-art--stack {
1440
1458
  color: #ffc46b;
1441
1459
  border: 1px solid rgba(255, 196, 107, 0.32);
@@ -1449,7 +1467,7 @@ pointer-events: none;
1449
1467
  .hero-art--stack .ha-b { bottom: 45%; left: 21%; width: 50%; background: rgba(255, 196, 107, 0.65); }
1450
1468
  .hero-art--stack .ha-c { bottom: 60%; left: 21%; width: 34%; background: rgba(255, 196, 107, 0.9); box-shadow: 0 0 8px rgba(255, 196, 107, 0.6); }
1451
1469
 
1452
- /* people — clients ringed around a center */
1470
+ /* people clients ringed around a center */
1453
1471
  .hero-art--people {
1454
1472
  color: #c8b4ff;
1455
1473
  border: 1px solid rgba(200, 180, 255, 0.32);
@@ -1460,7 +1478,7 @@ pointer-events: none;
1460
1478
  .hero-art--people .ha-b { top: 66%; left: 22%; width: 12px; height: 12px; background: rgba(160, 130, 240, 0.8); box-shadow: 0 0 8px rgba(160, 130, 240, 0.7); }
1461
1479
  .hero-art--people .ha-c { bottom: 22%; right: 24%; width: 8px; height: 8px; background: rgba(120, 200, 255, 0.8); box-shadow: 0 0 8px rgba(120, 200, 255, 0.7); }
1462
1480
 
1463
- /* wave — pulsing heartbeat line */
1481
+ /* wave pulsing heartbeat line */
1464
1482
  .hero-art--wave {
1465
1483
  color: #6fe8a8;
1466
1484
  border: 1px solid rgba(111, 232, 168, 0.32);
@@ -1472,7 +1490,7 @@ pointer-events: none;
1472
1490
  .hero-art--wave .ha-b { left: 44%; top: 26%; width: 8px; height: 26%; background: rgba(111, 232, 168, 0.55); transform: rotate(20deg); }
1473
1491
  .hero-art--wave .ha-c { left: 44%; bottom: 26%; width: 8px; height: 26%; background: rgba(111, 232, 168, 0.85); box-shadow: 0 0 8px rgba(111, 232, 168, 0.6); transform: rotate(-20deg); }
1474
1492
 
1475
- /* coins — stacked cylinder of payment */
1493
+ /* coins stacked cylinder of payment */
1476
1494
  .hero-art--coins {
1477
1495
  color: #ffd479;
1478
1496
  border: 1px solid rgba(255, 212, 121, 0.32);
@@ -1486,7 +1504,7 @@ pointer-events: none;
1486
1504
  .hero-art--coins .ha-b { bottom: 43%; background: rgba(255, 212, 121, 0.75); }
1487
1505
  .hero-art--coins .ha-c { bottom: 58%; background: rgba(255, 212, 121, 0.95); box-shadow: 0 0 8px rgba(255, 212, 121, 0.7); }
1488
1506
 
1489
- /* cubes — stacked database shelves */
1507
+ /* cubes stacked database shelves */
1490
1508
  .hero-art--cubes {
1491
1509
  color: #6fd6ff;
1492
1510
  border: 1px solid rgba(111, 214, 255, 0.32);
@@ -1500,7 +1518,7 @@ pointer-events: none;
1500
1518
  .hero-art--cubes .ha-b { top: 42%; left: 27%; background: rgba(111, 214, 255, 0.75); transform: skew(-10deg); }
1501
1519
  .hero-art--cubes .ha-c { top: 58%; left: 27%; background: rgba(111, 214, 255, 0.95); box-shadow: 0 0 8px rgba(111, 214, 255, 0.7); transform: skew(-10deg); }
1502
1520
 
1503
- /* spark — four-petal bloom around a center */
1521
+ /* spark four-petal bloom around a center */
1504
1522
  .hero-art--spark {
1505
1523
  color: #ff9fd6;
1506
1524
  border: 1px solid rgba(255, 159, 214, 0.32);
@@ -2456,7 +2474,7 @@ pointer-events: none;
2456
2474
  .account-divider { height: 1px; margin: 0.55rem 0; background: rgba(112, 168, 228, 0.14); }
2457
2475
  .account-item { display: flex; align-items: center; gap: 0.6rem; width: 100%; padding: 0.5rem 0.5rem; border: 1px solid transparent; border-radius: 0.5rem; color: #9ab2ce; background: transparent; text-align: left; }
2458
2476
  .account-item:hover { border-color: rgba(84, 183, 255, 0.26); color: #edf8ff; background: rgba(61, 139, 220, 0.14); }
2459
- .account-item-icon { display: grid; width: 24px; height: 24px; place-items: center; border-radius: 0.4rem; color: #61c7ff; background: rgba(40, 110, 178, 0.25); font-size: 0.7rem; }
2477
+ .account-item-icon { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; flex: 0 0 24px; place-items: center; border-radius: 0.4rem; color: #61c7ff; background: rgba(40, 110, 178, 0.25); line-height: 0; }
2460
2478
  .account-item > span:last-child { display: flex; flex-direction: column; gap: 0.1rem; }
2461
2479
  .account-item b { color: inherit; font-size: 0.62rem; font-weight: 600; }
2462
2480
  .account-item small { color: #6f89a7; font-size: 0.5rem; }
@@ -2487,7 +2505,7 @@ pointer-events: none;
2487
2505
  }
2488
2506
 
2489
2507
  .admin-tool-rail { position: fixed; top: 50%; right: 0.8rem; z-index: 4; display: flex; flex-direction: column; gap: 0.45rem; padding: 0.55rem 0.4rem; transform: translateY(-50%); border: 1px solid rgba(83, 177, 255, 0.35); border-radius: 999px; background: rgba(6, 19, 39, 0.78); box-shadow: 0 0 28px rgba(45, 125, 221, 0.18); backdrop-filter: blur(18px); }
2490
- .admin-tool-rail button { position: relative; display: grid; width: 30px; height: 30px; flex: 0 0 auto; place-items: center; border: 0; border-radius: 50%; color: #9fc7ec; background: transparent; font-size: 0.78rem; cursor: pointer; }
2508
+ .admin-tool-rail button { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; flex: 0 0 auto; place-items: center; border: 0; border-radius: 50%; color: #9fc7ec; background: transparent; line-height: 0; cursor: pointer; }
2491
2509
  .admin-tool-rail button:hover { color: white; background: rgba(71, 161, 247, 0.2); }
2492
2510
  .admin-tool-rail button.is-active { color: white; background: linear-gradient(135deg, rgba(38, 130, 225, 0.55), rgba(90, 67, 183, 0.45)); box-shadow: 0 0 12px rgba(45, 125, 221, 0.3); }
2493
2511
  .admin-tool-rail button[data-tip]::after {
@@ -2533,7 +2551,7 @@ pointer-events: none;
2533
2551
  .rail-list--nav li { padding: 0; }
2534
2552
  .rail-list--nav li button { display: flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.4rem 0.45rem; border: 0; border-radius: 0.4rem; color: #9ab2ce; background: transparent; text-align: left; cursor: pointer; }
2535
2553
  .rail-list--nav li button:hover, .rail-list--nav li button.is-on { color: #edf8ff; background: rgba(61, 139, 220, 0.14); }
2536
- .rail-fav-icon { width: 18px; text-align: center; }
2554
+ .rail-fav-icon { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; flex: 0 0 20px; line-height: 0; }
2537
2555
  .rail-fav-label { flex: 1; font-size: 0.66rem; }
2538
2556
  .rail-fav-check { color: #61c7ff; font-size: 0.66rem; }
2539
2557
  .rail-appearance { display: flex; flex-direction: column; gap: 0.3rem; margin-top: 0.4rem; }
@@ -3477,5 +3495,45 @@ pointer-events: none;
3477
3495
  }
3478
3496
  .safe-goto-link:hover { color: #bfdbfe; text-decoration: underline; }
3479
3497
 
3498
+ /* ===== Inline SVG icon system (see src/icons.tsx) ===== */
3499
+ .admin-shell svg,
3500
+ .admin-login svg {
3501
+ flex-shrink: 0;
3502
+ }
3503
+
3504
+ .admin-nav-icon svg,
3505
+ .admin-search-result-icon svg,
3506
+ .rail-fav-icon svg,
3507
+ .account-item-icon svg,
3508
+ .notify-head-icon svg,
3509
+ .notify-empty-icon svg,
3510
+ .log-ai-title svg,
3511
+ .provider-avatar svg {
3512
+ display: block;
3513
+ }
3514
+
3515
+ .hero-art strong svg,
3516
+ .hero-orbit-art strong svg,
3517
+ .env-lock-glyph svg {
3518
+ display: block;
3519
+ filter: drop-shadow(0 0 8px currentColor);
3520
+ }
3521
+
3522
+ .surface-icon svg {
3523
+ width: 20px;
3524
+ height: 20px;
3525
+ }
3526
+
3527
+ .log-empty-glyph svg {
3528
+ display: block;
3529
+ opacity: 0.85;
3530
+ color: var(--admin-muted, #98aec9);
3531
+ }
3532
+
3533
+ .admin-topbar .admin-icon-button svg,
3534
+ .admin-tool-rail button svg {
3535
+ display: block;
3536
+ }
3537
+
3480
3538
 
3481
3539
 
@@ -0,0 +1,94 @@
1
+ import type { AdminTheme, CustomTheme, ParticleConfig, ProviderPreset, ToggleStyle } from './types.js';
2
+ import type { IconName } from '../icons.js';
3
+
4
+ export const TOGGLE_STYLES: Array<{ id: ToggleStyle; label: string; hint: string }> = [
5
+ { id: 'chip', label: 'Chip ON/OFF', hint: 'Default · glass chip' },
6
+ { id: 'neon', label: 'Neon glass', hint: 'Blue→violet border' },
7
+ { id: 'icon', label: 'Icon knob', hint: 'Power · bolt glow' },
8
+ { id: 'orb', label: 'Orb dot', hint: 'Pulse halo' },
9
+ { id: 'track', label: 'Gradient-track', hint: 'Sliding trail' },
10
+ ];
11
+
12
+ export const CUSTOM_THEME_KEY = 'nexus-admin-custom-theme';
13
+ export const THEME_PRESETS: Record<AdminTheme, Omit<CustomTheme, 'active'>> = {
14
+ aurora: { blue: '#54b7ff', violet: '#9f7bff', pink: '#f36eb8', bg: '#0b0d1a', ink: '#eef6ff', muted: '#98aec9', mesh: 0.32, glass: 0.35, radius: 13, toggle: 'chip' },
15
+ midnight: { blue: '#78bfff', violet: '#7da5d3', pink: '#b8c7db', bg: '#02050b', ink: '#eaf3ff', muted: '#8fa6c2', mesh: 0.2, glass: 0.25, radius: 11, toggle: 'chip' },
16
+ violet: { blue: '#b4a1ff', violet: '#d78bff', pink: '#ff8ed1', bg: '#0b071b', ink: '#f3eaff', muted: '#a592c9', mesh: 0.4, glass: 0.3, radius: 15, toggle: 'chip' },
17
+ };
18
+
19
+ export const PARTICLES_KEY = 'nexus-admin-particles';
20
+ export const DEFAULT_PARTICLE_COLORS = ['#59d7ff', '#c68dff', '#ff8fd4', '#5ef0b3', '#d3a0ff', '#5fb6ff'];
21
+ export const DEFAULT_PARTICLES: ParticleConfig = { amount: 14, speed: 1, colors: [...DEFAULT_PARTICLE_COLORS] };
22
+ export const SWATCH_CPLAETTE = ['#59d7ff', '#c68dff', '#ff8fd4', '#5ef0b3', '#ffd479', '#f0995a', '#7fe3c8', '#f2a7ff'];
23
+
24
+ export const USER_KEY = 'nexus-admin-user';
25
+
26
+ export const CATEGORY_LABEL: Record<string, { label: string; hint: string }> = {
27
+ refused: { label: 'Connection refused', hint: 'the target address is reachable but nothing is listening — is the service running?' },
28
+ timeout: { label: 'Timed out', hint: 'the target did not respond in time — it may be down or filtering traffic.' },
29
+ dns: { label: 'DNS lookup failed', hint: 'the host could not be resolved.' },
30
+ ssl: { label: 'TLS handshake failed', hint: 'the target answered but the TLS handshake failed.' },
31
+ http: { label: 'HTTP error status', hint: 'the endpoint responded with an error status code.' },
32
+ engine_offline: { label: 'Diagnostics engine offline', hint: 'the Python engine is not running — start it with "python main.py" or "nexus dev".' },
33
+ };
34
+
35
+ /** Preset for the "Add provider" dialog — popular providers with autofill data
36
+ * and a list of well-known models (the user can still edit everything after). */
37
+ export const POPULAR_PROVIDERS: ProviderPreset[] = [
38
+ { id: 'ollama', label: 'Ollama (local)', baseUrl: 'http://localhost:11434', defaultModel: 'llama3:latest', models: ['llama3:latest', 'llama3.1:8b', 'mistral:latest', 'phi3:latest', 'qwen2.5:latest'] },
39
+ { id: 'openai', label: 'OpenAI', baseUrl: 'https://api.openai.com/v1', defaultModel: 'gpt-4o-mini', models: ['gpt-4o-mini', 'gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo', 'o1-mini', 'o1-preview'] },
40
+ { id: 'anthropic', label: 'Anthropic Claude', baseUrl: 'https://api.anthropic.com/v1', defaultModel: 'claude-sonnet-4-20250514', models: ['claude-sonnet-4-20250514', 'claude-3-7-sonnet-20250219', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'] },
41
+ { id: 'google', label: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1', defaultModel: 'gemini-2.0-flash', models: ['gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-pro', 'gemini-1.5-flash'] },
42
+ { id: 'groq', label: 'Groq', baseUrl: 'https://api.groq.com/openai/v1', defaultModel: 'llama-3.3-70b-versatile', models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768', 'gemma2-9b-it'] },
43
+ { id: 'mistral', label: 'Mistral AI', baseUrl: 'https://api.mistral.ai/v1', defaultModel: 'mistral-large-latest', models: ['mistral-large-latest', 'mistral-small-latest', 'open-mixtral-8x22b', 'open-mistral-7b', 'codestral-latest'] },
44
+ { id: 'cohere', label: 'Cohere', baseUrl: 'https://api.cohere.ai/v1', defaultModel: 'command-r-plus', models: ['command-r-plus', 'command-r', 'command', 'command-light'] },
45
+ { id: 'together', label: 'Together AI', baseUrl: 'https://api.together.xyz/v1', defaultModel: 'meta-llama/Llama-3-70b-chat-hf', models: ['meta-llama/Llama-3-70b-chat-hf', 'meta-llama/Llama-3-8b-chat-hf', 'mistralai/Mixtral-8x7B-Instruct-v0.1', 'Qwen/Qwen2.5-72B-Instruct-Turbo'] },
46
+ { id: 'fireworks', label: 'Fireworks AI', baseUrl: 'https://api.fireworks.ai/inference/v1', defaultModel: 'accounts/fireworks/models/llama-v3-70b-instruct', models: ['accounts/fireworks/models/llama-v3-70b-instruct', 'accounts/fireworks/models/llama-v3-8b-instruct', 'accounts/fireworks/models/mixtral-8x7b-instruct'] },
47
+ { id: 'deepseek', label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', defaultModel: 'deepseek-chat', models: ['deepseek-chat', 'deepseek-reasoner', 'deepseek-coder'] },
48
+ { id: 'perplexity', label: 'Perplexity', baseUrl: 'https://api.perplexity.ai', defaultModel: 'llama-3.1-sonar-large-128k-online', models: ['llama-3.1-sonar-large-128k-online', 'llama-3.1-sonar-small-128k-online', 'llama-3.1-sonar-huge-128k-online'] },
49
+ { id: 'xai', label: 'xAI (Grok)', baseUrl: 'https://api.x.ai/v1', defaultModel: 'grok-2-latest', models: ['grok-2-latest', 'grok-2-1212', 'grok-beta', 'grok-vision-beta'] },
50
+ { id: 'replicate', label: 'Replicate', baseUrl: 'https://api.replicate.com/v1', defaultModel: 'meta/llama-3-70b-instruct', models: ['meta/llama-3-70b-instruct', 'meta/llama-3-8b-instruct', 'mistralai/mixtral-8x7b-instruct-v0.1'] },
51
+ { id: 'huggingface', label: 'Hugging Face', baseUrl: 'https://api-inference.huggingface.co/models', defaultModel: 'meta-llama/Llama-3-70b-chat-hf', models: ['meta-llama/Llama-3-70b-chat-hf', 'mistralai/Mistral-7B-Instruct-v0.3', 'google/gemma-2-9b'] },
52
+ { id: 'nvidia', label: 'NVIDIA NIM', baseUrl: 'https://integrate.api.nvidia.com/v1', defaultModel: 'meta/llama-3.1-70b-instruct', models: ['meta/llama-3.1-70b-instruct', 'meta/llama-3.1-8b-instruct', 'nvidia/llama-3.1-nemotron-70b-instruct'] },
53
+ { id: 'openrouter', label: 'OpenRouter', baseUrl: 'https://openrouter.ai/api/v1', defaultModel: 'openai/gpt-4o-mini', models: ['openai/gpt-4o-mini', 'openai/gpt-4o', 'anthropic/claude-3.5-sonnet', 'google/gemini-2.0-flash-exp', 'meta-llama/llama-3.3-70b-instruct'] },
54
+ { id: 'lmstudio', label: 'LM Studio (local)', baseUrl: 'http://localhost:1234/v1', defaultModel: 'local-model', models: ['local-model', 'loaded-model'] },
55
+ { id: 'alephalpha', label: 'Aleph Alpha', baseUrl: 'https://api.aleph-alpha.com/v1', defaultModel: 'luminous-supreme-control', models: ['luminous-supreme-control', 'luminous-supreme', 'luminous-base'] },
56
+ { id: 'stability', label: 'Stability AI', baseUrl: 'https://api.stability.ai/v1', defaultModel: 'stable-diffusion-xl', models: ['stable-diffusion-xl', 'stable-diffusion-3', 'stable-image-core'] },
57
+ { id: 'azure', label: 'Azure OpenAI', baseUrl: 'https://your-resource.openai.azure.com', defaultModel: 'gpt-4o-mini', models: ['gpt-4o-mini', 'gpt-4o', 'gpt-35-turbo', 'dall-e-3'] },
58
+ ];
59
+
60
+ /** Find a preset by id (returns undefined for "other"/custom). */
61
+ export const PRESET_BY_ID: Record<string, ProviderPreset> = Object.fromEntries(POPULAR_PROVIDERS.map((p) => [p.id, p]));
62
+
63
+ export const ALL_PROVIDERS = ['razorpay', 'paypal', 'payu', 'skrill', 'payoneer'];
64
+
65
+ /** Known built-in provider ids (from nexus-core defaults). Used to classify custom providers. */
66
+ export const BUILTIN_PROVIDER_IDS = new Set([
67
+ 'ollama', 'openai', 'anthropic', 'google', 'groq', 'mistral', 'cohere', 'together',
68
+ 'fireworks', 'deepseek', 'perplexity', 'xai', 'replicate', 'huggingface', 'nvidia',
69
+ 'openrouter', 'lmstudio', 'alephalpha', 'stability', 'azure',
70
+ ]);
71
+
72
+ /** Display metadata for known providers: an icon + an HSL hue for the avatar accent. */
73
+ export const PROVIDER_META: Record<string, { glyph: IconName | string; hue: number }> = {
74
+ ollama: { glyph: 'bot', hue: 152 },
75
+ openai: { glyph: 'sparkles', hue: 165 },
76
+ anthropic: { glyph: 'message', hue: 265 },
77
+ google: { glyph: 'globe', hue: 217 },
78
+ groq: { glyph: 'zap', hue: 38 },
79
+ mistral: { glyph: 'wind', hue: 210 },
80
+ cohere: { glyph: 'layers', hue: 190 },
81
+ together: { glyph: 'users', hue: 230 },
82
+ fireworks: { glyph: 'flame', hue: 12 },
83
+ deepseek: { glyph: 'search', hue: 200 },
84
+ perplexity: { glyph: 'helpCircle', hue: 280 },
85
+ xai: { glyph: 'x', hue: 0 },
86
+ replicate: { glyph: 'repeat', hue: 160 },
87
+ huggingface: { glyph: 'smile', hue: 25 },
88
+ nvidia: { glyph: 'shield', hue: 120 },
89
+ openrouter: { glyph: 'gitFork', hue: 240 },
90
+ lmstudio: { glyph: 'monitor', hue: 180 },
91
+ alephalpha: { glyph: 'bookOpen', hue: 300 },
92
+ stability: { glyph: 'image', hue: 320 },
93
+ azure: { glyph: 'cloud', hue: 200 },
94
+ };
@@ -0,0 +1,80 @@
1
+ import type { CustomTheme, ParticleConfig } from './types.js';
2
+ import { CUSTOM_THEME_KEY, DEFAULT_PARTICLE_COLORS, DEFAULT_PARTICLES, PARTICLES_KEY, THEME_PRESETS } from './constants.js';
3
+
4
+ export function loadCustomTheme(): CustomTheme {
5
+ const fallback: CustomTheme = { ...THEME_PRESETS.aurora, active: false };
6
+ if (typeof window === 'undefined') return fallback;
7
+ try {
8
+ const raw = window.localStorage.getItem(CUSTOM_THEME_KEY);
9
+ if (raw) {
10
+ const parsed = JSON.parse(raw) as Partial<CustomTheme>;
11
+ if (parsed && typeof parsed === 'object') {
12
+ return { ...THEME_PRESETS.aurora, ...parsed, active: Boolean(parsed.active) };
13
+ }
14
+ }
15
+ } catch { /* ignore corrupt value */ }
16
+ return fallback;
17
+ }
18
+
19
+ export function loadParticles(): ParticleConfig {
20
+ const fallback: ParticleConfig = { amount: 14, speed: 1, colors: [...DEFAULT_PARTICLE_COLORS] };
21
+ if (typeof window === 'undefined') return fallback;
22
+ try {
23
+ const raw = window.localStorage.getItem(PARTICLES_KEY);
24
+ if (raw) {
25
+ const p = JSON.parse(raw) as Partial<ParticleConfig>;
26
+ if (p && typeof p === 'object') {
27
+ const amount = Math.max(0, Math.min(48, Math.round(Number(p.amount) || 14)));
28
+ const speed = Math.min(3, Math.max(0.2, Number(p.speed) || 1));
29
+ const colors = Array.isArray(p.colors) ? p.colors.filter((c): c is string => typeof c === 'string' && /^#[0-9a-fA-F]{3,8}$/.test(c)) : [];
30
+ return { amount, speed, colors: colors.length ? colors : [...DEFAULT_PARTICLE_COLORS] };
31
+ }
32
+ }
33
+ } catch { /* ignore corrupt value */ }
34
+ return fallback;
35
+ }
36
+
37
+ export function buildParticleLayout(amount: number) {
38
+ const list: { left: number; size: number; duration: number; delay: number; dx: number; dx2: number; opacity: string }[] = [];
39
+ for (let i = 0; i < amount; i++) {
40
+ list.push({
41
+ left: 1 + Math.random() * 98,
42
+ size: 3 + Math.random() * 7,
43
+ duration: 18 + Math.random() * 18,
44
+ delay: Math.random() * 32,
45
+ dx: (Math.random() - 0.5) * 70,
46
+ dx2: (Math.random() - 0.5) * 70,
47
+ opacity: (0.45 + Math.random() * 0.4).toFixed(2),
48
+ });
49
+ }
50
+ return list;
51
+ }
52
+
53
+ export function hslToHex(h: number, s: number, l: number): string {
54
+ const a = s * Math.min(l, 1 - l);
55
+ const f = (n: number) => {
56
+ const k = (n + h / 30) % 12;
57
+ const color = l - a * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)));
58
+ return Math.round(255 * color).toString(16).padStart(2, '0');
59
+ };
60
+ return `#${f(0)}${f(8)}${f(4)}`;
61
+ }
62
+
63
+ export function randomAccentPalette() {
64
+ const base = Math.round(Math.random() * 360);
65
+ return {
66
+ blue: hslToHex((base + 150) % 360, 72, 64),
67
+ violet: hslToHex((base + 60) % 360, 62, 68),
68
+ pink: hslToHex((base + 25) % 360, 80, 72),
69
+ };
70
+ }
71
+
72
+ export function randomParticleColors(): string[] {
73
+ const base = Math.round(Math.random() * 360);
74
+ const count = 4 + Math.round(Math.random() * 3);
75
+ const colors: string[] = [];
76
+ for (let i = 0; i < count; i++) {
77
+ colors.push(hslToHex((base + i * 55) % 360, 74 + Math.round(Math.random() * 12), 60 + Math.round(Math.random() * 16)));
78
+ }
79
+ return colors;
80
+ }
@@ -0,0 +1,39 @@
1
+ export type Tab = 'overview' | 'processes' | 'logs' | 'config' | 'env' | 'plugins' | 'users' | 'monitoring' | 'payments' | 'databases' | 'schema' | 'ai' | 'theme';
2
+ export type AdminTheme = 'aurora' | 'midnight' | 'violet';
3
+ export type UiMode = 'workspace' | 'compact' | 'focus';
4
+ export type ToggleStyle = 'chip' | 'neon' | 'icon' | 'orb' | 'track';
5
+
6
+ export interface CustomTheme { active: boolean; blue: string; violet: string; pink: string; bg: string; ink: string; muted: string; mesh: number; glass: number; radius: number; toggle: ToggleStyle; }
7
+
8
+ export interface ParticleConfig { amount: number; speed: number; colors: string[]; }
9
+
10
+ export interface AdminUser { email?: string; name?: string; roles?: string[]; }
11
+
12
+ export type ConfigValueType = 'string' | 'number' | 'boolean' | 'null' | 'object' | 'array';
13
+ export type ConfigEntry = { key: string; value: string; type: ConfigValueType };
14
+
15
+ export interface ProviderPreset { id: string; label: string; baseUrl: string; defaultModel: string; models: string[]; }
16
+
17
+ export type ProviderFilter = 'all' | 'enabled' | 'disabled' | 'custom';
18
+
19
+ export interface PaymentOrder {
20
+ provider: string;
21
+ orderId: string;
22
+ reference: string;
23
+ status: string;
24
+ amount: number;
25
+ currency: string;
26
+ paymentId?: string;
27
+ createdAt?: string;
28
+ }
29
+ export interface PaymentTransaction {
30
+ provider: string;
31
+ event: string;
32
+ verified: boolean;
33
+ orderId?: string;
34
+ paymentId?: string;
35
+ amount?: number;
36
+ currency?: string;
37
+ status?: string;
38
+ createdAt?: string;
39
+ }
@@ -0,0 +1,147 @@
1
+ import type { AdminUser, ConfigEntry } from './types.js';
2
+ import { USER_KEY, CATEGORY_LABEL, PROVIDER_META } from './constants.js';
3
+ import type { IconName } from '../icons.js';
4
+ import type { PreflightCheck, RequestSeriesRange } from '../api.js';
5
+
6
+ export function storedPreference<T extends string>(key: string, fallback: T, allowed: readonly T[]): T {
7
+ const value = typeof window === 'undefined' ? null : window.localStorage.getItem(key);
8
+ return value && allowed.includes(value as T) ? value as T : fallback;
9
+ }
10
+
11
+ export function formatProjectName(name: string): string {
12
+ const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
13
+ if (!words.length) return name;
14
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
15
+ }
16
+
17
+ export function readUser(): AdminUser | null {
18
+ if (typeof window === 'undefined') return null;
19
+ try { const raw = window.localStorage.getItem(USER_KEY); return raw ? JSON.parse(raw) as AdminUser : null; } catch { return null; }
20
+ }
21
+
22
+ export function formatUptime(seconds?: number): string {
23
+ if (!seconds || seconds <= 0) return '—';
24
+ const d = Math.floor(seconds / 86400);
25
+ const h = Math.floor((seconds % 86400) / 3600);
26
+ const m = Math.floor((seconds % 3600) / 60);
27
+ const s = Math.floor(seconds % 60);
28
+ if (d > 0) return `${d}d ${h}h ${m}m`;
29
+ if (h > 0) return `${h}h ${m}m ${s}s`;
30
+ if (m > 0) return `${m}m ${s}s`;
31
+ return `${s}s`;
32
+ }
33
+
34
+ export function flattenConfig(value: unknown, prefix = ''): ConfigEntry[] {
35
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
36
+ const rows = Object.entries(value as Record<string, unknown>);
37
+ if (!rows.length && prefix) return [{ key: prefix, value: '{}', type: 'object' }];
38
+ return rows.flatMap(([key, child]) => flattenConfig(child, prefix ? `${prefix}.${key}` : key));
39
+ }
40
+ if (!prefix) return [];
41
+ if (Array.isArray(value)) return [{ key: prefix, value: JSON.stringify(value), type: 'array' }];
42
+ if (value === null) return [{ key: prefix, value: '', type: 'null' }];
43
+ if (typeof value === 'number') return [{ key: prefix, value: String(value), type: 'number' }];
44
+ if (typeof value === 'boolean') return [{ key: prefix, value: String(value), type: 'boolean' }];
45
+ return [{ key: prefix, value: String(value), type: 'string' }];
46
+ }
47
+
48
+ export function configValue(entry: ConfigEntry): unknown {
49
+ if (entry.type === 'null') return null;
50
+ if (entry.type === 'number') return Number(entry.value);
51
+ if (entry.type === 'boolean') return entry.value === 'true';
52
+ if (entry.type === 'object' || entry.type === 'array') return JSON.parse(entry.value);
53
+ return entry.value;
54
+ }
55
+
56
+ export function buildConfig(entries: ConfigEntry[]): Record<string, unknown> {
57
+ const root: Record<string, unknown> = {};
58
+ for (const entry of entries) {
59
+ const parts = entry.key.trim().split('.').filter(Boolean);
60
+ if (!parts.length || parts.some((part) => ['__proto__', 'constructor', 'prototype'].includes(part))) continue;
61
+ const value = configValue(entry);
62
+ let node = root;
63
+ parts.forEach((part, index) => {
64
+ if (index === parts.length - 1) node[part] = value;
65
+ else {
66
+ const child = node[part];
67
+ if (!child || typeof child !== 'object' || Array.isArray(child)) node[part] = {};
68
+ node = node[part] as Record<string, unknown>;
69
+ }
70
+ });
71
+ }
72
+ return root;
73
+ }
74
+
75
+ export function topGroup(key: string): string {
76
+ const trimmed = key.trim();
77
+ if (!trimmed) return 'unsorted';
78
+ if (trimmed.startsWith('NEXUS_')) return trimmed.slice(6).split('_')[0].toLowerCase() || 'root';
79
+ return trimmed.split('.')[0] || 'root';
80
+ }
81
+
82
+ export function groupEntries<T extends { key: string }>(entries: T[]): Array<{ group: string; items: Array<{ index: number; entry: T }> }> {
83
+ const order: string[] = [];
84
+ const map = new Map<string, Array<{ index: number; entry: T }>>();
85
+ entries.forEach((entry, index) => {
86
+ const group = topGroup(entry.key);
87
+ if (!map.has(group)) { map.set(group, []); order.push(group); }
88
+ map.get(group)!.push({ index, entry });
89
+ });
90
+ return order.map((group) => ({ group, items: map.get(group)! }));
91
+ }
92
+
93
+ export function friendlyPreflightError(error?: string, category?: string): string {
94
+ if (!error) return 'unreachable';
95
+ const meta = category ? CATEGORY_LABEL[category] : undefined;
96
+ if (!meta || error.toLowerCase().startsWith(meta.label.toLowerCase())) return error;
97
+ return `${meta.label} — ${error}`;
98
+ }
99
+
100
+ export function preflightAddress(c: PreflightCheck): string {
101
+ if (c.kind === 'http' && c.url) return c.url;
102
+ if (c.kind === 'tcp' && c.host) return `${c.host}:${c.port}`;
103
+ return c.name;
104
+ }
105
+
106
+ /** Derive a deterministic HSL hue from an arbitrary string (for custom provider avatars). */
107
+ export function hashHue(s: string): number {
108
+ let h = 0;
109
+ for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 360;
110
+ return h;
111
+ }
112
+
113
+ export function providerMeta(id: string): { glyph: IconName | string; hue: number } {
114
+ if (PROVIDER_META[id]) return PROVIDER_META[id]!;
115
+ const letter = id.charAt(0).toUpperCase() || '?';
116
+ return { glyph: letter, hue: hashHue(id) };
117
+ }
118
+
119
+ /** Returns true if the provider's baseUrl points to a local host. */
120
+ export function isLocalProvider(baseUrl: string): boolean {
121
+ try {
122
+ const u = new URL(baseUrl);
123
+ return u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '0.0.0.0';
124
+ } catch { return false; }
125
+ }
126
+
127
+ /** Derive the marketing website from an API base URL (strip /v1, /api, etc). */
128
+ export function websiteFromBaseUrl(baseUrl: string): string {
129
+ try {
130
+ const u = new URL(baseUrl);
131
+ // For local providers (localhost / 127.0.0.1 / 0.0.0.0) just return the origin.
132
+ const isLocal = u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '0.0.0.0';
133
+ if (isLocal) return u.origin;
134
+ // For cloud providers, strip common API path segments and go to the root.
135
+ return `${u.protocol}//${u.hostname}`;
136
+ } catch {
137
+ return baseUrl;
138
+ }
139
+ }
140
+
141
+ export function formatPointTime(t: number, range: RequestSeriesRange): string {
142
+ const d = new Date(t);
143
+ const time = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
144
+ if (range === 'today' || range === '5d') return time;
145
+ if (range === 'week') return `${d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} ${time}`;
146
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
147
+ }
@@ -1,6 +1,7 @@
1
- import './index.css';
2
- import { App as NexusAdmin } from './App.js';
3
1
  import React from 'react';
2
+
3
+ import { App as NexusAdmin } from '@bhooai/nexus-admin';
4
+ import '@bhooai/nexus-admin/style.css';
4
5
  import { createRoot } from 'react-dom/client';
5
6
 
6
7
  const el = document.getElementById('root');