@bhooai/nexus-cli 2.0.6 → 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.
- package/package.json +1 -1
- package/src/commands/add.ts +8 -3
- package/src/commands/dev.ts +18 -2
- package/src/commands/init.ts +3 -0
- package/src/devPanel.ts +138 -11
- package/src/devServiceManager.ts +210 -11
- package/src/index.ts +19 -0
- package/templates/base/apps/admin/package.json.ejs +1 -0
- package/templates/base/apps/admin/src/App.tsx +38 -4127
- package/templates/base/apps/admin/src/alertCenter.tsx +3 -2
- package/templates/base/apps/admin/src/api.ts +11 -11
- package/templates/base/apps/admin/src/components/ThemeCentre.tsx +201 -0
- package/templates/base/apps/admin/src/components/shell/AppearancePanel.tsx +37 -0
- package/templates/base/apps/admin/src/components/shell/Dashboard.tsx +441 -0
- package/templates/base/apps/admin/src/components/shell/Login.tsx +69 -0
- package/templates/base/apps/admin/src/components/shell/NotifyPanel.tsx +102 -0
- package/templates/base/apps/admin/src/components/shell/RailPanel.tsx +133 -0
- package/templates/base/apps/admin/src/components/shell/SettingsDialog.tsx +217 -0
- package/templates/base/apps/admin/src/components/ui/ConfirmDialog.tsx +38 -0
- package/templates/base/apps/admin/src/components/ui/LintChecks.tsx +54 -0
- package/templates/base/apps/admin/src/components/ui/PageHead.tsx +13 -0
- package/templates/base/apps/admin/src/components/ui/PageHero.tsx +15 -0
- package/templates/base/apps/admin/src/icons.tsx +104 -0
- package/templates/base/apps/admin/src/index.css +77 -19
- package/templates/base/apps/admin/src/lib/constants.ts +94 -0
- package/templates/base/apps/admin/src/lib/theme.ts +80 -0
- package/templates/base/apps/admin/src/lib/types.ts +39 -0
- package/templates/base/apps/admin/src/lib/utils.ts +147 -0
- package/templates/base/apps/admin/src/main.tsx.ejs +3 -2
- package/templates/base/apps/admin/src/pages/Config.tsx +108 -0
- package/templates/base/apps/admin/src/pages/Databases.tsx +122 -0
- package/templates/base/apps/admin/src/pages/Environment.tsx +69 -0
- package/templates/base/apps/admin/src/pages/Logs.tsx +270 -0
- package/templates/base/apps/admin/src/pages/Monitoring.tsx +88 -0
- package/templates/base/apps/admin/src/pages/Overview.tsx +108 -0
- package/templates/base/apps/admin/src/pages/Payments.tsx +68 -0
- package/templates/base/apps/admin/src/pages/Plugins.tsx +36 -0
- package/templates/base/apps/admin/src/pages/Processes.tsx +67 -0
- package/templates/base/apps/admin/src/pages/Schema.tsx +130 -0
- package/templates/base/apps/admin/src/pages/Users.tsx +144 -0
- package/templates/base/apps/admin/src/pages/ai/AddProviderDialog.tsx +150 -0
- package/templates/base/apps/admin/src/pages/ai/AiAgents.tsx +31 -0
- package/templates/base/apps/admin/src/pages/ai/AiChatPlayground.tsx +208 -0
- package/templates/base/apps/admin/src/pages/ai/AiProviders.tsx +276 -0
- package/templates/base/apps/admin/src/pages/ai/AiSettings.tsx +84 -0
- package/templates/base/apps/admin/src/pages/ai/ProviderCard.tsx +145 -0
- package/templates/base/apps/admin/src/pages/ai/ProviderGroup.tsx +50 -0
- package/templates/base/apps/admin/src/pages/ai/ProviderKeyDialog.tsx +66 -0
- package/templates/base/apps/admin/src/pages/monitoring/RequestSeriesChart.tsx +70 -0
- package/templates/base/apps/admin/src/pages/overview/TrafficChart.tsx +32 -0
- package/templates/base/apps/admin/src/pages/payments/OrdersTable.tsx +27 -0
- package/templates/base/apps/admin/src/pages/payments/PaymentKeysDialog.tsx +74 -0
- package/templates/base/apps/admin/src/pages/payments/TestConsole.tsx +173 -0
- package/templates/base/apps/admin/src/pages/payments/TransactionsTable.tsx +26 -0
- package/templates/base/apps/admin/src/style.css +6517 -0
- package/templates/base/apps/admin/vite.config.ts.ejs +13 -8
- package/templates/base/apps/ai-server/main.py.ejs +2 -2
- package/templates/base/apps/backend/src/routes/index.ts.ejs +1 -1
- package/templates/base/apps/frontend/src/App.tsx.ejs +10 -0
- package/templates/base/apps/frontend/src/main.tsx.ejs +2 -10
- package/templates/base/apps/frontend/vite.config.ts.ejs +9 -4
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { getMetrics, getRequestSeries, getRequestLogs, type RequestSeries, type RequestLogEntry, type RequestSeriesRange } from '../api.js';
|
|
3
|
+
import { PageHead } from '../components/ui/PageHead.js';
|
|
4
|
+
import { PageHero } from '../components/ui/PageHero.js';
|
|
5
|
+
import { RequestSeriesChart } from './monitoring/RequestSeriesChart.js';
|
|
6
|
+
|
|
7
|
+
const SERIES_OPTIONS: Array<{ id: RequestSeriesRange; label: string }> = [
|
|
8
|
+
{ id: 'today', label: 'Today' },
|
|
9
|
+
{ id: '5d', label: '5 days' },
|
|
10
|
+
{ id: 'week', label: 'Week' },
|
|
11
|
+
{ id: 'month', label: 'Month' },
|
|
12
|
+
{ id: 'year', label: 'Year' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function Monitoring() {
|
|
16
|
+
const [data, setData] = useState<any>(null);
|
|
17
|
+
const [series, setSeries] = useState<RequestSeries | null>(null);
|
|
18
|
+
const [logs, setLogs] = useState<RequestLogEntry[]>([]);
|
|
19
|
+
const [range, setRange] = useState<RequestSeriesRange>('today');
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
getMetrics().then(setData).catch(() => {});
|
|
22
|
+
const t = setInterval(() => getMetrics().then(setData).catch(() => {}), 5000);
|
|
23
|
+
return () => clearInterval(t);
|
|
24
|
+
}, []);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
let alive = true;
|
|
27
|
+
const load = async () => {
|
|
28
|
+
try { const s = await getRequestSeries(range); if (alive) setSeries(s); } catch { if (alive) setSeries(null); }
|
|
29
|
+
};
|
|
30
|
+
void load();
|
|
31
|
+
const intervalMs = range === 'today' ? 5000 : range === '5d' ? 30000 : 60000;
|
|
32
|
+
const t = setInterval(load, intervalMs);
|
|
33
|
+
return () => { alive = false; clearInterval(t); };
|
|
34
|
+
}, [range]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
getRequestLogs().then(setLogs).catch(() => {});
|
|
37
|
+
const t = setInterval(() => getRequestLogs().then(setLogs).catch(() => {}), 5000);
|
|
38
|
+
return () => clearInterval(t);
|
|
39
|
+
}, []);
|
|
40
|
+
if (!data) return <p className="text-slate-400">Loading…</p>;
|
|
41
|
+
return (
|
|
42
|
+
<div className="space-y-6">
|
|
43
|
+
<PageHead title="Monitoring" />
|
|
44
|
+
<PageHero kicker="LIVE TELEMETRY" title={<>Watch the whole <em>machine.</em></>} desc="A calm, self-refreshing readout of the backend heartbeat — uptime, PID and request traffic without the noise." glyph="activity" art="coins" />
|
|
45
|
+
<div className="grid grid-cols-2 gap-4 lg:grid-cols-3">
|
|
46
|
+
<div className="glass-card"><div className="glass-h3">Uptime</div><div className="mt-2 text-3xl font-bold text-slate-100">{Math.round(data.uptime ?? 0)}<span className="ml-1 text-sm font-normal text-slate-400">s</span></div></div>
|
|
47
|
+
<div className="glass-card"><div className="glass-h3">PID</div><div className="mt-2 font-mono text-3xl font-bold text-slate-100">{data.pid}</div></div>
|
|
48
|
+
<div className="glass-card"><div className="glass-h3">HTTP requests</div><div className="mt-2 text-2xl font-bold text-indigo-300">{(() => { const v = data.metrics?.http_requests_total?.values; return v ? Object.values(v).reduce((a: number, b) => a + Number(b ?? 0), 0) : '—'; })()}</div></div>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="glass-card">
|
|
51
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
52
|
+
<h3 className="glass-h3">Request traffic</h3>
|
|
53
|
+
<div className="series-tabs">
|
|
54
|
+
{SERIES_OPTIONS.map((o) => (
|
|
55
|
+
<button key={o.id} type="button" className={range === o.id ? 'is-active' : ''} onClick={() => setRange(o.id)}>{o.label}</button>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<RequestSeriesChart series={series} />
|
|
60
|
+
</div>
|
|
61
|
+
<div className="glass-card">
|
|
62
|
+
<h3 className="glass-h3 mb-3">Recent requests</h3>
|
|
63
|
+
{logs.length ? (
|
|
64
|
+
<div className="req-log-table-wrap">
|
|
65
|
+
<table className="glass-table req-log-table">
|
|
66
|
+
<thead><tr className="text-slate-400"><th>Time</th><th>Method</th><th>Path</th><th className="text-center">Status</th><th className="text-right">Dur</th><th>IP</th><th>Referer</th></tr></thead>
|
|
67
|
+
<tbody>
|
|
68
|
+
{logs.map((r, i) => (
|
|
69
|
+
<tr key={i}>
|
|
70
|
+
<td className="req-td-time">{new Date(r.time).toLocaleTimeString()}</td>
|
|
71
|
+
<td className={`req-method req-method-${String(r.method).toLowerCase()}`}>{r.method}</td>
|
|
72
|
+
<td className="req-td-path" title={r.url ?? r.path}>{r.url ?? r.path}</td>
|
|
73
|
+
<td className={`text-center req-status ${r.status >= 500 ? 'req-status-err' : r.status >= 400 ? 'req-status-warn' : ''}`}>{r.status}</td>
|
|
74
|
+
<td className="text-right req-td-dur">{r.durationMs}ms</td>
|
|
75
|
+
<td className="req-td-ip">{r.ip ?? '—'}</td>
|
|
76
|
+
<td className="req-td-ref" title={r.referer}>{r.referer ?? '—'}</td>
|
|
77
|
+
</tr>
|
|
78
|
+
))}
|
|
79
|
+
</tbody>
|
|
80
|
+
</table>
|
|
81
|
+
</div>
|
|
82
|
+
) : (
|
|
83
|
+
<p className="text-sm text-slate-400">No requests logged yet.</p>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import type { Tab } from '../lib/types.js';
|
|
3
|
+
import { friendlyPreflightError, preflightAddress } from '../lib/utils.js';
|
|
4
|
+
import { getMetrics, getServices, runPreflight, type ServiceState, type PreflightReport } from '../api.js';
|
|
5
|
+
import { Icon } from '../icons.js';
|
|
6
|
+
import { PageHead } from '../components/ui/PageHead.js';
|
|
7
|
+
import { TrafficChart } from './overview/TrafficChart.js';
|
|
8
|
+
|
|
9
|
+
export function Overview({ onNavigate }: { onNavigate: (tab: Tab) => void }) {
|
|
10
|
+
const [metrics, setMetrics] = useState<any>(null);
|
|
11
|
+
const [services, setServices] = useState<ServiceState[]>([]);
|
|
12
|
+
const [preflight, setPreflight] = useState<PreflightReport | null>(null);
|
|
13
|
+
const [preflightBusy, setPreflightBusy] = useState(false);
|
|
14
|
+
const [preflightErr, setPreflightErr] = useState('');
|
|
15
|
+
const [traffic, setTraffic] = useState<number[]>([]);
|
|
16
|
+
const prevTotal = useRef<number | null>(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const load = async () => {
|
|
19
|
+
try { setMetrics(await getMetrics()); } catch { setMetrics(null); }
|
|
20
|
+
try { setServices(await getServices()); } catch { setServices([]); }
|
|
21
|
+
};
|
|
22
|
+
load();
|
|
23
|
+
const timer = setInterval(load, 5000);
|
|
24
|
+
return () => clearInterval(timer);
|
|
25
|
+
}, []);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!metrics?.metrics?.http_requests_total?.values) return;
|
|
28
|
+
const values = metrics.metrics.http_requests_total.values as Record<string, number>;
|
|
29
|
+
const total = Object.values(values).reduce((sum: number, value) => sum + Number(value ?? 0), 0);
|
|
30
|
+
const prev = prevTotal.current;
|
|
31
|
+
prevTotal.current = total;
|
|
32
|
+
if (prev == null) return;
|
|
33
|
+
const delta = total - prev;
|
|
34
|
+
if (delta < 0) return;
|
|
35
|
+
setTraffic((t) => [...t, delta].slice(-20));
|
|
36
|
+
}, [metrics]);
|
|
37
|
+
const running = services.filter((service) => service.status === 'running').length;
|
|
38
|
+
const requests = metrics?.metrics?.http_requests_total?.values;
|
|
39
|
+
const requestCount = requests ? Object.values(requests).reduce((sum: number, value) => sum + Number(value ?? 0), 0) : 0;
|
|
40
|
+
const runChecks = async () => {
|
|
41
|
+
setPreflightBusy(true);
|
|
42
|
+
setPreflightErr('');
|
|
43
|
+
try {
|
|
44
|
+
setPreflight(await runPreflight());
|
|
45
|
+
} catch (e: any) {
|
|
46
|
+
setPreflight(null);
|
|
47
|
+
setPreflightErr(e?.message || 'Preflight request failed');
|
|
48
|
+
}
|
|
49
|
+
setPreflightBusy(false);
|
|
50
|
+
};
|
|
51
|
+
return (
|
|
52
|
+
<div className="admin-view-stack">
|
|
53
|
+
<PageHead title="Project Overview"><button onClick={() => onNavigate('processes')} className="workspace-action">Open services <span>→</span></button></PageHead>
|
|
54
|
+
<div className="overview-hero"><div><span className="admin-overline">GOOD MORNING, ADMIN</span><h1>Your project is <em>in orbit.</em></h1><p>One calm surface for the services, configuration, data, and AI that power your Nexus application.</p></div><div className="hero-orbit-art"><span /><i /><b /><strong>NX</strong></div></div>
|
|
55
|
+
<div className="overview-stat-grid"><div className="overview-stat"><span>ACTIVE SERVICES</span><strong>{running}<small>/{services.length || 4}</small></strong><b className="stat-good">◠running now</b></div><div className="overview-stat"><span>HTTP REQUESTS</span><strong>{requestCount || '—'}</strong><b>since boot</b></div><div className="overview-stat"><span>UPTIME</span><strong>{metrics ? `${Math.round((metrics.uptime ?? 0) / 60)}m` : '—'}</strong><b>backend process</b></div><div className="overview-stat"><span>ENVIRONMENT</span><strong>DEV</strong><b className="stat-violet">local workspace</b></div></div>
|
|
56
|
+
<section className="workspace-panel live-traffic-panel"><div className="panel-title-row"><div><span className="admin-overline">HTTP REQUESTS</span><h3>Live traffic</h3></div><span className="live-badge"><i className="status-dot good" /> LIVE</span></div><TrafficChart samples={traffic} total={requestCount} last={traffic.length ? traffic[traffic.length - 1] : 0} onNavigate={onNavigate} /></section>
|
|
57
|
+
<div className="overview-columns"><section className="workspace-panel"><div className="panel-title-row"><div><span className="admin-overline">PROJECT SURFACES</span><h3>Everything in one place</h3></div><button onClick={() => onNavigate('config')} className="text-action">View config →</button></div><div className="surface-grid"><button className="surface-card acc-blue" onClick={() => onNavigate('config')}><span className="surface-icon"><Icon name="braces" size={20} /></span><strong>Runtime config</strong><small>nexus.runtime.json overrides</small><i className="surface-go">→</i></button><button className="surface-card acc-violet" onClick={() => onNavigate('env')}><span className="surface-icon"><Icon name="key" size={20} /></span><strong>Environment</strong><small>Masked secrets editor</small><i className="surface-go">→</i></button><button className="surface-card acc-pink" onClick={() => onNavigate('databases')}><span className="surface-icon"><Icon name="database" size={20} /></span><strong>Data layer</strong><small>Mongo databases & collections</small><i className="surface-go">→</i></button><button className="surface-card acc-blue" onClick={() => onNavigate('theme')}><span className="surface-icon"><Icon name="sparkles" size={20} /></span><strong>AI theme</strong><small>Generate schemas from English</small><i className="surface-go">→</i></button><button className="surface-card acc-violet" onClick={() => onNavigate('users')}><span className="surface-icon"><Icon name="users" size={20} /></span><strong>Users & roles</strong><small>Accounts, roles & grants</small><i className="surface-go">→</i></button><button className="surface-card acc-pink" onClick={() => onNavigate('payments')}><span className="surface-icon"><Icon name="dollar" size={20} /></span><strong>Payments</strong><small>Orders, transactions & providers</small><i className="surface-go">→</i></button><button className="surface-card acc-blue" onClick={() => onNavigate('ai')}><span className="surface-icon"><Icon name="bot" size={20} /></span><strong>AI agents</strong><small>Providers & chat playground</small><i className="surface-go">→</i></button></div></section><section className="workspace-panel pulse-panel"><div className="panel-title-row"><div><span className="admin-overline">SYSTEM PULSE</span><h3>Services are moving</h3></div><span className="live-badge"><i className="status-dot good" /> LIVE</span></div><div className="pulse-bars">{[34,55,42,78,62,88,52,72,48,66,84,58].map((height, index) => <i key={index} style={{ height: `${height}%` }} />)}</div><p>Live supervisor status refreshes every five seconds.</p><button onClick={() => onNavigate('monitoring')} className="text-action">Open monitoring →</button></section></div>
|
|
58
|
+
<section className="workspace-panel">
|
|
59
|
+
<div className="panel-title-row">
|
|
60
|
+
<div><span className="admin-overline">PREFLIGHT DIAGNOSTICS</span><h3>Dependencies are being probed</h3></div>
|
|
61
|
+
<button onClick={runChecks} disabled={preflightBusy} className="glass-chip-btn">{preflightBusy ? 'probing…' : <><Icon name="refresh" size={12} /> run checks</>}</button>
|
|
62
|
+
</div>
|
|
63
|
+
<p className="preflight-note">The Python server pings the backend API, AI server, GraphQL endpoint and data-store ports, and reports latency per target.</p>
|
|
64
|
+
{preflightErr && (
|
|
65
|
+
<div role="alert" className="mb-3 rounded-lg border border-rose-500/30 bg-rose-500/10 px-3 py-2 text-sm text-rose-400">
|
|
66
|
+
Preflight failed — {preflightErr}
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
{preflight ? (
|
|
70
|
+
<>
|
|
71
|
+
<div className="preflight-summary">
|
|
72
|
+
<span className="preflight-count ok">â— {preflight.passed} ok</span>
|
|
73
|
+
<span className="preflight-count warn">â— {preflight.warnings} slow</span>
|
|
74
|
+
<span className="preflight-count bad">â— {preflight.failed} failed</span>
|
|
75
|
+
<span className="preflight-meta">{preflight.durationMs}ms · {preflight.ranAt}</span>
|
|
76
|
+
</div>
|
|
77
|
+
{preflight.engineOk === false || (preflight.checks.length > 0 && preflight.failed === preflight.checks.length) ? (
|
|
78
|
+
<div className="preflight-guidance">
|
|
79
|
+
<strong>Nothing responded.</strong>
|
|
80
|
+
<span>The stack may not be running. Start everything with <code>nexus dev</code>, or Python-only with <code>python main.py</code>. Then press “run checks†again.</span>
|
|
81
|
+
<button onClick={() => onNavigate('processes')} className="text-action">Open services →</button>
|
|
82
|
+
</div>
|
|
83
|
+
) : null}
|
|
84
|
+
</>
|
|
85
|
+
) : preflightBusy ? (
|
|
86
|
+
<p className="preflight-empty">Probing dependencies…</p>
|
|
87
|
+
) : (
|
|
88
|
+
<p className="preflight-empty">No checks run yet — press “run checksâ€.</p>
|
|
89
|
+
)}
|
|
90
|
+
{preflight && (
|
|
91
|
+
<div className="preflight-list">
|
|
92
|
+
{preflight.checks.map((c) => (
|
|
93
|
+
<div key={c.name} className="preflight-row">
|
|
94
|
+
<span className={`preflight-dot ${c.ok ? 'good' : 'bad'}`} />
|
|
95
|
+
<span className="preflight-check-name">{c.name}</span>
|
|
96
|
+
<span className="preflight-kind">{c.kind}</span>
|
|
97
|
+
{preflightAddress(c) && <span className="preflight-address">{preflightAddress(c)}</span>}
|
|
98
|
+
{c.latencyMs != null && <span className="preflight-latency">{c.latencyMs}ms</span>}
|
|
99
|
+
<span className={`preflight-state ${c.ok ? 'ok' : 'fail'}`}>{c.ok ? 'ok' : 'fail'}</span>
|
|
100
|
+
{c.error && <span className="preflight-error" title={friendlyPreflightError(c.error, c.errorCategory)}>{friendlyPreflightError(c.error, c.errorCategory)}</span>}
|
|
101
|
+
</div>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</section>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Icon } from '../icons.js';
|
|
3
|
+
import {
|
|
4
|
+
getPaymentOrders, getPaymentTransactions, getPaymentStatus,
|
|
5
|
+
type PaymentProviderStatus,
|
|
6
|
+
} from '../api.js';
|
|
7
|
+
import { useAdminAlert } from '../alertCenter.js';
|
|
8
|
+
import type { ToggleStyle, PaymentOrder, PaymentTransaction } from '../lib/types.js';
|
|
9
|
+
import { ALL_PROVIDERS } from '../lib/constants.js';
|
|
10
|
+
import { PageHead } from '../components/ui/PageHead.js';
|
|
11
|
+
import { PageHero } from '../components/ui/PageHero.js';
|
|
12
|
+
import { OrdersTable } from './payments/OrdersTable.js';
|
|
13
|
+
import { TransactionsTable } from './payments/TransactionsTable.js';
|
|
14
|
+
import { TestConsole } from './payments/TestConsole.js';
|
|
15
|
+
|
|
16
|
+
export function Payments({ toggle }: { toggle: ToggleStyle }) {
|
|
17
|
+
const [tab, setTab] = useState<'orders' | 'transactions' | 'test'>('orders');
|
|
18
|
+
const [orders, setOrders] = useState<PaymentOrder[]>([]);
|
|
19
|
+
const [transactions, setTransactions] = useState<PaymentTransaction[]>([]);
|
|
20
|
+
const [status, setStatus] = useState<PaymentProviderStatus[]>([]);
|
|
21
|
+
const [provider, setProvider] = useState('all');
|
|
22
|
+
const [msg, setMsg] = useAdminAlert('payments');
|
|
23
|
+
|
|
24
|
+
const load = async (q = provider) => {
|
|
25
|
+
setMsg('');
|
|
26
|
+
try {
|
|
27
|
+
const filter = q === 'all' ? undefined : q;
|
|
28
|
+
const [o, t, s] = await Promise.all([getPaymentOrders(filter), getPaymentTransactions(filter), getPaymentStatus()]);
|
|
29
|
+
setOrders(o.orders ?? []);
|
|
30
|
+
setTransactions(t.transactions ?? []);
|
|
31
|
+
setStatus(s.providers ?? []);
|
|
32
|
+
} catch (e: any) { setMsg(String(e?.message ?? e)); }
|
|
33
|
+
};
|
|
34
|
+
useEffect(() => { load('all'); }, []);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="space-y-6">
|
|
38
|
+
<PageHead title="Payments">
|
|
39
|
+
<div className="flex items-center gap-3">
|
|
40
|
+
<select value={provider} onChange={(e) => { const v = e.target.value; setProvider(v); load(v); }} className="glass-input w-44 !py-1.5">
|
|
41
|
+
<option value="all" className="bg-slate-900">All providers</option>
|
|
42
|
+
{ALL_PROVIDERS.map((p) => <option key={p} value={p} className="bg-slate-900">{p}</option>)}
|
|
43
|
+
</select>
|
|
44
|
+
<button onClick={() => load()} className="glass-chip-btn"><Icon name="refresh" size={12} /> refresh</button>
|
|
45
|
+
</div>
|
|
46
|
+
</PageHead>
|
|
47
|
+
<PageHero kicker="PAYMENT OPERATIONS" title={<>Money flow, <em>on the ledger.</em></>} desc="Every charge, refund and provider event from your payment stack — one surface to reconcile what went through." glyph="dollar" art="cards" />
|
|
48
|
+
|
|
49
|
+
<div className="workspace-tabs">
|
|
50
|
+
{([
|
|
51
|
+
['orders', `Orders (${orders.length})`],
|
|
52
|
+
['transactions', `Transactions (${transactions.length})`],
|
|
53
|
+
['test', 'Test Console'],
|
|
54
|
+
] as const).map(([id, label]) => (
|
|
55
|
+
<button key={id} onClick={() => setTab(id)} className={tab === id ? 'is-active' : ''}>
|
|
56
|
+
{label}
|
|
57
|
+
</button>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{tab === 'orders' && <OrdersTable orders={orders} />}
|
|
62
|
+
{tab === 'transactions' && <TransactionsTable transactions={transactions} />}
|
|
63
|
+
{tab === 'test' && (
|
|
64
|
+
<TestConsole toggle={toggle} status={status} onRun={() => load()} onNotice={setMsg} onCreated={() => { setTab('orders'); load(); }} />
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { getPlugins } from '../api.js';
|
|
3
|
+
import { PageHead } from '../components/ui/PageHead.js';
|
|
4
|
+
import { PageHero } from '../components/ui/PageHero.js';
|
|
5
|
+
|
|
6
|
+
export function Plugins() {
|
|
7
|
+
const [data, setData] = useState<{ pages: any[]; slots: Record<string, any[]> } | null>(null);
|
|
8
|
+
useEffect(() => { getPlugins().then(setData).catch(() => {}); }, []);
|
|
9
|
+
if (!data) return <p className="text-slate-400">Loading…</p>;
|
|
10
|
+
return (
|
|
11
|
+
<div className="space-y-6">
|
|
12
|
+
<PageHead title="Plugins" />
|
|
13
|
+
<PageHero kicker="EXTENSION GALLERY" title={<>Extend Nexus, <em>effortlessly.</em></>} desc="Every admin page, slot and surface that plugin modules register — a living index of what's available to your build." glyph="puzzle" art="grid" />
|
|
14
|
+
<div className="glass-card space-y-4">
|
|
15
|
+
<h3 className="glass-h3">Admin pages</h3>
|
|
16
|
+
<ul className="space-y-1">
|
|
17
|
+
{data.pages.map((p) => (
|
|
18
|
+
<li key={p.path} className="flex items-center gap-2 font-mono text-sm text-slate-200">
|
|
19
|
+
<span className="chip">{p.group ?? 'Plugins'}</span> {p.title} —{' '}
|
|
20
|
+
<a href={p.path} target="_blank" rel="noreferrer" className="text-slate-400 underline decoration-dotted underline-offset-4 transition-colors hover:text-sky-300">
|
|
21
|
+
{p.path}
|
|
22
|
+
</a>
|
|
23
|
+
</li>
|
|
24
|
+
))}
|
|
25
|
+
{!data.pages.length && <li className="text-sm text-slate-500">No plugin pages registered.</li>}
|
|
26
|
+
</ul>
|
|
27
|
+
{Object.keys(data.slots).length > 0 && (
|
|
28
|
+
<>
|
|
29
|
+
<h3 className="glass-h3">Slots</h3>
|
|
30
|
+
<pre className="glass-code overflow-auto p-3">{JSON.stringify(data.slots, null, 2)}</pre>
|
|
31
|
+
</>
|
|
32
|
+
)}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { getServices, controlService, getServiceLogs, type ServiceState } from '../api.js';
|
|
3
|
+
import { Icon } from '../icons.js';
|
|
4
|
+
import { PageHead } from '../components/ui/PageHead.js';
|
|
5
|
+
import { PageHero } from '../components/ui/PageHero.js';
|
|
6
|
+
|
|
7
|
+
export function Processes() {
|
|
8
|
+
const [services, setServices] = useState<ServiceState[]>([]);
|
|
9
|
+
const [logs, setLogs] = useState<Record<string, string[]>>({});
|
|
10
|
+
const [sel, setSel] = useState<string | null>(null);
|
|
11
|
+
|
|
12
|
+
const refresh = async () => {
|
|
13
|
+
try { setServices(await getServices()); } catch { setServices([]); }
|
|
14
|
+
};
|
|
15
|
+
useEffect(() => { refresh(); const t = setInterval(refresh, 3000); return () => clearInterval(t); }, []);
|
|
16
|
+
|
|
17
|
+
const showLogs = async (name: string) => {
|
|
18
|
+
setSel(name);
|
|
19
|
+
setLogs({ ...logs, [name]: await getServiceLogs(name) });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const color = (s: string) => (s === 'running' ? 'text-emerald-400' : s === 'errored' ? 'text-rose-400' : 'text-slate-400');
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="space-y-6">
|
|
26
|
+
<PageHead title="Processes">
|
|
27
|
+
<button onClick={refresh} className="glass-chip-btn"><Icon name="refresh" size={12} /> refresh</button>
|
|
28
|
+
</PageHead>
|
|
29
|
+
<PageHero kicker="SERVICE CONTROL PLANE" title={<>Your services, <em>in check.</em></>} desc="Every backend, frontend and AI process the supervisor is tending — live status, uptime, PID and one-click control." glyph="server" art="bars" />
|
|
30
|
+
<div className="glass-panel overflow-hidden p-2">
|
|
31
|
+
<table className="glass-table">
|
|
32
|
+
<thead><tr className="text-slate-400"><th>Service</th><th className="text-center">Status</th><th className="text-center">PID</th><th className="text-center">Uptime</th><th className="text-center">Actions</th></tr></thead>
|
|
33
|
+
<tbody>
|
|
34
|
+
{services.map((s) => (
|
|
35
|
+
<tr key={s.name}>
|
|
36
|
+
<td className="font-mono text-slate-200">{s.name}</td>
|
|
37
|
+
<td className={`text-center ${color(s.status)}`}>
|
|
38
|
+
<span className="inline-flex items-center gap-2">
|
|
39
|
+
<span className={`inline-block h-2 w-2 rounded-full ${s.status === 'running' ? 'bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.9)]' : s.status === 'errored' ? 'bg-rose-500' : 'bg-slate-500'}`} />
|
|
40
|
+
{s.status}
|
|
41
|
+
</span>
|
|
42
|
+
</td>
|
|
43
|
+
<td className="text-center text-slate-400">{s.pid ?? '—'}</td>
|
|
44
|
+
<td className="text-center text-slate-400">{s.startedAt ? `${Math.round((Date.now() - s.startedAt) / 1000)}s` : '—'}</td>
|
|
45
|
+
<td className="text-center">
|
|
46
|
+
<span className="inline-flex gap-1.5">
|
|
47
|
+
<button onClick={() => controlService('start', s.name)} className="glass-chip-btn">start</button>
|
|
48
|
+
<button onClick={() => controlService('stop', s.name)} className="glass-chip-btn-danger">stop</button>
|
|
49
|
+
<button onClick={() => controlService('restart', s.name)} className="glass-chip-btn">restart</button>
|
|
50
|
+
<button onClick={() => showLogs(s.name)} className="glass-chip-btn">logs</button>
|
|
51
|
+
</span>
|
|
52
|
+
</td>
|
|
53
|
+
</tr>
|
|
54
|
+
))}
|
|
55
|
+
{!services.length && <tr><td colSpan={5} className="py-8 text-center text-slate-500">No services reported by the supervisor.</td></tr>}
|
|
56
|
+
</tbody>
|
|
57
|
+
</table>
|
|
58
|
+
</div>
|
|
59
|
+
{sel && (
|
|
60
|
+
<div className="glass-card space-y-2">
|
|
61
|
+
<h3 className="glass-h3">{sel} logs</h3>
|
|
62
|
+
<pre className="glass-code max-h-80 overflow-auto p-3">{((logs[sel] ?? []).join('\n')) || '— empty —'}</pre>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { getDatabases, createCollection, generateSchema, getAiStatus, type DatabaseInfo, type GeneratedSchema, type AiStatus } from '../api.js';
|
|
3
|
+
import { useAdminAlert } from '../alertCenter.js';
|
|
4
|
+
import { PageHead } from '../components/ui/PageHead.js';
|
|
5
|
+
import { PageHero } from '../components/ui/PageHero.js';
|
|
6
|
+
|
|
7
|
+
export function Schema() {
|
|
8
|
+
const [prompt, setPrompt] = useState('');
|
|
9
|
+
const [databases, setDatabases] = useState<DatabaseInfo[]>([]);
|
|
10
|
+
const [db, setDb] = useState('');
|
|
11
|
+
const [result, setResult] = useState<GeneratedSchema | null>(null);
|
|
12
|
+
const [edited, setEdited] = useState('');
|
|
13
|
+
const [busy, setBusy] = useState(false);
|
|
14
|
+
const [msg, setMsg] = useAdminAlert('schema');
|
|
15
|
+
const [aiStatus, setAiStatus] = useState<AiStatus | null>(null);
|
|
16
|
+
const [aiErr, setAiErr] = useState('');
|
|
17
|
+
|
|
18
|
+
useEffect(() => { getDatabases().then((d) => setDatabases(d.databases ?? [])).catch(() => {}); }, []);
|
|
19
|
+
|
|
20
|
+
const checkAi = async () => {
|
|
21
|
+
setAiErr('');
|
|
22
|
+
try { setAiStatus(await getAiStatus()); } catch (e: any) { setAiErr(String(e?.message ?? e)); }
|
|
23
|
+
};
|
|
24
|
+
useEffect(() => { checkAi(); }, []);
|
|
25
|
+
|
|
26
|
+
const generate = async () => {
|
|
27
|
+
if (!prompt.trim()) { setMsg('Describe the data you want to model, e.g. "products with a name, price and category".'); return; }
|
|
28
|
+
setBusy(true); setMsg('');
|
|
29
|
+
try {
|
|
30
|
+
const g = await generateSchema(prompt);
|
|
31
|
+
setResult(g);
|
|
32
|
+
setEdited(JSON.stringify(g.jsonSchema, null, 2));
|
|
33
|
+
setMsg(`Schema generated (${g.model}) — review and create.`);
|
|
34
|
+
} catch (e: any) { setMsg(`Generation failed: ${e?.message ?? e} — check the AI status below (is the AI server running on :8000?).`); }
|
|
35
|
+
finally { setBusy(false); }
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const create = async () => {
|
|
39
|
+
setMsg('');
|
|
40
|
+
let schema: unknown;
|
|
41
|
+
try { schema = JSON.parse(edited); } catch { setMsg('Edited schema is not valid JSON.'); return; }
|
|
42
|
+
const target = db || databases[0]?.name;
|
|
43
|
+
if (!target) { setMsg('Create a database first (Databases tab), then pick it here.'); return; }
|
|
44
|
+
try {
|
|
45
|
+
await createCollection(target, { name: result?.collection ?? 'ai_schema', jsonSchema: schema });
|
|
46
|
+
setMsg(`Created collection "${result?.collection ?? 'ai_schema'}" in "${target}" with the validator attached.`);
|
|
47
|
+
} catch (e: any) { setMsg(`Create failed: ${e?.message ?? e}`); }
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="space-y-6">
|
|
52
|
+
<PageHead title="AI Schema Creation" />
|
|
53
|
+
<PageHero kicker="AI WORKSPACE" title={<>Schemas, <em>born from language.</em></>} desc="Describe the data you need in plain English and let the model return a validated MongoDB schema you can review before it's live." glyph="sparkles" art="bolt" />
|
|
54
|
+
<p className="text-sm text-slate-400">Describe the data in plain English — the AI returns a MongoDB <code className="rounded bg-white/10 px-1 font-mono text-xs">$jsonSchema</code> validator plus a field list, which you review and create as a validator-protected collection.</p>
|
|
55
|
+
|
|
56
|
+
<section className="glass-card space-y-3">
|
|
57
|
+
<div className="flex items-center justify-between">
|
|
58
|
+
<h3 className="glass-h3">AI provider status</h3>
|
|
59
|
+
<button onClick={checkAi} className="glass-chip-btn">re-check</button>
|
|
60
|
+
</div>
|
|
61
|
+
{aiErr && <p className="text-xs text-rose-400">{aiErr}</p>}
|
|
62
|
+
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
|
63
|
+
{aiStatus ? (
|
|
64
|
+
<>
|
|
65
|
+
{([
|
|
66
|
+
['AI server', aiStatus.aiServer, aiStatus.aiServer.detail ? `health: ${aiStatus.aiServer.detail.status ?? 'unknown'}` : undefined],
|
|
67
|
+
['Ollama', aiStatus.ollama, aiStatus.ollama.detail?.modelCount != null ? `${aiStatus.ollama.detail.modelCount} model(s) reachable` : undefined],
|
|
68
|
+
] as const).map(([label, s, detail]) => (
|
|
69
|
+
<div key={label} className="glass flex items-center gap-2 rounded-xl px-3 py-2 text-sm">
|
|
70
|
+
<span className={s.ok ? 'dot-ok' : 'dot-bad'} />
|
|
71
|
+
<span className="font-medium text-slate-200">{label}</span>
|
|
72
|
+
<span className={`text-xs ${s.ok ? 'text-emerald-400' : 'text-rose-400'}`}>{s.ok ? (detail ?? 'ok') : (s.error ?? 'down')}</span>
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</>
|
|
76
|
+
) : (
|
|
77
|
+
<p className="col-span-3 text-xs text-slate-500">No status yet{aiErr ? '' : ' — run a check'}.</p>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
{aiStatus && <p className="text-xs text-slate-500">"auto" currently resolves to <span className="font-mono text-slate-300">{aiStatus.autoResolvesTo}</span> on the AI server.</p>}
|
|
81
|
+
</section>
|
|
82
|
+
|
|
83
|
+
<div className="glass-card space-y-3">
|
|
84
|
+
<div className="flex items-center justify-between">
|
|
85
|
+
<h3 className="glass-h3">Describe your collection</h3>
|
|
86
|
+
<span className="inline-flex items-center gap-1.5 rounded-full border border-indigo-300/30 bg-indigo-400/10 px-2.5 py-1 text-[0.6rem] font-semibold uppercase tracking-wider text-indigo-300"><i className="dot-ok" /> AI auto</span>
|
|
87
|
+
</div>
|
|
88
|
+
<textarea className="glass-code h-32 w-full p-3 text-sm" placeholder='e.g. "a products collection with name, price (number), in-stock flag, category from a fixed enum, created date and an optional description"' value={prompt} onChange={(e) => setPrompt(e.target.value)} />
|
|
89
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
90
|
+
<label className="mr-1.5 text-sm text-slate-400">Database</label>
|
|
91
|
+
<select className="glass-input w-48 !py-1.5 text-sm" value={db} onChange={(e) => setDb(e.target.value)}>
|
|
92
|
+
<option value="" className="bg-slate-900">{databases.length ? 'pick a database…' : 'no databases yet'}</option>
|
|
93
|
+
{databases.map((d) => <option key={d.name} value={d.name} className="bg-slate-900">{d.name}</option>)}
|
|
94
|
+
</select>
|
|
95
|
+
<span className="text-xs text-slate-500">Provider & model auto-selected on the AI server.</span>
|
|
96
|
+
<button onClick={generate} disabled={busy} className="glass-btn-primary ml-auto">{busy ? 'Generating…' : 'Generate with AI'}</button>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{result && (
|
|
101
|
+
<div className="space-y-3">
|
|
102
|
+
<div className="glass-card space-y-3">
|
|
103
|
+
<h3 className="glass-h3">Generated schema — <span className="font-mono text-indigo-300">{result.collection}</span></h3>
|
|
104
|
+
<div className="max-h-48 overflow-auto rounded-xl border border-white/10">
|
|
105
|
+
<table className="glass-table">
|
|
106
|
+
<thead><tr><th>Field</th><th>Type</th><th className="text-center">Required</th><th className="text-center">Unique</th><th>Description</th></tr></thead>
|
|
107
|
+
<tbody>
|
|
108
|
+
{result.fields.map((f) => (
|
|
109
|
+
<tr key={f.name}>
|
|
110
|
+
<td className="font-mono text-slate-200">{f.name}</td>
|
|
111
|
+
<td className="text-slate-300">{f.type}</td>
|
|
112
|
+
<td className="text-center text-emerald-400">{f.required ? '✓' : ''}</td>
|
|
113
|
+
<td className="text-center text-indigo-300">{f.unique ? '✓' : ''}</td>
|
|
114
|
+
<td className="text-slate-400">{f.description ?? ''}</td>
|
|
115
|
+
</tr>
|
|
116
|
+
))}
|
|
117
|
+
</tbody>
|
|
118
|
+
</table>
|
|
119
|
+
</div>
|
|
120
|
+
<div>
|
|
121
|
+
<h4 className="glass-h3 mb-2">$jsonSchema validator <span className="ml-1 font-normal normal-case text-slate-500">(editable before creating)</span></h4>
|
|
122
|
+
<textarea className="glass-code h-56 w-full p-3 text-xs" value={edited} onChange={(e) => setEdited(e.target.value)} spellCheck={false} />
|
|
123
|
+
</div>
|
|
124
|
+
<button onClick={create} className="glass-btn-success">Create collection with validator</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|