@bhooai/nexus-cli 2.0.7 → 2.0.8

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 (49) 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/src/App.tsx +0 -38
  4. package/templates/base/apps/admin/src/alertCenter.tsx +0 -151
  5. package/templates/base/apps/admin/src/api.ts +0 -474
  6. package/templates/base/apps/admin/src/assets/bhooai-nexus-logo.svg +0 -25
  7. package/templates/base/apps/admin/src/components/ThemeCentre.tsx +0 -201
  8. package/templates/base/apps/admin/src/components/shell/AppearancePanel.tsx +0 -37
  9. package/templates/base/apps/admin/src/components/shell/Dashboard.tsx +0 -441
  10. package/templates/base/apps/admin/src/components/shell/Login.tsx +0 -69
  11. package/templates/base/apps/admin/src/components/shell/NotifyPanel.tsx +0 -102
  12. package/templates/base/apps/admin/src/components/shell/RailPanel.tsx +0 -133
  13. package/templates/base/apps/admin/src/components/shell/SettingsDialog.tsx +0 -217
  14. package/templates/base/apps/admin/src/components/ui/ConfirmDialog.tsx +0 -38
  15. package/templates/base/apps/admin/src/components/ui/LintChecks.tsx +0 -54
  16. package/templates/base/apps/admin/src/components/ui/PageHead.tsx +0 -13
  17. package/templates/base/apps/admin/src/components/ui/PageHero.tsx +0 -15
  18. package/templates/base/apps/admin/src/icons.tsx +0 -104
  19. package/templates/base/apps/admin/src/index.css +0 -3539
  20. package/templates/base/apps/admin/src/lib/constants.ts +0 -94
  21. package/templates/base/apps/admin/src/lib/theme.ts +0 -80
  22. package/templates/base/apps/admin/src/lib/types.ts +0 -39
  23. package/templates/base/apps/admin/src/lib/utils.ts +0 -147
  24. package/templates/base/apps/admin/src/pages/Config.tsx +0 -108
  25. package/templates/base/apps/admin/src/pages/Databases.tsx +0 -122
  26. package/templates/base/apps/admin/src/pages/Environment.tsx +0 -69
  27. package/templates/base/apps/admin/src/pages/Logs.tsx +0 -270
  28. package/templates/base/apps/admin/src/pages/Monitoring.tsx +0 -88
  29. package/templates/base/apps/admin/src/pages/Overview.tsx +0 -108
  30. package/templates/base/apps/admin/src/pages/Payments.tsx +0 -68
  31. package/templates/base/apps/admin/src/pages/Plugins.tsx +0 -36
  32. package/templates/base/apps/admin/src/pages/Processes.tsx +0 -67
  33. package/templates/base/apps/admin/src/pages/Schema.tsx +0 -130
  34. package/templates/base/apps/admin/src/pages/Users.tsx +0 -144
  35. package/templates/base/apps/admin/src/pages/ai/AddProviderDialog.tsx +0 -150
  36. package/templates/base/apps/admin/src/pages/ai/AiAgents.tsx +0 -31
  37. package/templates/base/apps/admin/src/pages/ai/AiChatPlayground.tsx +0 -208
  38. package/templates/base/apps/admin/src/pages/ai/AiProviders.tsx +0 -276
  39. package/templates/base/apps/admin/src/pages/ai/AiSettings.tsx +0 -84
  40. package/templates/base/apps/admin/src/pages/ai/ProviderCard.tsx +0 -145
  41. package/templates/base/apps/admin/src/pages/ai/ProviderGroup.tsx +0 -50
  42. package/templates/base/apps/admin/src/pages/ai/ProviderKeyDialog.tsx +0 -66
  43. package/templates/base/apps/admin/src/pages/monitoring/RequestSeriesChart.tsx +0 -70
  44. package/templates/base/apps/admin/src/pages/overview/TrafficChart.tsx +0 -32
  45. package/templates/base/apps/admin/src/pages/payments/OrdersTable.tsx +0 -27
  46. package/templates/base/apps/admin/src/pages/payments/PaymentKeysDialog.tsx +0 -74
  47. package/templates/base/apps/admin/src/pages/payments/TestConsole.tsx +0 -173
  48. package/templates/base/apps/admin/src/pages/payments/TransactionsTable.tsx +0 -26
  49. package/templates/base/apps/admin/src/style.css +0 -6517
