@bhooai/nexus-cli 2.0.7 → 2.0.9

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 (51) hide show
  1. package/package.json +1 -1
  2. package/templates/base/apps/admin/package.json.ejs +3 -3
  3. package/templates/base/apps/admin/postcss.config.js +1 -1
  4. package/templates/base/apps/admin/tailwind.config.js +1 -0
  5. package/templates/base/apps/admin/src/App.tsx +0 -38
  6. package/templates/base/apps/admin/src/alertCenter.tsx +0 -151
  7. package/templates/base/apps/admin/src/api.ts +0 -474
  8. package/templates/base/apps/admin/src/assets/bhooai-nexus-logo.svg +0 -25
  9. package/templates/base/apps/admin/src/components/ThemeCentre.tsx +0 -201
  10. package/templates/base/apps/admin/src/components/shell/AppearancePanel.tsx +0 -37
  11. package/templates/base/apps/admin/src/components/shell/Dashboard.tsx +0 -441
  12. package/templates/base/apps/admin/src/components/shell/Login.tsx +0 -69
  13. package/templates/base/apps/admin/src/components/shell/NotifyPanel.tsx +0 -102
  14. package/templates/base/apps/admin/src/components/shell/RailPanel.tsx +0 -133
  15. package/templates/base/apps/admin/src/components/shell/SettingsDialog.tsx +0 -217
  16. package/templates/base/apps/admin/src/components/ui/ConfirmDialog.tsx +0 -38
  17. package/templates/base/apps/admin/src/components/ui/LintChecks.tsx +0 -54
  18. package/templates/base/apps/admin/src/components/ui/PageHead.tsx +0 -13
  19. package/templates/base/apps/admin/src/components/ui/PageHero.tsx +0 -15
  20. package/templates/base/apps/admin/src/icons.tsx +0 -104
  21. package/templates/base/apps/admin/src/index.css +0 -3539
  22. package/templates/base/apps/admin/src/lib/constants.ts +0 -94
  23. package/templates/base/apps/admin/src/lib/theme.ts +0 -80
  24. package/templates/base/apps/admin/src/lib/types.ts +0 -39
  25. package/templates/base/apps/admin/src/lib/utils.ts +0 -147
  26. package/templates/base/apps/admin/src/pages/Config.tsx +0 -108
  27. package/templates/base/apps/admin/src/pages/Databases.tsx +0 -122
  28. package/templates/base/apps/admin/src/pages/Environment.tsx +0 -69
  29. package/templates/base/apps/admin/src/pages/Logs.tsx +0 -270
  30. package/templates/base/apps/admin/src/pages/Monitoring.tsx +0 -88
  31. package/templates/base/apps/admin/src/pages/Overview.tsx +0 -108
  32. package/templates/base/apps/admin/src/pages/Payments.tsx +0 -68
  33. package/templates/base/apps/admin/src/pages/Plugins.tsx +0 -36
  34. package/templates/base/apps/admin/src/pages/Processes.tsx +0 -67
  35. package/templates/base/apps/admin/src/pages/Schema.tsx +0 -130
  36. package/templates/base/apps/admin/src/pages/Users.tsx +0 -144
  37. package/templates/base/apps/admin/src/pages/ai/AddProviderDialog.tsx +0 -150
  38. package/templates/base/apps/admin/src/pages/ai/AiAgents.tsx +0 -31
  39. package/templates/base/apps/admin/src/pages/ai/AiChatPlayground.tsx +0 -208
  40. package/templates/base/apps/admin/src/pages/ai/AiProviders.tsx +0 -276
  41. package/templates/base/apps/admin/src/pages/ai/AiSettings.tsx +0 -84
  42. package/templates/base/apps/admin/src/pages/ai/ProviderCard.tsx +0 -145
  43. package/templates/base/apps/admin/src/pages/ai/ProviderGroup.tsx +0 -50
  44. package/templates/base/apps/admin/src/pages/ai/ProviderKeyDialog.tsx +0 -66
  45. package/templates/base/apps/admin/src/pages/monitoring/RequestSeriesChart.tsx +0 -70
  46. package/templates/base/apps/admin/src/pages/overview/TrafficChart.tsx +0 -32
  47. package/templates/base/apps/admin/src/pages/payments/OrdersTable.tsx +0 -27
  48. package/templates/base/apps/admin/src/pages/payments/PaymentKeysDialog.tsx +0 -74
  49. package/templates/base/apps/admin/src/pages/payments/TestConsole.tsx +0 -173
  50. package/templates/base/apps/admin/src/pages/payments/TransactionsTable.tsx +0 -26
  51. package/templates/base/apps/admin/src/style.css +0 -6517
