@bhooai/nexus-cli 2.0.2 → 2.0.4

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 (46) hide show
  1. package/package.json +1 -1
  2. package/src/commands/add.ts +1 -1
  3. package/src/commands/dev.ts +74 -125
  4. package/src/commands/init.ts +87 -136
  5. package/src/devPanel.ts +696 -0
  6. package/src/devServiceManager.ts +229 -0
  7. package/src/dispatcher.ts +22 -1
  8. package/src/examples.ts +90 -0
  9. package/src/features.ts +261 -0
  10. package/src/launcher.ts +164 -0
  11. package/src/layout.ts +101 -0
  12. package/src/templating/tree.ts +66 -0
  13. package/src/tui.ts +170 -0
  14. package/src/wizard.ts +691 -0
  15. package/templates/base/Dockerfile.ejs +1 -0
  16. package/templates/base/apps/admin/nginx.conf.ejs +30 -1
  17. package/templates/base/apps/admin/package.json.ejs +7 -2
  18. package/templates/base/apps/admin/postcss.config.js +5 -0
  19. package/templates/base/apps/admin/src/App.tsx +4127 -0
  20. package/templates/base/apps/admin/src/alertCenter.tsx +150 -0
  21. package/templates/base/apps/admin/src/api.ts +474 -0
  22. package/templates/base/apps/admin/src/assets/bhooai-nexus-logo.svg +25 -0
  23. package/templates/base/apps/admin/src/index.css +3481 -0
  24. package/templates/base/apps/admin/src/main.tsx.ejs +3 -3
  25. package/templates/base/apps/admin/src/vite-env.d.ts +19 -0
  26. package/templates/base/apps/admin/tailwind.config.js +9 -0
  27. package/templates/base/apps/admin/vite.config.ts.ejs +21 -2
  28. package/templates/base/apps/ai-server/main.py.ejs +94 -6
  29. package/templates/base/apps/backend/package.json.ejs +27 -0
  30. package/templates/base/apps/frontend/package.json.ejs +7 -0
  31. package/templates/base/apps/frontend/vite.config.ts.ejs +0 -1
  32. package/templates/base/docker-compose.yml.ejs +6 -1
  33. package/templates/base/nexus.config.ts.ejs +4 -4
  34. package/templates/features/auth/apps/backend/src/models/User.ts +21 -0
  35. package/templates/features/auth/apps/backend/src/routes/auth.ts +95 -0
  36. package/templates/features/email/apps/backend/src/mail/mailables/WelcomeMail.ts +25 -0
  37. package/templates/features/email/apps/backend/src/mail/templates/welcome.ejs.ejs +10 -0
  38. package/templates/features/graphql/apps/backend/src/graphql/post.graph.ts +61 -0
  39. package/templates/features/graphql/apps/backend/src/models/Post.ts +15 -0
  40. package/templates/features/payments/apps/backend/src/routes/payments.ts +45 -0
  41. package/templates/features/queue/apps/backend/src/events/JobQueued.ts +14 -0
  42. package/templates/features/queue/apps/backend/src/jobs/ExampleJob.ts +18 -0
  43. package/templates/features/queue/apps/backend/src/listeners/OnJobQueued.ts +12 -0
  44. package/templates/features/realtime/apps/backend/src/models/Message.ts +14 -0
  45. package/templates/features/realtime/apps/backend/src/ws/chat.room.ts +56 -0
  46. package/templates/features/storage/apps/backend/src/routes/uploads.ts +91 -0
