Ship full operator dashboard frontend
All checks were successful
deploy / deploy (push) Successful in 1m5s

Proof: Include the full operator-dashboard frontend source tree so the deployment image can build and serve the dashboard UI that consumes runtime health severity.

Assumptions: The current static dashboard source tree in the worktree is the intended frontend for this turn and belongs with the deployed operator-dashboard backend.

Still fake: External alert receiver remains unconfigured; this commit only resolves the dashboard asset source gap in the deploy.
This commit is contained in:
philipp 2026-04-08 19:41:43 +02:00
parent 2811a84deb
commit 903287ec21
15 changed files with 1625 additions and 0 deletions

View file

@ -0,0 +1,27 @@
import EmptyState from './EmptyState.jsx';
import Pill from './Pill.jsx';
import { formatTimestamp } from '../lib/format.js';
export default function AlertsGrid({ items, emptyMessage = 'No alerts are active.' }) {
if (!items?.length) {
return <EmptyState>{emptyMessage}</EmptyState>;
}
return (
<div className="service-grid">
{items.map((item, index) => (
<div className="service-card" key={`${item.alert_code}:${item.raised_at || item.cleared_at || index}`}>
<div className="service-head">
<strong>{item.alert_code}</strong>
<Pill label={item.severity} stateLabel={item.severity} />
</div>
<div className="service-detail">
<div>{item.reason}</div>
<div>{`Scope ${item.service_scope}`}</div>
<div>{formatTimestamp(item.raised_at || item.cleared_at || item.last_evaluated_at)}</div>
</div>
</div>
))}
</div>
);
}

View file

@ -0,0 +1,3 @@
export default function EmptyState({ children }) {
return <div className="empty">{children}</div>;
}

View file

@ -0,0 +1,25 @@
import { signedClass } from '../lib/format.js';
export default function MetricCard({
label,
value,
meta = '',
signedValue = null,
hero = false,
valueClassName = '',
valueTitle = '',
}) {
const classes = ['metric-card', hero ? 'hero' : ''].filter(Boolean).join(' ');
return (
<div className={classes}>
<div className="eyebrow">{label}</div>
<div
className={['metric-value', signedClass(signedValue ?? value), valueClassName].filter(Boolean).join(' ')}
title={valueTitle || undefined}
>
{value || 'Unavailable'}
</div>
<div className="metric-meta">{meta || ''}</div>
</div>
);
}

View file

@ -0,0 +1,38 @@
const NAV_ITEMS = [
{
page: 'funds',
title: 'Funds',
description: 'Profitability, balances, funding, quotes, and trades.',
},
{
page: 'strategy',
title: 'Strategy',
description: 'Trading state, decision flow, and guarded omissions.',
},
{
page: 'system',
title: 'System',
description: 'Service health, alerts, persistence, and safe controls.',
},
];
export default function NavRail({ activePage, onPageChange }) {
return (
<aside className="side-nav">
<div className="nav-title">Operator desk</div>
<div className="nav-stack">
{NAV_ITEMS.map((item) => (
<button
key={item.page}
className={`nav-button ${activePage === item.page ? 'active' : ''}`}
onClick={() => onPageChange(item.page)}
type="button"
>
<div>{item.title}</div>
<div className="status-subtle">{item.description}</div>
</button>
))}
</div>
</aside>
);
}

View file

@ -0,0 +1,23 @@
export default function Pill({ label, stateLabel = label }) {
const normalized = String(stateLabel || '').toLowerCase();
let className = 'pill';
if (
normalized.includes('critical')
|| normalized.includes('failed')
|| normalized.includes('offline')
|| normalized.includes('frozen')
) {
className += ' severity-critical';
} else if (
normalized.includes('warning')
|| normalized.includes('degraded')
|| normalized.includes('paused')
) {
className += ' severity-warning';
} else {
className += ' severity-info';
}
return <span className={className}>{label || ''}</span>;
}

View file

@ -0,0 +1,52 @@
import { formatAge, formatBoolean, formatEur, formatTimestamp, signedClass, truncateMiddle } from '../lib/format.js';
function statusSubtitle(label, status, websocketState) {
switch (label) {
case 'Pair':
return `WebSocket ${websocketState}`;
case 'Market Freshness':
return formatTimestamp(status.market_observed_at);
case 'Inventory Freshness':
return formatTimestamp(status.inventory_observed_at);
case 'Trading':
return 'Successful submissions from durable history';
default:
return '';
}
}
export default function StatusBar({ status, websocketState }) {
const tiles = [
['Pair', truncateMiddle(status.active_pair, 40), status.active_pair],
['Portfolio', formatEur(status.current_total_portfolio_value_eure)],
['Reference BTC/EUR', formatEur(status.latest_reference_price_eure_per_btc)],
['Market Freshness', formatAge(status.market_freshness_ms)],
['Inventory Freshness', formatAge(status.inventory_freshness_ms)],
['Alerts', `${status.active_alert_count || 0} ${status.highest_alert_severity ? `(${status.highest_alert_severity})` : ''}`.trim()],
['Strategy Armed', formatBoolean(status.strategy_armed)],
['Executor Armed', formatBoolean(status.executor_armed)],
['Trading', `${status.recent_trade_count || 0} trades`],
['Last Trade', formatTimestamp(status.last_successful_trade_at)],
];
return (
<section className="status-bar">
{tiles.map(([label, value, fullValue = value]) => (
<div className="status-tile" key={label}>
<div className="status-label">{label}</div>
<div
className={[
'status-value',
signedClass(value),
label === 'Pair' ? 'truncate-line mono' : '',
].filter(Boolean).join(' ')}
title={fullValue || 'Unavailable'}
>
{value || 'Unavailable'}
</div>
<div className="status-subtle">{statusSubtitle(label, status, websocketState)}</div>
</div>
))}
</section>
);
}

