@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,70 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react';
|
|
2
|
+
import { formatPointTime } from '../../lib/utils.js';
|
|
3
|
+
import type { RequestSeries, RequestSeriesRange } from '../../api.js';
|
|
4
|
+
|
|
5
|
+
export function RequestSeriesChart({ series }: { series: RequestSeries | null }) {
|
|
6
|
+
const W = 720;
|
|
7
|
+
const H = 96;
|
|
8
|
+
const PAD = 4;
|
|
9
|
+
const svgRef = useRef<SVGSVGElement | null>(null);
|
|
10
|
+
const [tip, setTip] = useState<{ x: number; y: number; label: string; count: number } | null>(null);
|
|
11
|
+
const pts = series?.points?.length ? series.points : [];
|
|
12
|
+
if (!series || !pts.length) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="traffic-chart">
|
|
15
|
+
<div className="traffic-chart-body">
|
|
16
|
+
<svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" role="img" aria-label="No request data">
|
|
17
|
+
<line x1={PAD} y1={H - PAD} x2={W - PAD} y2={H - PAD} stroke="var(--admin-border, rgba(125,174,235,0.2))" strokeWidth="1" />
|
|
18
|
+
</svg>
|
|
19
|
+
</div>
|
|
20
|
+
<div className="traffic-meta"><span>no data for this range yet</span></div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
const max = Math.max(1, ...pts.map((p) => p.count));
|
|
25
|
+
const minT = pts[0].t;
|
|
26
|
+
const maxT = pts[pts.length - 1].t;
|
|
27
|
+
const span = Math.max(1, maxT - minT);
|
|
28
|
+
const coords = pts.map((p) => [PAD + ((p.t - minT) / span) * (W - PAD * 2), H - PAD - (p.count / max) * (H - PAD * 2)] as const);
|
|
29
|
+
const line = coords.map(([x, y], i) => `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`).join(' ');
|
|
30
|
+
const area = `${line} L${(W - PAD).toFixed(1)},${H - PAD} L${PAD},${H - PAD} Z`;
|
|
31
|
+
const showTip = (e: React.MouseEvent<SVGCircleElement>, i: number) => {
|
|
32
|
+
const svg = svgRef.current;
|
|
33
|
+
if (!svg) return;
|
|
34
|
+
const rect = svg.getBoundingClientRect();
|
|
35
|
+
const [vx, vy] = coords[i];
|
|
36
|
+
setTip({
|
|
37
|
+
x: (vx / W) * rect.width,
|
|
38
|
+
y: (vy / H) * rect.height,
|
|
39
|
+
label: formatPointTime(pts[i].t, series.range),
|
|
40
|
+
count: pts[i].count,
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
return (
|
|
44
|
+
<div className="traffic-chart">
|
|
45
|
+
<div className="traffic-chart-body">
|
|
46
|
+
<svg ref={svgRef} viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" role="img" aria-label="HTTP request traffic">
|
|
47
|
+
<defs>
|
|
48
|
+
<linearGradient id="seriesFill" x1="0" y1="0" x2="0" y2="1">
|
|
49
|
+
<stop offset="0%" stopColor="var(--admin-violet, #9f7bff)" stopOpacity="0.35" />
|
|
50
|
+
<stop offset="100%" stopColor="var(--admin-violet, #9f7bff)" stopOpacity="0" />
|
|
51
|
+
</linearGradient>
|
|
52
|
+
</defs>
|
|
53
|
+
<line x1={PAD} y1={H - PAD} x2={W - PAD} y2={H - PAD} stroke="var(--admin-border, rgba(125,174,235,0.2))" strokeWidth="1" />
|
|
54
|
+
<path d={area} fill="url(#seriesFill)" />
|
|
55
|
+
<path d={line} fill="none" stroke="var(--admin-violet, #9f7bff)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
56
|
+
{pts.map((p, i) => (
|
|
57
|
+
<circle key={i} cx={coords[i][0]} cy={coords[i][1]} r="7" fill="transparent" style={{ cursor: 'crosshair' }} onMouseEnter={(e) => showTip(e, i)} onMouseMove={(e) => showTip(e, i)} onMouseLeave={() => setTip(null)} />
|
|
58
|
+
))}
|
|
59
|
+
</svg>
|
|
60
|
+
{tip && (
|
|
61
|
+
<div className="traffic-tip" style={{ left: tip.x, top: tip.y }}>
|
|
62
|
+
<span className="traffic-tip-time">{tip.label}</span>
|
|
63
|
+
<span className="traffic-tip-count">{tip.count} req</span>
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
<div className="traffic-meta"><span>{series.total} requests in range</span><span>peak {max} / bucket</span></div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Tab } from '../../lib/types.js';
|
|
3
|
+
|
|
4
|
+
export function TrafficChart({ samples, total, last, onNavigate }: { samples: number[]; total: number; last: number; onNavigate: (tab: Tab) => void }) {
|
|
5
|
+
const W = 720;
|
|
6
|
+
const H = 96;
|
|
7
|
+
const PAD = 4;
|
|
8
|
+
const pts = samples.length ? samples : [0, 0];
|
|
9
|
+
const max = Math.max(4, ...pts);
|
|
10
|
+
const stepX = (W - PAD * 2) / (Math.max(pts.length - 1, 1));
|
|
11
|
+
const coords = pts.map((v, i) => [PAD + i * stepX, H - PAD - (v / max) * (H - PAD * 2)] as const);
|
|
12
|
+
const line = coords.map(([x, y], i) => `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`).join(' ');
|
|
13
|
+
const area = `${line} L${(W - PAD).toFixed(1)},${H - PAD} L${PAD},${H - PAD} Z`;
|
|
14
|
+
const lastPt = coords[coords.length - 1];
|
|
15
|
+
return (
|
|
16
|
+
<div className="traffic-chart">
|
|
17
|
+
<svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" role="img" aria-label="Live HTTP request rate">
|
|
18
|
+
<defs>
|
|
19
|
+
<linearGradient id="trafficFill" x1="0" y1="0" x2="0" y2="1">
|
|
20
|
+
<stop offset="0%" stopColor="var(--admin-blue, #54b7ff)" stopOpacity="0.35" />
|
|
21
|
+
<stop offset="100%" stopColor="var(--admin-blue, #54b7ff)" stopOpacity="0" />
|
|
22
|
+
</linearGradient>
|
|
23
|
+
</defs>
|
|
24
|
+
<line x1={PAD} y1={H - PAD} x2={W - PAD} y2={H - PAD} stroke="var(--admin-border, rgba(125,174,235,0.2))" strokeWidth="1" />
|
|
25
|
+
<path d={area} fill="url(#trafficFill)" />
|
|
26
|
+
<path d={line} fill="none" stroke="var(--admin-blue, #54b7ff)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
27
|
+
<circle cx={lastPt[0]} cy={lastPt[1]} r="3.5" fill="var(--admin-blue, #54b7ff)" />
|
|
28
|
+
</svg>
|
|
29
|
+
<div className="traffic-meta"><span>{last} req / 5s</span><span>{total} since boot</span><button onClick={() => onNavigate('monitoring')} className="text-action">Open monitoring →</button></div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { PaymentOrder } from '../../lib/types.js';
|
|
3
|
+
|
|
4
|
+
export function OrdersTable({ orders }: { orders: PaymentOrder[] }) {
|
|
5
|
+
const statusColor = (s: string) => (['paid', 'captured'].includes(s) ? 'text-emerald-400' : ['failed', 'refunded', 'cancelled'].includes(s) ? 'text-rose-400' : 'text-slate-300');
|
|
6
|
+
return (
|
|
7
|
+
<section className="glass-panel overflow-hidden p-2">
|
|
8
|
+
<table className="glass-table">
|
|
9
|
+
<thead><tr><th>Provider</th><th>Order</th><th>Reference</th><th className="text-center">Status</th><th className="text-right">Amount</th><th>Payment ID</th><th>Created</th></tr></thead>
|
|
10
|
+
<tbody>
|
|
11
|
+
{orders.map((o, i) => (
|
|
12
|
+
<tr key={`${o.orderId}-${i}`}>
|
|
13
|
+
<td><span className="chip">{o.provider}</span></td>
|
|
14
|
+
<td className="font-mono text-slate-200">{o.orderId || '—'}</td>
|
|
15
|
+
<td className="font-mono text-slate-400">{o.reference || '—'}</td>
|
|
16
|
+
<td className={`text-center ${statusColor(o.status)}`}>{o.status}</td>
|
|
17
|
+
<td className="text-right font-medium text-slate-200">{o.currency} {o.amount}</td>
|
|
18
|
+
<td className="font-mono text-slate-400">{o.paymentId || '—'}</td>
|
|
19
|
+
<td className="text-slate-400">{o.createdAt ? new Date(o.createdAt).toLocaleString() : '—'}</td>
|
|
20
|
+
</tr>
|
|
21
|
+
))}
|
|
22
|
+
{!orders.length && <tr><td colSpan={7} className="py-8 text-center text-slate-500">No orders recorded yet.</td></tr>}
|
|
23
|
+
</tbody>
|
|
24
|
+
</table>
|
|
25
|
+
</section>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import type { PaymentProviderStatus } from '../../api.js';
|
|
3
|
+
|
|
4
|
+
export function PaymentKeysDialog({ provider, value, onChange, onSave, onCancel, busy, msg, msgKind }: {
|
|
5
|
+
provider: PaymentProviderStatus;
|
|
6
|
+
value: Record<string, string>;
|
|
7
|
+
onChange: (v: Record<string, string>) => void;
|
|
8
|
+
onSave: () => void;
|
|
9
|
+
onCancel: () => void;
|
|
10
|
+
busy: boolean;
|
|
11
|
+
msg: string;
|
|
12
|
+
msgKind: 'ok' | 'err';
|
|
13
|
+
}) {
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const onKey = (event: KeyboardEvent) => { if (event.key === 'Escape') onCancel(); };
|
|
16
|
+
window.addEventListener('keydown', onKey);
|
|
17
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
18
|
+
}, [onCancel]);
|
|
19
|
+
|
|
20
|
+
const fields = provider.fields?.length ? provider.fields : (provider.name === 'skrill' ? [{ field: 'merchantEmail', label: 'Merchant email', hasValue: false }] : []);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="provider-modal-backdrop" onClick={onCancel}>
|
|
24
|
+
<div className="provider-modal space-y-4" role="dialog" aria-modal="true" aria-labelledby="payment-keys-title" onClick={(event) => event.stopPropagation()}>
|
|
25
|
+
<div className="flex items-start justify-between gap-3">
|
|
26
|
+
<div>
|
|
27
|
+
<span className="admin-overline">PAYMENT CREDENTIAL</span>
|
|
28
|
+
<h3 id="payment-keys-title" className="mt-1 text-base font-semibold text-slate-100">{provider.name}</h3>
|
|
29
|
+
<code className="text-xs text-slate-500">{provider.name}</code>
|
|
30
|
+
</div>
|
|
31
|
+
<button type="button" onClick={onCancel} disabled={busy} className="glass-chip-btn text-xs" aria-label="Close payment keys dialog">✕</button>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className="rounded-xl border border-sky-400/20 bg-black/20 px-3 py-2 text-xs text-slate-400">
|
|
35
|
+
Keys are stored server-side in <code className="font-mono">.env</code> as <code className="font-mono">NEXUS_PAYMENTS_{provider.name.toUpperCase()}_*</code> and never written to the config file.
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{msg && (
|
|
39
|
+
<div role="alert" className={`rounded-lg border px-3 py-1.5 text-sm ${msgKind === 'ok' ? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-300' : 'border-rose-500/30 bg-rose-500/10 text-rose-300'}`}>
|
|
40
|
+
{msg}
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
|
|
44
|
+
<div className="space-y-3">
|
|
45
|
+
{fields.map((f) => (
|
|
46
|
+
<label key={f.field} className="block">
|
|
47
|
+
<span className="admin-overline">{f.label}</span>
|
|
48
|
+
<input
|
|
49
|
+
className="glass-input mt-1 text-sm font-mono"
|
|
50
|
+
type="password"
|
|
51
|
+
id={`payment-${f.field}`}
|
|
52
|
+
name={`payment-${f.field}`}
|
|
53
|
+
autoComplete="new-password"
|
|
54
|
+
placeholder={f.hasValue ? `•••••••• (enter new to replace)` : `paste ${f.label.toLowerCase()}`}
|
|
55
|
+
value={value[f.field] ?? ''}
|
|
56
|
+
onChange={(event) => onChange({ ...value, [f.field]: event.target.value })}
|
|
57
|
+
onKeyDown={(event) => {
|
|
58
|
+
if (event.key === 'Enter' && !busy) { event.preventDefault(); onSave(); }
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
</label>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div className="flex justify-end gap-2 border-t border-sky-400/20 pt-3">
|
|
66
|
+
<button type="button" onClick={onCancel} disabled={busy} className="glass-chip-btn">Cancel</button>
|
|
67
|
+
<button type="button" onClick={onSave} disabled={busy || fields.length === 0} className="glass-chip-btn-primary">
|
|
68
|
+
{busy ? 'Saving…' : 'Save keys'}
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Icon } from '../../icons.js';
|
|
3
|
+
import {
|
|
4
|
+
createPaymentOrder, updatePaymentProvider, savePaymentProviderKeys,
|
|
5
|
+
type PaymentProviderStatus,
|
|
6
|
+
} from '../../api.js';
|
|
7
|
+
import type { ToggleStyle } from '../../lib/types.js';
|
|
8
|
+
import { PaymentKeysDialog } from './PaymentKeysDialog.js';
|
|
9
|
+
|
|
10
|
+
export function TestConsole({ toggle, status, onRun, onNotice, onCreated }: { toggle: ToggleStyle; status: PaymentProviderStatus[]; onRun: () => Promise<void> | void; onNotice: (msg: string) => void; onCreated: () => void }) {
|
|
11
|
+
const [probeBusy, setProbeBusy] = useState(false);
|
|
12
|
+
const [busy, setBusy] = useState(false);
|
|
13
|
+
const [toggleBusy, setToggleBusy] = useState<string>('');
|
|
14
|
+
const [switchAnim, setSwitchAnim] = useState<Record<string, 'out' | 'in' | null>>({});
|
|
15
|
+
const [provider, setProvider] = useState('');
|
|
16
|
+
const [amount, setAmount] = useState('100');
|
|
17
|
+
const [currency, setCurrency] = useState('INR');
|
|
18
|
+
const [err, setErr] = useState('');
|
|
19
|
+
const [result, setResult] = useState<any>(null);
|
|
20
|
+
const [keyProvider, setKeyProvider] = useState<PaymentProviderStatus | null>(null);
|
|
21
|
+
const [keyDraft, setKeyDraft] = useState<Record<string, string>>({});
|
|
22
|
+
const [keyBusy, setKeyBusy] = useState(false);
|
|
23
|
+
const [keyMsg, setKeyMsg] = useState('');
|
|
24
|
+
const [keyMsgKind, setKeyMsgKind] = useState<'ok' | 'err'>('ok');
|
|
25
|
+
|
|
26
|
+
const openKeys = (p: PaymentProviderStatus) => {
|
|
27
|
+
setKeyProvider(p);
|
|
28
|
+
setKeyDraft({});
|
|
29
|
+
setKeyMsg('');
|
|
30
|
+
setKeyMsgKind('ok');
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const saveKeys = async () => {
|
|
34
|
+
if (!keyProvider) return;
|
|
35
|
+
setKeyBusy(true); setKeyMsg(''); setKeyMsgKind('ok');
|
|
36
|
+
try {
|
|
37
|
+
const res = await savePaymentProviderKeys(keyProvider.name, keyDraft);
|
|
38
|
+
setKeyProvider(null);
|
|
39
|
+
setKeyMsg('');
|
|
40
|
+
await onRun();
|
|
41
|
+
onNotice(`Payment provider "${keyProvider.name}" keys saved. Enabled state persisted.`);
|
|
42
|
+
} catch (e: any) { setKeyMsg(String(e?.message ?? e)); setKeyMsgKind('err'); }
|
|
43
|
+
finally { setKeyBusy(false); }
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const testOrder = async (e: React.FormEvent) => {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
setBusy(true); setErr(''); setResult(null);
|
|
49
|
+
try {
|
|
50
|
+
const res = await createPaymentOrder({ provider, amount: Number(amount), currency });
|
|
51
|
+
setResult(res.order ?? res);
|
|
52
|
+
onCreated();
|
|
53
|
+
} catch (e: any) { setErr(String(e?.message ?? e)); }
|
|
54
|
+
finally { setBusy(false); }
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const toggleProvider = async (p: PaymentProviderStatus) => {
|
|
58
|
+
setToggleBusy(p.name);
|
|
59
|
+
setSwitchAnim((prev) => ({ ...prev, [p.name]: 'out' }));
|
|
60
|
+
await new Promise<void>((r) => window.setTimeout(r, 280));
|
|
61
|
+
try {
|
|
62
|
+
await updatePaymentProvider(p.name, { enabled: !p.enabled });
|
|
63
|
+
setSwitchAnim((prev) => ({ ...prev, [p.name]: 'in' }));
|
|
64
|
+
await onRun();
|
|
65
|
+
} catch { /* non-fatal */ }
|
|
66
|
+
finally {
|
|
67
|
+
setToggleBusy('');
|
|
68
|
+
await new Promise<void>((r) => window.setTimeout(r, 420));
|
|
69
|
+
setSwitchAnim((prev) => { const next = { ...prev }; delete next[p.name]; return next; });
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const selected = status.find((p) => p.name === provider);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="space-y-6">
|
|
77
|
+
<section className="glass-card space-y-3">
|
|
78
|
+
<div className="flex items-center justify-between">
|
|
79
|
+
<h3 className="glass-h3">Provider status</h3>
|
|
80
|
+
<button onClick={async () => { setProbeBusy(true); try { await onRun(); } finally { setProbeBusy(false); } }} className="glass-chip-btn" disabled={probeBusy}>
|
|
81
|
+
{probeBusy ? 'checking…' : <><Icon name="refresh" size={12} /> re-run checks</>}
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
|
|
85
|
+
{status.map((p) => {
|
|
86
|
+
const anim = switchAnim[p.name] ?? null;
|
|
87
|
+
return (
|
|
88
|
+
<div key={p.name} className={`provider-card ${p.enabled ? 'is-on' : 'is-off'}${anim === 'out' ? ' is-switch-out' : ''}${anim === 'in' ? ' is-switch-in' : ''}`}>
|
|
89
|
+
<div className="flex items-center gap-2.5">
|
|
90
|
+
<button
|
|
91
|
+
onClick={() => toggleProvider(p)}
|
|
92
|
+
disabled={!!toggleBusy || !!anim}
|
|
93
|
+
className={`ai-toggle tg-${toggle} ${p.enabled ? 'is-on' : ''}`}
|
|
94
|
+
title={p.enabled ? 'Enabled — click to disable' : 'Disabled — click to enable'}
|
|
95
|
+
>
|
|
96
|
+
<span className="ai-toggle-knob" />
|
|
97
|
+
</button>
|
|
98
|
+
<div className="flex-1 min-w-0">
|
|
99
|
+
<div className="font-medium text-sm text-slate-200 truncate">{p.name}</div>
|
|
100
|
+
<div className="text-[11px] text-slate-500 font-mono truncate">{p.sandbox ? 'sandbox mode' : 'live mode'}</div>
|
|
101
|
+
</div>
|
|
102
|
+
<span className={p.ok ? 'dot-ok' : 'dot-bad'} title={p.ok ? 'working' : 'not working'} />
|
|
103
|
+
<button onClick={() => openKeys(p)} className="glass-chip-btn shrink-0 text-[10px]" title="Set API key / secret"><Icon name="key" size={11} /> keys</button>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
106
|
+
{p.enabled
|
|
107
|
+
? <span className="provider-badge-ok">enabled</span>
|
|
108
|
+
: <span className="provider-badge-mute">disabled</span>}
|
|
109
|
+
{p.sandbox && <span className="provider-badge-warn">sandbox</span>}
|
|
110
|
+
{p.configured
|
|
111
|
+
? <span className="provider-badge-ok">keys configured</span>
|
|
112
|
+
: <span className="provider-badge-warn">missing keys</span>}
|
|
113
|
+
</div>
|
|
114
|
+
{p.detail && <div className="text-xs text-emerald-400">{p.detail}</div>}
|
|
115
|
+
{p.error && <div className="text-xs text-rose-400">{p.error}</div>}
|
|
116
|
+
{p.note && <div className="text-xs text-slate-500">{p.note}</div>}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
})}
|
|
120
|
+
</div>
|
|
121
|
+
</section>
|
|
122
|
+
|
|
123
|
+
<section className="glass-card space-y-3">
|
|
124
|
+
<div>
|
|
125
|
+
<h3 className="glass-h3">Create a test order</h3>
|
|
126
|
+
<p className="mt-1 text-sm text-slate-400">Creates a real order through the selected provider (hosted checkout, no charge) and records it in the orders table — verifies keys, upstream reachability and persistence.</p>
|
|
127
|
+
</div>
|
|
128
|
+
<form onSubmit={testOrder} className="flex flex-wrap items-end gap-3">
|
|
129
|
+
<label className="text-sm text-slate-300">Provider
|
|
130
|
+
<select value={provider} onChange={(e) => setProvider(e.target.value)} required className="glass-input mt-1 block w-44">
|
|
131
|
+
<option value="" disabled className="bg-slate-900">choose…</option>
|
|
132
|
+
{status.map((p) => (
|
|
133
|
+
<option key={p.name} value={p.name} className="bg-slate-900">{p.enabled ? p.name : `${p.name} (disabled)`}</option>
|
|
134
|
+
))}
|
|
135
|
+
</select>
|
|
136
|
+
</label>
|
|
137
|
+
<label className="text-sm text-slate-300">Amount
|
|
138
|
+
<input type="number" step="0.01" min="0.01" value={amount} onChange={(e) => setAmount(e.target.value)} className="glass-input mt-1 block w-28" />
|
|
139
|
+
</label>
|
|
140
|
+
<label className="text-sm text-slate-300">Currency
|
|
141
|
+
<input value={currency} maxLength={3} onChange={(e) => setCurrency(e.target.value.toUpperCase())} className="glass-input mt-1 block w-20" />
|
|
142
|
+
</label>
|
|
143
|
+
<button type="submit" disabled={busy || !provider} className="glass-btn-primary">
|
|
144
|
+
{busy ? 'Creating…' : 'Create test order'}
|
|
145
|
+
</button>
|
|
146
|
+
</form>
|
|
147
|
+
{err && <p className="text-sm text-rose-400">{err}</p>}
|
|
148
|
+
{selected && !selected.enabled && (
|
|
149
|
+
<p className="rounded-lg border border-amber-400/30 bg-amber-400/10 px-3 py-2 text-xs text-amber-200">
|
|
150
|
+
“{selected.name}†is disabled in the config — it will be rejected by the backend. To enable it, set{' '}
|
|
151
|
+
<code className="font-mono">NEXUS_PAYMENTS_{selected.name.toUpperCase()}_ENABLED=true</code> (plus its keys) in the project <code className="font-mono">.env</code>, or <code className="font-mono">payments.{selected.name}.enabled: true</code> in <code className="font-mono">nexus.config.ts</code>, then restart the backend. A provider also activates automatically once its keys are present.
|
|
152
|
+
</p>
|
|
153
|
+
)}
|
|
154
|
+
{result && (
|
|
155
|
+
<pre className="glass-code max-h-64 overflow-auto p-3">{JSON.stringify(result, null, 2)}</pre>
|
|
156
|
+
)}
|
|
157
|
+
</section>
|
|
158
|
+
|
|
159
|
+
{keyProvider && (
|
|
160
|
+
<PaymentKeysDialog
|
|
161
|
+
provider={keyProvider}
|
|
162
|
+
value={keyDraft}
|
|
163
|
+
onChange={setKeyDraft}
|
|
164
|
+
onSave={saveKeys}
|
|
165
|
+
onCancel={() => { setKeyProvider(null); setKeyMsg(''); }}
|
|
166
|
+
busy={keyBusy}
|
|
167
|
+
msg={keyMsg}
|
|
168
|
+
msgKind={keyMsgKind}
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { PaymentTransaction } from '../../lib/types.js';
|
|
3
|
+
|
|
4
|
+
export function TransactionsTable({ transactions }: { transactions: PaymentTransaction[] }) {
|
|
5
|
+
return (
|
|
6
|
+
<section className="glass-panel overflow-hidden p-2">
|
|
7
|
+
<table className="glass-table">
|
|
8
|
+
<thead><tr><th>Provider</th><th>Event</th><th className="text-center">Verified</th><th>Order</th><th>Payment</th><th className="text-right">Amount</th><th>Created</th></tr></thead>
|
|
9
|
+
<tbody>
|
|
10
|
+
{transactions.map((t, i) => (
|
|
11
|
+
<tr key={i}>
|
|
12
|
+
<td><span className="chip">{t.provider}</span></td>
|
|
13
|
+
<td className="font-mono text-slate-200">{t.event}</td>
|
|
14
|
+
<td className={`text-center ${t.verified ? 'text-emerald-400' : 'text-rose-400'}`}>{t.verified ? '✓' : '✗'}</td>
|
|
15
|
+
<td className="font-mono text-slate-400">{t.orderId || '—'}</td>
|
|
16
|
+
<td className="font-mono text-slate-400">{t.paymentId || '—'}</td>
|
|
17
|
+
<td className="text-right text-slate-200">{t.currency ? `${t.currency} ${t.amount ?? ''}` : '—'}</td>
|
|
18
|
+
<td className="text-slate-400">{t.createdAt ? new Date(t.createdAt).toLocaleString() : '—'}</td>
|
|
19
|
+
</tr>
|
|
20
|
+
))}
|
|
21
|
+
{!transactions.length && <tr><td colSpan={7} className="py-8 text-center text-slate-500">No webhook transactions yet.</td></tr>}
|
|
22
|
+
</tbody>
|
|
23
|
+
</table>
|
|
24
|
+
</section>
|
|
25
|
+
);
|
|
26
|
+
}
|