@bhooai/nexus-cli 2.0.5 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/package.json +1 -1
  2. package/src/commands/add.ts +8 -3
  3. package/src/commands/dev.ts +18 -2
  4. package/src/commands/init.ts +3 -0
  5. package/src/devPanel.ts +138 -11
  6. package/src/devServiceManager.ts +210 -11
  7. package/src/index.ts +19 -0
  8. package/templates/base/apps/admin/package.json.ejs +1 -0
  9. package/templates/base/apps/admin/src/App.tsx +38 -4127
  10. package/templates/base/apps/admin/src/alertCenter.tsx +3 -2
  11. package/templates/base/apps/admin/src/api.ts +11 -11
  12. package/templates/base/apps/admin/src/components/ThemeCentre.tsx +201 -0
  13. package/templates/base/apps/admin/src/components/shell/AppearancePanel.tsx +37 -0
  14. package/templates/base/apps/admin/src/components/shell/Dashboard.tsx +441 -0
  15. package/templates/base/apps/admin/src/components/shell/Login.tsx +69 -0
  16. package/templates/base/apps/admin/src/components/shell/NotifyPanel.tsx +102 -0
  17. package/templates/base/apps/admin/src/components/shell/RailPanel.tsx +133 -0
  18. package/templates/base/apps/admin/src/components/shell/SettingsDialog.tsx +217 -0
  19. package/templates/base/apps/admin/src/components/ui/ConfirmDialog.tsx +38 -0
  20. package/templates/base/apps/admin/src/components/ui/LintChecks.tsx +54 -0
  21. package/templates/base/apps/admin/src/components/ui/PageHead.tsx +13 -0
  22. package/templates/base/apps/admin/src/components/ui/PageHero.tsx +15 -0
  23. package/templates/base/apps/admin/src/icons.tsx +104 -0
  24. package/templates/base/apps/admin/src/index.css +77 -19
  25. package/templates/base/apps/admin/src/lib/constants.ts +94 -0
  26. package/templates/base/apps/admin/src/lib/theme.ts +80 -0
  27. package/templates/base/apps/admin/src/lib/types.ts +39 -0
  28. package/templates/base/apps/admin/src/lib/utils.ts +147 -0
  29. package/templates/base/apps/admin/src/main.tsx.ejs +3 -2
  30. package/templates/base/apps/admin/src/pages/Config.tsx +108 -0
  31. package/templates/base/apps/admin/src/pages/Databases.tsx +122 -0
  32. package/templates/base/apps/admin/src/pages/Environment.tsx +69 -0
  33. package/templates/base/apps/admin/src/pages/Logs.tsx +270 -0
  34. package/templates/base/apps/admin/src/pages/Monitoring.tsx +88 -0
  35. package/templates/base/apps/admin/src/pages/Overview.tsx +108 -0
  36. package/templates/base/apps/admin/src/pages/Payments.tsx +68 -0
  37. package/templates/base/apps/admin/src/pages/Plugins.tsx +36 -0
  38. package/templates/base/apps/admin/src/pages/Processes.tsx +67 -0
  39. package/templates/base/apps/admin/src/pages/Schema.tsx +130 -0
  40. package/templates/base/apps/admin/src/pages/Users.tsx +144 -0
  41. package/templates/base/apps/admin/src/pages/ai/AddProviderDialog.tsx +150 -0
  42. package/templates/base/apps/admin/src/pages/ai/AiAgents.tsx +31 -0
  43. package/templates/base/apps/admin/src/pages/ai/AiChatPlayground.tsx +208 -0
  44. package/templates/base/apps/admin/src/pages/ai/AiProviders.tsx +276 -0
  45. package/templates/base/apps/admin/src/pages/ai/AiSettings.tsx +84 -0
  46. package/templates/base/apps/admin/src/pages/ai/ProviderCard.tsx +145 -0
  47. package/templates/base/apps/admin/src/pages/ai/ProviderGroup.tsx +50 -0
  48. package/templates/base/apps/admin/src/pages/ai/ProviderKeyDialog.tsx +66 -0
  49. package/templates/base/apps/admin/src/pages/monitoring/RequestSeriesChart.tsx +70 -0
  50. package/templates/base/apps/admin/src/pages/overview/TrafficChart.tsx +32 -0
  51. package/templates/base/apps/admin/src/pages/payments/OrdersTable.tsx +27 -0
  52. package/templates/base/apps/admin/src/pages/payments/PaymentKeysDialog.tsx +74 -0
  53. package/templates/base/apps/admin/src/pages/payments/TestConsole.tsx +173 -0
  54. package/templates/base/apps/admin/src/pages/payments/TransactionsTable.tsx +26 -0
  55. package/templates/base/apps/admin/src/style.css +6517 -0
  56. package/templates/base/apps/admin/vite.config.ts.ejs +13 -8
  57. package/templates/base/apps/ai-server/main.py.ejs +2 -2
  58. package/templates/base/apps/backend/src/routes/index.ts.ejs +1 -1
  59. package/templates/base/apps/frontend/src/App.tsx.ejs +10 -0
  60. package/templates/base/apps/frontend/src/main.tsx.ejs +2 -10
  61. package/templates/base/apps/frontend/vite.config.ts.ejs +9 -4
  62. package/templates/base/docker-compose.yml.ejs +2 -2
