feat: NASA mission-control theme for all OMNL dashboards
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m41s
CI/CD Pipeline / Security Scanning (push) Successful in 2m32s
CI/CD Pipeline / Lint and Format (push) Failing after 51s
CI/CD Pipeline / Terraform Validation (push) Failing after 35s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 30s
Validation / validate-genesis (push) Successful in 34s
Validation / validate-terraform (push) Failing after 36s
Validation / validate-kubernetes (push) Failing after 11s
Validation / validate-smart-contracts (push) Failing after 11s
Validation / validate-security (push) Failing after 1m23s
Validation / validate-documentation (push) Failing after 21s

Replace Binance-style dashboard chrome with a shared telemetry UI, SVG icons, and mission headers across Central Bank, Office 24, trade, swap, and admin screens.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-28 17:10:24 -07:00
parent 4e047b9429
commit cbd669c762
18 changed files with 1592 additions and 997 deletions

View File

@@ -53,10 +53,10 @@ function App() {
<Route path="/central-bank" element={<CentralBankPage />} />
<Route path="/office-24" element={<Office24Page />} />
<Route path="/trade" element={<TradePage />} />
<Route path="/swap" element={<SwapPage />} />
</Route>
<Route element={<Layout />}>
<Route path="/" element={<BridgePage />} />
<Route path="/swap" element={<SwapPage />} />
<Route path="/reserve" element={<ReservePage />} />
<Route path="/history" element={<HistoryPage />} />
<Route path="/admin" element={<AdminPanel />} />

View File

@@ -10,6 +10,9 @@ import { CONTRACT_ADDRESSES } from '../../config/contracts'
import { MAINNET_TETHER_ABI } from '../../abis/MainnetTether'
import { subscribeToContractEvents } from '../../utils/contractEvents'
import toast from 'react-hot-toast'
import DashboardStatCard from '../../features/omnl-dashboard/DashboardStatCard'
import DashboardSection from '../../features/omnl-dashboard/DashboardSection'
import NasaIcon, { StatusIcon } from '../icons/NasaIcon'
export default function AdminDashboard() {
const { adminActions, auditLogs } = useAdmin()
@@ -68,7 +71,7 @@ export default function AdminDashboard() {
'Paused',
(_event) => {
setMainnetTetherPaused(true)
toast.success('MainnetTether paused event detected', { icon: '🔔' })
toast.success('MainnetTether paused event detected')
}
).then((unsub) => {
unsubscribePaused = unsub
@@ -81,7 +84,7 @@ export default function AdminDashboard() {
'Unpaused',
(_event) => {
setMainnetTetherPaused(false)
toast.success('MainnetTether unpaused event detected', { icon: '🔔' })
toast.success('MainnetTether unpaused event detected')
}
).then((unsub) => {
unsubscribeUnpaused = unsub
@@ -96,124 +99,77 @@ export default function AdminDashboard() {
}, [publicClient, address])
return (
<div className="space-y-6">
{/* Stats Grid */}
<div className="space-y-6 nasa-mission-root">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-black/20 rounded-xl p-4 border border-white/10">
<div className="text-white/70 text-sm mb-1">Total Actions</div>
<div className="text-2xl font-bold text-white">{stats.totalActions}</div>
</div>
<div className="bg-black/20 rounded-xl p-4 border border-white/10">
<div className="text-white/70 text-sm mb-1">Pending</div>
<div className="text-2xl font-bold text-yellow-400">{stats.pending}</div>
</div>
<div className="bg-black/20 rounded-xl p-4 border border-white/10">
<div className="text-white/70 text-sm mb-1">Executed</div>
<div className="text-2xl font-bold text-green-400">{stats.executed}</div>
</div>
<div className="bg-black/20 rounded-xl p-4 border border-white/10">
<div className="text-white/70 text-sm mb-1">Success Rate</div>
<div className="text-2xl font-bold text-blue-400">{stats.successRate}%</div>
</div>
<DashboardStatCard label="Total actions" value={String(stats.totalActions)} accent="blue" icon="admin" />
<DashboardStatCard label="Pending" value={String(stats.pending)} accent="gold" icon="alert" />
<DashboardStatCard label="Executed" value={String(stats.executed)} accent="green" icon="check" />
<DashboardStatCard label="Success rate" value={`${stats.successRate}%`} accent="blue" icon="chart" />
</div>
{/* Contract Status */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-black/20 rounded-xl p-4 border border-white/10">
<div className="dash-card">
<div className="flex items-center justify-between mb-2">
<div className="text-white/70 text-sm">MainnetTether</div>
<span
className={`px-3 py-1 rounded text-xs font-semibold ${
mainnetTetherPaused
? 'bg-red-500/20 text-red-300'
: 'bg-green-500/20 text-green-300'
}`}
>
<div className="dash-stat-label flex items-center gap-1">
<NasaIcon name="bridge" size={14} className="nasa-icon--telemetry" />
MainnetTether
</div>
<span className={`nasa-badge ${mainnetTetherPaused ? 'nasa-badge--caution' : 'nasa-badge--nominal'}`}>
{mainnetTetherPaused ? 'Paused' : 'Active'}
</span>
</div>
<p className="text-white/60 text-xs font-mono">
{CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER}
</p>
<p className="text-[#7a9bb8] text-xs font-mono">{CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER}</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Recent Actions */}
<div className="bg-black/20 rounded-xl p-6 border border-white/10">
<h3 className="text-lg font-bold text-white mb-4">Recent Actions</h3>
<DashboardSection title="Recent actions" icon="rocket">
{recentActions.length === 0 ? (
<p className="text-white/60 text-sm">No actions yet</p>
<p className="text-[#7a9bb8] text-sm">No actions yet</p>
) : (
<div className="space-y-3">
{recentActions.map((action) => (
<div
key={action.id}
className="bg-white/5 rounded-lg p-3 border border-white/10"
>
<div key={action.id} className="nasa-inset-panel">
<div className="flex justify-between items-start">
<div>
<p className="text-white font-semibold text-sm">{action.type}</p>
<p className="text-white/60 text-xs font-mono">
{action.contractAddress.slice(0, 20)}...
</p>
<p className="text-[#e8f4ff] font-semibold text-sm font-mono">{action.type}</p>
<p className="text-[#7a9bb8] text-xs font-mono">{action.contractAddress.slice(0, 20)}...</p>
</div>
<span
className={`px-2 py-1 rounded text-xs ${
action.status === TransactionRequestStatus.SUCCESS
? 'bg-green-500/20 text-green-300'
: action.status === TransactionRequestStatus.FAILED
? 'bg-red-500/20 text-red-300'
: 'bg-yellow-500/20 text-yellow-300'
}`}
>
<span className="flex items-center gap-1 text-xs font-mono">
<StatusIcon ok={action.status === TransactionRequestStatus.SUCCESS} />
{action.status}
</span>
</div>
<p className="text-white/50 text-xs mt-2">
{new Date(action.createdAt).toLocaleString()}
</p>
<p className="text-[#7a9bb8] text-xs mt-2">{new Date(action.createdAt).toLocaleString()}</p>
</div>
))}
</div>
)}
</div>
</DashboardSection>
{/* Recent Audit Logs */}
<div className="bg-black/20 rounded-xl p-6 border border-white/10">
<h3 className="text-lg font-bold text-white mb-4">Recent Audit Logs</h3>
<DashboardSection title="Recent audit logs" icon="ledger">
{recentLogs.length === 0 ? (
<p className="text-white/60 text-sm">No audit logs yet</p>
<p className="text-[#7a9bb8] text-sm">No audit logs yet</p>
) : (
<div className="space-y-3">
{recentLogs.map((log) => (
<div
key={log.id}
className="bg-white/5 rounded-lg p-3 border border-white/10"
>
<div key={log.id} className="nasa-inset-panel">
<div className="flex justify-between items-start">
<div>
<p className="text-white font-semibold text-sm">{log.action}</p>
<p className="text-white/60 text-xs font-mono">{log.user.slice(0, 10)}...</p>
<p className="text-[#e8f4ff] font-semibold text-sm font-mono">{log.action}</p>
<p className="text-[#7a9bb8] text-xs font-mono">{log.user.slice(0, 10)}...</p>
</div>
<span
className={`px-2 py-1 rounded text-xs ${
log.status === 'success'
? 'bg-green-500/20 text-green-300'
: 'bg-red-500/20 text-red-300'
}`}
>
<span className="flex items-center gap-1 text-xs font-mono">
<StatusIcon ok={log.status === 'success'} />
{log.status}
</span>
</div>
<p className="text-white/50 text-xs mt-2">
{new Date(log.timestamp).toLocaleString()}
</p>
<p className="text-[#7a9bb8] text-xs mt-2">{new Date(log.timestamp).toLocaleString()}</p>
</div>
))}
</div>
)}
</div>
</DashboardSection>
</div>
</div>
)

View File

@@ -4,8 +4,11 @@
*/
import { useState, useEffect } from 'react';
import { useAccount } from 'wagmi';
import toast from 'react-hot-toast';
import { useAccount } from 'wagmi';
import DashboardSection from '../../features/omnl-dashboard/DashboardSection';
import NasaIcon, { StatusIcon } from '../icons/NasaIcon';
import NasaPageHeader from '../../features/omnl-dashboard/NasaPageHeader';
interface ChainMetadata {
chainId: number;
@@ -37,13 +40,6 @@ export default function ChainManagementDashboard() {
const loadChains = async () => {
try {
setLoading(true);
// TODO: Connect to ChainRegistry contract
// const provider = new ethers.JsonRpcProvider(process.env.NEXT_PUBLIC_RPC_URL);
// const registry = new ethers.Contract(REGISTRY_ADDRESS, REGISTRY_ABI, provider);
// const [evmChainIds, evmChains] = await registry.getAllEVMChains();
// const [nonEvmIds, nonEvmChains] = await registry.getAllNonEVMChains();
// Mock data for now
setChains([
{
chainId: 138,
@@ -75,7 +71,6 @@ export default function ChainManagementDashboard() {
const toggleChain = async (currentStatus: boolean) => {
try {
// TODO: Call ChainRegistry.setChainActive()
toast.success(`Chain ${currentStatus ? 'disabled' : 'enabled'}`);
loadChains();
} catch (error: unknown) {
@@ -85,52 +80,56 @@ export default function ChainManagementDashboard() {
if (!isConnected) {
return (
<div className="p-6 bg-white/10 rounded-xl">
<p className="text-white/70">Please connect your wallet to manage chains.</p>
<div className="dash-card nasa-mission-root">
<p className="text-[#7a9bb8] flex items-center gap-2">
<NasaIcon name="wallet" size={16} className="nasa-icon--telemetry" />
Please connect your wallet to manage chains.
</p>
</div>
);
}
return (
<div className="space-y-6">
<div className="bg-black/20 rounded-xl p-6 border border-white/10">
<h2 className="text-xl font-bold text-white mb-4">Chain Management</h2>
<div className="space-y-6 nasa-mission-root">
<NasaPageHeader
icon="network"
missionId="NET-ADM"
title="Chain Management"
subtitle="Supported chains, adapters, and deployment registry"
/>
<DashboardSection title="Registered chains" icon="globe">
{loading ? (
<div className="text-white/70">Loading chains...</div>
<p className="text-[#7a9bb8] flex items-center gap-2">
<NasaIcon name="loading" size={14} className="nasa-icon--spin nasa-icon--telemetry" />
Loading chains
</p>
) : (
<div className="space-y-4">
{chains.map((chain) => (
<div
key={chain.chainId || chain.chainIdentifier}
className="bg-white/5 rounded-lg p-4 flex items-center justify-between"
>
<div key={chain.chainId || chain.chainIdentifier} className="nasa-inset-panel flex items-center justify-between gap-4 flex-wrap">
<div>
<h3 className="text-white font-semibold">
<h3 className="text-[#e8f4ff] font-semibold font-mono flex items-center gap-2">
<NasaIcon name="network" size={14} className="nasa-icon--telemetry" />
{chain.chainIdentifier} ({chain.chainType})
</h3>
<p className="text-white/70 text-sm">
Adapter: {chain.adapter.slice(0, 10)}...
</p>
<p className="text-white/70 text-sm">
Explorer: <a href={chain.explorerUrl} target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:underline">{chain.explorerUrl}</a>
<p className="text-[#7a9bb8] text-sm font-mono">Adapter: {chain.adapter.slice(0, 10)}</p>
<p className="text-[#7a9bb8] text-sm">
Explorer:{' '}
<a href={chain.explorerUrl} target="_blank" rel="noopener noreferrer" className="text-[#00d4ff] hover:underline inline-flex items-center gap-1">
<NasaIcon name="link" size={12} />
{chain.explorerUrl}
</a>
</p>
</div>
<div className="flex items-center gap-4">
<span className={`px-3 py-1 rounded-full text-sm ${
chain.isActive
? 'bg-green-500/20 text-green-200'
: 'bg-red-500/20 text-red-200'
}`}>
<span className={`nasa-badge flex items-center gap-1 ${chain.isActive ? 'nasa-badge--nominal' : 'nasa-badge--caution'}`}>
<StatusIcon ok={chain.isActive} />
{chain.isActive ? 'Active' : 'Inactive'}
</span>
<button
onClick={() => toggleChain(chain.isActive)}
className={`px-4 py-2 rounded-lg font-semibold transition-colors ${
chain.isActive
? 'bg-red-600 hover:bg-red-700 text-white'
: 'bg-green-600 hover:bg-green-700 text-white'
}`}
className={`nasa-btn ${chain.isActive ? '' : 'nasa-btn--primary'}`}
>
{chain.isActive ? 'Disable' : 'Enable'}
</button>
@@ -139,17 +138,16 @@ export default function ChainManagementDashboard() {
))}
</div>
)}
</div>
</DashboardSection>
<div className="bg-black/20 rounded-xl p-6 border border-white/10">
<h3 className="text-lg font-bold text-white mb-4">Add New Chain</h3>
<DashboardSection title="Add new chain" icon="rocket">
<div className="space-y-4">
<select
value={selectedChain}
onChange={(e) => setSelectedChain(e.target.value)}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
className="nasa-select"
>
<option value="">Select chain to add...</option>
<option value="">Select chain to add</option>
<option value="polygon">Polygon</option>
<option value="arbitrum">Arbitrum</option>
<option value="optimism">Optimism</option>
@@ -159,13 +157,14 @@ export default function ChainManagementDashboard() {
<option value="ethereum">Ethereum Mainnet</option>
</select>
<button
onClick={() => toast('Deployment feature coming soon', { icon: '' })}
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-semibold"
onClick={() => toast('Deployment feature coming soon')}
className="nasa-btn nasa-btn--primary w-full"
>
Deploy Chain Adapter
<NasaIcon name="rocket" size={14} />
Deploy chain adapter
</button>
</div>
</div>
</DashboardSection>
</div>
);
}

View File

@@ -0,0 +1,204 @@
import type { ReactNode, SVGProps } from 'react';
export type NasaIconName =
| 'central-bank'
| 'office'
| 'trade'
| 'swap'
| 'bridge'
| 'refresh'
| 'check'
| 'x'
| 'alert'
| 'link'
| 'arrow-right'
| 'network'
| 'satellite'
| 'ledger'
| 'rocket'
| 'search'
| 'loading'
| 'chart'
| 'wallet'
| 'globe'
| 'money'
| 'rail'
| 'admin';
type Props = SVGProps<SVGSVGElement> & {
name: NasaIconName;
size?: number;
};
const paths: Record<NasaIconName, ReactNode> = {
'central-bank': (
<>
<path d="M3 21h18" />
<path d="M5 21V7l7-4 7 4v14" />
<path d="M9 21v-6h6v6" />
<path d="M9 9h.01M15 9h.01M9 13h.01M15 13h.01" />
</>
),
office: (
<>
<path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18" />
<path d="M6 12h12M10 6h.01M14 6h.01M10 10h.01M14 10h.01M10 14h.01M14 14h.01M10 18h.01M14 18h.01" />
</>
),
trade: (
<>
<path d="M3 3v18h18" />
<path d="M7 14l4-4 3 3 5-6" />
</>
),
swap: (
<>
<path d="M7 16V4M7 4 3 8M7 4l4 4" />
<path d="M17 8v12M17 20l4-4M17 20l-4-4" />
</>
),
bridge: (
<>
<path d="M4 16h16" />
<path d="M6 16V8a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8" />
<path d="M8 12h8" />
</>
),
refresh: (
<>
<path d="M21 12a9 9 0 1 1-2.64-6.36" />
<path d="M21 3v6h-6" />
</>
),
check: <path d="M20 6 9 17l-5-5" />,
x: (
<>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</>
),
alert: (
<>
<path d="M12 9v4" />
<path d="M12 17h.01" />
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
</>
),
link: (
<>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</>
),
'arrow-right': (
<>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</>
),
network: (
<>
<circle cx="12" cy="12" r="2" />
<path d="M12 2v4M12 18v4M2 12h4M18 12h4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
</>
),
satellite: (
<>
<path d="M13 7 9 3 5 7l4 4" />
<path d="m17 11 4 4-4 4-4-4" />
<path d="m8 12 4 4 6-6-4-4Z" />
<path d="m16 8 3-3M8 16l-3 3" />
</>
),
ledger: (
<>
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
<path d="M8 7h8M8 11h8M8 15h5" />
</>
),
rocket: (
<>
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" />
<path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" />
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
</>
),
search: (
<>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</>
),
loading: <path d="M21 12a9 9 0 1 1-6.219-8.56" />,
chart: (
<>
<path d="M3 3v18h18" />
<path d="M18 17V9M13 17V5M8 17v-3" />
</>
),
wallet: (
<>
<path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1" />
<path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4" />
</>
),
globe: (
<>
<circle cx="12" cy="12" r="10" />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
<path d="M2 12h20" />
</>
),
money: (
<>
<circle cx="12" cy="12" r="10" />
<path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8" />
<path d="M12 6v2M12 16v2" />
</>
),
rail: (
<>
<path d="M2 17h20" />
<path d="M6 17V5h12v12" />
<path d="M6 9h12M10 13h4" />
</>
),
admin: (
<>
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z" />
</>
),
};
export default function NasaIcon({ name, size = 18, className = '', ...props }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={`nasa-icon ${className}`.trim()}
aria-hidden={props['aria-label'] ? undefined : true}
{...props}
>
{paths[name]}
</svg>
);
}
export function StatusIcon({ ok, className = '' }: { ok: boolean; className?: string }) {
return (
<NasaIcon
name={ok ? 'check' : 'x'}
size={14}
className={ok ? `nasa-icon--nominal ${className}` : `nasa-icon--critical ${className}`}
/>
);
}

View File

@@ -1,61 +1,60 @@
import { Link, Outlet, useLocation } from 'react-router-dom';
import { useRef } from 'react';
import WalletConnect from '../wallet/WalletConnect';
import WalletDisconnectNotice from '../wallet/WalletDisconnectNotice';
const PRODUCTS = [
{ path: '/central-bank', label: 'Central Bank', short: 'CB' },
{ path: '/office-24', label: 'Office 24', short: 'O24' },
{ path: '/trade', label: 'DBIS Trade', short: 'Trade' },
{ path: '/swap', label: 'Swap', short: 'Swap' },
] as const;
export default function OmnlProductLayout() {
const location = useLocation();
const userInitiatedDisconnectRef = useRef(false);
const isActive = (path: string) => location.pathname.startsWith(path);
return (
<div className="omnl-app min-h-screen flex flex-col bg-[#0b0e11]">
<WalletDisconnectNotice userInitiatedDisconnectRef={userInitiatedDisconnectRef} />
<header className="omnl-app-header sticky top-0 z-50 border-b border-[#2b3139] bg-[#181a20]">
<div className="max-w-[1600px] mx-auto px-4 h-14 flex items-center gap-4">
<Link to="/central-bank" className="flex items-center gap-2 shrink-0">
<span className="w-8 h-8 rounded bg-[#f0b90b] text-[#181a20] font-bold flex items-center justify-center text-sm">
O
</span>
<span className="font-semibold text-[#eaecef] hidden sm:inline">OMNL</span>
</Link>
<nav className="flex items-center gap-1 flex-1" aria-label="OMNL products">
{PRODUCTS.map(({ path, label }) => (
<Link
key={path}
to={path}
className={`px-4 py-2 rounded text-sm font-medium transition-colors ${
isActive(path)
? 'bg-[#2b3139] text-[#f0b90b]'
: 'text-[#848e9c] hover:text-[#eaecef] hover:bg-[#1e2329]'
}`}
>
{label}
</Link>
))}
</nav>
<Link
to="/"
className="text-xs text-[#848e9c] hover:text-[#f0b90b] hidden md:inline shrink-0"
>
Bridge
</Link>
<WalletConnect onBeforeDisconnect={() => { userInitiatedDisconnectRef.current = true; }} />
</div>
</header>
<main className="flex-1">
<Outlet />
</main>
</div>
);
}
import { Link, Outlet, useLocation } from 'react-router-dom';
import { useRef } from 'react';
import WalletConnect from '../wallet/WalletConnect';
import WalletDisconnectNotice from '../wallet/WalletDisconnectNotice';
import NasaIcon, { type NasaIconName } from '../icons/NasaIcon';
const PRODUCTS: { path: string; label: string; icon: NasaIconName }[] = [
{ path: '/central-bank', label: 'Central Bank', icon: 'central-bank' },
{ path: '/office-24', label: 'Office 24', icon: 'office' },
{ path: '/trade', label: 'DBIS Trade', icon: 'trade' },
{ path: '/swap', label: 'Digital Swap', icon: 'swap' },
];
export default function OmnlProductLayout() {
const location = useLocation();
const userInitiatedDisconnectRef = useRef(false);
const isActive = (path: string) => location.pathname.startsWith(path);
return (
<div className="omnl-app min-h-screen flex flex-col">
<WalletDisconnectNotice userInitiatedDisconnectRef={userInitiatedDisconnectRef} />
<header className="omnl-app-header sticky top-0 z-50 border-b">
<div className="max-w-[1600px] mx-auto px-4 h-14 flex items-center gap-4">
<Link to="/central-bank" className="flex items-center gap-2 shrink-0">
<span className="w-8 h-8 border border-[#1e4a8a] bg-[#0a1224] text-[#00d4ff] font-bold flex items-center justify-center text-sm">
<NasaIcon name="satellite" size={16} className="nasa-icon--telemetry" />
</span>
<span className="nasa-nav-brand font-semibold hidden sm:inline text-sm">OMNL Mission Control</span>
</Link>

View File

@@ -1,191 +1,147 @@
import { Link } from 'react-router-dom';
import { useCentralBank } from './useCentralBank';
import ChainMatrixPanel from './ChainMatrixPanel';
import FineractLedgerBanner from '../omnl-dashboard/FineractLedgerBanner';
import DashboardStatCard from '../omnl-dashboard/DashboardStatCard';
import DashboardSection from '../omnl-dashboard/DashboardSection';
import QuickPortalLinks from '../omnl-dashboard/QuickPortalLinks';
import { formatUsd } from '../../utils/formatMoney';
export default function CentralBankDashboard() {
const cb = useCentralBank();
const office = cb.office as {
officeId?: number;
name?: string;
externalId?: string;
settlement?: { moneyLayers?: string[]; glM0?: string; glM1?: string; glM2?: string };
} | null;
return (
<div className="dash-page cb-page max-w-[1400px] mx-auto px-4 py-6">
<header className="mb-6">
<div className="flex flex-wrap items-center gap-2 mb-3">
<span className="dash-badge dash-badge--blue">OMNL Central Bank</span>
<span className="dash-badge dash-badge--green">Production</span>
<span className="dash-badge dash-badge--gold">128 chains</span>
</div>
<h1 className="dash-page-title">Central Bank Operations</h1>
<p className="dash-page-subtitle">
Live money supply for {office?.name ?? 'HOSPITALLERS Ali Iraq-Iran'} (Office{' '}
{office?.officeId ?? 24}) · settlement · SWIFT · HYBX · Chain 138
</p>
</header>
<QuickPortalLinks />
{cb.error && (
<div className="dash-banner dash-banner--error mb-6">
<p className="dash-banner__title">Failed to load dashboard</p>
<p className="dash-banner__detail">{cb.error}</p>
</div>
)}
<FineractLedgerBanner
moneySupply={cb.moneySupply}
loading={cb.loading}
officeName={office?.name}
/>
<h2 className="dash-section-title mb-1">Money supply</h2>
<p className="dash-section-subtitle mb-4">
Meta-fiat layers from Fineract general ledger · USD
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<DashboardStatCard
label="M0 reserve"
value={formatUsd(cb.moneySupply?.M0?.gl1050)}
hint={`GL 1050 · ${cb.moneySupply?.M0?.backingRatio ?? 1.2}× backing policy`}
accent="blue"
/>
<DashboardStatCard
label="M1 circulating"
value={formatUsd(cb.moneySupply?.M1?.circulating)}
hint="GL 2100 · narrow money / eMoney"
accent="green"
/>
<DashboardStatCard
label="M2 broad money"
value={formatUsd(cb.moneySupply?.M2?.broadMoney)}
hint="GL 2200 · token-load source"
accent="gold"
/>
<DashboardStatCard
label="Due from head office"
value={formatUsd(cb.moneySupply?.interoffice?.dueFromHeadOffice)}
hint="GL 1410 · inter-office receivable"
accent="blue"
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<DashboardSection
title="Settlement service"
subtitle="Middleware health and refresh"
action={
<button
type="button"
className="dash-btn !w-auto px-4"
onClick={() => cb.refresh()}
disabled={cb.loading}
>
{cb.loading ? 'Refreshing…' : 'Refresh'}
</button>
}
>
<dl>
<div className="dash-kv">
<dt>Status</dt>
<dd className="dash-kv-value--ok">{(cb.health as { status?: string })?.status ?? '—'}</dd>
</div>
<div className="dash-kv">
<dt>Office</dt>
<dd>
{office?.officeId ?? 24} · {office?.externalId ?? 'HOSPITALLERS-ALI-IRAQ-IRAN'}
</dd>
</div>
<div className="dash-kv">
<dt>M2 tokens registered</dt>
<dd>{cb.tokenCount}</dd>
</div>
<div className="dash-kv">
<dt>Active chains</dt>
<dd>
{(cb.health as { chainSupport?: { active?: number; total?: number } })?.chainSupport
?.active ?? '—'}
{' / '}
{(cb.health as { chainSupport?: { active?: number; total?: number } })?.chainSupport
?.total ?? 128}
</dd>
</div>
</dl>
</DashboardSection>
<DashboardSection
title="General ledger map"
subtitle={`Money layers · Office ${office?.officeId ?? 24}`}
>
<div className="flex flex-wrap gap-2 mb-4">
{(office?.settlement?.moneyLayers ?? ['M0', 'M1', 'M2']).map((layer) => (
<span
key={layer}
className="px-3 py-1 rounded-full bg-[#2b3139] text-[#f0b90b] text-xs font-semibold"
>
{layer}
</span>
))}
</div>
<dl>
<div className="dash-kv">
<dt>M0 reserve</dt>
<dd>GL {office?.settlement?.glM0 ?? '1050'}</dd>
</div>
<div className="dash-kv">
<dt>M1 liability</dt>
<dd>GL {office?.settlement?.glM1 ?? '2100'}</dd>
</div>
<div className="dash-kv">
<dt>M2 broad</dt>
<dd>GL {office?.settlement?.glM2 ?? '2200'}</dd>
</div>
<div className="dash-kv">
<dt>Inter-office (HO)</dt>
<dd>GL 1410</dd>
</div>
</dl>
<p className="dash-section-subtitle mt-4">
<Link to="/office-24" className="text-[#f0b90b] hover:underline">
Open Office 24 dashboard
</Link>
{' · '}
<Link to="/trade" className="text-[#f0b90b] hover:underline">
DBIS Trade
</Link>
</p>
</DashboardSection>
<DashboardSection title="Recent exchange activity" subtitle="Last 8 swaps on DBIS Exchange" className="lg:col-span-2">
<ul className="space-y-0 text-sm">
{((cb.monitor as { swaps?: { status: string; tokenIn: string; createdAt: string }[] })?.swaps ?? [])
.slice(0, 8)
.map((s, i) => (
<li
key={i}
className="flex justify-between gap-3 py-2.5 border-b border-[#2b3139] last:border-0 text-sm"
>
<span className="text-[#848e9c] shrink-0">{s.status}</span>
<span className="text-[#eaecef] font-mono text-xs truncate">{s.tokenIn?.slice(0, 12)}</span>
<span className="text-[#848e9c] text-xs shrink-0">{s.createdAt?.slice(0, 16)}</span>
</li>
))}
{!((cb.monitor as { swaps?: unknown[] })?.swaps?.length) && (
<li className="text-[#848e9c] py-3">No swaps recorded yet</li>
)}
</ul>
</DashboardSection>
<ChainMatrixPanel />
</div>
</div>
);
}
import { Link } from 'react-router-dom';
import { useCentralBank } from './useCentralBank';
import ChainMatrixPanel from './ChainMatrixPanel';
import FineractLedgerBanner from '../omnl-dashboard/FineractLedgerBanner';
import DashboardStatCard from '../omnl-dashboard/DashboardStatCard';
import DashboardSection from '../omnl-dashboard/DashboardSection';
import QuickPortalLinks from '../omnl-dashboard/QuickPortalLinks';
import NasaPageHeader from '../omnl-dashboard/NasaPageHeader';
import NasaIcon from '../../components/icons/NasaIcon';
import { formatUsd } from '../../utils/formatMoney';
export default function CentralBankDashboard() {
const cb = useCentralBank();
const office = cb.office as {
officeId?: number;
name?: string;
externalId?: string;
settlement?: { moneyLayers?: string[]; glM0?: string; glM1?: string; glM2?: string };
} | null;
return (
<div className="dash-page cb-page max-w-[1400px] mx-auto px-4 py-6">
<NasaPageHeader
icon="central-bank"
missionId="CB-138"
title="Central Bank Operations"
subtitle={`Live money supply for ${office?.name ?? 'HOSPITALLERS Ali Iraq-Iran'} (Office ${office?.officeId ?? 24}) · settlement · SWIFT · HYBX · Chain 138`}
badges={
<>
<span className="nasa-badge nasa-badge--telemetry">OMNL Central Bank</span>
<span className="nasa-badge nasa-badge--nominal">Production</span>
<span className="nasa-badge nasa-badge--caution">128 chains</span>
</>
}
/>
<QuickPortalLinks />
{cb.error && (
<div className="dash-banner dash-banner--error mb-6">
<NasaIcon name="x" size={18} className="nasa-icon--critical shrink-0 mt-0.5" />
<div>
<p className="dash-banner__title">Failed to load dashboard</p>
<p className="dash-banner__detail">{cb.error}</p>
</div>
</div>
)}
<FineractLedgerBanner
moneySupply={cb.moneySupply}
loading={cb.loading}
officeName={office?.name}
/>
<h2 className="dash-section-title mb-1 flex items-center gap-2">
<NasaIcon name="money" size={14} className="nasa-icon--telemetry" />
Money supply
</h2>
<p className="dash-section-subtitle mb-4">Meta-fiat layers from Fineract general ledger · USD</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<DashboardStatCard label="M0 reserve" value={formatUsd(cb.moneySupply?.M0?.gl1050)} hint={`GL 1050 · ${cb.moneySupply?.M0?.backingRatio ?? 1.2}× backing policy`} accent="blue" icon="ledger" />
<DashboardStatCard label="M1 circulating" value={formatUsd(cb.moneySupply?.M1?.circulating)} hint="GL 2100 · narrow money / eMoney" accent="green" icon="money" />
<DashboardStatCard label="M2 broad money" value={formatUsd(cb.moneySupply?.M2?.broadMoney)} hint="GL 2200 · token-load source" accent="gold" icon="rocket" />
<DashboardStatCard label="Due from head office" value={formatUsd(cb.moneySupply?.interoffice?.dueFromHeadOffice)} hint="GL 1410 · inter-office receivable" accent="blue" icon="rail" />
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<DashboardSection
title="Settlement service"
subtitle="Middleware health and refresh"
icon="satellite"
action={
<button type="button" className="dash-btn !w-auto px-4 flex items-center gap-1" onClick={() => cb.refresh()} disabled={cb.loading}>

View File

@@ -1,100 +1,109 @@
import { useEffect, useState } from 'react';
import { fetchOmnlChainRegistry, type ChainRegistryResponse, type OmnlChainSummary } from '../../config/supported-chains';
type Filter = 'all' | 'active' | 'primary' | 'l2';
export default function ChainMatrixPanel() {
const [registry, setRegistry] = useState<ChainRegistryResponse | null>(null);
const [filter, setFilter] = useState<Filter>('active');
const [search, setSearch] = useState('');
useEffect(() => {
fetchOmnlChainRegistry().then(setRegistry);
}, []);
const chains = (registry?.chains ?? []).filter((c) => {
if (filter === 'active' && c.status !== 'active') return false;
if (filter === 'primary' && c.omnlRole !== 'settlement-hub' && c.omnlRole !== 'mirror-target') return false;
if (filter === 'l2' && c.tier !== 'l2') return false;
if (search) {
const q = search.toLowerCase();
return c.name.toLowerCase().includes(q) || String(c.chainId).includes(q) || c.nativeSymbol.toLowerCase().includes(q);
}
return true;
});
return (
<section className="dash-card cb-card lg:col-span-2">
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
<div>
<h2 className="text-lg font-semibold text-[#eaecef]">128-chain network matrix</h2>
<p className="text-xs text-[#848e9c]">
{registry?.stats?.active ?? 0} active · {registry?.stats?.staged ?? 0} staged ·{' '}
{registry?.stats?.reserved ?? 0} reserved · primary {registry?.primaryChainId ?? 138}
</p>
</div>
<input
type="search"
placeholder="Search chain…"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="bg-[#2b3139] border border-[#474d57] rounded px-3 py-1.5 text-sm w-48"
/>
</div>
<div className="flex gap-2 mb-4 flex-wrap">
{(['all', 'active', 'primary', 'l2'] as Filter[]).map((f) => (
<button
key={f}
type="button"
onClick={() => setFilter(f)}
className={`px-3 py-1 rounded text-xs capitalize ${
filter === f ? 'bg-[#f0b90b] text-[#181a20]' : 'bg-[#2b3139] text-[#848e9c]'
}`}
>
{f}
</button>
))}
</div>
<div className="overflow-x-auto max-h-[360px] overflow-y-auto">
<table className="w-full text-sm">
<thead className="sticky top-0 bg-[#1e2329]">
<tr className="text-[#848e9c] text-xs uppercase text-left">
<th className="py-2 pr-3">ID</th>
<th className="py-2 pr-3">Network</th>
<th className="py-2 pr-3">Role</th>
<th className="py-2 pr-3">Status</th>
<th className="py-2 pr-3">Swap</th>
<th className="py-2">Bridge</th>
</tr>
</thead>
<tbody>
{chains.map((c: OmnlChainSummary) => (
<tr key={c.chainId} className="border-t border-[#2b3139] hover:bg-[#2b3139]/40">
<td className="py-1.5 pr-3 font-mono text-[#f0b90b]">{c.chainId}</td>
<td className="py-1.5 pr-3 text-[#eaecef]">{c.name}</td>
<td className="py-1.5 pr-3 text-xs text-[#848e9c]">{c.omnlRole}</td>
<td className="py-1.5 pr-3">
<span
className={`text-xs px-1.5 py-0.5 rounded ${
c.status === 'active'
? 'bg-[#0ecb81]/15 text-[#0ecb81]'
: c.status === 'staged'
? 'bg-[#f0b90b]/15 text-[#f0b90b]'
: 'bg-[#848e9c]/15 text-[#848e9c]'
}`}
>
{c.status}
</span>
</td>
<td className="py-1.5 pr-3">{c.settlement?.swapEnabled ? '✓' : '—'}</td>
<td className="py-1.5">{c.settlement?.bridgeEnabled ? '✓' : '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
);
}
import { useEffect, useState } from 'react';
import { fetchOmnlChainRegistry, type ChainRegistryResponse, type OmnlChainSummary } from '../../config/supported-chains';
import NasaIcon, { StatusIcon } from '../../components/icons/NasaIcon';
type Filter = 'all' | 'active' | 'primary' | 'l2';
export default function ChainMatrixPanel() {
const [registry, setRegistry] = useState<ChainRegistryResponse | null>(null);
const [filter, setFilter] = useState<Filter>('active');
const [search, setSearch] = useState('');
useEffect(() => {
fetchOmnlChainRegistry().then(setRegistry);
}, []);
const chains = (registry?.chains ?? []).filter((c) => {
if (filter === 'active' && c.status !== 'active') return false;
if (filter === 'primary' && c.omnlRole !== 'settlement-hub' && c.omnlRole !== 'mirror-target') return false;
if (filter === 'l2' && c.tier !== 'l2') return false;
if (search) {
const q = search.toLowerCase();
return c.name.toLowerCase().includes(q) || String(c.chainId).includes(q) || c.nativeSymbol.toLowerCase().includes(q);
}
return true;
});
return (
<section className="dash-card cb-card lg:col-span-2">
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
<div>
<h2 className="dash-section-title flex items-center gap-2">
<NasaIcon name="network" size={14} className="nasa-icon--telemetry" />
128-chain network matrix
</h2>
<p className="text-xs text-[#7a9bb8] font-mono mt-1">
{registry?.stats?.active ?? 0} active · {registry?.stats?.staged ?? 0} staged ·{' '}
{registry?.stats?.reserved ?? 0} reserved · primary {registry?.primaryChainId ?? 138}
</p>
</div>
<div className="flex items-center gap-2">
<NasaIcon name="search" size={14} className="nasa-icon--telemetry" />
<input
type="search"
placeholder="Search chain…"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="nasa-input !min-h-[36px] !w-48 text-sm"
/>
</div>
</div>
<div className="flex gap-2 mb-4 flex-wrap">
{(['all', 'active', 'primary', 'l2'] as Filter[]).map((f) => (
<button
key={f}

View File

@@ -1,166 +1,151 @@
import { Link } from 'react-router-dom';
import { useOffice24 } from './useOffice24';
import FineractLedgerBanner from '../omnl-dashboard/FineractLedgerBanner';
import DashboardStatCard from '../omnl-dashboard/DashboardStatCard';
import DashboardSection from '../omnl-dashboard/DashboardSection';
import QuickPortalLinks from '../omnl-dashboard/QuickPortalLinks';
import { formatUsd } from '../../utils/formatMoney';
export default function Office24Dashboard() {
const { office, tokens, health, moneySupply, loading, refresh } = useOffice24();
const profile = office as {
officeId?: number;
externalId?: string;
name?: string;
locked?: boolean;
tenantUser?: string;
settlement?: { defaultCurrency?: string; moneyLayers?: string[] };
rails?: { swift?: { enabled?: boolean }; hybx?: { enabled?: boolean }; chain138?: { enabled?: boolean } };
} | null;
return (
<div className="dash-page o24-page max-w-[1400px] mx-auto px-4 py-6">
<header className="mb-6 flex flex-wrap items-start justify-between gap-4">
<div>
<div className="flex flex-wrap items-center gap-2 mb-3">
<span className="dash-badge dash-badge--green">Office 24</span>
{profile?.locked && <span className="dash-badge dash-badge--gold">Single office · locked</span>}
</div>
<h1 className="dash-page-title">{profile?.name ?? 'HOSPITALLERS Ali Iraq-Iran'}</h1>
<p className="dash-page-subtitle font-mono text-sm">{profile?.externalId ?? 'HOSPITALLERS-ALI-IRAQ-IRAN'}</p>
</div>
<div className="flex flex-wrap gap-2">
<Link
to="/central-bank"
className="px-4 py-2.5 rounded-md border border-[#474d57] text-[#eaecef] text-sm font-medium hover:border-[#f0b90b] hover:text-[#f0b90b]"
>
Central Bank
</Link>
<Link
to="/trade"
className="px-4 py-2.5 rounded-md bg-[#f0b90b] text-[#181a20] font-semibold text-sm hover:bg-[#fcd535]"
>
DBIS Trade
</Link>
</div>
</header>
<QuickPortalLinks />
<FineractLedgerBanner moneySupply={moneySupply} loading={loading} officeName={profile?.name} />
<h2 className="dash-section-title mb-1">Office balances</h2>
<p className="dash-section-subtitle mb-4">
Live Fineract · {profile?.settlement?.defaultCurrency ?? 'USD'}
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<DashboardStatCard
label="Office ID"
value={String(profile?.officeId ?? 24)}
hint="Fineract office · HOSPITALLERS Ali"
accent="green"
/>
<DashboardStatCard
label="M1 circulating"
value={formatUsd(moneySupply?.M1?.circulating)}
hint="GL 2100 · spendable M1 liability"
accent="green"
/>
<DashboardStatCard
label="Due from head office"
value={formatUsd(moneySupply?.interoffice?.dueFromHeadOffice)}
hint="GL 1410 · HO allocation receivable"
accent="blue"
/>
<DashboardStatCard
label="M2 tokens"
value={String(tokens.length)}
hint="Registered · swap · convert · transfer"
accent="gold"
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<DashboardSection
title="Settlement rails"
subtitle="Enabled payment and chain paths"
action={
<button type="button" onClick={() => refresh()} className="text-xs text-[#f0b90b] hover:underline">
{loading ? 'Refreshing…' : 'Refresh'}
</button>
}
>
<dl>
<div className="dash-kv">
<dt>SWIFT</dt>
<dd className={profile?.rails?.swift?.enabled ? 'dash-kv-value--ok' : undefined}>
{profile?.rails?.swift?.enabled ? 'Enabled' : 'Off'}
</dd>
</div>
<div className="dash-kv">
<dt>HYBX</dt>
<dd className={profile?.rails?.hybx?.enabled ? 'dash-kv-value--ok' : undefined}>
{profile?.rails?.hybx?.enabled ? 'Enabled' : 'Off'}
</dd>
</div>
<div className="dash-kv">
<dt>Chain 138</dt>
<dd className={profile?.rails?.chain138?.enabled ? 'dash-kv-value--ok' : undefined}>
{profile?.rails?.chain138?.enabled ? 'Enabled' : 'Off'}
</dd>
</div>
<div className="dash-kv">
<dt>DBIS Exchange</dt>
<dd className="dash-kv-value--ok">{(health as { status?: string })?.status ?? 'ok'}</dd>
</div>
<div className="dash-kv">
<dt>Fineract tenant</dt>
<dd className="text-xs">{profile?.tenantUser ?? 'ali_hospitallers_tenant'}</dd>
</div>
</dl>
</DashboardSection>
<DashboardSection
title="M2 token registry"
subtitle="Tradable lines on Chain 138"
className="lg:col-span-2"
>
<div className="overflow-x-auto max-h-[420px] overflow-y-auto rounded-md border border-[#2b3139]">
<table className="w-full text-sm">
<thead className="sticky top-0 bg-[#181a20]">
<tr className="text-[#848e9c] text-left text-xs uppercase">
<th className="py-2.5 px-3">Symbol</th>
<th className="py-2.5 px-3">Line</th>
<th className="py-2.5 px-3">Swap</th>
<th className="py-2.5 px-3">Convert</th>
<th className="py-2.5 px-3">Transfer</th>
</tr>
</thead>
<tbody>
{tokens.map((t) => (
<tr key={t.address} className="border-t border-[#2b3139] hover:bg-[#2b3139]/40">
<td className="py-2.5 px-3 font-medium text-[#eaecef]">{t.symbol}</td>
<td className="py-2.5 px-3 text-[#848e9c] text-xs">{t.omnlLine ?? 'M2'}</td>
<td className="py-2.5 px-3 text-[#0ecb81]">{t.swappable !== false ? 'Yes' : '—'}</td>
<td className="py-2.5 px-3 text-[#0ecb81]">{t.convertible !== false ? 'Yes' : '—'}</td>
<td className="py-2.5 px-3 text-[#0ecb81]">
{t.transferableInternal && t.transferableExternal ? 'Internal + external' : 'Yes'}
</td>
</tr>
))}
{!tokens.length && (
<tr>
<td colSpan={5} className="py-6 px-3 text-center text-[#848e9c]">
No M2 tokens loaded
</td>
</tr>
)}
</tbody>
</table>
</div>
</DashboardSection>
</div>
</div>
);
}
import { Link } from 'react-router-dom';
import { useOffice24 } from './useOffice24';
import FineractLedgerBanner from '../omnl-dashboard/FineractLedgerBanner';
import DashboardStatCard from '../omnl-dashboard/DashboardStatCard';
import DashboardSection from '../omnl-dashboard/DashboardSection';
import QuickPortalLinks from '../omnl-dashboard/QuickPortalLinks';
import NasaPageHeader from '../omnl-dashboard/NasaPageHeader';
import NasaIcon, { StatusIcon } from '../../components/icons/NasaIcon';
import { formatUsd } from '../../utils/formatMoney';
export default function Office24Dashboard() {
const { office, tokens, health, moneySupply, loading, refresh } = useOffice24();
const profile = office as {
officeId?: number;
externalId?: string;
name?: string;
locked?: boolean;
tenantUser?: string;
settlement?: { defaultCurrency?: string; moneyLayers?: string[] };
rails?: { swift?: { enabled?: boolean }; hybx?: { enabled?: boolean }; chain138?: { enabled?: boolean } };
} | null;
return (
<div className="dash-page o24-page max-w-[1400px] mx-auto px-4 py-6">
<div className="flex flex-wrap items-start justify-between gap-4 mb-2">
<div className="flex-1 min-w-0">
<NasaPageHeader
icon="office"
missionId="O24-138"
title={profile?.name ?? 'HOSPITALLERS Ali Iraq-Iran'}
subtitle={profile?.externalId ?? 'HOSPITALLERS-ALI-IRAQ-IRAN'}
badges={
<>
<span className="nasa-badge nasa-badge--nominal">Office 24</span>
{profile?.locked && <span className="nasa-badge nasa-badge--caution">Single office · locked</span>}
</>
}
/>
</div>
<div className="flex flex-wrap gap-2 pt-2">
<Link to="/central-bank" className="nasa-btn inline-flex items-center gap-1">
<NasaIcon name="central-bank" size={12} /> Central Bank
</Link>
<Link to="/trade" className="nasa-btn nasa-btn--primary inline-flex items-center gap-1">
<NasaIcon name="trade" size={12} /> DBIS Trade
</Link>
</div>
</div>
<QuickPortalLinks />
<FineractLedgerBanner moneySupply={moneySupply} loading={loading} officeName={profile?.name} />
<h2 className="dash-section-title mb-1 flex items-center gap-2">
<NasaIcon name="ledger" size={14} className="nasa-icon--telemetry" />
Office balances
</h2>
<p className="dash-section-subtitle mb-4">Live Fineract · {profile?.settlement?.defaultCurrency ?? 'USD'}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<DashboardStatCard label="Office ID" value={String(profile?.officeId ?? 24)} hint="Fineract office · HOSPITALLERS Ali" accent="green" icon="office" />
<DashboardStatCard label="M1 circulating" value={formatUsd(moneySupply?.M1?.circulating)} hint="GL 2100 · spendable M1 liability" accent="green" icon="money" />
<DashboardStatCard label="Due from head office" value={formatUsd(moneySupply?.interoffice?.dueFromHeadOffice)} hint="GL 1410 · HO allocation receivable" accent="blue" icon="rail" />
<DashboardStatCard label="M2 tokens" value={String(tokens.length)} hint="Registered · swap · convert · transfer" accent="gold" icon="rocket" />
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<DashboardSection
title="Settlement rails"
subtitle="Enabled payment and chain paths"
icon="network"
action={
<button type="button" onClick={() => refresh()} className="text-xs text-[#00d4ff] hover:underline inline-flex items-center gap-1">
<NasaIcon name="refresh" size={12} className={loading ? 'nasa-icon--spin' : ''} />
{loading ? 'Refreshing…' : 'Refresh'}
</button>

View File

@@ -1,28 +1,35 @@
import type { ReactNode } from 'react';
export default function DashboardSection({
title,
subtitle,
action,
children,
className = '',
}: {
title: string;
subtitle?: string;
action?: React.ReactNode;
children: ReactNode;
className?: string;
}) {
return (
<section className={`dash-card ${className}`}>
<div className="flex flex-wrap items-start justify-between gap-3 mb-4">
<div>
<h2 className="dash-section-title">{title}</h2>
{subtitle && <p className="dash-section-subtitle">{subtitle}</p>}
</div>
{action}
</div>
{children}
</section>
);
}
import type { ReactNode } from 'react';
import type { NasaIconName } from '../../components/icons/NasaIcon';
import NasaIcon from '../../components/icons/NasaIcon';
export default function DashboardSection({
title,
subtitle,
action,
children,
className = '',
icon,
}: {
title: string;
subtitle?: string;
action?: ReactNode;
children: ReactNode;
className?: string;
icon?: NasaIconName;

View File

@@ -1,28 +1,36 @@
type Accent = 'gold' | 'green' | 'blue' | 'neutral';
const ACCENT: Record<Accent, string> = {
blue: 'border-l-[#3861fb]',
green: 'border-l-[#0ecb81]',
gold: 'border-l-[#f0b90b]',
neutral: 'border-l-[#848e9c]',
};
export default function DashboardStatCard({
label,
value,
hint,
accent = 'neutral',
}: {
label: string;
value: string;
hint?: string;
accent?: Accent;
}) {
return (
<div className={`dash-card border-l-4 ${ACCENT[accent]}`}>
<p className="dash-stat-label">{label}</p>
<p className="dash-stat-value">{value}</p>
{hint && <p className="dash-stat-hint">{hint}</p>}
</div>
);
}
import type { NasaIconName } from '../../components/icons/NasaIcon';
import NasaIcon from '../../components/icons/NasaIcon';
type Accent = 'gold' | 'green' | 'blue' | 'neutral';
const ACCENT: Record<Accent, string> = {
blue: 'border-l-[#00d4ff]',
green: 'border-l-[#3dff8a]',
gold: 'border-l-[#ffb347]',
neutral: 'border-l-[#7a9bb8]',
};
export default function DashboardStatCard({
label,
value,
hint,
accent = 'neutral',
icon,

View File

@@ -1,72 +1,99 @@
import type { MoneySupply } from '../central-bank/useCentralBank';
type Props = {
moneySupply: MoneySupply | null;
loading?: boolean;
officeName?: string;
};
function formatAsOf(asOf?: string) {
if (!asOf) return null;
return asOf.slice(0, 19).replace('T', ' ') + ' UTC';
}
export default function FineractLedgerBanner({ moneySupply, loading, officeName }: Props) {
if (loading && !moneySupply) {
return (
<div className="dash-banner dash-banner--loading mb-6">
<p className="dash-banner__title">Loading live ledger balances</p>
<p className="dash-banner__detail">Fetching Fineract data for Office 24</p>
</div>
);
}
if (!moneySupply) {
return (
<div className="dash-banner dash-banner--warn mb-6">
<p className="dash-banner__title">Ledger data not available</p>
<p className="dash-banner__detail">
The money-supply API did not respond. Check settlement middleware on port 3011.
</p>
</div>
);
}
const asOf = formatAsOf(moneySupply.asOf);
const officeLabel = officeName ?? `Office ${moneySupply.officeId ?? 24}`;
if (moneySupply.fineractConnected === false) {
return (
<div className="dash-banner dash-banner--error mb-6">
<p className="dash-banner__title">Fineract offline</p>
<p className="dash-banner__detail">
Cannot reach Fineract for {officeLabel}. Verify OMNL_FINERACT_* credentials on the
settlement service.
</p>
</div>
);
}
if (moneySupply.fineractConnected && !moneySupply.fineractLive) {
return (
<div className="dash-banner dash-banner--warn mb-6">
<p className="dash-banner__title">Fineract connected opening journal pending</p>
<p className="dash-banner__detail">
{officeLabel} has no M1 allocation yet. Post the opening entry: Debit GL 1410 (Due from
Head Office) · Credit GL 2100 (M1 liabilities).
{asOf ? ` · Checked ${asOf}` : ''}
</p>
</div>
);
}
return (
<div className="dash-banner dash-banner--ok mb-6">
<p className="dash-banner__title">Live Fineract ledger · {officeLabel}</p>
<p className="dash-banner__detail">
M0 / M1 / M2 balances below are read from Fineract in real time.
{asOf ? ` Last updated ${asOf}.` : ''}
</p>
</div>
);
}
import type { MoneySupply } from '../central-bank/useCentralBank';
import NasaIcon from '../../components/icons/NasaIcon';
type Props = {
moneySupply: MoneySupply | null;
loading?: boolean;
officeName?: string;
};
function formatAsOf(asOf?: string) {
if (!asOf) return null;
return asOf.slice(0, 19).replace('T', ' ') + ' UTC';
}
function BannerIcon({ variant }: { variant: 'ok' | 'warn' | 'error' | 'loading' }) {
const map = {
ok: { name: 'check' as const, className: 'nasa-icon--nominal' },
warn: { name: 'alert' as const, className: 'nasa-icon--caution' },
error: { name: 'x' as const, className: 'nasa-icon--critical' },
loading: { name: 'loading' as const, className: 'nasa-icon--telemetry nasa-icon--spin' },
};
const { name, className } = map[variant];
return <NasaIcon name={name} size={18} className={`shrink-0 mt-0.5 ${className}`} />;
}
export default function FineractLedgerBanner({ moneySupply, loading, officeName }: Props) {
if (loading && !moneySupply) {
return (
<div className="dash-banner dash-banner--loading mb-6">
<BannerIcon variant="loading" />
<div>
<p className="dash-banner__title">Loading live ledger balances</p>
<p className="dash-banner__detail">Fetching Fineract data for Office 24</p>
</div>
</div>
);
}
if (!moneySupply) {
return (
<div className="dash-banner dash-banner--warn mb-6">
<BannerIcon variant="warn" />
<div>
<p className="dash-banner__title">Ledger data not available</p>
<p className="dash-banner__detail">
The money-supply API did not respond. Check settlement middleware on port 3011.
</p>
</div>
</div>
);

View File

@@ -0,0 +1,49 @@
import { useEffect, useState, type ReactNode } from 'react';
import type { NasaIconName } from '../../components/icons/NasaIcon';
import NasaIcon from '../../components/icons/NasaIcon';
type Props = {
icon: NasaIconName;
missionId: string;
title: string;
subtitle?: string;
badges?: ReactNode;
};
function missionClock() {
const now = new Date();
return now.toISOString().slice(11, 19) + ' UTC';
}
export default function NasaPageHeader({ icon, missionId, title, subtitle, badges }: Props) {
const [clock, setClock] = useState(missionClock);
useEffect(() => {
const id = setInterval(() => setClock(missionClock()), 1000);
return () => clearInterval(id);
}, []);
return (
<header className="nasa-header mb-6">
<div className="nasa-header__meta">
<span className="nasa-mission-id">
<NasaIcon name="satellite" size={12} />
{missionId}
</span>
<span>MET {clock}</span>
</div>
<div className="nasa-header__body">
<div className="nasa-header__title-row">
<div className="nasa-header__icon-wrap">
<NasaIcon name={icon} size={26} className="nasa-icon--telemetry" />
</div>
<div>
<h1 className="nasa-title">{title}</h1>
{subtitle && <p className="dash-page-subtitle !mt-1">{subtitle}</p>}
</div>
</div>
{badges && <div className="flex flex-wrap items-center gap-2">{badges}</div>}
</div>
</header>
);
}

View File

@@ -1,28 +1,28 @@
const LINKS = [
{ label: 'Central Bank', href: 'https://secure.omdnl.org/central-bank' },
{ label: 'Online banking', href: 'https://online.omdnl.org/central-bank' },
{ label: 'Office 24', href: 'https://office24.omdnl.org/office-24' },
{ label: 'DBIS Trade', href: 'https://exchange.omdnl.org/trade' },
{ label: 'Digital swap', href: 'https://digital.omdnl.org/swap' },
] as const;
export default function QuickPortalLinks() {
return (
<div className="dash-quick-links mb-8">
<p className="dash-quick-links__label">Public portals</p>
<div className="flex flex-wrap gap-2">
{LINKS.map(({ label, href }) => (
<a
key={href}
href={href}
target="_blank"
rel="noopener noreferrer"
className="dash-quick-links__chip"
>
{label}
</a>
))}
</div>
</div>
);
}
import NasaIcon from '../../components/icons/NasaIcon';
const LINKS = [
{ label: 'Central Bank', href: 'https://secure.omdnl.org/central-bank', icon: 'central-bank' as const },
{ label: 'Online banking', href: 'https://online.omdnl.org/central-bank', icon: 'globe' as const },
{ label: 'Office 24', href: 'https://office24.omdnl.org/office-24', icon: 'office' as const },
{ label: 'DBIS Trade', href: 'https://exchange.omdnl.org/trade', icon: 'trade' as const },
{ label: 'Digital swap', href: 'https://digital.omdnl.org/swap', icon: 'swap' as const },
] as const;
export default function QuickPortalLinks() {
return (
<div className="dash-quick-links mb-8">
<p className="dash-quick-links__label flex items-center gap-1.5">

View File

@@ -1,6 +1,8 @@
import { useEffect } from 'react';
import { useEffect, type ReactNode } from 'react';
import { useDbisSwap } from './useDbisSwap';
import { formatAmountFromWei } from '../../config/dex';
import NasaPageHeader from '../omnl-dashboard/NasaPageHeader';
import NasaIcon from '../../components/icons/NasaIcon';
function TokenSelect({
label,
@@ -15,9 +17,9 @@ function TokenSelect({
}) {
return (
<label className="block">
<span className="text-xs ui-muted uppercase tracking-wide mb-1 block">{label}</span>
<span className="dash-stat-label mb-1 block">{label}</span>
<select
className="ui-input w-full !min-h-[48px] font-semibold"
className="nasa-select"
value={value.address}
onChange={(e) => {
const t = tokens.find((x) => x.address === e.target.value);
@@ -34,6 +36,26 @@ function TokenSelect({
);
}
function SidePanel({
title,
icon,
children,
}: {
title: string;
icon: 'ledger' | 'rocket' | 'rail' | 'chart' | 'satellite';
children: ReactNode;
}) {
return (
<div className="dash-card">
<h3 className="dash-section-title mb-3 flex items-center gap-2">
<NasaIcon name={icon} size={14} className="nasa-icon--telemetry" />
{title}
</h3>
{children}
</div>
);
}
export default function DBISSwapPanel() {
const swap = useDbisSwap();
@@ -53,52 +75,57 @@ export default function DBISSwapPanel() {
: '—';
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
<div className="lg:col-span-2">
<div className="ui-card p-6 border border-[var(--ui-border)]">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold ui-accent-text">DBIS Exchange</h2>
<span className="text-xs ui-muted px-2 py-1 ui-inset rounded">Office 24 · M2 · {swap.tokens.length} tokens</span>
</div>
<div className="nasa-page px-4 py-6 max-w-6xl mx-auto">
<NasaPageHeader
icon="swap"
missionId="SWP-138"
title="Digital Swap Console"
subtitle={`M2 asset exchange · Office 24 · ${swap.tokens.length} registered tokens`}
badges={<span className="nasa-badge nasa-badge--telemetry">Chain 138</span>}
/>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 dash-card">
<div className="space-y-4">
<TokenSelect label="From" value={swap.tokenIn} onChange={swap.setTokenIn} tokens={swap.tokens} />
<TokenSelect label="Source asset" value={swap.tokenIn} onChange={swap.setTokenIn} tokens={swap.tokens} />
<div className="flex justify-center">
<button
type="button"
className="ui-btn-secondary !rounded-full !p-2"
className="nasa-btn !rounded-full !p-2.5"
onClick={() => {
const a = swap.tokenIn;
swap.setTokenIn(swap.tokenOut);
swap.setTokenOut(a);
}}
aria-label="Swap tokens"
aria-label="Invert swap pair"
>
<NasaIcon name="swap" size={18} />
</button>
</div>
<TokenSelect label="To" value={swap.tokenOut} onChange={swap.setTokenOut} tokens={swap.tokens} />
<TokenSelect label="Target asset" value={swap.tokenOut} onChange={swap.setTokenOut} tokens={swap.tokens} />
<label className="block">
<span className="text-xs ui-muted uppercase tracking-wide mb-1 block">Amount</span>
<span className="dash-stat-label mb-1 block">Amount</span>
<input
type="text"
inputMode="decimal"
placeholder="0.0"
value={swap.amountIn}
onChange={(e) => swap.setAmountIn(e.target.value)}
className="ui-input w-full !min-h-[56px] text-2xl font-semibold"
className="nasa-input !min-h-[56px] !text-2xl"
/>
</label>
<div className="flex items-center gap-3 text-sm">
<span className="ui-muted">Slippage</span>
<div className="flex items-center gap-3 text-sm font-mono">
<span className="text-[#7a9bb8] uppercase text-xs">Slippage</span>
{[25, 50, 100].map((bps) => (
<button
key={bps}
type="button"
className={`px-3 py-1 rounded text-xs font-medium ${
swap.slippageBps === bps ? 'ui-btn-primary' : 'ui-btn-secondary'
className={`px-3 py-1 text-xs border font-mono ${
swap.slippageBps === bps
? 'border-[#00d4ff] text-[#00d4ff] bg-[#00d4ff]/10'
: 'border-[#1e4a8a] text-[#7a9bb8]'
}`}
onClick={() => swap.setSlippageBps(bps)}
>
@@ -107,13 +134,13 @@ export default function DBISSwapPanel() {
))}
</div>
<div className="ui-inset p-4 rounded-lg">
<p className="text-sm ui-muted mb-1">Expected output</p>
<p className="text-2xl font-bold">
{outDisplay} <span className="text-base ui-accent-text">{swap.tokenOut.symbol}</span>
<div className="nasa-inset-panel">
<p className="dash-stat-label">Expected output</p>
<p className="dash-stat-value !text-2xl">
{outDisplay} <span className="text-base text-[#00d4ff]">{swap.tokenOut.symbol}</span>
</p>
{swap.plan?.plannerResponse?.routePlan?.legs && (
<p className="text-xs ui-muted mt-2">
<p className="text-xs text-[#7a9bb8] mt-2 font-mono">
Route:{' '}
{swap.plan.plannerResponse.routePlan.legs
.map((l: { provider?: string }) => l.provider)
@@ -123,174 +150,107 @@ export default function DBISSwapPanel() {
)}
</div>
{swap.error && <p className="text-sm text-[var(--ui-danger)]">{swap.error}</p>}
{swap.error && (
<p className="text-sm text-[#fc3d21] flex items-center gap-1">
<NasaIcon name="alert" size={14} />
{swap.error}
</p>
)}
{swap.txHash && (
<p className="text-xs ui-muted break-all">
<p className="text-xs text-[#7a9bb8] break-all font-mono">
Tx:{' '}
<a
href={`https://explorer.d-bis.org/tx/${swap.txHash}`}
target="_blank"
rel="noreferrer"
className="ui-accent-text hover:underline"
>
<a href={`https://explorer.d-bis.org/tx/${swap.txHash}`} target="_blank" rel="noreferrer" className="text-[#00d4ff] hover:underline">
{swap.txHash}
</a>
</p>
)}
<div className="flex gap-3 pt-2">
<button
type="button"
className="ui-btn-secondary flex-1 !min-h-[48px]"
disabled={swap.loading || !swap.amountIn}
onClick={() => swap.fetchPlan()}
>
{swap.loading ? '…' : 'Get quote'}
<button type="button" className="nasa-btn flex-1" disabled={swap.loading || !swap.amountIn} onClick={() => swap.fetchPlan()}>
<NasaIcon name="chart" size={14} />
{swap.loading ? 'Computing…' : 'Get quote'}
</button>
<button
type="button"
className="ui-btn-primary flex-1 !min-h-[48px] font-bold"
className="nasa-btn nasa-btn--primary flex-1"
disabled={swap.loading || !swap.isConnected || !swap.amountIn}
onClick={() => swap.executeSwap()}
>
{!swap.isConnected ? 'Connect wallet' : swap.loading ? 'Swapping…' : 'Swap'}
<NasaIcon name="wallet" size={14} />
{!swap.isConnected ? 'Connect wallet' : swap.loading ? 'Executing…' : 'Execute swap'}
</button>
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div className="ui-card p-4">
<h3 className="font-semibold mb-3 text-sm ui-accent-text">OMNL Office 24 ledger</h3>
{!swap.isConnected ? (
<p className="text-sm ui-muted">Connect wallet for crypto balances</p>
) : swap.ledger ? (
<div className="text-xs space-y-2">
<p>
<span className="ui-muted">M1 circulating:</span>{' '}
{(swap.ledger as { fundLedger?: { M1?: { circulating?: string } } }).fundLedger?.M1?.circulating ?? '—'}
</p>
<p>
<span className="ui-muted">M2 broad:</span>{' '}
{(swap.ledger as { fundLedger?: { M2?: { broadMoney?: string } } }).fundLedger?.M2?.broadMoney ?? '—'}
</p>
{(swap.ledger as { cryptoLedger?: { balances?: Record<string, string> } }).cryptoLedger?.balances && (
<div className="mt-2 pt-2 border-t border-[var(--ui-border)]">
<p className="ui-muted mb-1">Wallet balances</p>
{Object.entries(
(swap.ledger as { cryptoLedger: { balances: Record<string, string> } }).cryptoLedger.balances,
).map(([sym, bal]) => (
<p key={sym}>
{sym}: {Number(bal).toFixed(4)}
</p>
))}
</div>
)}
</div>
) : (
<button type="button" className="ui-btn-secondary text-xs w-full" onClick={() => swap.loadLedgers()}>
Load ledgers
<div className="space-y-4">
<SidePanel title="Office 24 ledger" icon="ledger">
{!swap.isConnected ? (
<p className="text-sm text-[#7a9bb8]">Connect wallet for crypto balances</p>
) : swap.ledger ? (
<div className="text-xs space-y-2 font-mono">
<p>M1: {(swap.ledger as { fundLedger?: { M1?: { circulating?: string } } }).fundLedger?.M1?.circulating ?? '—'}</p>
<p>M2: {(swap.ledger as { fundLedger?: { M2?: { broadMoney?: string } } }).fundLedger?.M2?.broadMoney ?? '—'}</p>
{(swap.ledger as { cryptoLedger?: { balances?: Record<string, string> } }).cryptoLedger?.balances && (
<div className="mt-2 pt-2 border-t border-[#1e4a8a]">
<p className="text-[#7a9bb8] mb-1">Wallet balances</p>
{Object.entries(
(swap.ledger as { cryptoLedger: { balances: Record<string, string> } }).cryptoLedger.balances,
).map(([sym, bal]) => (
<p key={sym}>{sym}: {Number(bal).toFixed(4)}</p>
))}
</div>
)}
</div>
) : (
<button type="button" className="nasa-btn w-full text-xs" onClick={() => swap.loadLedgers()}>
Load ledgers
</button>
)}
</SidePanel>
<SidePanel title="M2 token load" icon="rocket">
<p className="text-xs text-[#7a9bb8] mb-3">GL 2200 on-chain M2</p>
<input type="text" inputMode="decimal" placeholder="Fiat amount" value={swap.loadAmount} onChange={(e) => swap.setLoadAmount(e.target.value)} className="nasa-input mb-2 text-sm" />
<button type="button" className="nasa-btn nasa-btn--primary w-full text-xs" disabled={swap.loading || !swap.isConnected || !swap.loadAmount} onClick={() => swap.loadM2Token()}>
Load {swap.tokenOut.symbol}
</button>
)}
</div>
</SidePanel>
<div className="ui-card p-4">
<h3 className="font-semibold mb-3 text-sm ui-accent-text">M2 token load</h3>
<p className="text-xs ui-muted mb-3">Load from M2 fiat (GL 2200) on-chain · swappable · convertible</p>
<input
type="text"
inputMode="decimal"
placeholder="Amount (fiat)"
value={swap.loadAmount}
onChange={(e) => swap.setLoadAmount(e.target.value)}
className="ui-input w-full mb-2 text-sm"
/>
<button
type="button"
className="ui-btn-primary text-xs w-full !min-h-[40px]"
disabled={swap.loading || !swap.isConnected || !swap.loadAmount}
onClick={() => swap.loadM2Token()}
>
Load {swap.tokenOut.symbol} from M2
</button>
</div>
<div className="ui-card p-4">
<h3 className="font-semibold mb-3 text-sm">Transfer</h3>
<input
type="text"
placeholder="Recipient (0x… or address)"
value={swap.transferTo}
onChange={(e) => swap.setTransferTo(e.target.value)}
className="ui-input w-full mb-2 text-sm font-mono"
/>
<input
type="text"
inputMode="decimal"
placeholder="Amount"
value={swap.transferAmount}
onChange={(e) => swap.setTransferAmount(e.target.value)}
className="ui-input w-full mb-2 text-sm"
/>
<div className="flex gap-2 mb-2">
<button
type="button"
className="ui-btn-secondary text-xs flex-1 !min-h-[36px]"
disabled={swap.loading || !swap.isConnected}
onClick={() => swap.internalTransfer()}
>
Internal
<SidePanel title="Transfer" icon="rail">
<input type="text" placeholder="Recipient 0x…" value={swap.transferTo} onChange={(e) => swap.setTransferTo(e.target.value)} className="nasa-input mb-2 text-sm" />
<input type="text" inputMode="decimal" placeholder="Amount" value={swap.transferAmount} onChange={(e) => swap.setTransferAmount(e.target.value)} className="nasa-input mb-2 text-sm" />
<button type="button" className="nasa-btn w-full text-xs mb-2" disabled={swap.loading || !swap.isConnected} onClick={() => swap.internalTransfer()}>
Internal transfer
</button>
</div>
<input
type="text"
placeholder="External IBAN (SWIFT)"
value={swap.externalIban}
onChange={(e) => swap.setExternalIban(e.target.value)}
className="ui-input w-full mb-2 text-sm font-mono"
/>
<button
type="button"
className="ui-btn-secondary text-xs w-full !min-h-[36px]"
disabled={swap.loading || !swap.isConnected || !swap.externalIban}
onClick={() => swap.externalTransfer()}
>
External (SWIFT)
</button>
</div>
<input type="text" placeholder="External IBAN (SWIFT)" value={swap.externalIban} onChange={(e) => swap.setExternalIban(e.target.value)} className="nasa-input mb-2 text-sm" />
<button type="button" className="nasa-btn w-full text-xs" disabled={swap.loading || !swap.isConnected || !swap.externalIban} onClick={() => swap.externalTransfer()}>
External (SWIFT)
</button>
</SidePanel>
{swap.lastSettlement && (
<div className="ui-card p-4">
<h3 className="font-semibold mb-2 text-sm">Last settlement</h3>
<p className="text-xs ui-muted break-all">
{(swap.lastSettlement as { phase?: string }).phase ?? 'OK'} ·{' '}
{(swap.lastSettlement as { settlementId?: string }).settlementId ?? '—'}
</p>
</div>
)}
<div className="ui-card p-4">
<h3 className="font-semibold mb-3 text-sm">Swap monitor</h3>
{swap.monitor ? (
<div className="text-xs space-y-2">
<p>
Total: {(swap.monitor as { stats?: { total?: number } }).stats?.total ?? 0} · 24h:{' '}
{(swap.monitor as { stats?: { last24h?: number } }).stats?.last24h ?? 0}
{swap.lastSettlement && (
<SidePanel title="Last settlement" icon="satellite">
<p className="text-xs text-[#7a9bb8] font-mono break-all">
{(swap.lastSettlement as { phase?: string }).phase ?? 'OK'} · {(swap.lastSettlement as { settlementId?: string }).settlementId ?? '—'}
</p>
<ul className="max-h-40 overflow-y-auto space-y-1">
{((swap.monitor as { swaps?: { tokenIn: string; status: string; createdAt: string }[] }).swaps ?? [])
.slice(0, 5)
.map((s, i) => (
<li key={i} className="ui-muted truncate">
{s.status} · {s.createdAt.slice(0, 16)}
</li>
))}
</ul>
</div>
) : (
<p className="text-xs ui-muted">Loading</p>
</SidePanel>
)}
<SidePanel title="Swap monitor" icon="chart">
{swap.monitor ? (
<div className="text-xs space-y-2 font-mono">
<p>Total: {(swap.monitor as { stats?: { total?: number } }).stats?.total ?? 0} · 24h: {(swap.monitor as { stats?: { last24h?: number } }).stats?.last24h ?? 0}</p>
<ul className="max-h-40 overflow-y-auto space-y-1 text-[#7a9bb8]">
{((swap.monitor as { swaps?: { tokenIn: string; status: string; createdAt: string }[] }).swaps ?? []).slice(0, 5).map((s, i) => (
<li key={i} className="truncate">{s.status} · {s.createdAt.slice(0, 16)}</li>
))}
</ul>
</div>
) : (
<p className="text-xs text-[#7a9bb8]">Loading telemetry</p>
)}
</SidePanel>
</div>
</div>
</div>

View File

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useDbisSwap } from '../swap/useDbisSwap';
import { formatAmountFromWei, type DexToken } from '../../config/dex';
import TokenIcon from '../../components/ui/TokenIcon';
import NasaIcon from '../../components/icons/NasaIcon';
type Side = 'buy' | 'sell';
@@ -24,11 +25,12 @@ function MarketRow({
active ? 'trade-market-row--active' : ''
}`}
>
<span className="font-medium text-[#eaecef]">
<span className="font-medium text-[#e8f4ff] flex items-center gap-1.5">
<NasaIcon name="money" size={12} className="nasa-icon--telemetry" />
{token.symbol}
<span className="text-[#848e9c]">/{quote}</span>
<span className="text-[#7a9bb8]">/{quote}</span>
</span>
<span className="text-[#0ecb81] text-xs">M2</span>
<span className="text-[#3dff8a] text-xs font-mono">M2</span>
</button>
);
}
@@ -42,7 +44,7 @@ function OrderBookMock({ price }: { price: string }) {
});
return (
<div className="trade-orderbook text-xs font-mono">
<div className="flex justify-between text-[#848e9c] px-2 py-1 border-b border-[#2b3139]">
<div className="flex justify-between text-[#7a9bb8] px-2 py-1 border-b border-[#1e4a8a]">
<span>Price</span>
<span>Amount</span>
</div>
@@ -113,20 +115,24 @@ export default function BinanceTradePanel() {
: '—';
return (
<div className="trade-layout">
<div className="trade-layout nasa-mission-root">
{/* Markets sidebar */}
<aside className="trade-markets hidden lg:flex flex-col border-r border-[#2b3139] bg-[#181a20]">
<div className="p-3 border-b border-[#2b3139]">
<aside className="trade-markets hidden lg:flex flex-col border-r border-[#1e4a8a]">
<div className="p-3 border-b border-[#1e4a8a] flex items-center gap-2">
<NasaIcon name="search" size={14} className="nasa-icon--telemetry shrink-0" />
<input
type="search"
placeholder="Search markets"
value={marketSearch}
onChange={(e) => setMarketSearch(e.target.value)}
className="w-full bg-[#2b3139] border border-[#474d57] rounded px-3 py-2 text-sm text-[#eaecef] placeholder:text-[#848e9c]"
className="nasa-input !min-h-[36px] !text-sm flex-1"
/>
</div>
<div className="flex-1 overflow-y-auto">
<p className="px-3 py-2 text-xs text-[#848e9c] uppercase">M2 · {markets.length} pairs</p>
<p className="px-3 py-2 text-xs text-[#7a9bb8] uppercase font-mono flex items-center gap-1.5">
<NasaIcon name="trade" size={12} className="nasa-icon--telemetry" />
M2 · {markets.length} pairs
</p>
{markets.map((t) => (
<MarketRow
key={t.address}
@@ -141,16 +147,25 @@ export default function BinanceTradePanel() {
{/* Chart + order book */}
<section className="trade-center flex flex-col min-w-0">
<div className="trade-pair-header flex flex-wrap items-center gap-4 px-4 py-3 border-b border-[#2b3139] bg-[#181a20]">
<div className="trade-pair-header flex flex-wrap items-center gap-4 px-4 py-3 border-b border-[#1e4a8a]">
<div className="flex items-center gap-2">
<TokenIcon symbol={swap.tokenOut.symbol} size={28} />
<h1 className="text-xl font-bold text-[#eaecef]">
{swap.tokenOut.symbol}/{quoteToken?.symbol ?? 'USDT'}
</h1>
<div>
<p className="text-[0.65rem] font-mono uppercase tracking-widest text-[#7a9bb8] flex items-center gap-1">
<NasaIcon name="trade" size={12} className="nasa-icon--telemetry" />
TRD-138 · Exchange terminal
</p>
<h1 className="text-xl font-bold text-[#e8f4ff] font-mono uppercase tracking-wide">
{swap.tokenOut.symbol}/{quoteToken?.symbol ?? 'USDT'}
</h1>
</div>
</div>
<span className="text-2xl font-semibold text-[#0ecb81]">{displayPrice}</span>
<span className="text-xs px-2 py-1 rounded bg-[#0ecb81]/10 text-[#0ecb81]">Office 24 · Chain 138</span>
<span className="text-xs text-[#848e9c]">37 M2 tokens · tradable &amp; swappable</span>
<span className="text-2xl font-semibold text-[#3dff8a] font-mono">{displayPrice}</span>
<span className="nasa-badge nasa-badge--nominal text-xs">Office 24 · Chain 138</span>
<span className="text-xs text-[#7a9bb8] font-mono flex items-center gap-1">
<NasaIcon name="satellite" size={12} />
37 M2 tokens · tradable &amp; swappable
</span>
</div>
<div className="flex-1 grid grid-cols-1 xl:grid-cols-[1fr_280px] min-h-[320px]">

View File

@@ -5,6 +5,7 @@ import { EventEmitter } from 'events'
import App from './App.tsx'
import './index.css'
import './styles/mobile.css'
import './styles/nasa-dashboard.css'
// Polyfill Buffer / EventEmitter for browser (ethers, wallet libs)
type GlobalPolyfills = typeof globalThis & {

View File

@@ -1,13 +1,10 @@
import PageShell from '../components/ui/PageShell'
import DBISSwapPanel from '../features/swap/DBISSwapPanel'
export default function SwapPage() {
return (
<PageShell
title="DBIS Exchange"
subtitle="Classic swap UI · use DBIS Trade for Binance-style terminal"
>
<DBISSwapPanel />
</PageShell>
)
}
import { Link } from 'react-router-dom';
import DBISSwapPanel from '../features/swap/DBISSwapPanel';
export default function SwapPage() {
return (

View File

@@ -0,0 +1,423 @@
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Inter:wght@400;500;600;700&display=swap');
.omnl-app,
.nasa-mission-root {
--nasa-bg: #05070d;
--nasa-panel: #0a1224;
--nasa-panel-border: #1e4a8a;
--nasa-telemetry: #00d4ff;
--nasa-nominal: #3dff8a;
--nasa-caution: #ffb347;
--nasa-critical: #fc3d21;
--nasa-text: #e8f4ff;
--nasa-muted: #7a9bb8;
--nasa-mono: 'Share Tech Mono', ui-monospace, monospace;
}
.omnl-app {
background:
radial-gradient(ellipse at 20% 0%, rgba(11, 61, 145, 0.35) 0%, transparent 55%),
radial-gradient(ellipse at 80% 100%, rgba(0, 212, 255, 0.08) 0%, transparent 50%),
var(--nasa-bg);
color: var(--nasa-text);
}
.nasa-icon--telemetry { color: var(--nasa-telemetry); }
.nasa-icon--nominal { color: var(--nasa-nominal); }
.nasa-icon--critical { color: var(--nasa-critical); }
.nasa-icon--caution { color: var(--nasa-caution); }
.nasa-icon--spin { animation: nasa-spin 1s linear infinite; }
@keyframes nasa-spin {
to { transform: rotate(360deg); }
}
.dash-page, .o24-page, .nasa-page, .cb-page {
min-height: calc(100vh - 56px);
font-family: 'Inter', sans-serif;
}
.nasa-panel, .dash-card, .o24-card, .cb-card {
position: relative;
background: linear-gradient(180deg, rgba(10, 18, 36, 0.95) 0%, rgba(5, 10, 22, 0.98) 100%);
border: 1px solid var(--nasa-panel-border);
border-radius: 2px;
padding: 1.15rem 1.25rem;
box-shadow: inset 0 1px 0 rgba(0, 212, 255, 0.08), 0 4px 24px rgba(0, 0, 0, 0.45);
}
.dash-card::before {
content: '';
position: absolute;
top: 0; left: 0;
width: 10px; height: 10px;
border-top: 2px solid var(--nasa-telemetry);
border-left: 2px solid var(--nasa-telemetry);
opacity: 0.7;
}
.dash-card::after {
content: '';
position: absolute;
bottom: 0; right: 0;
width: 10px; height: 10px;
border-bottom: 2px solid var(--nasa-telemetry);
border-right: 2px solid var(--nasa-telemetry);
opacity: 0.7;
}
.nasa-header {
border: 1px solid var(--nasa-panel-border);
background: rgba(11, 61, 145, 0.12);
padding: 1rem 1.25rem;
}
.nasa-header__meta {
display: flex;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
font-family: var(--nasa-mono);
font-size: 0.7rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--nasa-muted);
}
.nasa-mission-id {
display: inline-flex;
align-items: center;
gap: 0.4rem;
color: var(--nasa-telemetry);
}
.nasa-header__body {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 1rem;
}
.nasa-header__title-row {
display: flex;
gap: 1rem;
}
.nasa-header__icon-wrap {
width: 52px; height: 52px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--nasa-panel-border);
background: rgba(0, 212, 255, 0.06);
}
.nasa-title, .dash-page-title {
font-family: var(--nasa-mono);
font-size: 1.65rem;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--nasa-text);
line-height: 1.2;
}
.dash-page-subtitle {
margin-top: 0.35rem;
color: var(--nasa-muted);
font-size: 0.9rem;
max-width: 48rem;
}
.nasa-badge, .dash-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.6rem;
border: 1px solid var(--nasa-panel-border);
font-family: var(--nasa-mono);
font-size: 0.65rem;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.nasa-badge--telemetry, .dash-badge--blue {
background: rgba(0, 212, 255, 0.1);
color: var(--nasa-telemetry);
border-color: rgba(0, 212, 255, 0.35);
}
.nasa-badge--nominal, .dash-badge--green {
background: rgba(61, 255, 138, 0.08);
color: var(--nasa-nominal);
border-color: rgba(61, 255, 138, 0.35);
}
.nasa-badge--caution, .dash-badge--gold {
background: rgba(255, 179, 71, 0.08);
color: var(--nasa-caution);
border-color: rgba(255, 179, 71, 0.35);
}
.dash-banner {
border-radius: 2px;
padding: 1rem 1.15rem;
border: 1px solid var(--nasa-panel-border);
display: flex;
gap: 0.75rem;
align-items: flex-start;
}
.dash-banner__title {
font-size: 0.9rem;
font-weight: 600;
font-family: var(--nasa-mono);
letter-spacing: 0.04em;
text-transform: uppercase;
margin-bottom: 0.25rem;
}
.dash-banner__detail {
font-size: 0.85rem;
color: var(--nasa-muted);
line-height: 1.45;
}
.dash-banner--ok {
background: rgba(61, 255, 138, 0.06);
border-color: rgba(61, 255, 138, 0.35);
}
.dash-banner--ok .dash-banner__title { color: var(--nasa-nominal); }
.dash-banner--warn {
background: rgba(255, 179, 71, 0.06);
border-color: rgba(255, 179, 71, 0.35);
}
.dash-banner--warn .dash-banner__title { color: var(--nasa-caution); }
.dash-banner--error {
background: rgba(252, 61, 33, 0.06);
border-color: rgba(252, 61, 33, 0.35);
}
.dash-banner--error .dash-banner__title { color: var(--nasa-critical); }
.dash-banner--loading {
background: rgba(10, 18, 36, 0.8);
}
.dash-stat-label {
font-family: var(--nasa-mono);
font-size: 0.65rem;
color: var(--nasa-muted);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 0.35rem;
}
.dash-stat-value {
font-family: var(--nasa-mono);
font-size: 1.55rem;
font-weight: 700;
color: var(--nasa-text);
font-variant-numeric: tabular-nums;
}
.dash-stat-hint {
margin-top: 0.35rem;
font-size: 0.75rem;
color: var(--nasa-muted);
}
.dash-section-title {
font-family: var(--nasa-mono);
font-size: 0.95rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--nasa-telemetry);
}
.dash-section-subtitle {
margin-top: 0.2rem;
font-size: 0.8rem;
color: var(--nasa-muted);
}
.dash-kv {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0.45rem 0;
font-size: 0.875rem;
border-bottom: 1px solid rgba(30, 74, 138, 0.35);
}
.dash-kv:last-child { border-bottom: none; }
.dash-kv dt { color: var(--nasa-muted); flex-shrink: 0; }
.dash-kv dd { color: var(--nasa-text); text-align: right; word-break: break-word; }
.dash-kv .dash-kv-value--ok { color: var(--nasa-nominal); font-weight: 500; }
.dash-quick-links__label {
font-family: var(--nasa-mono);
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--nasa-muted);
margin-bottom: 0.5rem;
}
.dash-quick-links__chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.75rem;
font-family: var(--nasa-mono);
font-size: 0.7rem;
letter-spacing: 0.04em;
color: var(--nasa-text);
background: rgba(10, 18, 36, 0.8);
border: 1px solid var(--nasa-panel-border);
transition: border-color 0.15s, color 0.15s;
}
.dash-quick-links__chip:hover {
border-color: var(--nasa-telemetry);
color: var(--nasa-telemetry);
}
.dash-btn, .nasa-btn {
padding: 0.55rem 1rem;
border: 1px solid var(--nasa-panel-border);
font-family: var(--nasa-mono);
font-size: 0.8rem;
letter-spacing: 0.04em;
text-transform: uppercase;
background: rgba(10, 18, 36, 0.9);
color: var(--nasa-text);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
transition: border-color 0.15s, background 0.15s;
}
.dash-btn:hover, .nasa-btn:hover {
border-color: var(--nasa-telemetry);
background: rgba(0, 212, 255, 0.08);
}
.nasa-btn--primary {
background: rgba(0, 212, 255, 0.15);
border-color: var(--nasa-telemetry);
color: var(--nasa-telemetry);
}
.nasa-btn--primary:hover {
background: rgba(0, 212, 255, 0.25);
}
.dash-btn:disabled, .nasa-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.nasa-input, .nasa-select {
width: 100%;
min-height: 44px;
padding: 0.5rem 0.75rem;
background: #05070d;
border: 1px solid var(--nasa-panel-border);
color: var(--nasa-text);
font-family: var(--nasa-mono);
font-size: 0.9rem;
}
.nasa-input:focus, .nasa-select:focus {
outline: none;
border-color: var(--nasa-telemetry);
box-shadow: 0 0 0 1px rgba(0, 212, 255, 0.25);
}
.nasa-inset-panel {
background: rgba(5, 7, 13, 0.6);
border: 1px solid rgba(30, 74, 138, 0.5);
padding: 1rem;
}
.omnl-app-header {
background: rgba(5, 7, 13, 0.95) !important;
border-color: var(--nasa-panel-border) !important;
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.5);
}
.nasa-nav-brand {
font-family: var(--nasa-mono);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--nasa-telemetry) !important;
}
.nasa-nav-link {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.45rem 0.85rem;
font-family: var(--nasa-mono);
font-size: 0.72rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--nasa-muted);
border: 1px solid transparent;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.nasa-nav-link:hover {
color: var(--nasa-text);
background: rgba(0, 212, 255, 0.06);
}
.nasa-nav-link--active {
color: var(--nasa-telemetry) !important;
border-color: var(--nasa-panel-border);
background: rgba(0, 212, 255, 0.1);
}
.nasa-table thead {
background: rgba(11, 61, 145, 0.2);
font-family: var(--nasa-mono);
font-size: 0.65rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--nasa-muted);
}
.nasa-table tbody tr { border-top: 1px solid rgba(30, 74, 138, 0.35); }
.nasa-table tbody tr:hover { background: rgba(0, 212, 255, 0.04); }
.trade-layout {
display: grid;
grid-template-columns: 240px 1fr;
min-height: calc(100vh - 56px);
background: var(--nasa-bg);
}
.trade-markets, .trade-panel, .trade-pair-header, .trade-recent, .trade-book {
background: var(--nasa-panel) !important;
border-color: var(--nasa-panel-border) !important;
}
.trade-center { min-height: 0; background: var(--nasa-bg); }
.trade-chart { background: var(--nasa-bg) !important; border-color: var(--nasa-panel-border) !important; }
.trade-market-row:hover { background: rgba(0, 212, 255, 0.08); }
.trade-market-row--active {
background: rgba(0, 212, 255, 0.12);
border-left: 2px solid var(--nasa-telemetry) !important;
}
.trade-orderbook { max-height: 360px; overflow-y: auto; font-family: var(--nasa-mono); }
@media (max-width: 1023px) {
.trade-layout { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto; }
}
@media (min-width: 1024px) {
.trade-layout { grid-template-columns: 240px 1fr 320px; }
}