@@ -0,0 +1,150 @@
1
+ import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from 'react';
2
+
3
+ export type AlertKind = 'ok' | 'err' | 'warn';
4
+
5
+ export interface AdminAlert {
6
+ id: string;
7
+ kind: AlertKind;
8
+ message: string;
9
+ source: string;
10
+ at: string;
11
+ }
12
+
13
+ interface AlertContextValue {
14
+ alerts: AdminAlert[];
15
+ toasts: AdminAlert[];
16
+ push: (kind: AlertKind, message: string, source: string) => void;
17
+ clear: () => void;
18
+ dismiss: (id: string) => void;
19
+ dismissToast: (id: string) => void;
20
+ }
21
+
22
+ export const TOAST_TTL = 4500;
23
+
24
+ const ALERTS_KEY = 'nexus-admin-alerts';
25
+ const MAX_ALERTS = 50;
26
+
27
+ const AlertContext = createContext<AlertContextValue | null>(null);
28
+
29
+ function loadAlerts(): AdminAlert[] {
30
+ if (typeof window === 'undefined') return [];
31
+ try {
32
+ const raw = window.localStorage.getItem(ALERTS_KEY);
33
+ if (!raw) return [];
34
+ const parsed = JSON.parse(raw) as AdminAlert[];
35
+ if (!Array.isArray(parsed)) return [];
36
+ return parsed
37
+ .filter((a) => a && typeof a.message === 'string' && typeof a.source === 'string')
38
+ .slice(0, MAX_ALERTS);
39
+ } catch { /* ignore corrupt value */ }
40
+ return [];
41
+ }
42
+
43
+ export function AlertProvider({ children }: { children: ReactNode }) {
44
+ const [alerts, setAlerts] = useState<AdminAlert[]>(() => loadAlerts());
45
+ const [toasts, setToasts] = useState<AdminAlert[]>([]);
46
+ const idRef = useRef(0);
47
+
48
+ useEffect(() => {
49
+ try { window.localStorage.setItem(ALERTS_KEY, JSON.stringify(alerts)); } catch { /* storage unavailable */ }
50
+ }, [alerts]);
51
+
52
+ const push = useCallback((kind: AlertKind, message: string, source: string) => {
53
+ const text = String(message ?? '').trim();
54
+ if (!text) return;
55
+ idRef.current += 1;
56
+ const alert: AdminAlert = {
57
+ id: `${Date.now()}-${idRef.current}`,
58
+ kind,
59
+ message: text,
60
+ source: source || 'admin',
61
+ at: new Date().toLocaleString(),
62
+ };
63
+ setAlerts((prev) => [alert, ...prev].slice(0, MAX_ALERTS));
64
+ setToasts((prev) => [...prev.slice(-3), alert]);
65
+ window.setTimeout(() => {
66
+ setToasts((prev) => prev.filter((t) => t.id !== alert.id));
67
+ }, TOAST_TTL);
68
+ }, []);
69
+
70
+ const clear = useCallback(() => setAlerts([]), []);
71
+
72
+ const dismiss = useCallback((id: string) => {
73
+ setAlerts((prev) => prev.filter((a) => a.id !== id));
74
+ }, []);
75
+
76
+ const dismissToast = useCallback((id: string) => {
77
+ setToasts((prev) => prev.filter((t) => t.id !== id));
78
+ }, []);
79
+
80
+ return (
81
+ <AlertContext.Provider value={{ alerts, toasts, push, clear, dismiss, dismissToast }}>
82
+ {children}
83
+ </AlertContext.Provider>
84
+ );
85
+ }
86
+
87
+ export function useAlerts(): AlertContextValue {
88
+ const ctx = useContext(AlertContext);
89
+ if (!ctx) throw new Error('useAlerts must be used within <AlertProvider>');
90
+ return ctx;
91
+ }
92
+
93
+ /** Pages that default their action messages to "warn" unless they look like errors. */
94
+ const WARN_SOURCES = new Set<string>();
95
+
96
+ function kindFor(source: string, value: string): AlertKind {
97
+ if (WARN_SOURCES.has(source)) return 'warn';
98
+ return /fail|error|could not|no .* (found|available)|unavailable|invalid|missing|rejected|failed|did not|unreachable/i.test(value) ? 'err' : 'ok';
99
+ }
100
+
101
+ export function useAdminAlert(source: string): [
102
+ string | null,
103
+ (value: string | { kind: AlertKind; text: string } | null) => void,
104
+ ] {
105
+ const { push } = useAlerts();
106
+ const [msg, setMsg] = useState<string | null>(null);
107
+ const currentRef = useRef<string | null>(null);
108
+
109
+ const set = useCallback((value: string | { kind: AlertKind; text: string } | null) => {
110
+ if (!value) { currentRef.current = null; setMsg(null); return; }
111
+ if (typeof value === 'object') {
112
+ const text = String(value.text ?? '').trim();
113
+ if (!text) return;
114
+ if (text === currentRef.current) { setMsg(text); return; }
115
+ currentRef.current = text;
116
+ setMsg(text);
117
+ push(value.kind, text, source);
118
+ return;
119
+ }
120
+ const text = String(value).trim();
121
+ const current = currentRef.current;
122
+ currentRef.current = text || null;
123
+ setMsg(text || null);
124
+ if (text && text !== current) push(kindFor(source, text), text, source);
125
+ }, [push, source]);
126
+
127
+ return [msg, set];
128
+ }
129
+
130
+ const TOAST_ICONS: Record<AlertKind, string> = { ok: '✓', err: '✕', warn: '!' };
131
+
132
+ /** Floating alert popups, top-right, themed via the surrounding admin shell. */
133
+ export function ToastStack() {
134
+ const { toasts, dismissToast } = useAlerts();
135
+ if (!toasts.length) return null;
136
+ return (
137
+ <div className="admin-toasts" role="region" aria-label="Notifications">
138
+ {toasts.map((t) => (
139
+ <div key={t.id} className={`admin-toast is-${t.kind}`} role="status">
140
+ <span className="admin-toast-icon">{TOAST_ICONS[t.kind]}</span>
141
+ <span className="admin-toast-body">
142
+ <b>{t.message}</b>
143
+ <small>{t.source} · {t.at}</small>
144
+ </span>
145
+ <button type="button" className="admin-toast-x" aria-label="Dismiss notification" onClick={() => dismissToast(t.id)}>×</button>
146
+ </div>
147
+ ))}
148
+ </div>
149
+ );
150
+ }
@@ -0,0 +1,474 @@
1
+ // Admin API client. Auth/admin/plugin calls go to the backend (proxied by Vite
2
+ // in dev); process-control calls go to the supervisor control API
3
+ // (default :7474, auto-allotted upward when busy — the backend reports the real port).
4
+
5
+ const SUPERVISOR_FALLBACK = (import.meta.env.VITE_SUPERVISOR_URL as string | undefined) ?? 'http://localhost:7474';
6
+
7
+ let supervisorUrl = '';
8
+ export async function resolveSupervisor(): Promise<string> {
9
+ if (supervisorUrl) return supervisorUrl;
10
+ if (import.meta.env.VITE_SUPERVISOR_URL) {
11
+ supervisorUrl = import.meta.env.VITE_SUPERVISOR_URL as string;
12
+ return supervisorUrl;
13
+ }
14
+ try {
15
+ const info = await adminFetch('/admin/supervisor');
16
+ if (info?.url) {
17
+ supervisorUrl = info.url;
18
+ return supervisorUrl;
19
+ }
20
+ if (info?.port) {
21
+ supervisorUrl = `http://127.0.0.1:${info.port}`;
22
+ return supervisorUrl;
23
+ }
24
+ } catch { /* fall through to the default */ }
25
+ supervisorUrl = SUPERVISOR_FALLBACK;
26
+ return supervisorUrl;
27
+ }
28
+
29
+ let csrfToken = '';
30
+ let accessToken = '';
31
+ export function setAccessToken(t: string) { accessToken = t; }
32
+ export function getAccessToken() { return accessToken; }
33
+
34
+ async function refreshCsrf(): Promise<void> {
35
+ // The server rotates the CSRF cookie on EVERY safe request, so a cached
36
+ // token goes stale the moment any other GET runs (status polling, tables…).
37
+ // Always re-fetch so the header matches the current cookie.
38
+ const r = await fetch('/csrf-token', { credentials: 'include' });
39
+ csrfToken = (await r.json()).token ?? '';
40
+ }
41
+
42
+ function authHeaders(): Record<string, string> {
43
+ const h: Record<string, string> = {};
44
+ if (accessToken) h.authorization = `Bearer ${accessToken}`;
45
+ return h;
46
+ }
47
+
48
+ async function adminFetch(path: string, init: RequestInit = {}): Promise<any> {
49
+ const r = await fetch(path, { credentials: 'include', cache: 'no-store', ...init, headers: { ...authHeaders(), ...(init.headers ?? {}) } });
50
+ if (r.status === 401) throw new Error('Unauthorized');
51
+ if (!r.ok) throw new Error(`${path}: ${r.status} ${await r.text()}`);
52
+ return r.json();
53
+ }
54
+
55
+ // ---- auth ----
56
+ /** Extract a friendly message from a failed /auth response (server sends { error: { message } }). */
57
+ async function authErrorMessage(r: Response, fallback: string): Promise<string> {
58
+ try {
59
+ const body = await r.json();
60
+ const msg = body?.error?.message ?? body?.message ?? body?.detail;
61
+ if (typeof msg === 'string' && msg.trim()) return msg;
62
+ } catch { /* non-JSON body — fall through */ }
63
+ switch (r.status) {
64
+ case 400: return 'Invalid request — please check the form.';
65
+ case 401: return 'Invalid email or password.';
66
+ case 403: return 'Access denied.';
67
+ case 404: return 'Auth endpoint not found — is the backend running?';
68
+ case 409: return 'A user with that email already exists.';
69
+ case 429: return 'Too many attempts — please wait a moment and retry.';
70
+ case 500: return 'Server error — please try again later.';
71
+ default: return `${fallback} (HTTP ${r.status})`;
72
+ }
73
+ }
74
+
75
+ export async function login(email: string, password: string): Promise<{ user: any; accessToken: string }> {
76
+ await refreshCsrf();
77
+ let r: Response;
78
+ try {
79
+ r = await fetch('/auth/login', {
80
+ method: 'POST',
81
+ credentials: 'include',
82
+ headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken },
83
+ body: JSON.stringify({ email, password }),
84
+ });
85
+ } catch {
86
+ throw new Error('Cannot reach the backend — is the server running?');
87
+ }
88
+ if (!r.ok) throw new Error(await authErrorMessage(r, 'Login failed'));
89
+ const data = await r.json();
90
+ if (!data?.accessToken) throw new Error('Login succeeded but no access token was returned.');
91
+ accessToken = data.accessToken;
92
+ return data;
93
+ }
94
+
95
+ export async function registerAndLogin(email: string, password: string, name: string): Promise<{ user: any; accessToken: string }> {
96
+ await refreshCsrf();
97
+ let r: Response;
98
+ try {
99
+ r = await fetch('/auth/register', {
100
+ method: 'POST',
101
+ credentials: 'include',
102
+ headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken },
103
+ body: JSON.stringify({ email, password, name }),
104
+ });
105
+ } catch {
106
+ throw new Error('Cannot reach the backend — is the server running?');
107
+ }
108
+ if (!r.ok) throw new Error(await authErrorMessage(r, 'Registration failed'));
109
+ const data = await r.json();
110
+ if (!data?.accessToken) throw new Error('Registration succeeded but no access token was returned.');
111
+ accessToken = data.accessToken;
112
+ return data;
113
+ }
114
+
115
+ let refreshInFlight: Promise<boolean> | null = null;
116
+
117
+ /**
118
+ * Restore the admin session from the HttpOnly refresh cookie (app boot).
119
+ * /auth/refresh returns a fresh access token; the user is then fetched via
120
+ * /auth/me. Single-flight so React StrictMode's double effect-run issues one
121
+ * network request — the backend rotates refresh tokens and revokes a session
122
+ * family on reuse, so concurrent refreshes would log the user out.
123
+ */
124
+ export async function refresh(): Promise<boolean> {
125
+ if (refreshInFlight) return refreshInFlight;
126
+ refreshInFlight = (async () => {
127
+ try {
128
+ await refreshCsrf();
129
+ const r = await fetch('/auth/refresh', {
130
+ method: 'POST',
131
+ credentials: 'include',
132
+ headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken },
133
+ });
134
+ if (!r.ok) return false;
135
+ const data = await r.json();
136
+ accessToken = data.accessToken;
137
+ const me = await adminFetch('/auth/me');
138
+ return !!me?.user?.email;
139
+ } catch {
140
+ return false;
141
+ }
142
+ })();
143
+ try {
144
+ return await refreshInFlight;
145
+ } finally {
146
+ refreshInFlight = null;
147
+ }
148
+ }
149
+
150
+ // ---- admin (backend) ----
151
+ export interface ConfigFile { path: string; content: string; }
152
+ export interface AdminConfig { runtime: Record<string, unknown>; config: Record<string, unknown>; file: ConfigFile | null; project?: { name: string; path: string; dbName: string; status?: string; version?: string }; }
153
+ export interface EnvEntry { key: string; value: string | null; secret: boolean; configPath?: string; }
154
+ export interface AdminEnv { fileName?: string; path: string; exists: boolean; entries: EnvEntry[]; note?: string; }
155
+ export interface DatabaseInfo { name: string; sizeOnDisk: number; collections: Array<{ name: string; count: number }>; }
156
+ export interface UserRecord { _id: string; email: string; name?: string; roles: string[]; emailVerified?: boolean; createdAt?: string; updatedAt?: string; }
157
+ export interface RolePermission { label: string; detail: string; }
158
+ export interface RoleDefinition { id: string; label: string; icon: string; accent: string; description: string; grants: RolePermission[]; restricts: RolePermission[]; }
159
+ export interface GeneratedSchema {
160
+ collection: string;
161
+ model: string;
162
+ fields: Array<{ name: string; type: string; required?: boolean; unique?: boolean; enum?: string[]; description?: string }>;
163
+ jsonSchema: { $jsonSchema: { bsonType: string; required?: string[]; properties: Record<string, unknown> } };
164
+ }
165
+
166
+ export const getAdminConfig = () => adminFetch('/admin/config');
167
+ export const getAdminEnv = (file?: string): Promise<AdminEnv> => adminFetch(file ? `/admin/env?file=${encodeURIComponent(file)}` : '/admin/env');
168
+ export async function putAdminEnv(entries: EnvEntry[], file?: string): Promise<any> {
169
+ await refreshCsrf();
170
+ return adminFetch('/admin/env', { method: 'PUT', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify({ entries, file }) });
171
+ }
172
+ export async function putAdminConfig(overrides: Record<string, unknown>): Promise<any> {
173
+ await refreshCsrf();
174
+ return adminFetch('/admin/config', { method: 'PUT', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify(overrides) });
175
+ }
176
+ export async function putAdminConfigFile(content: string): Promise<any> {
177
+ await refreshCsrf();
178
+ return adminFetch('/admin/config/file', { method: 'PUT', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify({ content }) });
179
+ }
180
+ export const getPlugins = () => adminFetch('/admin/plugins');
181
+ export const getUsers = () => adminFetch('/admin/users');
182
+ export const getRoles = () => adminFetch('/admin/roles');
183
+ export async function putUserRoles(id: string, roles: string[]): Promise<any> {
184
+ await refreshCsrf();
185
+ return adminFetch(`/admin/users/${encodeURIComponent(id)}`, { method: 'PUT', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify({ roles }) });
186
+ }
187
+ export const getMetrics = () => adminFetch('/admin/metrics');
188
+ export async function updateProfile(name: string): Promise<{ ok: boolean; user: any }> {
189
+ await refreshCsrf();
190
+ return adminFetch('/auth/profile', { method: 'PUT', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify({ name }) });
191
+ }
192
+ export async function changePassword(currentPassword: string, newPassword: string): Promise<{ ok: boolean }> {
193
+ await refreshCsrf();
194
+ return adminFetch('/auth/change-password', { method: 'POST', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify({ currentPassword, newPassword }) });
195
+ }
196
+ export async function updateProject(patch: { name?: string; version?: string }): Promise<{ ok: boolean; project?: AdminConfig['project'] }> {
197
+ await refreshCsrf();
198
+ return adminFetch('/admin/project', { method: 'PUT', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify(patch) });
199
+ }
200
+
201
+ // ---- preflight diagnostics (computed by the Python AI server) ----
202
+ export interface PreflightCheck { name: string; kind: string; ok: boolean; latencyMs?: number; status?: number | null; url?: string; host?: string; port?: number; error?: string; errorCategory?: string; }
203
+ export interface PreflightReport { ranAt: string; durationMs: number; engineOk?: boolean; passed: number; warnings: number; failed: number; checks: PreflightCheck[]; }
204
+ export async function runPreflight(): Promise<PreflightReport> {
205
+ await refreshCsrf();
206
+ return adminFetch('/admin/preflight', { method: 'POST', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: '{}' });
207
+ }
208
+
209
+ // ---- config / env linting (computed by the Python AI server) ----
210
+ export interface LintCheck { key: string; severity: 'error' | 'warning' | 'info' | 'ok'; kind: string; message: string; errorCategory?: string; }
211
+ export interface LintReport { ranAt: string; engineOk?: boolean; summary: { error: number; warning: number; info: number; ok: number }; checks: LintCheck[]; }
212
+ export async function runLintEnv(file?: string): Promise<LintReport> {
213
+ await refreshCsrf();
214
+ return adminFetch('/admin/lint/env', { method: 'POST', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify(file ? { env: file } : {}) });
215
+ }
216
+ export async function runLintConfig(): Promise<LintReport> {
217
+ await refreshCsrf();
218
+ return adminFetch('/admin/lint/config', { method: 'POST', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: '{}' });
219
+ }
220
+
221
+ // ---- payments (admin) ----
222
+ export interface PaymentProviderField {
223
+ field: string;
224
+ label: string;
225
+ hasValue: boolean;
226
+ }
227
+ export interface PaymentProviderStatus {
228
+ name: string;
229
+ enabled: boolean;
230
+ sandbox: boolean;
231
+ configured: boolean;
232
+ live: boolean;
233
+ ok: boolean;
234
+ detail?: string;
235
+ error?: string;
236
+ note?: string;
237
+ fields?: PaymentProviderField[];
238
+ }
239
+ export const getPaymentOrders = (provider?: string) => adminFetch(`/admin/payments/orders${provider ? `?provider=${encodeURIComponent(provider)}` : ''}`);
240
+ export const getPaymentTransactions = (provider?: string) => adminFetch(`/admin/payments/transactions${provider ? `?provider=${encodeURIComponent(provider)}` : ''}`);
241
+ export const getPaymentStatus = () => adminFetch('/admin/payments/status');
242
+ export async function updatePaymentProvider(id: string, patch: { enabled?: boolean; sandbox?: boolean }): Promise<{ ok: boolean; provider: { name: string; enabled: boolean; sandbox: boolean } }> {
243
+ await refreshCsrf();
244
+ return adminFetch(`/admin/payments/providers/${encodeURIComponent(id)}`, { method: 'PUT', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify(patch) });
245
+ }
246
+ export async function savePaymentProviderKeys(id: string, keys: Record<string, string>): Promise<{ ok: boolean; savedFields: string[]; persistence: { runtime: boolean; database: boolean }; provider: { name: string; enabled: boolean; sandbox: boolean; configured: boolean } }> {
247
+ await refreshCsrf();
248
+ return adminFetch(`/admin/payments/providers/${encodeURIComponent(id)}`, { method: 'PUT', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify(keys) });
249
+ }
250
+ export async function createPaymentOrder(opts: { provider: string; amount: number; currency?: string; reference?: string; description?: string }): Promise<any> {
251
+ await refreshCsrf();
252
+ return adminFetch('/payments/order', {
253
+ method: 'POST',
254
+ headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken },
255
+ body: JSON.stringify({ ...opts, reference: opts.reference ?? `test_${Date.now()}` }),
256
+ });
257
+ }
258
+
259
+ // ---- databases (admin) ----
260
+ export const getDatabases = () => adminFetch('/admin/databases');
261
+ export async function createDatabase(name: string): Promise<any> {
262
+ await refreshCsrf();
263
+ return adminFetch('/admin/databases', { method: 'POST', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify({ name }) });
264
+ }
265
+ export async function deleteDatabase(db: string): Promise<any> {
266
+ await refreshCsrf();
267
+ return adminFetch(`/admin/databases/${encodeURIComponent(db)}`, { method: 'DELETE', headers: { 'x-csrf-token': csrfToken } });
268
+ }
269
+ export async function createCollection(db: string, payload: { name: string; jsonSchema?: unknown }): Promise<any> {
270
+ await refreshCsrf();
271
+ return adminFetch(`/admin/databases/${encodeURIComponent(db)}/collections`, { method: 'POST', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify(payload) });
272
+ }
273
+ export async function modifyCollection(db: string, name: string, payload: { newName?: string; validator?: unknown }): Promise<any> {
274
+ await refreshCsrf();
275
+ return adminFetch(`/admin/databases/${encodeURIComponent(db)}/collections/${encodeURIComponent(name)}`, { method: 'PUT', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify(payload) });
276
+ }
277
+ export async function dropCollection(db: string, name: string): Promise<any> {
278
+ await refreshCsrf();
279
+ return adminFetch(`/admin/databases/${encodeURIComponent(db)}/collections/${encodeURIComponent(name)}`, { method: 'DELETE', headers: { 'x-csrf-token': csrfToken } });
280
+ }
281
+ export async function getCollectionDocs(db: string, name: string): Promise<{ count: number; docs: unknown[] }> {
282
+ return adminFetch(`/admin/databases/${encodeURIComponent(db)}/collections/${encodeURIComponent(name)}/docs`);
283
+ }
284
+
285
+ // ---- AI schema generation (admin) ----
286
+ export interface AiStatus {
287
+ checkedAt: string;
288
+ aiServer: { ok: boolean; error?: string; detail?: { status?: string; providers?: string[] } };
289
+ openai: { ok: boolean; error?: string; detail?: { provider?: string; modelCount?: number; models?: string[] } };
290
+ ollama: { ok: boolean; error?: string; detail?: { provider?: string; modelCount?: number; models?: string[] } };
291
+ autoResolvesTo: 'openai' | 'ollama';
292
+ }
293
+ export const getAiStatus = () => adminFetch('/admin/ai/status');
294
+ export async function generateSchema(prompt: string, opts: { model?: string; provider?: string } = {}): Promise<GeneratedSchema> {
295
+ await refreshCsrf();
296
+ return adminFetch('/admin/schemas/generate', { method: 'POST', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify({ prompt, ...opts }) });
297
+ }
298
+
299
+ // ---- AI agents playground (admin) ----
300
+ export interface AiModelInfo { id: string; owned_by?: string }
301
+ export interface AiModelsResponse { provider?: string; data: AiModelInfo[]; error?: string }
302
+ export async function getAiModels(provider?: string): Promise<AiModelsResponse> {
303
+ const q = provider ? `?provider=${encodeURIComponent(provider)}` : '';
304
+ return adminFetch(`/ai/models${q}`);
305
+ }
306
+ export interface AiChatMessage { role: 'system' | 'user' | 'assistant'; content: string }
307
+ export async function aiChat(model: string, messages: AiChatMessage[], provider?: string): Promise<{ content: string; model: string; raw: any }> {
308
+ await refreshCsrf();
309
+ const res = await fetch('/ai/chat/completions', {
310
+ method: 'POST',
311
+ credentials: 'include',
312
+ headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken, ...authHeaders() },
313
+ body: JSON.stringify({ model, messages, stream: false, provider }),
314
+ });
315
+ if (!res.ok) throw new Error(`AI chat failed: ${res.status} ${await res.text()}`);
316
+ const data = await res.json();
317
+ return { content: data?.choices?.[0]?.message?.content ?? '', model: data?.model ?? model, raw: data };
318
+ }
319
+ export async function aiChatStream(model: string, messages: AiChatMessage[], provider: string | undefined, onChunk: (text: string) => void, signal?: AbortSignal): Promise<void> {
320
+ await refreshCsrf();
321
+ const res = await fetch('/ai/chat/completions', {
322
+ method: 'POST',
323
+ credentials: 'include',
324
+ headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken, ...authHeaders() },
325
+ body: JSON.stringify({ model, messages, stream: true, provider }),
326
+ signal,
327
+ });
328
+ if (!res.ok || !res.body) throw new Error(`AI chat stream failed: ${res.status} ${await res.text()}`);
329
+ const reader = res.body.getReader();
330
+ const decoder = new TextDecoder();
331
+ let buffer = '';
332
+ while (true) {
333
+ const { done, value } = await reader.read();
334
+ if (done) break;
335
+ buffer += decoder.decode(value, { stream: true });
336
+ const lines = buffer.split('\n');
337
+ buffer = lines.pop() ?? '';
338
+ for (const line of lines) {
339
+ const trimmed = line.trim();
340
+ if (!trimmed.startsWith('data: ')) continue;
341
+ const payload = trimmed.slice(6);
342
+ if (payload === '[DONE]') return;
343
+ try {
344
+ const json = JSON.parse(payload);
345
+ // Detect error events from the backend (e.g. invalid API key, provider down).
346
+ if (json?.error?.message) throw new Error(String(json.error.message));
347
+ const delta = json?.choices?.[0]?.delta?.content;
348
+ if (delta) onChunk(delta);
349
+ } catch (e) {
350
+ // Re-throw actual errors (not JSON parse failures) so the UI shows them.
351
+ if (e instanceof Error && e.message && !e.message.includes('JSON')) throw e;
352
+ }
353
+ }
354
+ }
355
+ }
356
+
357
+ // ---- AI provider management (admin) ----
358
+ export interface AiProviderView {
359
+ id: string;
360
+ label: string;
361
+ baseUrl: string;
362
+ enabled: boolean;
363
+ apiKey: string;
364
+ hasApiKey: boolean;
365
+ defaultModel?: string;
366
+ }
367
+ export interface AiProviderPersistence {
368
+ runtime: boolean;
369
+ database: boolean;
370
+ }
371
+ export async function getAiProviders(): Promise<{ providers: AiProviderView[] }> {
372
+ return adminFetch('/admin/ai/providers');
373
+ }
374
+ export async function updateAiProvider(id: string, patch: { apiKey?: string; enabled?: boolean; defaultModel?: string; label?: string; baseUrl?: string }): Promise<{ ok: boolean; persistence?: AiProviderPersistence; provider: AiProviderView }> {
375
+ await refreshCsrf();
376
+ return adminFetch(`/admin/ai/providers/${encodeURIComponent(id)}`, { method: 'PUT', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify(patch) });
377
+ }
378
+ export async function addAiProvider(provider: { id: string; label: string; baseUrl: string; apiKey?: string; defaultModel?: string; enabled?: boolean }): Promise<{ ok: boolean; persistence?: AiProviderPersistence; provider: AiProviderView }> {
379
+ await refreshCsrf();
380
+ return adminFetch('/admin/ai/providers', { method: 'POST', headers: { 'content-type': 'application/json', 'x-csrf-token': csrfToken }, body: JSON.stringify(provider) });
381
+ }
382
+ export async function deleteAiProvider(id: string): Promise<{ ok: boolean; persistence?: AiProviderPersistence }> {
383
+ await refreshCsrf();
384
+ return adminFetch(`/admin/ai/providers/${encodeURIComponent(id)}`, { method: 'DELETE', headers: { 'x-csrf-token': csrfToken } });
385
+ }
386
+
387
+ /** Result of probing a single provider's connectivity via the Python AI server. */
388
+ export interface AiProviderTestResult {
389
+ ok: boolean;
390
+ provider: string;
391
+ modelCount?: number;
392
+ models?: string[];
393
+ error?: string;
394
+ checkedAt: string;
395
+ }
396
+
397
+ /** POST /admin/ai/providers/:id/test — probe a single provider (lists models). */
398
+ export async function testAiProvider(id: string): Promise<AiProviderTestResult> {
399
+ await refreshCsrf();
400
+ return adminFetch(`/admin/ai/providers/${encodeURIComponent(id)}/test`, {
401
+ method: 'POST',
402
+ headers: { 'x-csrf-token': csrfToken },
403
+ });
404
+ }
405
+
406
+ // ---- supervisor control API ----
407
+ export interface ServiceState { name: string; status: string; pid?: number; startedAt?: number; lastExitCode?: number | null; }
408
+ export async function getServices(): Promise<ServiceState[]> {
409
+ const r = await fetch(`${await resolveSupervisor()}/status`);
410
+ return (await r.json()).services ?? [];
411
+ }
412
+ export async function controlService(action: 'start' | 'stop' | 'restart', name: string): Promise<void> {
413
+ await fetch(`${await resolveSupervisor()}/${action}?name=${encodeURIComponent(name)}`, { method: 'POST' });
414
+ }
415
+ export async function getServiceLogs(name: string): Promise<string[]> {
416
+ const r = await fetch(`${await resolveSupervisor()}/logs?name=${encodeURIComponent(name)}`);
417
+ return (await r.json()).logs ?? [];
418
+ }
419
+ /** Structured cross-service log entry from the supervisor's aggregated buffer. */
420
+ export interface LogEntry {
421
+ service: string;
422
+ ts: number;
423
+ source: 'stdout' | 'stderr' | 'system';
424
+ level: 'info' | 'warn' | 'error';
425
+ line: string;
426
+ }
427
+ export async function getAllLogs(opts: { service?: string; level?: 'error' | 'warn' | 'info'; q?: string } = {}): Promise<LogEntry[]> {
428
+ const params = new URLSearchParams();
429
+ if (opts.service) params.set('service', opts.service);
430
+ if (opts.level) params.set('level', opts.level);
431
+ if (opts.q) params.set('q', opts.q);
432
+ const r = await fetch(`${await resolveSupervisor()}/logs/all${params.toString() ? `?${params}` : ''}`);
433
+ return (await r.json()).logs ?? [];
434
+ }
435
+ export async function clearLogs(): Promise<void> {
436
+ await fetch(`${await resolveSupervisor()}/logs/clear`, { method: 'POST' });
437
+ }
438
+ export async function getSupervisorInfo(): Promise<{ port?: number | null; url?: string | null }> {
439
+ return adminFetch('/admin/supervisor');
440
+ }
441
+
442
+ // ---- HTTP request log (backend /admin/requests/*) ----
443
+ export interface RequestLogEntry {
444
+ time: number;
445
+ method: string;
446
+ path: string;
447
+ url?: string;
448
+ status: number;
449
+ durationMs: number;
450
+ ip?: string;
451
+ referer?: string;
452
+ userAgent?: string;
453
+ origin?: string;
454
+ requestId?: string;
455
+ route?: string;
456
+ }
457
+ export type RequestSeriesRange = 'today' | '5d' | 'week' | 'month' | 'year';
458
+ export interface RequestSeries {
459
+ range: RequestSeriesRange;
460
+ bucketMs: number;
461
+ start: number;
462
+ end: number;
463
+ total: number;
464
+ points: Array<{ t: number; count: number }>;
465
+ }
466
+ export async function getRequestLogs(limit = 100): Promise<RequestLogEntry[]> {
467
+ const r = await adminFetch(`/admin/requests?limit=${limit}`);
468
+ return r.requests ?? [];
469
+ }
470
+ export async function getRequestSeries(range: RequestSeriesRange): Promise<RequestSeries> {
471
+ const r = await adminFetch(`/admin/requests/series?range=${encodeURIComponent(range)}`);
472
+ return r.series;
473
+ }
474
+
@@ -0,0 +1,25 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1024" viewBox="0 0 1920 1024" role="img" aria-labelledby="title desc">
2
+ <title id="title">BhooAI Nexus</title>
3
+ <desc id="desc">BhooAI Nexus futuristic hexagonal logo and wordmark.</desc>
4
+ <defs>
5
+ <linearGradient id="edge" x1="140" y1="120" x2="820" y2="900" gradientUnits="userSpaceOnUse"><stop stop-color="#67e8f9"/><stop offset=".5" stop-color="#818cf8"/><stop offset="1" stop-color="#d8b4fe"/></linearGradient>
6
+ <linearGradient id="word" x1="900" y1="360" x2="1740" y2="680" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset=".55" stop-color="#a5f3fc"/><stop offset="1" stop-color="#c4b5fd"/></linearGradient>
7
+ <radialGradient id="core" cx="50%" cy="42%" r="65%"><stop stop-color="#172554"/><stop offset=".72" stop-color="#081326"/><stop offset="1" stop-color="#030712"/></radialGradient>
8
+ <filter id="glow" x="-40%" y="-40%" width="180%" height="180%"><feGaussianBlur stdDeviation="16" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
9
+ </defs>
10
+ <g transform="translate(95 72)">
11
+ <path d="M385 0 770 222v444L385 888 0 666V222z" fill="url(#core)" stroke="url(#edge)" stroke-width="18" stroke-linejoin="round"/>
12
+ <path d="M385 58 710 246v376L385 810 60 622V246z" fill="none" stroke="#67e8f9" stroke-opacity=".22" stroke-width="6"/>
13
+ <path d="M385 118 654 273v318L385 746 116 591V273z" fill="none" stroke="#a78bfa" stroke-opacity=".16" stroke-width="4"/>
14
+ <path d="M192 660V240l386 420V240" fill="none" stroke="url(#edge)" stroke-linecap="round" stroke-linejoin="round" stroke-width="76" filter="url(#glow)"/>
15
+ <path d="M192 660V240l386 420V240" fill="none" stroke="#071225" stroke-linecap="round" stroke-linejoin="round" stroke-width="38"/>
16
+ <circle cx="192" cy="240" r="30" fill="#67e8f9" filter="url(#glow)"/><circle cx="578" cy="660" r="30" fill="#f0abfc" filter="url(#glow)"/>
17
+ <circle cx="192" cy="240" r="12" fill="#fff"/><circle cx="578" cy="660" r="12" fill="#fff"/>
18
+ </g>
19
+ <g transform="translate(980 250)">
20
+ <text x="0" y="270" fill="url(#word)" font-family="Arial, Helvetica, sans-serif" font-size="230" font-weight="800" letter-spacing="10">BhooAI</text>
21
+ <text x="16" y="465" fill="url(#edge)" font-family="Arial, Helvetica, sans-serif" font-size="126" font-weight="700" letter-spacing="38">NEXUS</text>
22
+ <path d="M18 550h840" stroke="url(#edge)" stroke-width="8" stroke-linecap="round"/><circle cx="890" cy="550" r="11" fill="#d8b4fe"/>
23
+ <text x="18" y="620" fill="#9ab0c9" font-family="Arial, Helvetica, sans-serif" font-size="27" font-weight="500" letter-spacing="8">ONE RUNTIME. EVERY SIGNAL.</text>
24
+ </g>
25
+ </svg>