diff --git a/src/operator-dashboard/static/components/AlertsGrid.jsx b/src/operator-dashboard/static/components/AlertsGrid.jsx
new file mode 100644
index 0000000..6030673
--- /dev/null
+++ b/src/operator-dashboard/static/components/AlertsGrid.jsx
@@ -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
| Asset | +Spendable | +Pending inbound | +Pending outbound | +EUR value | +
|---|---|---|---|---|
|
+ {item.symbol}
+ {item.asset_id}
+ |
+ {item.spendable} | +{item.pending_inbound} | +{item.pending_outbound} | +{formatEur(item.eur_value_eure)} | +
| Status | +Asset | +Amount | +Handle | +Observed | +
|---|---|---|---|---|
| {item.asset_symbol || item.asset_id} | +{item.amount_display || item.amount || 'Unavailable'} | +{truncateMiddle(item.funding_handle || '', 36)} | +{formatTimestamp(item.credited_at || item.last_seen_at || item.first_seen_at)} | +
| Asset | +Pending amount | +Observation count | +Last seen | +
|---|---|---|---|
| {item.asset_symbol || item.asset_id} | +{item.amount_display || item.amount || 'Unavailable'} | +{item.observation_count || 0} | +{formatTimestamp(item.last_seen_at)} | +
| Status | +Asset | +Amount | +Destination | +Observed | +
|---|---|---|---|---|
| {item.asset_symbol || item.asset_id} | +{item.amount_display || item.amount || 'Unavailable'} | +{truncateMiddle(item.destination_address || '', 36)} | +{formatTimestamp(item.completed_at || item.requested_at)} | +
| Observed | +Pair | +Kind | +Amount in | +Amount out | +
|---|---|---|---|---|
| {formatTimestamp(item.observed_at || item.ingested_at)} | +{truncateMiddle(item.pair || '', 38)} | +{item.request_kind || ''} | +{item.amount_in || ''} | +{item.amount_out || ''} | +
| Observed | +Quote | +Spend | +Receive | +
|---|---|---|---|
| {formatTimestamp(item.observed_at)} | +{item.quote_id || ''} | +{`${item.amount_in} ${item.asset_in_symbol}`} | +{`${item.amount_out} ${item.asset_out_symbol}`} | +
| Observed | +Pair | +Kind | +Submitted | +Edge | +Result | +
|---|---|---|---|---|---|
| {formatTimestamp(trade.observed_at)} | +{truncateMiddle(trade.pair || '', 38)} | +{trade.request_kind || ''} | +{`${trade.amount_in_display || ''} ${trade.asset_in_symbol || ''} -> ${trade.amount_out_display || ''} ${trade.asset_out_symbol || ''}`} | +0 ? 'value-positive' : Number(trade.gross_edge_pct) < 0 ? 'value-negative' : ''}>{trade.gross_edge_pct || ''} | +
| Last control result | +{stringifyJson(result)} |
+
| At | +Strategy verdict | +Pair | +Reason | +Edge % | +Notional | +
|---|---|---|---|---|---|
| {formatTimestamp(item.decision_at)} | +{truncateMiddle(item.pair || '', 32)} | +{item.decision_reason || ''} | +0 ? 'value-positive' : Number(item.gross_edge_pct) < 0 ? 'value-negative' : ''}>{item.gross_edge_pct || ''} | +{item.eure_notional || ''} | +