@bhooai/nexus-cli 2.0.7 → 2.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/base/apps/admin/package.json.ejs +3 -3
- package/templates/base/apps/admin/src/App.tsx +0 -38
- package/templates/base/apps/admin/src/alertCenter.tsx +0 -151
- package/templates/base/apps/admin/src/api.ts +0 -474
- package/templates/base/apps/admin/src/assets/bhooai-nexus-logo.svg +0 -25
- package/templates/base/apps/admin/src/components/ThemeCentre.tsx +0 -201
- package/templates/base/apps/admin/src/components/shell/AppearancePanel.tsx +0 -37
- package/templates/base/apps/admin/src/components/shell/Dashboard.tsx +0 -441
- package/templates/base/apps/admin/src/components/shell/Login.tsx +0 -69
- package/templates/base/apps/admin/src/components/shell/NotifyPanel.tsx +0 -102
- package/templates/base/apps/admin/src/components/shell/RailPanel.tsx +0 -133
- package/templates/base/apps/admin/src/components/shell/SettingsDialog.tsx +0 -217
- package/templates/base/apps/admin/src/components/ui/ConfirmDialog.tsx +0 -38
- package/templates/base/apps/admin/src/components/ui/LintChecks.tsx +0 -54
- package/templates/base/apps/admin/src/components/ui/PageHead.tsx +0 -13
- package/templates/base/apps/admin/src/components/ui/PageHero.tsx +0 -15
- package/templates/base/apps/admin/src/icons.tsx +0 -104
- package/templates/base/apps/admin/src/index.css +0 -3539
- package/templates/base/apps/admin/src/lib/constants.ts +0 -94
- package/templates/base/apps/admin/src/lib/theme.ts +0 -80
- package/templates/base/apps/admin/src/lib/types.ts +0 -39
- package/templates/base/apps/admin/src/lib/utils.ts +0 -147
- package/templates/base/apps/admin/src/pages/Config.tsx +0 -108
- package/templates/base/apps/admin/src/pages/Databases.tsx +0 -122
- package/templates/base/apps/admin/src/pages/Environment.tsx +0 -69
- package/templates/base/apps/admin/src/pages/Logs.tsx +0 -270
- package/templates/base/apps/admin/src/pages/Monitoring.tsx +0 -88
- package/templates/base/apps/admin/src/pages/Overview.tsx +0 -108
- package/templates/base/apps/admin/src/pages/Payments.tsx +0 -68
- package/templates/base/apps/admin/src/pages/Plugins.tsx +0 -36
- package/templates/base/apps/admin/src/pages/Processes.tsx +0 -67
- package/templates/base/apps/admin/src/pages/Schema.tsx +0 -130
- package/templates/base/apps/admin/src/pages/Users.tsx +0 -144
- package/templates/base/apps/admin/src/pages/ai/AddProviderDialog.tsx +0 -150
- package/templates/base/apps/admin/src/pages/ai/AiAgents.tsx +0 -31
- package/templates/base/apps/admin/src/pages/ai/AiChatPlayground.tsx +0 -208
- package/templates/base/apps/admin/src/pages/ai/AiProviders.tsx +0 -276
- package/templates/base/apps/admin/src/pages/ai/AiSettings.tsx +0 -84
- package/templates/base/apps/admin/src/pages/ai/ProviderCard.tsx +0 -145
- package/templates/base/apps/admin/src/pages/ai/ProviderGroup.tsx +0 -50
- package/templates/base/apps/admin/src/pages/ai/ProviderKeyDialog.tsx +0 -66
- package/templates/base/apps/admin/src/pages/monitoring/RequestSeriesChart.tsx +0 -70
- package/templates/base/apps/admin/src/pages/overview/TrafficChart.tsx +0 -32
- package/templates/base/apps/admin/src/pages/payments/OrdersTable.tsx +0 -27
- package/templates/base/apps/admin/src/pages/payments/PaymentKeysDialog.tsx +0 -74
- package/templates/base/apps/admin/src/pages/payments/TestConsole.tsx +0 -173
- package/templates/base/apps/admin/src/pages/payments/TransactionsTable.tsx +0 -26
- package/templates/base/apps/admin/src/style.css +0 -6517
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import type { Tab } from '../lib/types.js';
|
|
3
|
-
import { groupEntries } from '../lib/utils.js';
|
|
4
|
-
import { getAdminEnv, putAdminEnv, runLintEnv, type AdminEnv, type EnvEntry } from '../api.js';
|
|
5
|
-
import { useAdminAlert } from '../alertCenter.js';
|
|
6
|
-
import { Icon } from '../icons.js';
|
|
7
|
-
import { PageHead } from '../components/ui/PageHead.js';
|
|
8
|
-
import { LintChecks } from '../components/ui/LintChecks.js';
|
|
9
|
-
import { ConfirmDialog } from '../components/ui/ConfirmDialog.js';
|
|
10
|
-
|
|
11
|
-
export function Environment({ onNavigate }: { onNavigate?: (tab: Tab) => void }) {
|
|
12
|
-
const [data, setData] = useState<AdminEnv | null>(null);
|
|
13
|
-
const [entries, setEntries] = useState<EnvEntry[]>([]);
|
|
14
|
-
const [msg, setMsg] = useAdminAlert('environment');
|
|
15
|
-
const [busy, setBusy] = useState(false);
|
|
16
|
-
const [restartDialog, setRestartDialog] = useState(false);
|
|
17
|
-
const [newKey, setNewKey] = useState('');
|
|
18
|
-
const [newValue, setNewValue] = useState('');
|
|
19
|
-
const [file, setFile] = useState('.env');
|
|
20
|
-
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
|
21
|
-
const load = async (target: string = file) => { try { const result = await getAdminEnv(target); setData(result); setEntries(result.entries); setMsg(result.exists ? '' : `No ${target} file exists yet. Saving will create it.`); } catch (e: any) { setMsg(String(e?.message ?? e)); } };
|
|
22
|
-
useEffect(() => { load(); }, []);
|
|
23
|
-
const addVariable = () => {
|
|
24
|
-
if (!newKey.trim()) return;
|
|
25
|
-
const secret = /SECRET|PASSWORD|PASS|TOKEN|PRIVATE|API_KEY|CLIENT_SECRET|ACCESS_KEY|KEY_SECRET/i.test(newKey);
|
|
26
|
-
setEntries([...entries, { key: newKey.trim(), value: newValue, secret }]);
|
|
27
|
-
setNewKey(''); setNewValue('');
|
|
28
|
-
};
|
|
29
|
-
const save = async () => { setBusy(true); setMsg(''); try { await putAdminEnv(entries, file); setMsg(`Saved ${file}. Restart services to apply environment changes.`); setRestartDialog(true); await load(); } catch (e: any) { setMsg(`Error: ${e?.message ?? e}`); } finally { setBusy(false); } };
|
|
30
|
-
return (
|
|
31
|
-
<div className="admin-view-stack">
|
|
32
|
-
<PageHead title="Environment"><div className="heading-actions"><span className="secret-badge">SECRETS MASKED</span><button onClick={() => load()} className="workspace-action"><Icon name="refresh" size={12} /> Reload</button></div></PageHead>
|
|
33
|
-
<div className="config-intro env-intro"><div><span className="admin-overline">LOCAL ENVIRONMENT · {data?.path ?? '.env'}</span><h1>Your environment, <em>under control.</em></h1><p>Manage project variables as key/value pairs. Secret values stay on disk and are never returned to the browser.</p></div><div className="env-lock-art"><i className="env-lock-glyph"><Icon name="lock" size={26} /></i><span>PRIVATE</span></div></div>
|
|
34
|
-
<section className="workspace-panel config-panel"><div className="panel-title-row"><div><span className="admin-overline">{file.toUpperCase()} FILE · PRESERVES COMMENTS</span><h3>Environment variables</h3></div><button onClick={addVariable} className="workspace-action">+ Add variable</button></div><div className="env-add-bar">
|
|
35
|
-
<input value={newKey} onChange={(event) => setNewKey(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') addVariable(); }} placeholder="NEXUS_SERVER_PORT" />
|
|
36
|
-
<div className="env-value-wrap"><input value={newValue} onChange={(event) => setNewValue(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') addVariable(); }} placeholder="value" /><span className="secret-mark">{newKey && /SECRET|PASSWORD|PASS|TOKEN|PRIVATE|API_KEY|CLIENT_SECRET|ACCESS_KEY|KEY_SECRET/i.test(newKey) ? 'â—' : 'â—‹'}</span></div>
|
|
37
|
-
</div><div className="key-value-table env-table">{groupEntries(entries).map(({ group, items }) => (
|
|
38
|
-
<div className="key-group" key={group}>
|
|
39
|
-
<div className="key-group-head"><span className="key-group-name">{group}</span><small>{items.length} variable{items.length === 1 ? '' : 's'}</small></div>
|
|
40
|
-
<div className="key-value-head"><span>VARIABLE</span><span>VALUE</span></div>
|
|
41
|
-
{items.map(({ index, entry }) => (
|
|
42
|
-
<div className="key-value-row" key={`${entry.key}-${index}`}>
|
|
43
|
-
<input value={entry.key} onChange={(event) => setEntries(entries.map((item, itemIndex) => itemIndex === index ? { ...item, key: event.target.value, secret: /SECRET|PASSWORD|PASS|TOKEN|PRIVATE|API_KEY|CLIENT_SECRET|ACCESS_KEY|KEY_SECRET/i.test(event.target.value) } : item))} placeholder="NEXUS_SERVER_PORT" />
|
|
44
|
-
<div className="env-value-wrap"><input type={entry.secret ? 'password' : 'text'} value={entry.value ?? ''} placeholder={entry.secret ? '•••••••• · unchanged' : 'value'} onChange={(event) => setEntries(entries.map((item, itemIndex) => itemIndex === index ? { ...item, value: event.target.value } : item))} /><span className={entry.secret ? 'secret-mark is-secret' : 'secret-mark'}>{entry.secret ? 'â—' : 'â—‹'}</span><button className="row-delete" onClick={() => setDeleteTarget(entry.key)} aria-label={`Delete ${entry.key || 'entry'}`} title={`Delete ${entry.key || 'entry'}`}>×</button></div>
|
|
45
|
-
</div>
|
|
46
|
-
))}
|
|
47
|
-
</div>
|
|
48
|
-
))}
|
|
49
|
-
{!entries.length && <div className="key-value-empty">No environment variables found.</div>}</div><div className="config-footer"><span>{entries.length} variable{entries.length === 1 ? '' : 's'} · masked secrets are preserved when unchanged</span><button onClick={save} disabled={busy} className="glass-chip-btn-primary shrink-0">{busy ? 'Saving…' : `Save ${file}`} <span>→</span></button></div></section>
|
|
50
|
-
<LintChecks run={() => runLintEnv(file)} title="Environment checks" overline={`VALIDATOR · ${file}`} />
|
|
51
|
-
<ConfirmDialog
|
|
52
|
-
open={restartDialog}
|
|
53
|
-
title="Restart required"
|
|
54
|
-
message="Your environment changes have been saved. Restart the backend services to apply the new variables."
|
|
55
|
-
confirmLabel="Open services"
|
|
56
|
-
onConfirm={() => { setRestartDialog(false); onNavigate?.('processes'); }}
|
|
57
|
-
onCancel={() => setRestartDialog(false)}
|
|
58
|
-
/>
|
|
59
|
-
<ConfirmDialog
|
|
60
|
-
open={deleteTarget !== null}
|
|
61
|
-
title="Delete variable"
|
|
62
|
-
message={`Delete "${deleteTarget}" from ${file}? This removes it from the file on the next save.`}
|
|
63
|
-
confirmLabel="Delete"
|
|
64
|
-
onConfirm={() => { if (deleteTarget) setEntries(entries.filter((e) => e.key !== deleteTarget)); setDeleteTarget(null); }}
|
|
65
|
-
onCancel={() => setDeleteTarget(null)}
|
|
66
|
-
/>
|
|
67
|
-
</div>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
2
|
-
import type { Tab } from '../lib/types.js';
|
|
3
|
-
import { getAllLogs, getServices, clearLogs, getAiStatus, getAiProviders, getAiModels, aiChatStream, type LogEntry, type AiProviderView, type AiChatMessage } from '../api.js';
|
|
4
|
-
import { Icon } from '../icons.js';
|
|
5
|
-
import { PageHead } from '../components/ui/PageHead.js';
|
|
6
|
-
import { PageHero } from '../components/ui/PageHero.js';
|
|
7
|
-
|
|
8
|
-
/** Color of a service tag in the log viewer (mirrors the supervisor prefixes). */
|
|
9
|
-
const SERVICE_TAG_COLORS: Record<string, string> = {
|
|
10
|
-
backend: 'text-emerald-400 border-emerald-400/30 bg-emerald-400/10',
|
|
11
|
-
frontend: 'text-cyan-300 border-cyan-300/30 bg-cyan-300/10',
|
|
12
|
-
'ai-server': 'text-amber-300 border-amber-300/30 bg-amber-300/10',
|
|
13
|
-
admin: 'text-pink-300 border-pink-300/30 bg-pink-300/10',
|
|
14
|
-
};
|
|
15
|
-
const LEVEL_CLASS: Record<LogEntry['level'], string> = {
|
|
16
|
-
error: 'log-row--error',
|
|
17
|
-
warn: 'log-row--warn',
|
|
18
|
-
info: 'log-row--info',
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export function Logs({ onNavigate }: { onNavigate: (tab: Tab) => void }) {
|
|
22
|
-
const [entries, setEntries] = useState<LogEntry[]>([]);
|
|
23
|
-
const [services, setServices] = useState<string[]>([]);
|
|
24
|
-
const [service, setService] = useState('all');
|
|
25
|
-
const [level, setLevel] = useState<'all' | LogEntry['level']>('all');
|
|
26
|
-
const [q, setQ] = useState('');
|
|
27
|
-
const [live, setLive] = useState(true);
|
|
28
|
-
const [sticky, setSticky] = useState(true);
|
|
29
|
-
const [copied, setCopied] = useState(false);
|
|
30
|
-
const [err, setErr] = useState('');
|
|
31
|
-
const [criticalFirst, setCriticalFirst] = useState(false);
|
|
32
|
-
const [aiSummary, setAiSummary] = useState('');
|
|
33
|
-
const [aiSummaryBusy, setAiSummaryBusy] = useState(false);
|
|
34
|
-
const [aiSummaryErr, setAiSummaryErr] = useState('');
|
|
35
|
-
const [aiSummaryOpen, setAiSummaryOpen] = useState(false);
|
|
36
|
-
const [aiProvider, setAiProvider] = useState('');
|
|
37
|
-
const [aiModel, setAiModel] = useState('');
|
|
38
|
-
const aiAbortRef = useRef<AbortController | null>(null);
|
|
39
|
-
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
40
|
-
|
|
41
|
-
const refresh = useCallback(async () => {
|
|
42
|
-
try {
|
|
43
|
-
const [logs, svc] = await Promise.all([getAllLogs(), getServices()]);
|
|
44
|
-
setEntries(logs);
|
|
45
|
-
setServices(svc.map((s) => s.name));
|
|
46
|
-
setErr('');
|
|
47
|
-
} catch (e: any) {
|
|
48
|
-
setErr(String(e?.message ?? e));
|
|
49
|
-
}
|
|
50
|
-
}, []);
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
void refresh();
|
|
54
|
-
if (!live) return;
|
|
55
|
-
const t = setInterval(() => void refresh(), 1500);
|
|
56
|
-
return () => clearInterval(t);
|
|
57
|
-
}, [refresh, live]);
|
|
58
|
-
|
|
59
|
-
const filtered = useMemo(() => {
|
|
60
|
-
const needle = q.trim().toLowerCase();
|
|
61
|
-
return entries.filter((e) => {
|
|
62
|
-
if (service !== 'all' && e.service !== service) return false;
|
|
63
|
-
if (level !== 'all' && e.level !== level) return false;
|
|
64
|
-
if (needle && !e.line.toLowerCase().includes(needle)) return false;
|
|
65
|
-
return true;
|
|
66
|
-
});
|
|
67
|
-
}, [entries, service, level, q]);
|
|
68
|
-
|
|
69
|
-
const counts = useMemo(() => {
|
|
70
|
-
const c = { error: 0, warn: 0, info: 0 };
|
|
71
|
-
for (const e of entries) c[e.level] += 1;
|
|
72
|
-
return c;
|
|
73
|
-
}, [entries]);
|
|
74
|
-
|
|
75
|
-
const ordered = useMemo(() => {
|
|
76
|
-
if (!criticalFirst) return filtered;
|
|
77
|
-
const rank = (e: LogEntry) => {
|
|
78
|
-
const lvl = e.level === 'error' ? 0 : e.level === 'warn' ? 1 : 2;
|
|
79
|
-
const src = e.source === 'stderr' ? 0 : e.source === 'system' ? 1 : 2;
|
|
80
|
-
return lvl * 3 + src;
|
|
81
|
-
};
|
|
82
|
-
return [...filtered].sort((a, b) => rank(a) - rank(b) || b.ts - a.ts);
|
|
83
|
-
}, [filtered, criticalFirst]);
|
|
84
|
-
|
|
85
|
-
const criticalRunEnd = useMemo(() => {
|
|
86
|
-
if (!criticalFirst) return 0;
|
|
87
|
-
let i = 0;
|
|
88
|
-
while (i < ordered.length && ordered[i].level !== 'info') i += 1;
|
|
89
|
-
return i;
|
|
90
|
-
}, [ordered, criticalFirst]);
|
|
91
|
-
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
const el = scrollRef.current;
|
|
94
|
-
if (el && sticky) el.scrollTop = criticalFirst ? 0 : el.scrollHeight;
|
|
95
|
-
}, [ordered, sticky, criticalFirst]);
|
|
96
|
-
|
|
97
|
-
const onScroll = () => {
|
|
98
|
-
const el = scrollRef.current;
|
|
99
|
-
if (!el) return;
|
|
100
|
-
setSticky(criticalFirst ? el.scrollTop < 80 : el.scrollHeight - el.scrollTop - el.clientHeight < 80);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const copyAll = async () => {
|
|
104
|
-
const text = filtered.map((e) => `[${e.service}] ${e.line}`).join('\n');
|
|
105
|
-
try { await navigator.clipboard.writeText(text); } catch { /* ignore */ }
|
|
106
|
-
setCopied(true);
|
|
107
|
-
window.setTimeout(() => setCopied(false), 1200);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const runAiSummary = async () => {
|
|
111
|
-
if (aiSummaryBusy) return;
|
|
112
|
-
if (!ordered.length) {
|
|
113
|
-
setAiSummary('');
|
|
114
|
-
setAiSummaryErr('No log lines to summarize — widen the filters first.');
|
|
115
|
-
setAiSummaryOpen(true);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
try {
|
|
119
|
-
let provider = aiProvider;
|
|
120
|
-
let model = aiModel;
|
|
121
|
-
if (!provider || !model) {
|
|
122
|
-
const s = await getAiStatus();
|
|
123
|
-
let pv: AiProviderView | undefined;
|
|
124
|
-
try { pv = (await getAiProviders()).providers?.find((p) => p.enabled); } catch { /* fall back to status */ }
|
|
125
|
-
if (pv) {
|
|
126
|
-
provider = pv.id;
|
|
127
|
-
model = pv.defaultModel ?? '';
|
|
128
|
-
}
|
|
129
|
-
if (!provider) provider = s.autoResolvesTo ?? (s.ollama?.ok ? 'ollama' : s.openai?.ok ? 'openai' : '');
|
|
130
|
-
if (!provider) throw new Error('No AI provider is available. Configure one in the AI Agents tab.');
|
|
131
|
-
if (!model) {
|
|
132
|
-
try { model = (await getAiModels(provider)).data?.[0]?.id ?? ''; } catch { /* fall back below */ }
|
|
133
|
-
}
|
|
134
|
-
if (!model) model = 'llama3:latest';
|
|
135
|
-
setAiProvider(provider);
|
|
136
|
-
setAiModel(model);
|
|
137
|
-
}
|
|
138
|
-
const sample = ordered.slice(-400).map((e) => `[${new Date(e.ts).toLocaleTimeString()}] ${e.service} ${e.source} ${e.level}: ${e.line}`).join('\n');
|
|
139
|
-
const messages: AiChatMessage[] = [
|
|
140
|
-
{ role: 'system', content: 'You are a log analyzer for BhooAI Nexus. Summarize the provided service logs concisely. FIRST list the most critical errors (service, source, likely cause and next step), then warnings, then notable patterns. Use short bullet points and do not invent details that are not present in the logs.' },
|
|
141
|
-
{ role: 'user', content: `Service logs (${sample.split('\n').length} lines):\n${sample}` },
|
|
142
|
-
];
|
|
143
|
-
const controller = new AbortController();
|
|
144
|
-
aiAbortRef.current = controller;
|
|
145
|
-
setAiSummary('');
|
|
146
|
-
setAiSummaryErr('');
|
|
147
|
-
setAiSummaryOpen(true);
|
|
148
|
-
setAiSummaryBusy(true);
|
|
149
|
-
let acc = '';
|
|
150
|
-
try {
|
|
151
|
-
await aiChatStream(model, messages, provider, (chunk) => { acc += chunk; setAiSummary(acc); }, controller.signal);
|
|
152
|
-
setAiSummary(acc || 'AI returned an empty summary.');
|
|
153
|
-
} catch (e: any) {
|
|
154
|
-
if (e?.name === 'AbortError') return;
|
|
155
|
-
setAiSummaryErr(String(e?.message ?? e));
|
|
156
|
-
} finally {
|
|
157
|
-
aiAbortRef.current = null;
|
|
158
|
-
setAiSummaryBusy(false);
|
|
159
|
-
}
|
|
160
|
-
} catch (e: any) {
|
|
161
|
-
setAiSummary('');
|
|
162
|
-
setAiSummaryErr(String(e?.message ?? e));
|
|
163
|
-
setAiSummaryOpen(true);
|
|
164
|
-
setAiSummaryBusy(false);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
const stopAiSummary = () => { aiAbortRef.current?.abort(); };
|
|
169
|
-
|
|
170
|
-
const copyAiSummary = async () => {
|
|
171
|
-
try { await navigator.clipboard.writeText(aiSummary); } catch { /* ignore */ }
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
return (
|
|
175
|
-
<div className="space-y-6">
|
|
176
|
-
<PageHead title="Logs">
|
|
177
|
-
<div className="flex items-center gap-2">
|
|
178
|
-
<button aria-label="Refresh logs" onClick={() => void refresh()} className="glass-chip-btn"><Icon name="refresh" size={12} /> refresh</button>
|
|
179
|
-
<button aria-pressed={live} onClick={() => setLive(!live)} className={`glass-chip-btn log-live-btn${live ? ' is-live' : ''}`}>{live ? 'â— live' : 'â—‹ paused'}</button>
|
|
180
|
-
<button aria-label="Copy filtered logs" onClick={() => void copyAll()} className="glass-chip-btn">{copied ? 'Copied' : <><Icon name="copy" size={12} /> copy</>}</button>
|
|
181
|
-
<button aria-label="Summarize logs with AI" onClick={() => void runAiSummary()} disabled={aiSummaryBusy} className="glass-chip-btn log-ai-btn"><Icon name="sparkles" size={12} /> {aiSummaryBusy ? 'summarizing…' : 'ai summary'}</button>
|
|
182
|
-
<button aria-label="Clear all logs" onClick={async () => { try { await clearLogs(); await refresh(); } catch { /* supervisor may not be reachable */ } }} className="glass-chip-btn-danger"><Icon name="trash" size={12} /> clear</button>
|
|
183
|
-
</div>
|
|
184
|
-
</PageHead>
|
|
185
|
-
<PageHero kicker="ERROR CONSOLE" title={<>Every failure, <em>in one stream.</em></>} desc="Aggregated stdout, stderr and supervisor events from every service — live as the supervisor records them, filterable and searchable." glyph="terminal" art="wave" />
|
|
186
|
-
{err && (
|
|
187
|
-
<div role="alert" className="rounded-lg border border-rose-500/30 bg-rose-500/10 px-3 py-2 text-sm text-rose-400">
|
|
188
|
-
Logs unavailable — {err}. Is <code className="font-mono">nexus dev</code> running?
|
|
189
|
-
</div>
|
|
190
|
-
)}
|
|
191
|
-
<div className="glass-card log-card">
|
|
192
|
-
<div className="log-stat-strip">
|
|
193
|
-
<span className="log-stat log-stat--all"><i /><b>{entries.length}</b><em>Total lines</em></span>
|
|
194
|
-
<span className="log-stat log-stat--error"><i /><b>{counts.error}</b><em>Errors</em></span>
|
|
195
|
-
<span className="log-stat log-stat--warn"><i /><b>{counts.warn}</b><em>Warnings</em></span>
|
|
196
|
-
<span className="log-stat log-stat--info"><i /><b>{counts.info}</b><em>Info</em></span>
|
|
197
|
-
</div>
|
|
198
|
-
<div className="log-toolbar">
|
|
199
|
-
<div className="glass-panel log-filter-group" role="group" aria-label="Filter by level">
|
|
200
|
-
{([['all', 'All'], ['error', 'Errors'], ['warn', 'Warnings'], ['info', 'Info']] as const).map(([id, label]) => (
|
|
201
|
-
<button key={id} aria-pressed={level === id} onClick={() => { setLevel(id); setSticky(true); }} className={level === id ? 'log-filter-btn is-active' : 'log-filter-btn'}>
|
|
202
|
-
{label} <b>{id === 'all' ? entries.length : counts[id]}</b>
|
|
203
|
-
</button>
|
|
204
|
-
))}
|
|
205
|
-
</div>
|
|
206
|
-
<button aria-pressed={criticalFirst} onClick={() => { setCriticalFirst(!criticalFirst); setSticky(true); }} className={criticalFirst ? 'log-crit-btn is-active' : 'log-crit-btn'}>⤓ critical first</button>
|
|
207
|
-
<div className="log-toolbar-right">
|
|
208
|
-
<select value={service} onChange={(e) => { setService(e.target.value); setSticky(true); }} aria-label="Filter by service" className="glass-input log-service-input">
|
|
209
|
-
<option value="all" className="bg-slate-900">All services</option>
|
|
210
|
-
{services.map((s) => <option key={s} value={s} className="bg-slate-900">{s}</option>)}
|
|
211
|
-
</select>
|
|
212
|
-
<input value={q} onChange={(e) => { setQ(e.target.value); setSticky(true); }} placeholder="Filter lines…" aria-label="Filter log lines" className="glass-input log-search-input" />
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
{aiSummaryOpen && (
|
|
216
|
-
<div className="log-ai-panel">
|
|
217
|
-
<div className="log-ai-head">
|
|
218
|
-
<span className="log-ai-title"><Icon name="sparkles" size={12} /> AI summary{aiModel ? ` · ${aiModel}` : ''}</span>
|
|
219
|
-
<div className="log-ai-actions">
|
|
220
|
-
{aiSummaryBusy && <button onClick={stopAiSummary} className="glass-chip-btn-danger"><Icon name="square" size={10} /> stop</button>}
|
|
221
|
-
{!aiSummaryBusy && aiSummary && <button onClick={() => void copyAiSummary()} className="glass-chip-btn"><Icon name="copy" size={12} /> copy</button>}
|
|
222
|
-
<button onClick={() => { setAiSummaryOpen(false); setAiSummary(''); setAiSummaryErr(''); }} className="glass-chip-btn"><Icon name="x" size={12} /> dismiss</button>
|
|
223
|
-
</div>
|
|
224
|
-
</div>
|
|
225
|
-
{aiSummaryErr && (
|
|
226
|
-
<div role="alert" className="log-ai-err">
|
|
227
|
-
AI summary failed: {aiSummaryErr}
|
|
228
|
-
{aiSummaryErr.includes('AI provider') && (
|
|
229
|
-
<button onClick={() => onNavigate('ai')} className="glass-chip-btn log-ai-open">Open AI Agents →</button>
|
|
230
|
-
)}
|
|
231
|
-
</div>
|
|
232
|
-
)}
|
|
233
|
-
{aiSummaryBusy && !aiSummary && <div className="log-ai-note">Analyzing {ordered.length} lines…</div>}
|
|
234
|
-
{aiSummary && <pre className="log-ai-body">{aiSummary}</pre>}
|
|
235
|
-
</div>
|
|
236
|
-
)}
|
|
237
|
-
<div ref={scrollRef} onScroll={onScroll} className="log-viewer" role="list" aria-label="Service log output" tabIndex={0}>
|
|
238
|
-
<div className="log-colhead" aria-hidden="true">
|
|
239
|
-
<span className="log-time">Time</span>
|
|
240
|
-
<span className="log-service">Service</span>
|
|
241
|
-
<span className="log-source">Source</span>
|
|
242
|
-
<span className="log-level">Level</span>
|
|
243
|
-
<span className="log-line">Message</span>
|
|
244
|
-
</div>
|
|
245
|
-
{ordered.length === 0 ? (
|
|
246
|
-
<div className="log-empty">
|
|
247
|
-
<span className="log-empty-glyph"><Icon name="terminal" size={26} /></span>
|
|
248
|
-
<strong>No log lines match the current filters.</strong>
|
|
249
|
-
<p>Try widening the level filter or clearing the search text.</p>
|
|
250
|
-
</div>
|
|
251
|
-
) : (
|
|
252
|
-
ordered.map((e, i) => (
|
|
253
|
-
<div key={`${e.ts}-${i}`} className={`log-row ${LEVEL_CLASS[e.level]}${criticalFirst && i < criticalRunEnd ? ' log-row--critical' : ''}`} role="listitem">
|
|
254
|
-
<span className="log-time">{new Date(e.ts).toLocaleTimeString()}</span>
|
|
255
|
-
<span className={`log-service log-tag ${SERVICE_TAG_COLORS[e.service] ?? 'log-tag--default'}`}>{e.service}</span>
|
|
256
|
-
<span className={`log-source log-source--${e.source}`}>{e.source}</span>
|
|
257
|
-
<span className={`log-level log-level--${e.level}`}>{e.level}</span>
|
|
258
|
-
<span className="log-line">{e.line || ' '}</span>
|
|
259
|
-
</div>
|
|
260
|
-
))
|
|
261
|
-
)}
|
|
262
|
-
</div>
|
|
263
|
-
<div className="log-footer">
|
|
264
|
-
<span className="log-foot-left">Aggregated across services · last {entries.length} lines · {live ? <span className="log-foot-left"><span className="log-live-dot" /> polling every 1.5s</span> : 'paused'}</span>
|
|
265
|
-
{!sticky && <button onClick={() => setSticky(true)} className="glass-chip-btn">{criticalFirst ? 'â–² jump to top' : 'â–¼ jump to latest'}</button>}
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
|
-
</div>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
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
|
-
}
|