View file

@ -0,0 +1,8 @@
export default function TableFrame({ children, className = '', style = undefined }) {
const classes = ['table-wrap', className].filter(Boolean).join(' ');
return (
<div className={classes} style={style}>
{children}
</div>
);
}

View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>unrip operator dashboard</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/main.jsx"></script>
</body>
</html>

View file

@ -0,0 +1,11 @@
export async function fetchJson(url, options = {}) {
const response = await fetch(url, options);
const text = await response.text();
const data = text ? JSON.parse(text) : null;
if (!response.ok) {
throw new Error(data?.error || `HTTP ${response.status}`);
}
return data;
}

View file

@ -0,0 +1,51 @@
export function formatBoolean(value) {
if (value == null) return 'Unknown';
return value ? 'Yes' : 'No';
}
export function formatTimestamp(value) {
if (!value) return 'Unavailable';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return 'Unavailable';
return date.toLocaleString();
}
export function formatAge(value) {
if (value == null) return 'Unavailable';
if (value < 1000) return `${value} ms`;
if (value < 60_000) return `${(value / 1000).toFixed(1)} s`;
if (value < 3_600_000) return `${(value / 60_000).toFixed(1)} min`;
return `${(value / 3_600_000).toFixed(1)} h`;
}
export function formatEur(value) {
if (value == null || value === '') return 'Unavailable';
const numeric = Number(value);
if (!Number.isFinite(numeric)) return String(value);
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'EUR',
maximumFractionDigits: 2,
}).format(numeric);
}
export function signedClass(value) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return '';
if (numeric > 0) return 'value-positive';
if (numeric < 0) return 'value-negative';
return '';
}
export function truncateMiddle(value, maxLength = 40) {
const text = String(value || '');
if (!text || text.length <= maxLength) return text;
const visible = Math.max(8, maxLength - 1);
const startLength = Math.ceil(visible / 2);
const endLength = Math.floor(visible / 2);
return `${text.slice(0, startLength)}${text.slice(-endLength)}`;
}
export function stringifyJson(value) {
return JSON.stringify(value, null, 2);
}

View file

@ -0,0 +1,6 @@
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './styles.css';
createRoot(document.getElementById('app')).render(<App />);

View file