@@ -1,441 +0,0 @@
1
- import React, { useEffect, useMemo, useRef, useState } from 'react';
2
- import logoUrl from '../../assets/bhooai-nexus-logo.svg';
3
- import { Icon, type IconName } from '../../icons.js';
4
- import { SafeGotoDialog } from '@bhooai/nexus-safe-goto';
5
- import { useAlerts, ToastStack } from '../../alertCenter.js';
6
- import type { AdminUser, AdminTheme, CustomTheme, ParticleConfig, Tab, UiMode } from '../../lib/types.js';
7
- import { CUSTOM_THEME_KEY, DEFAULT_PARTICLE_COLORS, PARTICLES_KEY } from '../../lib/constants.js';
8
- import { buildParticleLayout, loadCustomTheme, loadParticles } from '../../lib/theme.js';
9
- import { formatProjectName, storedPreference } from '../../lib/utils.js';
10
- import { getAdminConfig } from '../../api.js';
11
- import { AppearancePanel } from './AppearancePanel.js';
12
- import { NotifyPanel } from './NotifyPanel.js';
13
- import { RailPanel } from './RailPanel.js';
14
- import { SettingsDialog } from './SettingsDialog.js';
15
- import { Overview } from '../../pages/Overview.js';
16
- import { Processes } from '../../pages/Processes.js';
17
- import { Logs } from '../../pages/Logs.js';
18
- import { Config } from '../../pages/Config.js';
19
- import { Environment } from '../../pages/Environment.js';
20
- import { Plugins } from '../../pages/Plugins.js';
21
- import { Users } from '../../pages/Users.js';
22
- import { Monitoring } from '../../pages/Monitoring.js';
23
- import { Payments } from '../../pages/Payments.js';
24
- import { Databases } from '../../pages/Databases.js';
25
- import { Schema } from '../../pages/Schema.js';
26
- import { AiAgents } from '../../pages/ai/AiAgents.js';
27
- import { ThemeCentre } from '../ThemeCentre.js';
28
-
29
- export function Dashboard({ user, onUserChange, onLogout }: { user: AdminUser | null; onUserChange: (u: AdminUser) => void; onLogout: () => void }) {
30
- const [tab, setTab] = useState<Tab>('overview');
31
- const [settingsOpen, setSettingsOpen] = useState(false);
32
- const [search, setSearch] = useState('');
33
- const [searchOpen, setSearchOpen] = useState(false);
34
- const searchRef = useRef<HTMLDivElement | null>(null);
35
- const searchInputRef = useRef<HTMLInputElement | null>(null);
36
- const [mobileNav, setMobileNav] = useState(false);
37
- const [mobileSearch, setMobileSearch] = useState(false);
38
- const sidebarRef = useRef<HTMLElement | null>(null);
39
- const [railOpen, setRailOpen] = useState<string | null>(null);
40
- const railRef = useRef<HTMLDivElement | null>(null);
41
- const railPanelRef = useRef<HTMLDivElement | null>(null);
42
- const notifyRef = useRef<HTMLDivElement | null>(null);
43
- const FAVORITES_KEY = 'nexus-admin-favorites';
44
- const ACTIVITY_KEY = 'nexus-admin-activity';
45
- const NOTES_KEY = 'nexus-admin-notes';
46
- const [favorites, setFavorites] = useState<Tab[]>(() => {
47
- try {
48
- const raw = window.localStorage.getItem(FAVORITES_KEY);
49
- if (raw) {
50
- const arr = JSON.parse(raw) as Tab[];
51
- if (Array.isArray(arr) && arr.every((x) => typeof x === 'string')) return arr;
52
- }
53
- } catch { /* ignore */ }
54
- return ['overview', 'config', 'env', 'logs'];
55
- });
56
- const [notes, setNotes] = useState<string>(() => {
57
- try { return window.localStorage.getItem(NOTES_KEY) ?? ''; } catch { return ''; }
58
- });
59
- const [activity, setActivity] = useState<{ at: string; label: string }[]>(() => {
60
- try {
61
- const raw = window.localStorage.getItem(ACTIVITY_KEY);
62
- if (raw) {
63
- const arr = JSON.parse(raw) as { at: string; label: string }[];
64
- if (Array.isArray(arr)) return arr.slice(0, 20);
65
- }
66
- } catch { /* ignore */ }
67
- return [];
68
- });
69
- const saveNotes = (v: string) => { setNotes(v); try { window.localStorage.setItem(NOTES_KEY, v); } catch { /* ignore */ } };
70
- const toggleFavorite = (id: Tab) => {
71
- setFavorites((prev) => {
72
- const next = prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id];
73
- try { window.localStorage.setItem(FAVORITES_KEY, JSON.stringify(next)); } catch { /* ignore */ }
74
- return next;
75
- });
76
- };
77
- const goTab = (id: Tab) => {
78
- setTab(id);
79
- setMobileNav(false);
80
- setSearch('');
81
- setSearchOpen(false);
82
- setRailOpen(null);
83
- const label = nav.find((n) => n.id === id)?.label ?? id;
84
- setActivity((prev) => {
85
- const next = [{ at: new Date().toLocaleTimeString(), label }, ...prev].slice(0, 20);
86
- try { window.localStorage.setItem(ACTIVITY_KEY, JSON.stringify(next)); } catch { /* ignore */ }
87
- return next;
88
- });
89
- };
90
- const [theme, setTheme] = useState<AdminTheme>(() => storedPreference('nexus-admin-theme', 'aurora', ['aurora', 'midnight', 'violet']));
91
- const [uiMode, setUiMode] = useState<UiMode>(() => storedPreference('nexus-admin-ui', 'workspace', ['workspace', 'compact', 'focus']));
92
- const [customTheme, setCustomTheme] = useState<CustomTheme>(() => loadCustomTheme());
93
- const updateCustomTheme = (partial: Partial<CustomTheme>) => {
94
- setCustomTheme((prev) => {
95
- const next = { ...prev, ...partial };
96
- try { window.localStorage.setItem(CUSTOM_THEME_KEY, JSON.stringify(next)); } catch { /* storage unavailable */ }
97
- return next;
98
- });
99
- };
100
- const [particles, setParticles] = useState<ParticleConfig>(() => loadParticles());
101
- const updateParticles = (partial: Partial<ParticleConfig>) => {
102
- setParticles((prev) => {
103
- const next = { ...prev, ...partial };
104
- try { window.localStorage.setItem(PARTICLES_KEY, JSON.stringify(next)); } catch { /* storage unavailable */ }
105
- return next;
106
- });
107
- };
108
- const particleLayout = useMemo(() => buildParticleLayout(particles.amount), [particles.amount]);
109
- const [appearanceOpen, setAppearanceOpen] = useState(false);
110
- const [accountOpen, setAccountOpen] = useState(false);
111
- const { alerts } = useAlerts();
112
- const accountRef = useRef<HTMLDivElement | null>(null);
113
- const appearanceRef = useRef<HTMLDivElement | null>(null);
114
- useEffect(() => {
115
- if (!accountOpen && !appearanceOpen) return;
116
- const onDown = (e: MouseEvent) => {
117
- const target = e.target as Node;
118
- if (accountOpen && accountRef.current && !accountRef.current.contains(target)) setAccountOpen(false);
119
- if (appearanceOpen && appearanceRef.current && !appearanceRef.current.contains(target)) setAppearanceOpen(false);
120
- };
121
- document.addEventListener('mousedown', onDown);
122
- return () => document.removeEventListener('mousedown', onDown);
123
- }, [accountOpen, appearanceOpen]);
124
- useEffect(() => {
125
- const onDown = (e: MouseEvent) => {
126
- if (railOpen && !(railRef.current?.contains(e.target as Node) || railPanelRef.current?.contains(e.target as Node) || notifyRef.current?.contains(e.target as Node))) setRailOpen(null);
127
- };
128
- document.addEventListener('mousedown', onDown);
129
- return () => document.removeEventListener('mousedown', onDown);
130
- }, [railOpen]);
131
- useEffect(() => {
132
- const onDown = (e: MouseEvent) => {
133
- if (searchOpen && searchRef.current && !searchRef.current.contains(e.target as Node)) setSearchOpen(false);
134
- };
135
- document.addEventListener('mousedown', onDown);
136
- return () => document.removeEventListener('mousedown', onDown);
137
- }, [searchOpen]);
138
- useEffect(() => {
139
- if (!mobileNav) return;
140
- const onDown = (e: MouseEvent) => {
141
- const el = e.target as HTMLElement;
142
- if (el && sidebarRef.current && !sidebarRef.current.contains(el) && !el.closest('.admin-mobile-menu')) setMobileNav(false);
143
- };
144
- document.addEventListener('mousedown', onDown);
145
- return () => document.removeEventListener('mousedown', onDown);
146
- }, [mobileNav]);
147
- useEffect(() => {
148
- const onKey = (e: KeyboardEvent) => {
149
- if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
150
- e.preventDefault();
151
- searchInputRef.current?.focus();
152
- setSearchOpen(true);
153
- }
154
- };
155
- window.addEventListener('keydown', onKey);
156
- return () => window.removeEventListener('keydown', onKey);
157
- }, []);
158
- const [projectName, setProjectName] = useState('');
159
- const [projectDb, setProjectDb] = useState('');
160
- const [projectVersion, setProjectVersion] = useState('');
161
- useEffect(() => {
162
- let cancelled = false;
163
- let tries = 0;
164
- const load = async () => {
165
- if (cancelled) return;
166
- try {
167
- const c = await getAdminConfig();
168
- const name = c.project?.name ?? '';
169
- if (name) { setProjectName(name); setProjectDb(c.project?.dbName ?? ''); setProjectVersion(c.project?.version ?? ''); return; }
170
- } catch { /* retry below */ }
171
- if (!cancelled && tries < 15) { tries += 1; window.setTimeout(load, 1500); }
172
- };
173
- void load();
174
- return () => { cancelled = true; };
175
- }, []);
176
- const [shellWidth] = useState(() => Math.round((typeof window === 'undefined' ? 0 : window.screen.width) * 0.8));
177
- const displayName = user?.name || user?.email?.split('@')[0] || 'Admin';
178
- const initials = displayName.trim().split(/\s+/).map((p) => p[0]).join('').slice(0, 2).toUpperCase() || '?';
179
- const nav: { id: Tab; label: string; icon: IconName; keywords?: string[]; desc?: string }[] = [
180
- { id: 'overview', label: 'Overview', icon: 'house', keywords: ['home', 'dashboard', 'project', 'summary'], desc: 'Project overview, services pulse and quick links.' },
181
- { id: 'processes', label: 'Processes', icon: 'server', keywords: ['services', 'supervisor', 'dev', 'run', 'terminal', 'server'], desc: 'Live service states with start/stop/restart control.' },
182
- { id: 'logs', label: 'Logs', icon: 'terminal', keywords: ['console', 'error', 'output', 'stdout', 'stderr', 'supervisor'], desc: 'Aggregated service output, filterable and searchable.' },
183
- { id: 'config', label: 'Config JSON', icon: 'braces', keywords: ['configuration', 'runtime', 'settings', 'json', 'source file', 'nexus.config'], desc: 'Runtime overrides are stored in nexus.runtime.json.' },
184
- { id: 'env', label: 'Environment', icon: 'key', keywords: ['dotenv', 'variables', 'secrets', 'keys', '.env'], desc: 'Masked environment values are ready to edit.' },
185
- { id: 'plugins', label: 'Plugins', icon: 'puzzle', keywords: ['extensions', 'modules', 'addons', 'gallery'], desc: 'Registered plugin pages, slots and surfaces.' },
186
- { id: 'users', label: 'Users', icon: 'users', keywords: ['roles', 'permissions', 'accounts', 'team', 'access', 'rbac'], desc: 'Accounts, roles and permission grants.' },
187
- { id: 'monitoring', label: 'Monitoring', icon: 'activity', keywords: ['metrics', 'telemetry', 'health', 'uptime', 'traffic', 'stats'], desc: 'Live backend metrics and request traffic.' },
188
- { id: 'payments', label: 'Payments', icon: 'dollar', keywords: ['orders', 'transactions', 'money', 'ledger', 'checkout', 'razorpay', 'paypal', 'stripe'], desc: 'Orders, transactions and provider status.' },
189
- { id: 'databases', label: 'Databases', icon: 'database', keywords: ['mongo', 'mongodb', 'collections', 'data', 'tables', 'docs'], desc: 'Databases, collections and their documents.' },
190
- { id: 'schema', label: 'AI Schema', icon: 'sparkles', keywords: ['generate', 'ai', 'model', 'json schema', 'validator'], desc: 'Generate MongoDB schemas from plain English.' },
191
- { id: 'ai', label: 'AI Agents', icon: 'bot', keywords: ['providers', 'chat', 'playground', 'ollama', 'openai', 'models', 'settings'], desc: 'AI settings, providers and chat playground.' },
192
- { id: 'theme', label: 'Theme Centre', icon: 'palette', keywords: ['appearance', 'colors', 'palette', 'ui', 'customize', 'surface'], desc: 'Curated themes, palettes and surface tuning.' },
193
- ];
194
- const searchResults = useMemo(() => {
195
- const q = search.trim().toLowerCase();
196
- if (!q) return [];
197
- return nav.filter((n) => {
198
- if (n.label.toLowerCase().includes(q)) return true;
199
- return (n.keywords ?? []).some((k) => k.toLowerCase().includes(q));
200
- });
201
- }, [search, nav]);
202
- const title = nav.find((item) => item.id === tab)?.label ?? 'Overview';
203
- const displayProject = projectName ? formatProjectName(projectName) : '';
204
- const shellStyle: React.CSSProperties = {
205
- ['--admin-shell-width' as string]: `${shellWidth}px`,
206
- ['--admin-mesh' as string]: String(customTheme.mesh),
207
- ['--admin-glass' as string]: String(customTheme.glass),
208
- ['--admin-radius' as string]: `${customTheme.radius}px`,
209
- ['--admin-mesh-opacity' as string]: String(0.2 + customTheme.mesh * 0.8),
210
- };
211
- if (customTheme.active) {
212
- Object.assign(shellStyle, {
213
- ['--admin-blue' as string]: customTheme.blue,
214
- ['--admin-violet' as string]: customTheme.violet,
215
- ['--admin-pink' as string]: customTheme.pink,
216
- ['--admin-bg' as string]: customTheme.bg,
217
- ['--admin-ink' as string]: customTheme.ink,
218
- ['--admin-muted' as string]: customTheme.muted,
219
- background: `radial-gradient(60rem 60rem at 85% -10%, color-mix(in srgb, ${customTheme.violet} ${Math.round(28 + customTheme.mesh * 34)}%, transparent), transparent 70%), radial-gradient(50rem 50rem at -10% 110%, color-mix(in srgb, ${customTheme.blue} ${Math.round(24 + customTheme.mesh * 30)}%, transparent), transparent 70%), radial-gradient(45rem 45rem at 50% 50%, color-mix(in srgb, ${customTheme.pink} ${Math.round(14 + customTheme.mesh * 22)}%, transparent), transparent 70%), ${customTheme.bg}`,
220
- });
221
- }
222
- const particleColors = particles.colors.length ? particles.colors : DEFAULT_PARTICLE_COLORS;
223
- return (
224
- <div className={`admin-shell bg-mesh theme-${theme} ui-${uiMode}`} style={shellStyle}>
225
- <div className="admin-particles" aria-hidden="true">
226
- {particleLayout.map((p, i) => {
227
- const color = particleColors[i % particleColors.length];
228
- return (
229
- <i key={i} style={{
230
- left: `${p.left}%`,
231
- width: `${p.size}px`,
232
- height: `${p.size}px`,
233
- background: `radial-gradient(circle, ${color}, transparent 72%)`,
234
- animation: `admin-float ${(p.duration / particles.speed).toFixed(2)}s linear infinite`,
235
- animationDelay: `-${p.delay.toFixed(2)}s`,
236
- ['--dx' as string]: `${p.dx}px`,
237
- ['--dx2' as string]: `${p.dx2}px`,
238
- ['--o' as string]: p.opacity,
239
- }} />
240
- );
241
- })}
242
- </div>
243
- <aside ref={sidebarRef} className={`admin-sidebar ${mobileNav ? 'is-open' : ''}`}>
244
- <div className="admin-brand">
245
- <img src={logoUrl} alt="BhooAI Nexus" />
246
- <div>
247
- <strong>NEXUS</strong>
248
- <span>Admin workspace</span>
249
- </div>
250
- <button className="admin-close-mobile" onClick={() => setMobileNav(false)} aria-label="Close navigation">×</button>
251
- </div>
252
- <div className="admin-project-card">
253
- <span className="admin-overline">ACTIVE PROJECT</span>
254
- <strong>{displayProject || 'Loading…'}</strong>
255
- {projectDb && <em className="admin-project-db">db · {projectDb}</em>}
256
- <span><i className="status-dot good" /> Development</span>
257
- </div>
258
- <div className="admin-nav-label">Workspace</div>
259
- <nav className="admin-nav">
260
- {nav.slice(0, 5).map((n) => (
261
- <button key={n.id} onClick={() => { setTab(n.id); setMobileNav(false); }} className={tab === n.id ? 'is-active' : ''}>
262
- <span className="admin-nav-icon"><Icon name={n.icon} size={16} /></span>{n.label}
263
- {n.id === 'env' && <b>NEW</b>}
264
- </button>
265
- ))}
266
- </nav>
267
- <div className="admin-nav-label">Operations</div>
268
- <nav className="admin-nav">
269
- {nav.slice(5).map((n) => (
270
- <button key={n.id} onClick={() => { setTab(n.id); setMobileNav(false); }} className={tab === n.id ? 'is-active' : ''}>
271
- <span className="admin-nav-icon"><Icon name={n.icon} size={16} /></span>{n.label}
272
- </button>
273
- ))}
274
- </nav>
275
- </aside>
276
- <div className="admin-main-shell">
277
- <header className={`admin-topbar${mobileSearch ? ' is-mobile-search' : ''}`}>
278
- <button className="admin-mobile-menu" onClick={() => setMobileNav(true)} aria-label="Open navigation"><Icon name="menu" size={16} /></button>
279
- <div className="admin-window-tab"><span className="window-tab-mark">◈</span><strong>{title}</strong></div>
280
- <div className="admin-search" ref={searchRef}>
281
- <span className="admin-search-icon"><Icon name="search" size={14} /></span>
282
- <input
283
- ref={searchInputRef}
284
- value={search}
285
- onChange={(e) => { setSearch(e.target.value); setSearchOpen(true); }}
286
- onFocus={() => setSearchOpen(true)}
287
- onKeyDown={(e) => {
288
- if (e.key === 'Enter' && searchResults.length) {
289
- e.preventDefault();
290
- setTab(searchResults[0].id);
291
- setSearch('');
292
- setSearchOpen(false);
293
- setMobileSearch(false);
294
- } else if (e.key === 'Escape') {
295
- setSearch('');
296
- setSearchOpen(false);
297
- setMobileSearch(false);
298
- searchInputRef.current?.blur();
299
- }
300
- }}
301
- placeholder="Search workspace"
302
- />
303
- <kbd>⌘ K</kbd>
304
- {searchOpen && search.length > 0 && (
305
- <div className="admin-search-results">
306
- {searchResults.length === 0 ? (
307
- <div className="admin-search-empty">No matches for “{search}”</div>
308
- ) : (
309
- searchResults.map((n) => (
310
- <button key={n.id} className={tab === n.id ? 'is-active' : ''} onMouseDown={(e) => { e.preventDefault(); setTab(n.id); setSearch(''); setSearchOpen(false); setMobileSearch(false); }}>
311
- <span className="admin-search-result-icon"><Icon name={n.icon} size={14} /></span>{n.label}
312
- </button>
313
- ))
314
- )}
315
- </div>
316
- )}
317
- </div>
318
- <div className="admin-top-actions">
319
- <span className="top-live"><i className="status-dot good" /> All systems nominal</span>
320
- <button className="admin-mobile-search" aria-label="Search" aria-expanded={mobileSearch} onClick={() => {
321
- const next = !mobileSearch;
322
- setMobileSearch(next);
323
- if (next) {
324
- setAppearanceOpen(false);
325
- setAccountOpen(false);
326
- window.setTimeout(() => { searchInputRef.current?.focus(); setSearchOpen(true); }, 60);
327
- } else {
328
- setSearch('');
329
- setSearchOpen(false);
330
- }
331
- }}>{mobileSearch ? <Icon name="x" size={16} /> : <Icon name="search" size={16} />}</button>
332
- <div ref={appearanceRef} className="admin-account">
333
- <button className="admin-icon-button" onClick={() => { setAppearanceOpen(!appearanceOpen); setAccountOpen(false); }} aria-label="Open appearance settings" aria-expanded={appearanceOpen}><Icon name="palette" size={16} /></button>
334
- {appearanceOpen && <AppearancePanel theme={theme} uiMode={uiMode} onThemeChange={(value) => { setTheme(value); window.localStorage.setItem('nexus-admin-theme', value); }} onUiModeChange={(value) => { setUiMode(value); window.localStorage.setItem('nexus-admin-ui', value); }} />}
335
- </div>
336
- <div className="admin-notify" ref={notifyRef}>
337
- <button className={`admin-icon-button${railOpen === 'notify' ? ' is-active' : ''}`} aria-label="Notifications" aria-expanded={railOpen === 'notify'} onClick={() => setRailOpen(railOpen === 'notify' ? null : 'notify')}><Icon name="bell" size={16} />{alerts.length > 0 && <b>{alerts.length > 99 ? '99+' : alerts.length}</b>}</button>
338
- {railOpen === 'notify' && <NotifyPanel onClose={() => setRailOpen(null)} />}
339
- </div>
340
- <div className="admin-account" ref={accountRef}>
341
- <button className="admin-user-avatar" aria-label="Account" aria-expanded={accountOpen} onClick={() => { setAccountOpen(!accountOpen); setAppearanceOpen(false); }}>{initials}</button>
342
- {accountOpen && (
343
- <div className="account-panel">
344
- <div className="account-head">
345
- <span className="account-avatar">{initials}</span>
346
- <span className="account-meta">
347
- <strong>{displayName}</strong>
348
- <small>{user?.email ?? 'admin@bhooai.local'}</small>
349
- </span>
350
- </div>
351
- <div className="account-divider" />
352
- <button className="account-item" onClick={() => { setSettingsOpen(true); setAccountOpen(false); }}>
353
- <span className="account-item-icon"><Icon name="settings" size={15} /></span><span><b>Settings</b><small>Profile, security, project & package</small></span>
354
- </button>
355
- <button className="account-item account-danger" onClick={onLogout}>
356
- <span className="account-item-icon"><Icon name="logOut" size={15} /></span><span><b>Sign out</b><small>End this session</small></span>
357
- </button>
358
- </div>
359
- )}
360
- </div>
361
- </div>
362
- </header>
363
- <div className="admin-breadcrumb"><strong>{displayProject || 'Project'}</strong><b>›</b><span>{title}</span><button>⋯</button></div>
364
- <div className="admin-content-layout">
365
- <main className="admin-content">
366
- {tab === 'overview' && <Overview onNavigate={setTab} />}
367
- {tab === 'processes' && <Processes />}
368
- {tab === 'logs' && <Logs onNavigate={setTab} />}
369
- {tab === 'config' && <Config onNavigate={setTab} />}
370
- {tab === 'env' && <Environment onNavigate={setTab} />}
371
- {tab === 'plugins' && <Plugins />}
372
- {tab === 'users' && <Users currentUserEmail={user?.email} />}
373
- {tab === 'monitoring' && <Monitoring />}
374
- {tab === 'payments' && <Payments toggle={customTheme.toggle} />}
375
- {tab === 'databases' && <Databases />}
376
- {tab === 'schema' && <Schema />}
377
- {tab === 'ai' && <AiAgents toggle={customTheme.toggle} />}
378
- {tab === 'theme' && <ThemeCentre theme={theme} onThemeChange={(value) => { setTheme(value); window.localStorage.setItem('nexus-admin-theme', value); }} uiMode={uiMode} onUiModeChange={(value) => { setUiMode(value); window.localStorage.setItem('nexus-admin-ui', value); }} custom={customTheme} onCustom={updateCustomTheme} particles={particles} onParticles={updateParticles} />}
379
- </main>
380
- <aside className="admin-inspector">
381
- <div className="inspector-heading"><span>WORKSPACE INSPECTOR</span><button aria-label="Close inspector">×</button></div>
382
- <div className="inspector-preview"><div className="preview-orbit"><span /><i /><b /></div><strong>Project Nexus</strong><span>Full-stack application</span></div>
383
- <div className="inspector-tabs"><b>Summary</b><span>Activity</span></div>
384
- <div className="inspector-card"><span className="admin-overline">CURRENT SURFACE</span><strong>{title}</strong><p>{nav.find((item) => item.id === tab)?.desc ?? 'Manage the running Nexus project from this workspace.'}</p></div>
385
- <div className="inspector-card inspector-list"><span className="admin-overline">QUICK LINKS</span><button onClick={() => setTab('config')}>Configuration <b>→</b></button><button onClick={() => setTab('env')}>Environment <b>→</b></button><button onClick={() => setTab('logs')}>Logs <b>→</b></button></div>
386
- <button className="glass-chip-btn-primary w-full justify-between" onClick={() => setTab('overview')}><Icon name="activity" size={13} /> Open project overview <span>→</span></button>
387
- <div className="admin-side-widget">
388
- <div className="admin-widget-heading"><span>RUNTIME LOAD</span><b>LIVE</b></div>
389
- <div className="load-line"><span>CPU</span><i><b style={{ width: '32%' }} /></i><strong>32%</strong></div>
390
- <div className="load-line"><span>MEM</span><i><b style={{ width: '61%' }} /></i><strong>61%</strong></div>
391
- <div className="load-line"><span>DB</span><i><b className="violet" style={{ width: '44%' }} /></i><strong>44%</strong></div>
392
- </div>
393
- </aside>
394
- </div>
395
- <div className="admin-tool-rail" ref={railRef}>
396
- <button data-tip="Smart search" className={railOpen === 'search' ? 'is-active' : ''} onClick={() => { setRailOpen(railOpen === 'search' ? null : 'search'); if (railOpen !== 'search') { searchInputRef.current?.focus(); setSearchOpen(true); } }}><Icon name="search" size={16} /></button>
397
- <button data-tip="AI Notes" className={railOpen === 'notes' ? 'is-active' : ''} onClick={() => setRailOpen(railOpen === 'notes' ? null : 'notes')}><Icon name="fileText" size={16} /></button>
398
- <button data-tip="Screen recall" className={railOpen === 'activity' ? 'is-active' : ''} onClick={() => setRailOpen(railOpen === 'activity' ? null : 'activity')}><Icon name="activity" size={16} /></button>
399
- </div>
400
- {railOpen && railOpen !== 'notify' && (
401
- <div ref={railPanelRef}>
402
- <RailPanel
403
- open={railOpen}
404
- onClose={() => setRailOpen(null)}
405
- nav={nav}
406
- tab={tab}
407
- favorites={favorites}
408
- onToggleFavorite={toggleFavorite}
409
- onGo={goTab}
410
- notes={notes}
411
- onNotes={saveNotes}
412
- activity={activity}
413
- onClearActivity={() => { setActivity([]); try { window.localStorage.removeItem(ACTIVITY_KEY); } catch { /* ignore */ } }}
414
- onSearch={() => { setRailOpen(null); searchInputRef.current?.focus(); setSearchOpen(true); }}
415
- theme={theme}
416
- onThemeChange={(value) => { setTheme(value); window.localStorage.setItem('nexus-admin-theme', value); }}
417
- uiMode={uiMode}
418
- onUiModeChange={(value) => { setUiMode(value); window.localStorage.setItem('nexus-admin-ui', value); }}
419
- />
420
- </div>
421
- )}
422
- <footer className="admin-dock"><span><b className="dock-logo">◈</b> BhooAI Nexus</span><span>Connected to <strong>localhost:4000</strong></span><span className="dock-right"><i className="status-dot good" /> Secure admin session · {title}</span></footer>
423
- </div>
424
- <SafeGotoDialog />
425
- <SettingsDialog
426
- open={settingsOpen}
427
- onClose={() => setSettingsOpen(false)}
428
- user={user}
429
- onUserChange={onUserChange}
430
- projectName={projectName}
431
- projectDb={projectDb}
432
- projectVersion={projectVersion}
433
- onProjectChange={(name, version) => {
434
- if (name) setProjectName(name);
435
- if (version) setProjectVersion(version);
436
- }}
437
- />
438
- <ToastStack />
439
- </div>
440
- );
441
- }
@@ -1,69 +0,0 @@
1
- import React from 'react';
2
- import { useState } from 'react';
3
- import logoUrl from '../../assets/bhooai-nexus-logo.svg';
4
- import type { AdminUser, AdminTheme } from '../../lib/types.js';
5
- import { storedPreference } from '../../lib/utils.js';
6
- import { login, registerAndLogin } from '../../api.js';
7
-
8
- export function Login({ onAuthed }: { onAuthed: (user: AdminUser) => void }) {
9
- const [email, setEmail] = useState('');
10
- const [password, setPassword] = useState('');
11
- const [name, setName] = useState('');
12
- const [err, setErr] = useState('');
13
- const [busy, setBusy] = useState(false);
14
- const [mode, setMode] = useState<'login' | 'register'>('login');
15
- const [theme] = useState<AdminTheme>(() => storedPreference('nexus-admin-theme', 'aurora', ['aurora', 'midnight', 'violet']));
16
-
17
- const submit = async (e: React.FormEvent) => {
18
- e.preventDefault();
19
- if (busy) return;
20
- setErr('');
21
- setBusy(true);
22
- try {
23
- const data = mode === 'login'
24
- ? await login(email, password)
25
- : await registerAndLogin(email, password, name || email.split('@')[0]);
26
- onAuthed({ email: data?.user?.email ?? email, name: data?.user?.name ?? email.split('@')[0] });
27
- } catch (e: any) {
28
- const message = e?.message as string | undefined;
29
- setErr(
30
- message
31
- || (mode === 'login'
32
- ? 'Sign in failed. Please check your email and password.'
33
- : 'Registration failed. Please try again.'),
34
- );
35
- } finally {
36
- setBusy(false);
37
- }
38
- };
39
-
40
- return (
41
- <div className={`admin-login bg-mesh theme-${theme} flex min-h-screen items-center justify-center p-4`}>
42
- <form onSubmit={submit} className="admin-login-card glass-card w-96 space-y-4 p-8">
43
- <div className="space-y-1">
44
- <img src={logoUrl} alt="BhooAI Nexus Admin" className="mb-4 h-auto w-64 max-w-full" />
45
- <h1 className="brand-text text-2xl font-bold">Admin control panel</h1>
46
- <p className="text-sm text-slate-400">
47
- {mode === 'login' ? 'Sign in to manage the project.' : 'The first registered user becomes admin.'}
48
- </p>
49
- </div>
50
- {mode === 'register' && (
51
- <input className="glass-input" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} />
52
- )}
53
- <input className="glass-input" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} required autoComplete="username" />
54
- <input className="glass-input" type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} required autoComplete="current-password" />
55
- {err && (
56
- <div role="alert" className="rounded-lg border border-rose-500/30 bg-rose-500/10 px-3 py-2 text-sm text-rose-400">
57
- {err}
58
- </div>
59
- )}
60
- <button className="glass-btn-primary w-full disabled:opacity-60 disabled:cursor-not-allowed" type="submit" disabled={busy}>
61
- {busy ? 'Please wait…' : mode === 'login' ? 'Sign in' : 'Register & sign in'}
62
- </button>
63
- <button type="button" className="w-full cursor-pointer text-sm text-slate-400 transition-colors hover:text-slate-200" onClick={() => setMode(mode === 'login' ? 'register' : 'login')}>
64
- {mode === 'login' ? 'Need an account? Register' : 'Already have one? Sign in'}
65
- </button>
66
- </form>
67
- </div>
68
- );
69
- }
@@ -1,102 +0,0 @@
1
- import React from 'react';
2
- import { useState, useEffect } from 'react';
3
- import { Icon } from '../../icons.js';
4
- import { useAlerts } from '../../alertCenter.js';
5
- import { getAllLogs, type LogEntry } from '../../api.js';
6
-
7
- export function NotifyPanel({ onClose }: { onClose: () => void }) {
8
- const [notify, setNotify] = useState<LogEntry[] | null>(null);
9
- const [notifyBusy, setNotifyBusy] = useState(false);
10
- const { alerts, clear: clearAlerts, dismiss: dismissAlert } = useAlerts();
11
-
12
- const loadNotify = async () => {
13
- setNotifyBusy(true);
14
- try { setNotify(await getAllLogs({ level: 'error' })); } catch { setNotify([]); }
15
- finally { setNotifyBusy(false); }
16
- };
17
-
18
- useEffect(() => {
19
- void loadNotify();
20
- const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
21
- window.addEventListener('keydown', onKey);
22
- return () => window.removeEventListener('keydown', onKey);
23
- }, [onClose]);
24
-
25
- const errs = alerts.filter((a) => a.kind === 'err').length;
26
- const warns = alerts.filter((a) => a.kind === 'warn').length;
27
- const oks = alerts.length - errs - warns;
28
- const loading = notify === null;
29
- const hasAlerts = alerts.length > 0;
30
- const hasLogs = (notify?.length ?? 0) > 0;
31
- const hasAny = hasAlerts || hasLogs;
32
-
33
- return (
34
- <div className="notify-panel" role="dialog" aria-modal="false">
35
- <div className="notify-head">
36
- <span className="notify-head-icon"><Icon name="bell" size={15} /></span>
37
- <div className="notify-head-titles">
38
- <span className="admin-overline">NOTIFICATIONS</span>
39
- <strong>{hasAny ? `${alerts.length + (notify?.length ?? 0)} notification${alerts.length + (notify?.length ?? 0) === 1 ? '' : 's'}` : 'All clear'}</strong>
40
- </div>
41
- <button type="button" className="notify-refresh" aria-label="Refresh notifications" onClick={() => void loadNotify()} disabled={notifyBusy}>{notifyBusy ? '…' : <Icon name="refresh" size={13} />}</button>
42
- <button type="button" className="rail-x" aria-label="Close notifications" onClick={onClose}>×</button>
43
- </div>
44
-
45
- <div className="notify-body">
46
- {hasAny ? (
47
- <>
48
- <div className="notify-summary">
49
- {errs > 0 && <span className="is-err">● {errs} error{errs === 1 ? '' : 's'}</span>}
50
- {warns > 0 && <span className="is-warn">● {warns} warning{warns === 1 ? '' : 's'}</span>}
51
- {oks > 0 && <span className="is-ok">● {oks} notice{oks === 1 ? '' : 's'}</span>}
52
- {hasLogs && <span className="is-log">● {notify!.length} log line{(notify?.length ?? 0) === 1 ? '' : 's'}</span>}
53
- </div>
54
-
55
- {hasAlerts && (
56
- <>
57
- <div className="notify-section-title">
58
- <span>ADMIN ALERTS</span>
59
- <button type="button" onClick={clearAlerts} disabled={alerts.length === 0}>Clear all</button>
60
- </div>
61
- <ul className="notify-alerts">
62
- {alerts.slice(0, 8).map((a) => (
63
- <li key={a.id} className={`is-${a.kind}`}>
64
- <span className="notify-alert-bar" />
65
- <span className="notify-alert-dot" />
66
- <span className="notify-alert-body">
67
- <b>{a.message}</b>
68
- <small>{a.source} · {a.at}</small>
69
- </span>
70
- <button type="button" className="notify-alert-x" aria-label="Dismiss alert" onClick={() => dismissAlert(a.id)}>×</button>
71
- </li>
72
- ))}
73
- </ul>
74
- </>
75
- )}
76
-
77
- {hasLogs && (
78
- <>
79
- <div className="notify-section-title">
80
- <span>SYSTEM ERRORS</span>
81
- <span className="notify-section-meta">supervisor · error level</span>
82
- </div>
83
- <ul className="notify-logs">
84
- {notify!.slice(0, 8).map((l, i) => (
85
- <li key={i}>
86
- <b>{l.service}</b><span>{new Date(l.ts).toLocaleTimeString()}</span>
87
- <p>{l.line.length > 96 ? `${l.line.slice(0, 96)}…` : l.line}</p>
88
- </li>
89
- ))}
90
- </ul>
91
- </>
92
- )}
93
- </>
94
- ) : loading ? (
95
- <div className="notify-empty"><span className="notify-empty-icon"><Icon name="bell" size={18} /></span><strong>Loading notifications…</strong></div>
96
- ) : (
97
- <div className="notify-empty"><span className="notify-empty-icon"><Icon name="bell" size={18} /></span><strong>All clear</strong><p>No errors or alerts right now.</p></div>
98
- )}
99
- </div>
100
- </div>
101
- );
102
- }