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
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:
@@ -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 />} />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
204
frontend-dapp/src/components/icons/NasaIcon.tsx
Normal file
204
frontend-dapp/src/components/icons/NasaIcon.tsx
Normal 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}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
);
|
||||
|
||||
49
frontend-dapp/src/features/omnl-dashboard/NasaPageHeader.tsx
Normal file
49
frontend-dapp/src/features/omnl-dashboard/NasaPageHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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 & swappable
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 grid grid-cols-1 xl:grid-cols-[1fr_280px] min-h-[320px]">
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
|
||||
423
frontend-dapp/src/styles/nasa-dashboard.css
Normal file
423
frontend-dapp/src/styles/nasa-dashboard.css
Normal 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; }
|
||||
}
|
||||
Reference in New Issue
Block a user