@ -0,0 +1,612 @@
import { useEffect, useState } from 'react';
import EmptyState from '../components/EmptyState.jsx';
import MetricCard from '../components/MetricCard.jsx';
import Pill from '../components/Pill.jsx';
import TableFrame from '../components/TableFrame.jsx';
import { formatEur, formatTimestamp, stringifyJson, truncateMiddle } from '../lib/format.js';
function buildInitialEstimateForm(balances, withdrawalDefaults) {
const firstAssetId = balances?.[0]?.asset_id || '';
return {
asset_id: firstAssetId,
amount: '',
destination_address:
withdrawalDefaults?.[firstAssetId]
|| Object.values(withdrawalDefaults || {})[0]
|| '',
chain: '',
};
}
function BalancesTable({ items }) {
if (!items?.length) return <EmptyState>No inventory snapshot is available yet.</EmptyState>;
return (
<TableFrame>
<table>
<thead>
<tr>
<th>Asset</th>
<th>Spendable</th>
<th>Pending inbound</th>
<th>Pending outbound</th>
<th>EUR value</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.asset_id}>
<td>
<strong>{item.symbol}</strong>
<div className="muted mono">{item.asset_id}</div>
</td>
<td className="mono">{item.spendable}</td>
<td className="mono">{item.pending_inbound}</td>
<td className="mono">{item.pending_outbound}</td>
<td className="mono">{formatEur(item.eur_value_eure)}</td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function HandlesList({ handles }) {
if (!handles?.length) return <EmptyState>No funding handles are available.</EmptyState>;
return (
<div className="service-grid">
{handles.map((handle) => (
<div className="service-card" key={`${handle.chain}:${handle.address || handle.asset_id}`}>
<div className="service-head">
<strong>{handle.symbol}</strong>
<Pill label={handle.chain} stateLabel="healthy" />
</div>
<div className="service-detail">
<div className="mono">{handle.address || 'Unavailable'}</div>
{handle.memo ? <div className="mono">{`Memo ${handle.memo}`}</div> : null}
<div>{`Refreshed ${formatTimestamp(handle.refreshed_at)}`}</div>
</div>
</div>
))}
</div>
);
}
function FundingTable({ items, creditedOnly = false }) {
if (!items?.length) {
return (
<EmptyState>
{creditedOnly ? 'No credited deposits have been recorded yet.' : 'No funding observations are available.'}
</EmptyState>
);
}
return (
<TableFrame>
<table>
<thead>
<tr>
<th>Status</th>
<th>Asset</th>
<th>Amount</th>
<th>Handle</th>
<th>Observed</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.funding_observation_id || `${item.tx_hash}:${item.status}`}>
<td><Pill label={item.status} stateLabel={item.status} /></td>
<td>{item.asset_symbol || item.asset_id}</td>
<td className="mono">{item.amount_display || item.amount || 'Unavailable'}</td>
<td className="mono truncate-cell" title={item.funding_handle || ''}>{truncateMiddle(item.funding_handle || '', 36)}</td>
<td>{formatTimestamp(item.credited_at || item.last_seen_at || item.first_seen_at)}</td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function PreCreditAssetTable({ items }) {
if (!items?.length) return <EmptyState>No pre-credit funding is currently tracked.</EmptyState>;
return (
<TableFrame>
<table>
<thead>
<tr>
<th>Asset</th>
<th>Pending amount</th>
<th>Observation count</th>
<th>Last seen</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.asset_id}>
<td>{item.asset_symbol || item.asset_id}</td>
<td className="mono">{item.amount_display || item.amount || 'Unavailable'}</td>
<td>{item.observation_count || 0}</td>
<td>{formatTimestamp(item.last_seen_at)}</td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function WithdrawalsTable({ items }) {
if (!items?.length) return <EmptyState>No withdrawals are tracked yet.</EmptyState>;
return (
<TableFrame>
<table>
<thead>
<tr>
<th>Status</th>
<th>Asset</th>
<th>Amount</th>
<th>Destination</th>
<th>Observed</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.withdrawal_id || `${item.asset_id}:${item.requested_at}`}>
<td><Pill label={item.status} stateLabel={item.status} /></td>
<td>{item.asset_symbol || item.asset_id}</td>
<td className="mono">{item.amount_display || item.amount || 'Unavailable'}</td>
<td className="mono truncate-cell" title={item.destination_address || ''}>{truncateMiddle(item.destination_address || '', 36)}</td>
<td>{formatTimestamp(item.completed_at || item.requested_at)}</td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function QuotesTable({ items }) {
if (!items?.length) return <EmptyState>No quotes have been captured yet.</EmptyState>;
return (
<TableFrame>
<table className="pair-compact-table">
<thead>
<tr>
<th>Observed</th>
<th>Pair</th>
<th>Kind</th>
<th>Amount in</th>
<th>Amount out</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={`${item.quote_id}:${item.ingested_at || item.observed_at}`}>
<td>{formatTimestamp(item.observed_at || item.ingested_at)}</td>
<td className="mono truncate-cell" title={item.pair || ''}>{truncateMiddle(item.pair || '', 38)}</td>
<td>{item.request_kind || ''}</td>
<td className="mono">{item.amount_in || ''}</td>
<td className="mono">{item.amount_out || ''}</td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function AssetChangeTable({ items }) {
if (!items?.length) return <EmptyState>No successful trades are available yet.</EmptyState>;
return (
<TableFrame>
<table>
<thead>
<tr>
<th>Observed</th>
<th>Quote</th>
<th>Spend</th>
<th>Receive</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={`${item.quote_id}:${item.observed_at}`}>
<td>{formatTimestamp(item.observed_at)}</td>
<td className="mono">{item.quote_id || ''}</td>
<td>{`${item.amount_in} ${item.asset_in_symbol}`}</td>
<td>{`${item.amount_out} ${item.asset_out_symbol}`}</td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function TradesTable({ items }) {
if (!items?.length) return <EmptyState>No successful trades are stored yet.</EmptyState>;
return (
<TableFrame>
<table className="pair-compact-table">
<thead>
<tr>
<th>Observed</th>
<th>Pair</th>
<th>Kind</th>
<th>Submitted</th>
<th>Edge</th>
<th>Result</th>
</tr>
</thead>
<tbody>
{items.map((trade) => (
<tr key={`${trade.command_id || trade.quote_id}:${trade.observed_at}`}>
<td>{formatTimestamp(trade.observed_at)}</td>
<td className="mono truncate-cell" title={trade.pair || ''}>{truncateMiddle(trade.pair || '', 38)}</td>
<td>{trade.request_kind || ''}</td>
<td>{`${trade.amount_in_display || ''} ${trade.asset_in_symbol || ''} -> ${trade.amount_out_display || ''} ${trade.asset_out_symbol || ''}`}</td>
<td className={Number(trade.gross_edge_pct) > 0 ? 'value-positive' : Number(trade.gross_edge_pct) < 0 ? 'value-negative' : ''}>{trade.gross_edge_pct || ''}</td>
<td><Pill label={trade.result_code || trade.status} stateLabel={trade.result_code || trade.status} /></td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function WithdrawalEstimateForm({ balances, withdrawalDefaults, onControl }) {
const [form, setForm] = useState(() => buildInitialEstimateForm(balances, withdrawalDefaults));
useEffect(() => {
setForm(buildInitialEstimateForm(balances, withdrawalDefaults));
}, [balances, withdrawalDefaults]);
async function handleSubmit(event) {
event.preventDefault();
const body = { ...form };
if (!body.chain) delete body.chain;
await onControl('liquidity-manager', 'withdrawal-estimate', body, { reload: false });
}
return (
<form onSubmit={handleSubmit}>
<div className="form-grid">
<div className="field">
<label htmlFor="estimate-asset">Asset</label>
<select
id="estimate-asset"
name="asset_id"
onChange={(event) => {
const assetId = event.target.value;
setForm((current) => ({
...current,
asset_id: assetId,
destination_address:
withdrawalDefaults?.[assetId]
|| current.destination_address
|| '',
}));
}}
value={form.asset_id}
>
{(balances || []).map((item) => (
<option key={item.asset_id} value={item.asset_id}>
{item.symbol}
</option>
))}
</select>
</div>
<div className="field">
<label htmlFor="estimate-amount">Amount units</label>
<input
id="estimate-amount"
name="amount"
onChange={(event) => setForm((current) => ({ ...current, amount: event.target.value }))}
placeholder="1000000"
required
value={form.amount}
/>
</div>
<div className="field">
<label htmlFor="estimate-destination">Destination</label>
<input
id="estimate-destination"
name="destination_address"
onChange={(event) => setForm((current) => ({ ...current, destination_address: event.target.value }))}
value={form.destination_address}
/>
</div>
<div className="field">
<label htmlFor="estimate-chain">Chain override</label>
<input
id="estimate-chain"
name="chain"
onChange={(event) => setForm((current) => ({ ...current, chain: event.target.value }))}
placeholder="Optional"
value={form.chain}
/>
</div>
</div>
<div className="button-row">
<button className="button" type="submit">Estimate Withdrawal</button>
</div>
</form>
);
}
function LastControlResult({ result }) {
if (!result) return null;
return (
<TableFrame style={{ marginTop: 14 }}>
<table>
<tbody>
<tr>
<td><strong>Last control result</strong></td>
<td className="mono"><pre>{stringifyJson(result)}</pre></td>
</tr>
</tbody>
</table>
</TableFrame>
);
}
export default function FundsPage({
funds,
onTradesPageChange,
onControl,
lastControlResult,
}) {
const profitability = funds.profitability;
const trades = funds.successful_trades;
const controlState = funds.funding.control_state || {};
const externalFlowAdjusted = profitability.external_flow_adjusted;
const externalFlowCount = profitability.external_flow_count || 0;
const baselineLabel = externalFlowAdjusted ? 'PnL vs net funded capital' : 'PnL vs deposit baseline';
const baselineMeta = externalFlowAdjusted
? `Adjusted for ${externalFlowCount} later deposit or withdrawal flows`
: 'Baseline anchored before first live trade';
const simpleHoldMeta = externalFlowAdjusted
? 'Simple hold includes later credited deposits and completed withdrawals'
: 'Trading contribution over simple hold';
const marketMoveMeta = externalFlowAdjusted
? 'Simple-hold market move on baseline plus later external flows'
: 'Baseline mark move only';
const tradingContributionMeta = externalFlowAdjusted
? 'Current minus cash-flow-adjusted simple hold'
: 'Current minus simple hold';
return (
<>
<section className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Default view</div>
<h2>Funds</h2>
<div className="panel-subtitle">
Profitability comes first. Spendable inventory remains verifier or bridge credit only,
while pre-credit funding stays separate.
</div>
</div>
<div className="pills">
<Pill
label={`Withdrawals ${controlState.withdrawals_frozen ? 'Frozen' : 'Unfrozen'}`}
stateLabel={controlState.withdrawals_frozen ? 'frozen' : 'healthy'}
/>
<Pill
label={`Funding observer ${controlState.funding_observer_paused ? 'Paused' : 'Running'}`}
stateLabel={controlState.funding_observer_paused ? 'paused' : 'healthy'}
/>
</div>
</div>
<div className="stack-grid">
<MetricCard
hero
label="Current EUR value"
meta={
profitability.computed_at
? `Latest durable snapshot ${formatTimestamp(profitability.computed_at)}`
: 'Latest durable profitability snapshot'
}
value={formatEur(profitability.current_total_portfolio_value_eure)}
/>
<MetricCard
label={baselineLabel}
meta={baselineMeta}
signedValue={profitability.pnl_vs_deposit_baseline_eure}
value={formatEur(profitability.pnl_vs_deposit_baseline_eure)}
/>
<MetricCard
label="PnL vs simple hold"
meta={simpleHoldMeta}
signedValue={profitability.pnl_vs_simple_hold_eure}
value={formatEur(profitability.pnl_vs_simple_hold_eure)}
/>
<MetricCard
label="Market move"
meta={marketMoveMeta}
signedValue={profitability.market_move_contribution_eure}
value={formatEur(profitability.market_move_contribution_eure)}
/>
<MetricCard
label="Trading contribution"
meta={tradingContributionMeta}
signedValue={profitability.trading_contribution_eure}
value={formatEur(profitability.trading_contribution_eure)}
/>
<MetricCard
label="Recent trading"
meta={formatTimestamp(profitability.last_successful_trade_at)}
value={`${profitability.recent_trade_count || 0} trades`}
/>
</div>
<div className="panel-subtitle">
{profitability.caveats.map((item) => (
<div key={item}>{item}</div>
))}
</div>
</section>
<section className="stack-grid">
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Current balances</div>
<h3>Spendable and pending</h3>
</div>
<div className="status-subtle">{`Inventory synced ${formatTimestamp(funds.balances.synced_at)}`}</div>
</div>
<BalancesTable items={funds.balances.items} />
</div>
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Operator controls</div>
<h3>Safe actions</h3>
</div>
</div>
<div className="button-row">
<button className="button secondary" onClick={() => onControl('market-reference-ingest', 'refresh')} type="button">Refresh Price</button>
<button className="button secondary" onClick={() => onControl('inventory-sync', 'refresh')} type="button">Refresh Inventory</button>
<button className="button secondary" onClick={() => onControl('liquidity-manager', 'refresh')} type="button">Refresh Liquidity</button>
<button
className="button secondary"
disabled={controlState.funding_observer_paused}
onClick={() => onControl('liquidity-manager', 'pause-funding-observer')}
type="button"
>
Pause Funding Observer
</button>
<button
className="button secondary"
disabled={!controlState.funding_observer_paused}
onClick={() => onControl('liquidity-manager', 'resume-funding-observer')}
type="button"
>
Resume Funding Observer
</button>
<button
className="button secondary"
disabled={controlState.withdrawals_frozen}
onClick={() => onControl('liquidity-manager', 'freeze-withdrawals', { frozen: true })}
type="button"
>
Freeze Withdrawals
</button>
<button
className="button secondary"
disabled={!controlState.withdrawals_frozen}
onClick={() => onControl('liquidity-manager', 'freeze-withdrawals', { frozen: false })}
type="button"
>
Unfreeze Withdrawals
</button>
</div>
<div className="panel-subtitle">
Risky treasury submit paths remain absent. Strategy and executor arm or disarm remain absent.
</div>
<WithdrawalEstimateForm
balances={funds.balances.items}
onControl={onControl}
withdrawalDefaults={controlState.withdrawal_defaults || {}}
/>
<LastControlResult result={lastControlResult} />
</div>
</section>
<section className="stack-grid">
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Funding handles</div>
<h3>Credited and pre-credit funding</h3>
</div>
</div>
<HandlesList handles={funds.funding.handles} />
<h3 style={{ marginTop: 18 }}>Credited deposits</h3>
<FundingTable creditedOnly items={funds.funding.credited_deposits} />
<h3 style={{ marginTop: 18 }}>Pre-credit by asset</h3>
<PreCreditAssetTable items={funds.funding.pre_credit_by_asset} />
</div>
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Transfer history</div>
<h3>Withdrawals and observations</h3>
</div>
</div>
<h3>Recent withdrawals</h3>
<WithdrawalsTable items={funds.recent_withdrawals} />
<h3 style={{ marginTop: 18 }}>Recent funding observations</h3>
<FundingTable items={funds.funding.recent_observations} />
</div>
</section>
<section className="stack-grid">
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Live feed</div>
<h3>Recent incoming quotes</h3>
</div>
<div className="status-subtle">WebSocket live feed, capped at 10</div>
</div>
<QuotesTable items={funds.recent_quotes || []} />
</div>
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Trade-driven changes</div>
<h3>Recent asset deltas</h3>
</div>
</div>
<AssetChangeTable items={funds.trade_asset_changes} />
</div>
</section>
<section className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Durable ledger</div>
<h3>Successful trades</h3>
</div>
<div className="status-subtle">PostgreSQL-backed pagination</div>
</div>
<TradesTable items={trades.items} />
<div className="pagination">
<div className="status-subtle">{`Page ${trades.page} of ${trades.total_pages} - ${trades.total} successful trades`}</div>
<div className="button-row">
<button
className="button secondary"
disabled={trades.page <= 1}
onClick={() => onTradesPageChange(trades.page - 1)}
type="button"
>
Previous
</button>
<button
className="button secondary"
disabled={trades.page >= trades.total_pages}
onClick={() => onTradesPageChange(trades.page + 1)}
type="button"
>
Next
</button>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,120 @@
import AlertsGrid from '../components/AlertsGrid.jsx';
import EmptyState from '../components/EmptyState.jsx';
import MetricCard from '../components/MetricCard.jsx';
import Pill from '../components/Pill.jsx';
import TableFrame from '../components/TableFrame.jsx';
import { formatBoolean, formatTimestamp, truncateMiddle } from '../lib/format.js';
function describeStrategyDecision(decision) {
if (decision === 'actionable') {
return {
label: 'Actionable',
stateLabel: 'healthy',
};
}
if (decision === 'rejected') {
return {
label: 'Rejected by strategy',
stateLabel: 'warning',
};
}
return {
label: decision || 'Unknown',
stateLabel: decision || 'unknown',
};
}
function DecisionsTable({ items }) {
if (!items?.length) return <EmptyState>No strategy decisions have been observed yet.</EmptyState>;
return (
<TableFrame>
<table className="decision-table">
<thead>
<tr>
<th>At</th>
<th>Strategy verdict</th>
<th>Pair</th>
<th>Reason</th>
<th>Edge %</th>
<th>Notional</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
const verdict = describeStrategyDecision(item.decision);
return (
<tr key={`${item.decision_id || item.decision_at || index}`}>
<td>{formatTimestamp(item.decision_at)}</td>
<td><Pill label={verdict.label} stateLabel={verdict.stateLabel} /></td>
<td className="mono truncate-cell" title={item.pair || ''}>{truncateMiddle(item.pair || '', 32)}</td>
<td>{item.decision_reason || ''}</td>
<td className={Number(item.gross_edge_pct) > 0 ? 'value-positive' : Number(item.gross_edge_pct) < 0 ? 'value-negative' : ''}>{item.gross_edge_pct || ''}</td>
<td>{item.eure_notional || ''}</td>
</tr>
);
})}
</tbody>
</table>
</TableFrame>
);
}
export default function StrategyPage({ strategy }) {
return (
<>
<section className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Trading state</div>
<h2>Strategy and executor</h2>
<div className="panel-subtitle">
This page shows whether the system is actively deciding and submitting without offering risky arming controls.
</div>
</div>
</div>
<div className="metric-grid">
<MetricCard label="Strategy armed" meta={`Paused ${formatBoolean(strategy.strategy_state.paused)}`} value={formatBoolean(strategy.strategy_state.armed)} />
<MetricCard label="Threshold %" meta="Current gross threshold" value={strategy.strategy_state.threshold_pct ?? 'Unavailable'} />
<MetricCard label="Max notional EURe" meta="Current cap" value={strategy.strategy_state.max_notional_eure ?? 'Unavailable'} />
<MetricCard label="Executor armed" meta={`Paused ${formatBoolean(strategy.executor_state.paused)}`} value={formatBoolean(strategy.executor_state.armed)} />
<MetricCard label="Executor in flight" meta={`Completed ${strategy.executor_state.completed_count || 0}`} value={String(strategy.executor_state.in_flight_count || 0)} />
<MetricCard label="Signer registered" meta={strategy.executor_state.account_id || ''} value={formatBoolean(strategy.executor_state.signer_registered)} />
</div>
</section>
<section className="stack-grid">
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Decision flow</div>
<h3>Recent decisions and skip reasons</h3>
<div className="panel-subtitle">
Rejected by strategy means the strategy engine did not emit a trade command. Venue or executor rejections appear elsewhere.
</div>
</div>
</div>
<DecisionsTable items={strategy.strategy_state.recent_decisions} />
</div>
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Guard rails</div>
<h3>Omitted risky controls</h3>
</div>
</div>
<EmptyState>
{strategy.omitted_controls.map((item) => (
<div key={item}>{item}</div>
))}
</EmptyState>
<h3 style={{ marginTop: 18 }}>Relevant alerts</h3>
<AlertsGrid items={strategy.relevant_alerts} />
</div>
</section>
</>
);
}

View file

@ -0,0 +1,141 @@
function applySocketMessage(dashboard, payload, session) {
if (!dashboard) return { dashboard, session };
switch (payload.type) {
case 'session.ready':
return {
session: payload.session || session,
dashboard: {
...dashboard,
funds: {
...dashboard.funds,
recent_quotes: payload.live?.recent_quotes || dashboard.funds.recent_quotes,
},
status_bar: {
...dashboard.status_bar,
...(payload.live?.status_bar || {}),
},
},
};
case 'quotes.recent':
return {
session,
dashboard: {
...dashboard,
funds: {
...dashboard.funds,
recent_quotes: payload.recent_quotes,
},
},
};
case 'status_bar.updated':
return {
session,
dashboard: {
...dashboard,
status_bar: {
...dashboard.status_bar,
...payload.status_bar,
},
funds: {
...dashboard.funds,
profitability: {
...dashboard.funds.profitability,
recent_trade_count:
payload.status_bar.recent_trade_count
?? dashboard.funds.profitability.recent_trade_count,
last_successful_trade_at:
payload.status_bar.last_successful_trade_at
|| dashboard.funds.profitability.last_successful_trade_at,
},
},
},
};
case 'alerts.updated':
return {
session,
dashboard: {
...dashboard,
status_bar: {
...dashboard.status_bar,
active_alert_count: payload.alerts.active_alert_count,
highest_alert_severity: payload.alerts.highest_alert_severity,
},
},
};
default:
return { dashboard, session };
}
}
export const initialDashboardState = {
session: null,
dashboard: null,
page: null,
notice: null,
error: null,
websocketState: 'connecting',
lastControlResult: null,
};
export function dashboardReducer(state, action) {
switch (action.type) {
case 'session.loaded':
return {
...state,
session: action.session,
};
case 'bootstrap.loaded':
return {
...state,
dashboard: action.dashboard,
page: state.page || action.dashboard.default_page || 'funds',
};
case 'trades.loaded':
return {
...state,
dashboard: {
...state.dashboard,
funds: {
...state.dashboard.funds,
successful_trades: action.successfulTrades,
},
},
};
case 'page.changed':
return {
...state,
page: action.page,
};
case 'notice.changed':
return {
...state,
notice: action.notice,
};
case 'error.changed':
return {
...state,
error: action.error,
};
case 'control.result':
return {
...state,
lastControlResult: action.result,
};
case 'websocket.state.changed':
return {
...state,
websocketState: action.websocketState,
};
case 'socket.message.received': {
const next = applySocketMessage(state.dashboard, action.payload, state.session);
return {
...state,
session: next.session,
dashboard: next.dashboard,
};
}
default:
return state;
}
}

View file

@ -0,0 +1,496 @@
:root {
--bg: #0f1714;
--bg-2: #16211d;
--panel: rgba(244, 240, 232, 0.93);
--panel-2: rgba(255, 252, 246, 0.98);
--ink: #18211e;
--muted: #60716a;
--line: rgba(24, 33, 30, 0.14);
--accent: #1f7a5a;
--accent-2: #0f5b84;
--warn: #b36a0c;
--critical: #b64328;
--ok: #1f7a5a;
--shadow: 0 24px 60px rgba(0, 0, 0, 0.28);
--radius: 20px;
--radius-sm: 14px;
--mono: "IBM Plex Mono", "SFMono-Regular", "Menlo", monospace;
--sans: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
font-family: var(--sans);
background:
radial-gradient(circle at top left, rgba(31, 122, 90, 0.28), transparent 32%),
radial-gradient(circle at top right, rgba(15, 91, 132, 0.22), transparent 28%),
linear-gradient(180deg, #09100e 0%, #101815 55%, #0a100f 100%);
color: #f7f4ec;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background:
linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
background-size: 32px 32px;
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.78), transparent 100%);
}
a {
color: inherit;
}
button,
input,
select {
font: inherit;
}
.shell {
width: min(1440px, calc(100vw - 28px));
margin: 0 auto;
padding: 18px 0 36px;
}
.status-bar {
position: sticky;
top: 14px;
z-index: 20;
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
padding: 18px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 24px;
background: rgba(8, 12, 11, 0.84);
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.26);
backdrop-filter: blur(14px);
}
.status-tile {
min-width: 0;
min-height: 92px;
padding: 12px 14px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.status-label,
.eyebrow {
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: rgba(247, 244, 236, 0.72);
}
.status-value,
.metric-value {
margin-top: 10px;
font-size: 1.22rem;
font-weight: 700;
}
.status-subtle,
.muted {
margin-top: 6px;
color: rgba(247, 244, 236, 0.66);
font-size: 0.88rem;
}
.app-grid {
margin-top: 20px;
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
gap: 20px;
}
.side-nav {
padding: 16px;
border-radius: var(--radius);
background: rgba(8, 12, 11, 0.78);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: var(--shadow);
height: fit-content;
}
.nav-title {
margin: 0 0 14px;
font-size: 1rem;
font-weight: 700;
}
.nav-stack {
display: grid;
gap: 10px;
}
.nav-button,
.button {
border: 0;
border-radius: 14px;
padding: 12px 14px;
text-align: left;
color: inherit;
background: rgba(255, 255, 255, 0.06);
cursor: pointer;
transition: transform 160ms ease, background 160ms ease;
}
.nav-button:hover,
.button:hover {
transform: translateY(-1px);
background: rgba(255, 255, 255, 0.1);
}
.nav-button.active {
background: linear-gradient(135deg, rgba(31, 122, 90, 0.88), rgba(15, 91, 132, 0.88));
box-shadow: 0 16px 34px rgba(0, 0, 0, 0.24);
}
.content {
display: grid;
gap: 18px;
}
.banner {
padding: 14px 18px;
border-radius: 18px;
background: rgba(8, 12, 11, 0.78);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.banner + .banner {
margin-top: 12px;
}
.banner.ok {
border-color: rgba(31, 122, 90, 0.48);
}
.banner.error {
border-color: rgba(182, 67, 40, 0.58);
}
.panel {
padding: 20px;
border-radius: var(--radius);
background: var(--panel);
color: var(--ink);
box-shadow: var(--shadow);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.panel h2,
.panel h3 {
margin: 0;
}
.panel-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 16px;
}
.panel-subtitle {
margin-top: 8px;
color: var(--muted);
max-width: 72ch;
}
.metric-grid {
display: grid;
gap: 14px;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.metric-card {
min-width: 0;
padding: 16px;
border-radius: 18px;
background: var(--panel-2);
border: 1px solid var(--line);
}
.metric-card.hero {
background: linear-gradient(135deg, rgba(31, 122, 90, 0.16), rgba(15, 91, 132, 0.12));
}
.metric-meta {
margin-top: 8px;
color: var(--muted);
font-size: 0.9rem;
}
.value-positive {
color: var(--ok);
}
.value-negative {
color: var(--critical);
}
.section-grid {
display: grid;
gap: 18px;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}
.stack-grid {
display: grid;
gap: 18px;
grid-template-columns: 1fr;
}
.table-wrap {
overflow-x: auto;
border: 1px solid var(--line);
border-radius: 18px;
background: rgba(255, 255, 255, 0.52);
}
table {
width: 100%;
border-collapse: collapse;
min-width: 620px;
}
table.pair-compact-table,
table.decision-table {
min-width: 0;
}
table.pair-compact-table,
table.decision-table {
table-layout: fixed;
}
table.pair-compact-table td:nth-child(2),
table.pair-compact-table th:nth-child(2) {
width: 24%;
}
th,
td {
padding: 12px 14px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
}
th {
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
background: rgba(255, 255, 255, 0.72);
}
tr:last-child td {
border-bottom: 0;
}
.mono {
font-family: var(--mono);
}
.truncate-line,
.truncate-cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font: inherit;
}
table.decision-table td:nth-child(2),
table.decision-table th:nth-child(2) {
width: 34%;
}
table.decision-table td:nth-child(3),
table.decision-table th:nth-child(3) {
width: 36%;
}
.pills,
.button-row,
.form-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 11px;
border-radius: 999px;
background: rgba(24, 33, 30, 0.08);
color: var(--ink);
font-size: 0.82rem;
}
.pill.severity-critical,
.pill.bad {
background: rgba(182, 67, 40, 0.12);
color: var(--critical);
}
.pill.severity-warning,
.pill.warn {
background: rgba(179, 106, 12, 0.12);
color: var(--warn);
}
.pill.severity-info,
.pill.good {
background: rgba(31, 122, 90, 0.12);
color: var(--ok);
}
.button {
background: rgba(24, 33, 30, 0.92);
color: #f7f4ec;
}
.button.secondary {
background: rgba(24, 33, 30, 0.08);
color: var(--ink);
border: 1px solid var(--line);
}
.button[disabled] {
opacity: 0.45;
cursor: default;
transform: none;
}
.button-row {
margin-top: 12px;
}
.form-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
margin-top: 14px;
}
.field {
display: grid;
gap: 6px;
}
.field label {
font-size: 0.84rem;
color: var(--muted);
}
.field input,
.field select {
width: 100%;
padding: 11px 12px;
border-radius: 12px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.82);
color: var(--ink);
}
.split {
display: grid;
gap: 18px;
grid-template-columns: 1.3fr 0.9fr;
}
.service-grid {
display: grid;
gap: 14px;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.service-card {
padding: 16px;
border-radius: 18px;
background: var(--panel-2);
border: 1px solid var(--line);
}
.service-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
}
.service-detail {
margin-top: 12px;
display: grid;
gap: 8px;
color: var(--muted);
}
.pagination {
margin-top: 12px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.empty {
padding: 18px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.56);
color: var(--muted);
}
@media (max-width: 1100px) {
.app-grid,
.split {
grid-template-columns: 1fr;
}
.side-nav {
position: static;
}
}
@media (max-width: 720px) {
.shell {
width: min(100vw - 16px, 100%);
padding-top: 10px;
}
.status-bar {
top: 8px;
border-radius: 20px;
}
.panel,
.side-nav {
padding: 16px;
}
table {
min-width: 540px;
}
}