@@ -0,0 +1,133 @@
1
+ import React from 'react';
2
+ import { useEffect } from 'react';
3
+ import type { Tab, AdminTheme, UiMode } from '../../lib/types.js';
4
+ import { Icon, type IconName } from '../../icons.js';
5
+
6
+ export function RailPanel({
7
+ open,
8
+ onClose,
9
+ nav,
10
+ tab,
11
+ favorites,
12
+ onToggleFavorite,
13
+ onGo,
14
+ notes,
15
+ onNotes,
16
+ activity,
17
+ onClearActivity,
18
+ onSearch,
19
+ theme,
20
+ onThemeChange,
21
+ uiMode,
22
+ onUiModeChange,
23
+ }: {
24
+ open: string;
25
+ onClose: () => void;
26
+ nav: { id: Tab; label: string; icon: IconName; keywords?: string[]; desc?: string }[];
27
+ tab: Tab;
28
+ favorites: Tab[];
29
+ onToggleFavorite: (id: Tab) => void;
30
+ onGo: (id: Tab) => void;
31
+ notes: string;
32
+ onNotes: (v: string) => void;
33
+ activity: { at: string; label: string }[];
34
+ onClearActivity: () => void;
35
+ onSearch: () => void;
36
+ theme: AdminTheme;
37
+ onThemeChange: (theme: AdminTheme) => void;
38
+ uiMode: UiMode;
39
+ onUiModeChange: (mode: UiMode) => void;
40
+ }) {
41
+ useEffect(() => {
42
+ const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
43
+ window.addEventListener('keydown', onKey);
44
+ return () => window.removeEventListener('keydown', onKey);
45
+ }, [onClose]);
46
+
47
+ if (!open || open === 'notify') return null;
48
+
49
+ return (
50
+ <div className="rail-panel" role="dialog" aria-modal="false">
51
+ {open === 'search' && (
52
+ <>
53
+ <div className="rail-panel-head"><span className="admin-overline">SMART SEARCH</span></div>
54
+ <p className="rail-panel-text">Press <kbd>⌘ K</kbd> (or <kbd>Ctrl K</kbd>) to jump to any surface from the top search bar — or click below to focus it now.</p>
55
+ <button type="button" className="glass-chip-btn-primary w-full justify-center" onClick={onSearch}><Icon name="search" size={12} /> Focus search</button>
56
+ <div className="rail-divider" />
57
+ <span className="rail-panel-hint">Try: roles, mongo, palette, stripe, ollama, payments…</span>
58
+ </>
59
+ )}
60
+
61
+ {open === 'notes' && (
62
+ <>
63
+ <div className="rail-panel-head"><span className="admin-overline">AI NOTES</span><button type="button" className="rail-x" onClick={onClose} aria-label="Close">×</button></div>
64
+ <p className="rail-panel-text">Jot quick notes — saved locally in your browser.</p>
65
+ <textarea className="rail-notes" value={notes} onChange={(e) => onNotes(e.target.value)} placeholder="Type a note…" spellCheck={false} />
66
+ <div className="rail-row">
67
+ <button type="button" className="glass-chip-btn" onClick={() => onNotes('')}>Clear</button>
68
+ <span className="rail-panel-hint">{notes.length} chars</span>
69
+ </div>
70
+ </>
71
+ )}
72
+
73
+ {open === 'activity' && (
74
+ <>
75
+ <div className="rail-panel-head"><span className="admin-overline">SCREEN RECALL</span><button type="button" className="rail-x" onClick={onClose} aria-label="Close">×</button></div>
76
+ <p className="rail-panel-text">Your recent admin activity.</p>
77
+ {activity.length === 0 ? (
78
+ <p className="rail-empty">No activity yet.</p>
79
+ ) : (
80
+ <ul className="rail-list">
81
+ {activity.map((a, i) => (
82
+ <li key={i}><b>{a.label}</b><span>{a.at}</span></li>
83
+ ))}
84
+ </ul>
85
+ )}
86
+ <div className="rail-row"><button type="button" className="glass-chip-btn" onClick={onClearActivity}>Clear activity</button></div>
87
+ </>
88
+ )}
89
+
90
+ {open === 'appearance' && (
91
+ <>
92
+ <div className="rail-panel-head"><span className="admin-overline">APPEARANCE</span><button type="button" className="rail-x" onClick={onClose} aria-label="Close">×</button></div>
93
+ <span className="rail-panel-hint">Choose your surface</span>
94
+ <div className="rail-appearance">
95
+ {([['aurora', 'Aurora', 'Electric blue'], ['midnight', 'Midnight', 'Deep navy'], ['violet', 'Violet', 'Indigo glow']] as const).map(([id, label, detail]) => (
96
+ <button key={id} onClick={() => onThemeChange(id)} className={theme === id ? 'is-selected' : ''}>
97
+ <span className={`theme-preview ${id}`} />
98
+ <span className="rail-appearance-label"><b>{label}</b><small>{detail}</small></span>
99
+ {theme === id && <i>✓</i>}
100
+ </button>
101
+ ))}
102
+ </div>
103
+ <div className="rail-divider" />
104
+ <span className="rail-panel-hint">Interface mode</span>
105
+ <div className="rail-appearance">
106
+ {([['workspace', 'Workspace', 'Inspector + tools'], ['compact', 'Compact', 'More content'], ['focus', 'Focus', 'Canvas first']] as const).map(([id, label, detail]) => (
107
+ <button key={id} onClick={() => onUiModeChange(id)} className={uiMode === id ? 'is-selected' : ''}>
108
+ <span className="rail-appearance-label"><b>{label}</b><small>{detail}</small></span>
109
+ {uiMode === id && <i>✓</i>}
110
+ </button>
111
+ ))}
112
+ </div>
113
+ </>
114
+ )}
115
+
116
+ {open === 'launcher' && (
117
+ <>
118
+ <div className="rail-panel-head"><span className="admin-overline">ADD TOOL</span><button type="button" className="rail-x" onClick={onClose} aria-label="Close">×</button></div>
119
+ <p className="rail-panel-text">Pin your most-used surfaces to the tool rail. Click to toggle.</p>
120
+ <ul className="rail-list rail-list--nav">
121
+ {nav.map((n) => (
122
+ <li key={n.id}>
123
+ <button type="button" className={favorites.includes(n.id) ? 'is-on' : ''} onClick={() => onToggleFavorite(n.id)}>
124
+ <span className="rail-fav-icon"><Icon name={n.icon} size={14} /></span><span className="rail-fav-label">{n.label}</span><span className="rail-fav-check">{favorites.includes(n.id) ? '✓' : '+'}</span>
125
+ </button>
126
+ </li>
127
+ ))}
128
+ </ul>
129
+ </>
130
+ )}
131
+ </div>
132
+ );
133
+ }
@@ -0,0 +1,217 @@
1
+ import React from 'react';
2
+ import { useState, useEffect } from 'react';
3
+ import type { AdminUser } from '../../lib/types.js';
4
+ import { useAlerts } from '../../alertCenter.js';
5
+ import { formatUptime } from '../../lib/utils.js';
6
+ import { getAdminConfig, getMetrics, updateProfile, changePassword, updateProject, type AdminConfig } from '../../api.js';
7
+
8
+ export function SettingsDialog({ open, onClose, user, onUserChange, projectName, projectDb, projectVersion, onProjectChange }: {
9
+ open: boolean;
10
+ onClose: () => void;
11
+ user: AdminUser | null;
12
+ onUserChange: (u: AdminUser) => void;
13
+ projectName: string;
14
+ projectDb: string;
15
+ projectVersion: string;
16
+ onProjectChange: (name?: string, version?: string) => void;
17
+ }) {
18
+ const isAdmin = Array.isArray(user?.roles) && (user!.roles as string[]).includes('admin');
19
+ const [nameDraft, setNameDraft] = useState(user?.name ?? '');
20
+ const [profileMsg, setProfileMsg] = useState<{ text: string; kind: 'ok' | 'err' } | null>(null);
21
+ const [profileBusy, setProfileBusy] = useState(false);
22
+ const [curPw, setCurPw] = useState('');
23
+ const [newPw, setNewPw] = useState('');
24
+ const [confirmPw, setConfirmPw] = useState('');
25
+ const [pwMsg, setPwMsg] = useState<{ text: string; kind: 'ok' | 'err' } | null>(null);
26
+ const [pwBusy, setPwBusy] = useState(false);
27
+ const [pkgName, setPkgName] = useState(projectName);
28
+ const [pkgVersion, setPkgVersion] = useState(projectVersion);
29
+ const [pkgMsg, setPkgMsg] = useState<{ text: string; kind: 'ok' | 'err' } | null>(null);
30
+ const [pkgBusy, setPkgBusy] = useState(false);
31
+ const [about, setAbout] = useState<{ path?: string; version?: string | null; node?: string; uptime?: number; pid?: number } | null>(null);
32
+ const { push } = useAlerts();
33
+
34
+ useEffect(() => {
35
+ if (!open) return;
36
+ setNameDraft(user?.name ?? '');
37
+ setCurPw(''); setNewPw(''); setConfirmPw('');
38
+ setProfileMsg(null); setPwMsg(null); setPkgMsg(null);
39
+ setPkgName(projectName); setPkgVersion(projectVersion);
40
+ setAbout(null);
41
+ let cancelled = false;
42
+ void Promise.all([getAdminConfig().catch(() => null), getMetrics().catch(() => null)]).then(([c, m]) => {
43
+ if (cancelled) return;
44
+ const proj = c?.project as AdminConfig['project'] | undefined;
45
+ setPkgName(proj?.name ?? projectName);
46
+ setPkgVersion(proj?.version ?? m?.version ?? projectVersion);
47
+ setAbout({
48
+ path: proj?.path,
49
+ version: m?.version ?? proj?.version ?? null,
50
+ node: m?.node ?? null,
51
+ uptime: m?.uptime,
52
+ pid: m?.pid,
53
+ });
54
+ });
55
+ return () => { cancelled = true; };
56
+ }, [open, user, projectName, projectVersion]);
57
+
58
+ useEffect(() => {
59
+ if (!open) return;
60
+ const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
61
+ window.addEventListener('keydown', onKey);
62
+ return () => window.removeEventListener('keydown', onKey);
63
+ }, [open, onClose]);
64
+
65
+ if (!open) return null;
66
+
67
+ const saveProfile = async () => {
68
+ const name = nameDraft.trim();
69
+ if (!name) { setProfileMsg({ text: 'Name is required.', kind: 'err' }); return; }
70
+ const prevName = user?.name ?? '';
71
+ if (name === prevName) { setProfileMsg({ text: 'Display name is unchanged.', kind: 'ok' }); return; }
72
+ setProfileBusy(true); setProfileMsg(null);
73
+ try {
74
+ const res = await updateProfile(name);
75
+ onUserChange((res.user ?? { ...(user ?? {}), name }) as AdminUser);
76
+ setProfileMsg({ text: `Display name changed to "${name}".`, kind: 'ok' });
77
+ push('ok', `Display name changed to "${name}".`, 'settings');
78
+ } catch (e) {
79
+ setProfileMsg({ text: e instanceof Error ? e.message : 'Failed to save profile.', kind: 'err' });
80
+ } finally {
81
+ setProfileBusy(false);
82
+ }
83
+ };
84
+
85
+ const savePassword = async () => {
86
+ if (newPw.length < 8) { setPwMsg({ text: 'New password must be at least 8 characters.', kind: 'err' }); return; }
87
+ if (newPw !== confirmPw) { setPwMsg({ text: 'Passwords do not match.', kind: 'err' }); return; }
88
+ setPwBusy(true); setPwMsg(null);
89
+ try {
90
+ await changePassword(curPw, newPw);
91
+ setCurPw(''); setNewPw(''); setConfirmPw('');
92
+ setPwMsg({ text: 'Password updated.', kind: 'ok' });
93
+ } catch (e) {
94
+ setPwMsg({ text: e instanceof Error ? e.message : 'Failed to change password.', kind: 'err' });
95
+ } finally {
96
+ setPwBusy(false);
97
+ }
98
+ };
99
+
100
+ const savePackage = async () => {
101
+ const patch: { name?: string; version?: string } = {};
102
+ const name = pkgName.trim();
103
+ if (name && name !== projectName) patch.name = name;
104
+ const version = pkgVersion.trim();
105
+ if (version && version !== (about?.version ?? projectVersion)) patch.version = version;
106
+ if (!patch.name && !patch.version) { setPkgMsg({ text: 'Nothing to update.', kind: 'err' }); return; }
107
+ setPkgBusy(true); setPkgMsg(null);
108
+ try {
109
+ const res = await updateProject(patch);
110
+ const proj = res.project as { name?: string; version?: string } | undefined;
111
+ if (proj?.name) onProjectChange(proj.name);
112
+ if (proj?.version) onProjectChange(undefined, proj.version);
113
+ if (proj?.version) setAbout((prev) => ({ ...(prev ?? {}), version: proj!.version ?? prev?.version }));
114
+ setPkgName(proj?.name ?? projectName);
115
+ setPkgVersion(proj?.version ?? about?.version ?? projectVersion);
116
+ setPkgMsg({ text: 'package.json updated. Restart the backend to pick up changes.', kind: 'ok' });
117
+ } catch (e) {
118
+ setPkgMsg({ text: e instanceof Error ? e.message : 'Failed to update package.', kind: 'err' });
119
+ } finally {
120
+ setPkgBusy(false);
121
+ }
122
+ };
123
+
124
+ const statusLine = (msg: { text: string; kind: 'ok' | 'err' } | null) =>
125
+ msg ? <div role="status" className={`settings-status ${msg.kind}`}>{msg.text}</div> : null;
126
+
127
+ return (
128
+ <div className="provider-modal-backdrop" onClick={onClose}>
129
+ <div className="settings-dialog" role="dialog" aria-modal="true" aria-labelledby="settings-title" onClick={(event) => event.stopPropagation()}>
130
+ <div className="settings-header">
131
+ <div>
132
+ <span className="admin-overline">ACCOUNT & WORKSPACE</span>
133
+ <h3 id="settings-title">Settings</h3>
134
+ </div>
135
+ <button type="button" onClick={onClose} className="glass-chip-btn" aria-label="Close settings">✕</button>
136
+ </div>
137
+
138
+ <div className="settings-body">
139
+ <section className="settings-section">
140
+ <h4>Profile</h4>
141
+ <p className="settings-desc">Your display name across the admin workspace.</p>
142
+ <div className="settings-row">
143
+ <label className="settings-field">
144
+ <span className="admin-overline">DISPLAY NAME</span>
145
+ <input className="glass-input" type="text" value={nameDraft} onChange={(e) => setNameDraft(e.target.value)} />
146
+ </label>
147
+ <label className="settings-field">
148
+ <span className="admin-overline">EMAIL</span>
149
+ <input className="glass-input" type="text" readOnly value={user?.email ?? ''} aria-readonly="true" />
150
+ </label>
151
+ </div>
152
+ {statusLine(profileMsg)}
153
+ <div className="settings-actions">
154
+ <button type="button" onClick={saveProfile} disabled={profileBusy} className="glass-chip-btn-primary">{profileBusy ? 'Saving…' : 'Save profile'}</button>
155
+ </div>
156
+ </section>
157
+
158
+ <section className="settings-section">
159
+ <h4>Security</h4>
160
+ <p className="settings-desc">Change the password for this account.</p>
161
+ <div className="settings-row">
162
+ <label className="settings-field">
163
+ <span className="admin-overline">CURRENT PASSWORD</span>
164
+ <input className="glass-input" type="password" autoComplete="current-password" value={curPw} onChange={(e) => setCurPw(e.target.value)} />
165
+ </label>
166
+ </div>
167
+ <div className="settings-row">
168
+ <label className="settings-field">
169
+ <span className="admin-overline">NEW PASSWORD</span>
170
+ <input className="glass-input" type="password" autoComplete="new-password" value={newPw} onChange={(e) => setNewPw(e.target.value)} />
171
+ </label>
172
+ <label className="settings-field">
173
+ <span className="admin-overline">CONFIRM PASSWORD</span>
174
+ <input className="glass-input" type="password" autoComplete="new-password" value={confirmPw} onChange={(e) => setConfirmPw(e.target.value)} />
175
+ </label>
176
+ </div>
177
+ {statusLine(pwMsg)}
178
+ <div className="settings-actions">
179
+ <button type="button" onClick={savePassword} disabled={pwBusy} className="glass-chip-btn-primary">{pwBusy ? 'Updating…' : 'Change password'}</button>
180
+ </div>
181
+ </section>
182
+
183
+ <section className="settings-section">
184
+ <h4>About</h4>
185
+ <div className="settings-facts">
186
+ <div><span>Package</span><b>{projectName || '—'}</b></div>
187
+ <div><span>Version</span><b>{about?.version ?? (projectVersion || '—')}</b></div>
188
+ <div><span>Database</span><b>{projectDb || '—'}</b></div>
189
+ <div><span>Path</span><b>{about?.path ?? '—'}</b></div>
190
+ <div><span>Node.js</span><b>{about?.node ?? '—'}</b></div>
191
+ <div><span>Uptime</span><b>{formatUptime(about?.uptime)}</b></div>
192
+ <div><span>PID</span><b>{about?.pid ?? '—'}</b></div>
193
+ </div>
194
+ {isAdmin && (
195
+ <div className="settings-package">
196
+ <div className="settings-row">
197
+ <label className="settings-field">
198
+ <span className="admin-overline">PACKAGE NAME</span>
199
+ <input className="glass-input" type="text" value={pkgName} onChange={(e) => setPkgName(e.target.value)} />
200
+ </label>
201
+ <label className="settings-field">
202
+ <span className="admin-overline">VERSION</span>
203
+ <input className="glass-input" type="text" value={pkgVersion} onChange={(e) => setPkgVersion(e.target.value)} placeholder="1.0.0" />
204
+ </label>
205
+ </div>
206
+ {statusLine(pkgMsg)}
207
+ <div className="settings-actions">
208
+ <button type="button" onClick={savePackage} disabled={pkgBusy} className="glass-chip-btn-primary">{pkgBusy ? 'Updating…' : 'Update package'}</button>
209
+ </div>
210
+ </div>
211
+ )}
212
+ </section>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ );
217
+ }
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { Icon } from '../../icons.js';
3
+
4
+ export function ConfirmDialog(props: {
5
+ open: boolean;
6
+ title: string;
7
+ message: React.ReactNode;
8
+ confirmLabel?: string;
9
+ danger?: boolean;
10
+ busy?: boolean;
11
+ onConfirm: () => void;
12
+ onCancel: () => void;
13
+ }) {
14
+ if (!props.open) return null;
15
+ return (
16
+ <div className="confirm-overlay" onClick={props.onCancel}>
17
+ <div className="confirm-dialog" role="dialog" aria-modal="true" onClick={(event) => event.stopPropagation()}>
18
+ <div className="confirm-dialog-head">
19
+ <span className="confirm-dialog-mark">{props.danger ? '!' : <Icon name="info" size={15} />}</span>
20
+ <strong>{props.title}</strong>
21
+ <button className="confirm-dialog-close" onClick={props.onCancel} aria-label="Cancel">×</button>
22
+ </div>
23
+ <p className="confirm-dialog-message">{props.message}</p>
24
+ <div className="confirm-dialog-actions">
25
+ <button className="glass-chip-btn" onClick={props.onCancel}>Cancel</button>
26
+ <button
27
+ className={`glass-chip-btn-danger confirm-dialog-primary${props.danger ? ' danger' : ''}`}
28
+ onClick={props.onConfirm}
29
+ disabled={props.busy}
30
+ autoFocus
31
+ >
32
+ {props.busy ? 'Working…' : (props.confirmLabel ?? 'Confirm')}
33
+ </button>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import { useState } from 'react';
3
+ import { Icon } from '../../icons.js';
4
+ import type { LintReport } from '../../api.js';
5
+
6
+ export function LintChecks({ run, title, overline }: { run: () => Promise<LintReport>; title: string; overline: string }) {
7
+ const [report, setReport] = useState<LintReport | null>(null);
8
+ const [busy, setBusy] = useState(false);
9
+ const [err, setErr] = useState('');
10
+ const doRun = async () => {
11
+ setBusy(true); setErr('');
12
+ try { setReport(await run()); } catch (e: any) { setErr(String(e?.message ?? e)); setReport(null); }
13
+ setBusy(false);
14
+ };
15
+ return (
16
+ <section className="workspace-panel lint-panel">
17
+ <div className="panel-title-row">
18
+ <div><span className="admin-overline">{overline}</span><h3>{title}</h3></div>
19
+ <button onClick={doRun} disabled={busy} className="glass-chip-btn">{busy ? 'linting…' : <><Icon name="refresh" size={12} /> run lint</>}</button>
20
+ </div>
21
+ {err && <p className="preflight-empty">{err}</p>}
22
+ {report ? (
23
+ <>
24
+ <div className="preflight-summary">
25
+ <span className="preflight-count bad">● {report.summary.error} error{report.summary.error === 1 ? '' : 's'}</span>
26
+ <span className="preflight-count warn">◐ {report.summary.warning} warning{report.summary.warning === 1 ? '' : 's'}</span>
27
+ <span className="preflight-count inf">◌ {report.summary.info} info</span>
28
+ {report.summary.ok > 0 && <span className="preflight-count ok">✓ {report.summary.ok} ok</span>}
29
+ <span className="preflight-meta">{report.ranAt}</span>
30
+ </div>
31
+ {report.engineOk === false && (
32
+ <div className="preflight-guidance">
33
+ <strong>Diagnostics engine offline.</strong>
34
+ <span>The Python engine that runs these checks is not running. Start it with <code>python main.py</code>, or the whole stack with <code>nexus dev</code>.</span>
35
+ </div>
36
+ )}
37
+ </>
38
+ ) : busy ? <p className="preflight-empty">Linting…</p> : <p className="preflight-empty">Run the linter to validate.</p>}
39
+ {report && (
40
+ <div className="preflight-list">
41
+ {report.checks.map((c) => (
42
+ <div key={`${c.severity}-${c.key}-${c.message}`} className="preflight-row">
43
+ <span className={`preflight-dot ${c.severity === 'error' ? 'bad' : c.severity === 'warning' ? 'warn' : 'good'}`} />
44
+ <span className="preflight-check-name">{c.key}</span>
45
+ <span className="preflight-kind">{c.kind}</span>
46
+ <span className={`preflight-message ${c.severity === 'ok' ? 'is-ok' : ''}`}>{c.message}</span>
47
+ <span className={`preflight-state ${c.severity === 'error' ? 'fail' : c.severity === 'warning' ? 'is-warn' : c.severity === 'ok' ? 'ok' : ''}`}>{c.severity}</span>
48
+ </div>
49
+ ))}
50
+ </div>
51
+ )}
52
+ </section>
53
+ );
54
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+
3
+ export function PageHead({ title, children }: { title: string; children?: React.ReactNode }) {
4
+ return (
5
+ <div className="section-heading">
6
+ <div className="section-heading-title">
7
+ <span className="section-heading-mark" />
8
+ <h2>{title}</h2>
9
+ </div>
10
+ {children}
11
+ </div>
12
+ );
13
+ }
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { Icon, type IconName } from '../../icons.js';
3
+
4
+ export function PageHero({ kicker, title, desc, glyph, art }: { kicker: string; title: React.ReactNode; desc: string; glyph: IconName; art?: string }) {
5
+ return (
6
+ <div className="config-intro">
7
+ <div><span className="admin-overline">{kicker}</span><h1>{title}</h1><p>{desc}</p></div>
8
+ {art ? (
9
+ <div className={`hero-art hero-art--${art}`}><strong><Icon name={glyph} size={26} /></strong><span className="ha-a" /><span className="ha-b" /><span className="ha-c" /></div>
10
+ ) : (
11
+ <div className="hero-orbit-art"><span /><i /><b /><strong><Icon name={glyph} size={26} /></strong></div>
12
+ )}
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,104 @@
1
+ import type { ReactNode, SVGProps } from 'react';
2
+
3
+ export type IconName =
4
+ | 'house' | 'server' | 'terminal' | 'braces' | 'key' | 'puzzle' | 'users' | 'activity'
5
+ | 'dollar' | 'database' | 'sparkles' | 'bot' | 'palette'
6
+ | 'search' | 'bell' | 'settings' | 'logOut' | 'menu' | 'x' | 'check' | 'chevronRight'
7
+ | 'arrowRight' | 'refresh' | 'copy' | 'externalLink' | 'pencil' | 'trash' | 'plus'
8
+ | 'square' | 'chevronDown' | 'chevronUp' | 'zap' | 'warning' | 'lock' | 'download'
9
+ | 'upload' | 'globe' | 'cloud' | 'flame' | 'wind' | 'smile' | 'monitor' | 'image'
10
+ | 'repeat' | 'shield' | 'helpCircle' | 'bookOpen' | 'layers' | 'fileText' | 'message'
11
+ | 'gitFork' | 'grid' | 'info' | 'alertCircle' | 'clock' | 'triangle' | 'more' | 'send';
12
+
13
+ const ICONS: Record<IconName, ReactNode> = {
14
+ house: <><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /><polyline points="9 22 9 12 15 12 15 22" /></>,
15
+ server: <><rect x="2" y="2" width="20" height="8" rx="2" /><rect x="2" y="14" width="20" height="8" rx="2" /><line x1="6" y1="6" x2="6.01" y2="6" /><line x1="6" y1="18" x2="6.01" y2="18" /></>,
16
+ terminal: <><polyline points="4 17 10 11 4 5" /><line x1="12" y1="19" x2="20" y2="19" /></>,
17
+ braces: <><path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1" /><path d="M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1" /></>,
18
+ key: <><path d="m21 2-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3m-3.5 3.5L19 4" /></>,
19
+ puzzle: <><path d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.077.877.528 1.073 1.01a2.5 2.5 0 1 0 3.259-3.259c-.482-.196-.933-.558-1.01-1.073-.05-.336.062-.676.303-.917l1.525-1.525A2.402 2.402 0 0 1 12 1.998c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.237 3.237c-.464.18-.894.527-.967 1.02z" /></>,
20
+ users: <><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /></>,
21
+ activity: <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />,
22
+ dollar: <><line x1="12" y1="1" x2="12" y2="23" /><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" /></>,
23
+ database: <><ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M3 5V19A9 3 0 0 0 21 19V5" /><path d="M3 12A9 3 0 0 0 21 12" /></>,
24
+ sparkles: <><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" /><path d="M5 3v4" /><path d="M19 17v4" /><path d="M3 5h4" /><path d="M17 19h4" /></>,
25
+ bot: <><path d="M12 8V4H8" /><rect width="16" height="12" x="4" y="8" rx="2" /><path d="M2 14h2" /><path d="M20 14h2" /><path d="M15 13v2" /><path d="M9 13v2" /></>,
26
+ palette: <><circle cx="13.5" cy="6.5" r="0.5" fill="currentColor" /><circle cx="17.5" cy="10.5" r="0.5" fill="currentColor" /><circle cx="8.5" cy="7.5" r="0.5" fill="currentColor" /><circle cx="6.5" cy="12.5" r="0.5" fill="currentColor" /><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z" /></>,
27
+ search: <><circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" /></>,
28
+ bell: <><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" /><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" /></>,
29
+ settings: <><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" /></>,
30
+ logOut: <><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><polyline points="16 17 21 12 16 7" /><line x1="21" y1="12" x2="9" y2="12" /></>,
31
+ menu: <><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="18" x2="21" y2="18" /></>,
32
+ x: <><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></>,
33
+ check: <polyline points="20 6 9 17 4 12" />,
34
+ chevronRight: <polyline points="9 18 15 12 9 6" />,
35
+ arrowRight: <><line x1="5" y1="12" x2="19" y2="12" /><polyline points="12 5 19 12 12 19" /></>,
36
+ refresh: <><polyline points="23 4 23 10 17 10" /><polyline points="1 20 1 14 7 14" /><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" /></>,
37
+ copy: <><rect x="9" y="9" width="13" height="13" rx="2" ry="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></>,
38
+ externalLink: <><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" /></>,
39
+ pencil: <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" />,
40
+ trash: <><polyline points="3 6 5 6 21 6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></>,
41
+ plus: <><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></>,
42
+ square: <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />,
43
+ chevronDown: <polyline points="6 9 12 15 18 9" />,
44
+ chevronUp: <polyline points="18 15 12 9 6 15" />,
45
+ zap: <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />,
46
+ warning: <><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" /><line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" /></>,
47
+ lock: <><rect x="3" y="11" width="18" height="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></>,
48
+ download: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" /></>,
49
+ upload: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="17 8 12 3 7 8" /><line x1="12" y1="3" x2="12" y2="15" /></>,
50
+ globe: <><circle cx="12" cy="12" r="10" /><line x1="2" y1="12" x2="22" y2="12" /><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" /></>,
51
+ cloud: <path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" />,
52
+ flame: <path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z" />,
53
+ wind: <><path d="M9.59 4.59A2 2 0 1 1 11 8H2" /><path d="M12.59 16.59A2 2 0 1 0 14 13H2" /><path d="M17.73 8.73A2.5 2.5 0 1 1 19.5 12H2" /></>,
54
+ smile: <><circle cx="12" cy="12" r="10" /><path d="M8 14s1.5 2 4 2 4-2 4-2" /><line x1="9" y1="9" x2="9.01" y2="9" /><line x1="15" y1="9" x2="15.01" y2="9" /></>,
55
+ monitor: <><rect x="2" y="3" width="20" height="14" rx="2" ry="2" /><line x1="8" y1="21" x2="16" y2="21" /><line x1="12" y1="17" x2="12" y2="21" /></>,
56
+ image: <><rect x="3" y="3" width="18" height="18" rx="2" ry="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" /></>,
57
+ repeat: <><polyline points="17 1 21 5 17 9" /><path d="M3 11V9a4 4 0 0 1 4-4h14" /><polyline points="7 23 3 19 7 15" /><path d="M21 13v2a4 4 0 0 1-4 4H3" /></>,
58
+ shield: <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />,
59
+ helpCircle: <><circle cx="12" cy="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><line x1="12" y1="17" x2="12.01" y2="17" /></>,
60
+ bookOpen: <><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" /><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" /></>,
61
+ layers: <><polygon points="12 2 2 7 12 12 22 7 12 2" /><polyline points="2 17 12 22 22 17" /><polyline points="2 12 12 17 22 12" /></>,
62
+ fileText: <><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /></>,
63
+ message: <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />,
64
+ gitFork: <><circle cx="12" cy="18" r="3" /><circle cx="6" cy="6" r="3" /><circle cx="18" cy="6" r="3" /><path d="M18 9v1a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V9" /><path d="M12 12v3" /></>,
65
+ grid: <><rect x="3" y="3" width="7" height="7" /><rect x="14" y="3" width="7" height="7" /><rect x="14" y="14" width="7" height="7" /><rect x="3" y="14" width="7" height="7" /></>,
66
+ info: <><circle cx="12" cy="12" r="10" /><line x1="12" y1="16" x2="12" y2="12" /><line x1="12" y1="8" x2="12.01" y2="8" /></>,
67
+ alertCircle: <><circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" /></>,
68
+ clock: <><circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" /></>,
69
+ triangle: <path d="m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 20h16a2 2 0 0 0 1.73-2Z" />,
70
+ more: <><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></>,
71
+ send: <><line x1="22" y1="2" x2="11" y2="13" /><polygon points="22 2 15 22 11 13 2 9 22 2" /></>,
72
+ };
73
+
74
+ const ICON_NAMES = new Set<string>(Object.keys(ICONS));
75
+
76
+ export function isIconName(value: string): value is IconName {
77
+ return ICON_NAMES.has(value);
78
+ }
79
+
80
+ export interface IconProps extends SVGProps<SVGSVGElement> {
81
+ name: IconName;
82
+ size?: number;
83
+ }
84
+
85
+ export function Icon({ name, size = 16, className, style, ...rest }: IconProps) {
86
+ return (
87
+ <svg
88
+ width={size}
89
+ height={size}
90
+ viewBox="0 0 24 24"
91
+ fill="none"
92
+ stroke="currentColor"
93
+ strokeWidth="2"
94
+ strokeLinecap="round"
95
+ strokeLinejoin="round"
96
+ className={className}
97
+ style={{ display: 'block', flexShrink: 0, ...(style as React.CSSProperties) }}
98
+ aria-hidden="true"
99
+ {...rest}
100
+ >
101
+ {ICONS[name]}
102
+ </svg>
103
+ );
104
+ }