@@ -1,94 +0,0 @@
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
- };
@@ -1,80 +0,0 @@
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
- }
@@ -1,39 +0,0 @@
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
- }
@@ -1,147 +0,0 @@
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,108 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import type { Tab, ConfigValueType, ConfigEntry } from '../lib/types.js';
3
- import { flattenConfig, buildConfig, groupEntries } from '../lib/utils.js';
4
- import { getAdminConfig, putAdminConfig, putAdminConfigFile, runLintConfig, type AdminConfig } from '../api.js';
5
- import { useAdminAlert } from '../alertCenter.js';
6
- import { Icon } from '../icons.js';
7
- import { PageHead } from '../components/ui/PageHead.js';
8
- import { LintChecks } from '../components/ui/LintChecks.js';
9
- import { ConfirmDialog } from '../components/ui/ConfirmDialog.js';
10
-
11
- export function Config({ onNavigate }: { onNavigate?: (tab: Tab) => void }) {
12
- const [cfg, setCfg] = useState<AdminConfig | null>(null);
13
- const [entries, setEntries] = useState<ConfigEntry[]>([]);
14
- const [fileText, setFileText] = useState('');
15
- const [filePath, setFilePath] = useState('');
16
- const [mode, setMode] = useState<'runtime' | 'source' | 'effective'>('runtime');
17
- const [msg, setMsg] = useAdminAlert('config');
18
- const [busy, setBusy] = useState(false);
19
- const [restartDialog, setRestartDialog] = useState(false);
20
- const [newKey, setNewKey] = useState('');
21
- const [newType, setNewType] = useState<ConfigValueType>('string');
22
- const [newValue, setNewValue] = useState('');
23
-
24
- const addKey = () => {
25
- if (!newKey.trim()) return;
26
- setEntries([...entries, { key: newKey.trim(), value: newType === 'null' ? '' : newValue, type: newType }]);
27
- setNewKey(''); setNewType('string'); setNewValue('');
28
- };
29
-
30
- const load = async () => {
31
- try {
32
- const c = await getAdminConfig();
33
- setCfg(c);
34
- const overrides = flattenConfig(c.runtime);
35
- // Prefer the runtime overrides if there are any; otherwise seed the
36
- // key/value grid from the current effective config so the user starts
37
- // with the real values (redacted secret placeholders are skipped so they
38
- // are never written back as literal overrides).
39
- const base = overrides.length
40
- ? overrides
41
- : flattenConfig(c.config).filter((entry) => entry.value !== '***');
42
- setEntries(base);
43
- setFilePath(c.file?.path ?? '');
44
- setFileText(c.file?.content ?? '');
45
- setMsg(c.file ? '' : 'No human-edited config file found.');
46
- } catch (e: any) { setMsg(String(e?.message ?? e)); }
47
- };
48
- useEffect(() => { load(); }, []);
49
-
50
- const saveRuntime = async () => {
51
- setBusy(true); setMsg('');
52
- try {
53
- const overrides = buildConfig(entries);
54
- await putAdminConfig(overrides);
55
- setMsg('Saved to nexus.runtime.json. Restart services to apply changes.');
56
- setRestartDialog(true);
57
- } catch (e: any) { setMsg(`Error: ${e?.message ?? e}`); }
58
- finally { setBusy(false); }
59
- };
60
-
61
- const saveFile = async () => {
62
- if (!filePath) { setMsg('No config file found.'); return; }
63
- setBusy(true); setMsg('');
64
- try { await putAdminConfigFile(fileText); setMsg(`Saved ${filePath}. Restart services to apply changes.`); setRestartDialog(true); }
65
- catch (e: any) { setMsg(`Error: ${e?.message ?? e}`); }
66
- finally { setBusy(false); }
67
- };
68
-
69
- return (
70
- <div className="admin-view-stack">
71
- <PageHead title="Configuration"><div className="heading-actions"><span className="restart-badge">RESTART REQUIRED AFTER SAVE</span><button onClick={load} className="workspace-action"><Icon name="refresh" size={12} /> Reload</button></div></PageHead>
72
- <div className="config-intro"><div><span className="admin-overline">PROJECT CONTROL PLANE</span><h1>Configure your <em>runtime.</em></h1><p>Use key/value rows for safe JSON overrides, or switch to source mode for the typed project configuration.</p></div><div className="hero-art hero-art--cubes"><strong>{'{ }'}</strong><span className="ha-a" /><span className="ha-b" /><span className="ha-c" /></div></div>
73
- <div className="workspace-tabs">{([['runtime', 'Runtime JSON'], ['source', 'Source file'], ['effective', 'Effective view']] as const).map(([id, label]) => <button key={id} onClick={() => setMode(id)} className={mode === id ? 'is-active' : ''}>{label}</button>)}</div>
74
- {mode === 'runtime' && <section className="workspace-panel config-panel"><div className="panel-title-row"><div><span className="admin-overline">NEXUS CONFIG · KEY/VALUE</span><h3>Configuration overrides</h3></div><button onClick={addKey} className="workspace-action">+ Add key</button></div><p className="panel-description">Existing configuration is loaded into the rows below. Changes are written to nexus.runtime.json and validated against the Nexus schema before saving. Secrets are redacted and managed in the Environment surface.</p><div className="key-add-bar">
75
- <input value={newKey} onChange={(event) => setNewKey(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') addKey(); }} placeholder="key path — e.g. server.port" />
76
- <select value={newType} onChange={(event) => { setNewType(event.target.value as ConfigValueType); if (event.target.value === 'null') setNewValue(''); }}><option value="string">string</option><option value="number">number</option><option value="boolean">boolean</option><option value="null">null</option><option value="object">object</option><option value="array">array</option></select>
77
- <input value={newValue} disabled={newType === 'null'} onChange={(event) => setNewValue(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') addKey(); }} placeholder={newType === 'object' || newType === 'array' ? '{ }' : 'value'} />
78
- </div><div className="key-value-table">
79
- {groupEntries(entries).map(({ group, items }) => (
80
- <div className="key-group" key={group}>
81
- <div className="key-group-head"><span className="key-group-name">{group}</span><small>{items.length} key{items.length === 1 ? '' : 's'}</small></div>
82
- <div className="key-value-head"><span>KEY PATH</span><span>TYPE</span><span>VALUE</span><span /></div>
83
- {items.map(({ index, entry }) => (
84
- <div className="key-value-row" key={`${entry.key}-${index}`}>
85
- <input value={entry.key} onChange={(event) => setEntries(entries.map((item, itemIndex) => itemIndex === index ? { ...item, key: event.target.value } : item))} placeholder="server.port" />
86
- <select value={entry.type} onChange={(event) => setEntries(entries.map((item, itemIndex) => itemIndex === index ? { ...item, type: event.target.value as ConfigValueType, value: event.target.value === 'null' ? '' : item.value } : item))}><option value="string">string</option><option value="number">number</option><option value="boolean">boolean</option><option value="null">null</option><option value="object">object</option><option value="array">array</option></select>
87
- <input value={entry.value} disabled={entry.type === 'null'} onChange={(event) => setEntries(entries.map((item, itemIndex) => itemIndex === index ? { ...item, value: event.target.value } : item))} placeholder={entry.type === 'object' || entry.type === 'array' ? '{ }' : 'value'} />
88
- <button className="row-delete" onClick={() => setEntries(entries.filter((_, itemIndex) => itemIndex !== index))} aria-label={`Delete ${entry.key || 'entry'}`}>×</button>
89
- </div>
90
- ))}
91
- </div>
92
- ))}
93
- {!entries.length && <div className="key-value-empty">No configuration keys loaded.</div>}
94
- </div><div className="config-footer"><span>{entries.length} override{entries.length === 1 ? '' : 's'} · validated on save</span><button onClick={saveRuntime} disabled={busy} className="glass-chip-btn-primary shrink-0">{busy ? 'Saving…' : 'Save runtime JSON'} <span>→</span></button></div></section>}
95
- {mode === 'source' && <section className="workspace-panel config-panel"><div className="panel-title-row"><div><span className="admin-overline">HUMAN-EDITED SOURCE</span><h3>{filePath ? filePath.split(/[\\/]/).pop() : 'nexus.config.ts'}</h3></div><button onClick={saveFile} disabled={busy} className="glass-chip-btn-primary shrink-0">{busy ? 'Saving…' : 'Save source'} <span>→</span></button></div><textarea className="source-editor" value={fileText} onChange={(event) => setFileText(event.target.value)} spellCheck={false} /></section>}
96
- {mode === 'effective' && <section className="workspace-panel config-panel"><div className="panel-title-row"><div><span className="admin-overline">READ-ONLY · SECRETS REDACTED</span><h3>Effective merged config</h3></div></div><pre className="source-editor effective-editor">{cfg ? JSON.stringify(cfg.config, null, 2) : 'Loading configuration…'}</pre></section>}
97
- <LintChecks run={runLintConfig} title="Configuration checks" overline="VALIDATOR · nexus.runtime.json" />
98
- <ConfirmDialog
99
- open={restartDialog}
100
- title="Restart required"
101
- message="Your changes have been saved. Restart the backend services to apply the new configuration."
102
- confirmLabel="Open services"
103
- onConfirm={() => { setRestartDialog(false); onNavigate?.('processes'); }}
104
- onCancel={() => setRestartDialog(false)}
105
- />
106
- </div>
107
- );
108
- }
@@ -1,122 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { getDatabases, createDatabase, deleteDatabase, createCollection, dropCollection, modifyCollection, getCollectionDocs, type DatabaseInfo } from '../api.js';
3
- import { useAdminAlert } from '../alertCenter.js';
4
- import { Icon } from '../icons.js';
5
- import { PageHead } from '../components/ui/PageHead.js';
6
- import { PageHero } from '../components/ui/PageHero.js';
7
- import { ConfirmDialog } from '../components/ui/ConfirmDialog.js';
8
-
9
- export function Databases() {
10
- const [databases, setDatabases] = useState<DatabaseInfo[]>([]);
11
- const [msg, setMsg] = useAdminAlert('databases');
12
- const [newDb, setNewDb] = useState('');
13
- const [expanded, setExpanded] = useState<string | null>(null);
14
- const [docs, setDocs] = useState<Record<string, { count: number; docs: unknown[] }>>({});
15
- const [renames, setRenames] = useState<Record<string, string>>({});
16
- const [newColl, setNewColl] = useState<Record<string, string>>({});
17
- const [confirm, setConfirm] = useState<{ title: string; message: React.ReactNode; action: () => Promise<void> } | null>(null);
18
- const [busy, setBusy] = useState(false);
19
-
20
- const refresh = async () => {
21
- try { setDatabases((await getDatabases()).databases ?? []); setMsg(''); } catch (e: any) { setMsg(String(e?.message ?? e)); }
22
- };
23
- useEffect(() => { refresh(); }, []);
24
-
25
- const withOk = async (fn: () => Promise<any>): Promise<boolean> => {
26
- setMsg('');
27
- try { await fn(); return true; } catch (e: any) { setMsg(`Error: ${e?.message ?? e}`); return false; }
28
- };
29
-
30
- const toggleDocs = async (db: string, coll: string) => {
31
- const key = `${db}/${coll}`;
32
- if (docs[key]) { const next = { ...docs }; delete next[key]; setDocs(next); return; }
33
- try { setDocs({ ...docs, [key]: await getCollectionDocs(db, coll) }); } catch (e: any) { setMsg(`Error: ${e?.message ?? e}`); }
34
- };
35
-
36
- const doRename = async (db: string, coll: string) => {
37
- const newName = (renames[`${db}/${coll}`] ?? '').trim();
38
- if (!newName) return;
39
- if (await withOk(() => modifyCollection(db, coll, { newName }))) { await refresh(); setRenames({ ...renames, [`${db}/${coll}`]: '' }); }
40
- };
41
-
42
- const doCreateColl = async (db: string) => {
43
- const name = (newColl[db] ?? '').trim();
44
- if (!name) return;
45
- if (await withOk(() => createCollection(db, { name }))) { await refresh(); setNewColl({ ...newColl, [db]: '' }); }
46
- };
47
-
48
- const runConfirm = async () => {
49
- if (!confirm) return;
50
- setBusy(true);
51
- try { await confirm.action(); } finally { setBusy(false); setConfirm(null); }
52
- };
53
-
54
- return (
55
- <div className="space-y-6">
56
- <PageHead title="Databases">
57
- <button onClick={refresh} className="glass-chip-btn"><Icon name="refresh" size={12} /> refresh</button>
58
- </PageHead>
59
- <PageHero kicker="DATA FOUNDATION" title={<>Your data, <em>organized.</em></>} desc="Databases and collections backing your project — inspect, create and keep the layer that stores everything sane." glyph="database" art="cubes" />
60
-
61
- <div className="glass-card flex gap-2">
62
- <input className="glass-input max-w-xs" placeholder="new database name" value={newDb} onChange={(e) => setNewDb(e.target.value)} />
63
- <button onClick={async () => { if (newDb.trim() && await withOk(() => createDatabase(newDb.trim()))) { setNewDb(''); await refresh(); } }} className="glass-btn-primary">Create database</button>
64
- </div>
65
-
66
- <div className="space-y-3">
67
- {databases.map((db) => (
68
- <div key={db.name} className="glass-card">
69
- <div className="flex items-center gap-3">
70
- <button onClick={() => setExpanded(expanded === db.name ? null : db.name)} className="cursor-pointer font-mono text-base font-semibold text-slate-100 transition-colors hover:text-indigo-300">
71
- <span className={`mr-1 inline-block transition-transform ${expanded === db.name ? 'rotate-90' : ''}`}><Icon name="chevronRight" size={14} /></span>{db.name}
72
- </button>
73
- <span className="text-xs text-slate-500">{(db.sizeOnDisk / 1024).toFixed(1)} KB · {db.collections.length} collections</span>
74
- <button onClick={() => setConfirm({ title: `Drop database “${db.name}”?`, message: <>This will permanently delete the database <strong>{db.name}</strong> and all of its collections. This cannot be undone.</>, action: async () => { if (await withOk(() => deleteDatabase(db.name))) await refresh(); } })} className="glass-chip-btn-danger ml-auto">drop</button>
75
- </div>
76
-
77
- {expanded === db.name && (
78
- <div className="mt-4 space-y-2.5">
79
- <div className="flex gap-2">
80
- <input className="glass-input max-w-xs !py-1.5 text-sm" placeholder="new collection" value={newColl[db.name] ?? ''} onChange={(e) => setNewColl({ ...newColl, [db.name]: e.target.value })} />
81
- <button onClick={() => doCreateColl(db.name)} className="glass-chip-btn">create</button>
82
- </div>
83
- {db.collections.length === 0 && <p className="text-sm text-slate-500">No collections.</p>}
84
- {db.collections.map((c) => (
85
- <div key={`${db.name}/${c.name}`} className="flex flex-wrap items-center gap-2 border-t border-white/[0.06] pt-2.5 text-sm">
86
- <span className="font-mono text-slate-200">{c.name}</span>
87
- <span className="text-xs text-slate-500">{c.count} docs</span>
88
- <button onClick={() => toggleDocs(db.name, c.name)} className="glass-chip-btn ml-auto">docs</button>
89
- <input className="glass-input w-36 !py-1 text-xs" placeholder="rename to…" value={renames[`${db.name}/${c.name}`] ?? ''} onChange={(e) => setRenames({ ...renames, [`${db.name}/${c.name}`]: e.target.value })} />
90
- <button onClick={() => doRename(db.name, c.name)} className="glass-chip-btn">rename</button>
91
- <button onClick={() => setConfirm({ title: `Drop collection “${c.name}”?`, message: <>This will permanently delete the collection <strong>{db.name}/{c.name}</strong> and its {c.count} document{c.count === 1 ? '' : 's'}. This cannot be undone.</>, action: async () => { if (await withOk(() => dropCollection(db.name, c.name))) await refresh(); } })} className="glass-chip-btn-danger">drop</button>
92
- </div>
93
- ))}
94
- </div>
95
- )}
96
- </div>
97
- ))}
98
- {!databases.length && !msg && <p className="text-sm text-slate-500">No databases found.</p>}
99
- {Object.entries(docs).map(([key, val]) => {
100
- const [db, coll] = key.split('/') as [string, string];
101
- return expanded === db ? (
102
- <div key={key} className="glass-code max-h-64 overflow-auto p-3 text-xs">
103
- <span className="text-slate-400">{db}/{coll} — {val.count} docs (showing {val.docs.length}):</span>
104
- <pre className="mt-1 font-mono">{JSON.stringify(val.docs[0] ?? {}, null, 2)}{val.docs.length > 1 ? `\n… and ${val.docs.length - 1} more` : ''}</pre>
105
- </div>
106
- ) : null;
107
- })}
108
- </div>
109
-
110
- <ConfirmDialog
111
- open={!!confirm}
112
- title={confirm?.title ?? ''}
113
- message={confirm?.message ?? ''}
114
- danger
115
- busy={busy}
116
- confirmLabel="Drop"
117
- onConfirm={runConfirm}
118
- onCancel={() => setConfirm(null)}
119
- />
120
- </div>
121
- );
122
- }