SPB Git forge
1commits 1branches 0releases
189.0 KBsize
maindefault branch
6 h agolast push
Python 54.7% TypeScript 42.5% SQL 1.9%
5.6 KB · 60 lines tsx
Raw Blame History
1"use client";2import { useMutation, useQuery } from "@tanstack/react-query";3import { useSystemStatus } from "@/components/shell";4import { Badge, Button, Card, Empty, ErrorBox, KV, PageHeader, Spinner, Table, Td, Th } from "@/components/ui";5import { api, ApiError } from "@/lib/api";67type Preflight = { checked_at: number; data_key: Record<string, { ok?: boolean; permission_denied?: boolean; error?: string; has_trade_permission?: boolean | null }> & { warning?: string } | null; trade_key: Record<string, unknown> | null };89export default function SettingsPage() {10  const sys = useSystemStatus();11  const balance = useQuery({ queryKey: ["balance"], queryFn: () => api<{ balances: Record<string, string>; fetched_at: number }>("/api/account/balance"), retry: false, enabled: !!sys.data?.credentials.data_key });12  const preflight = useMutation({ mutationFn: () => api<Preflight>("/api/account/preflight", { method: "POST" }) });13  const s = sys.data;14  const pf = (preflight.data ?? s?.preflight) as Preflight | null | undefined;15  return (16    <>17      <PageHeader title="Settings" subtitle="Credentials are injected server-side and never displayed. Only their presence and probed permissions are shown." />18      <div className="grid gap-4 md:grid-cols-2">19        <Card title="Exchange credentials" subtitle="Least privilege: data/account key must not have order permission; the trade key is only read when live mode is armed." actions={<Button size="sm" loading={preflight.isPending} onClick={() => preflight.mutate()} disabled={!s?.credentials.data_key && !s?.credentials.trade_key}>Run preflight</Button>}>20          {sys.isLoading && <Spinner />}21          {s && (22            <KV items={[23              ["Data / account key", s.credentials.data_key ? <Badge key="d" tone="up">configured</Badge> : <Badge key="d" tone="muted">not configured</Badge>],24              ["Trade key", s.credentials.trade_key ? <Badge key="t" tone="warn">configured (live flag off)</Badge> : <Badge key="t" tone="muted">not configured</Badge>],25              ["Withdrawal permission", <span key="w" className="text-fg-muted">{s.credentials.withdrawal}</span>],26              ["Live trading flag", s.live_trading_enabled ? <Badge key="l" tone="down">ENABLED</Badge> : <Badge key="l" tone="up">disabled</Badge>],27            ]} />28          )}29          {preflight.error && <div className="mt-3"><ErrorBox error={preflight.error instanceof ApiError ? preflight.error : String(preflight.error)} /></div>}30          {pf ? (31            <div className="mt-3 text-xs">32              <div className="mb-1 text-fg-dim">Preflight {pf.checked_at ? `at ${new Date(pf.checked_at * 1000).toISOString().slice(0, 19)}Z` : ""}</div>33              {pf.data_key?.warning && <div role="alert" className="mb-2 rounded border border-warn/40 bg-warn/10 px-2 py-1 text-warn">{pf.data_key.warning}</div>}34              {pf.data_key ? (35                <Table><thead><tr><Th>Probe (data key)</Th><Th>Result</Th></tr></thead><tbody>{Object.entries(pf.data_key).filter(([k]) => k !== "warning").map(([k, v]) => <tr key={k}><Td>{k}</Td><Td>{"has_trade_permission" in (v as object) ? ((v as { has_trade_permission?: boolean | null }).has_trade_permission ? <Badge tone="warn">has order permission</Badge> : <Badge tone="up">no order permission</Badge>) : (v as { ok?: boolean }).ok ? <Badge tone="up">ok</Badge> : <span className="text-down">{(v as { error?: string }).error}</span>}</Td></tr>)}</tbody></Table>36              ) : <p className="text-fg-dim">No data key to probe.</p>}37              {pf.trade_key && <pre className="mt-2 overflow-auto rounded bg-bg p-2 text-[11px]">{JSON.stringify(pf.trade_key, null, 1)}</pre>}38            </div>39          ) : <p className="mt-3 text-xs text-fg-dim">No preflight has been run. The trade-permission probe uses AddOrder with validate=true on an order that could never fill; it is recorded in the audit log.</p>}40        </Card>41        <Card title="Account balances" subtitle="Read-only Balance endpoint; non-zero assets only.">42          {!s?.credentials.data_key ? <Empty title="No data key configured">Set KTRADE_KRAKEN_API_KEY / SECRET on the server to enable account reads.</Empty> : balance.isLoading ? <Spinner /> : balance.error ? <ErrorBox error={balance.error} /> : balance.data && Object.keys(balance.data.balances).length ? (43            <Table><thead><tr><Th>Asset</Th><Th align="right">Amount</Th></tr></thead><tbody>{Object.entries(balance.data.balances).map(([k, v]) => <tr key={k}><Td>{k}</Td><Td align="right" num>{v}</Td></tr>)}</tbody></Table>44          ) : <Empty title="No non-zero balances" />}45        </Card>46        <Card title="Environment">47          {s && <KV items={[["Environment", s.env], ["Version", s.version], ["Trading mode", s.mode], ["Universe", s.universe.join(", ")], ["Candle intervals", s.intervals.map((i) => `${i}m`).join(", ")], ["TimescaleDB", s.timescale ? "enabled (hypertables)" : "not installed (plain tables with indexes)"], ["Kraken REST", `${s.kraken.status}${s.kraken.rtt_ms != null ? ` · ${s.kraken.rtt_ms} ms` : ""}`]]} />}48        </Card>49        <Card title="Alerts, access control, retention" subtitle="Planned for Phase 4.">50          <ul className="list-disc space-y-1 pl-5 text-xs text-fg-muted">51            <li>Alert channels (e-mail / webhook) for circuit breakers, staleness and reconciliation mismatches.</li>52            <li>Single-owner sessions today; role-based viewers/operators later.</li>53            <li>Retention: trades 30 days, book samples 14 days (enforced by the worker); candles and audit log kept indefinitely; Postgres backups via the operator runbook.</li>54          </ul>55        </Card>56      </div>57    </>58  );59}60