feat(swift): production listener config, MT202/910 outbound, activate scripts
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m18s
CI/CD Pipeline / Security Scanning (push) Successful in 2m53s
CI/CD Pipeline / Lint and Format (push) Failing after 49s
CI/CD Pipeline / Terraform Validation (push) Failing after 24s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 26s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 42s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 39s
Validation / validate-genesis (push) Successful in 33s
Validation / validate-terraform (push) Failing after 28s
Validation / validate-kubernetes (push) Failing after 12s
Validation / validate-smart-contracts (push) Failing after 13s
Validation / validate-security (push) Failing after 1m21s
Validation / validate-documentation (push) Failing after 22s
Verify Deployment / Verify Deployment (push) Failing after 1m0s

This commit is contained in:
2026-07-03 00:36:21 -07:00
parent b5d4f268bf
commit 4919328162
110 changed files with 2468 additions and 1512 deletions

View File

@@ -527,6 +527,33 @@
},
"inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8",
"inboundSettlementType": "gnosis-safe"
},
{
"lineId": "0x9f3e2a1b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f",
"officeId": 29,
"externalId": "ZBANK-OMNL",
"entityName": "zBank (OneX Bank)",
"walletId": "HYBX-OFFICE-029",
"treasuryWalletMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8",
"treasuryWalletChain138": "0xc93522ae4ab7482572cc29edd68fbb6d5ec6a585",
"treasuryWalletShivaOneX": "0x0000000000000000000000000000000000000000",
"chains": {
"138": {
"tokenM0": "0x0000000000000000000000000000000000000000",
"tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b"
},
"900001": {
"tokenM0": "0x0000000000000000000000000000000000000000",
"tokenM1": "0x0000000000000000000000000000000000000000"
},
"1": {
"tokenM0": "0x0000000000000000000000000000000000000000",
"tokenM1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
}
},
"inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8",
"inboundSettlementType": "gnosis-safe",
"integrationRef": "config/onex-shiva-integration.v1.json"
}
],
"globalInboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8"

View File

@@ -3,7 +3,7 @@
"version": "1.0.0",
"officeId": 24,
"externalId": "HOSPITALLERS-ZARDASHT-IRAQ-IRAN",
"name": "HOSPITALLERS Zardasht (Iraq / Iran Operations)",
"name": "Office 24 Central Bank — HOSPITALLERS Zardasht (Iraq / Iran)",
"fineractTenant": "omnl",
"tenantUser": "ali_hospitallers_tenant",
"adminUser": "ali_hospitallers_admin",

View File

@@ -1224,20 +1224,23 @@
},
{
"chainId": 900001,
"name": "OMNL Expansion Slot 1",
"name": "Shiva Blockchain (OneX)",
"type": "evm",
"nativeSymbol": "ETH",
"tier": "reserved",
"omnlRole": "expansion",
"status": "reserved",
"nativeSymbol": "SHIVA",
"tier": "primary",
"omnlRole": "onex-shiva",
"status": "active",
"rpcEnv": "CHAIN_900001_RPC_URL",
"explorerUrl": "https://explorer.shiva.onex.omdnl.org",
"rpcDefault": "https://rpc.shiva.onex.omdnl.org",
"settlement": {
"m2LoadEnabled": false,
"swapEnabled": false,
"bridgeEnabled": false,
"officeId": 24
"m2LoadEnabled": true,
"swapEnabled": true,
"bridgeEnabled": true,
"officeId": 29
},
"complianceEnv": "OMNL_COMPLIANCE_CORE_900001"
"complianceEnv": "OMNL_COMPLIANCE_CORE_900001",
"integrationRef": "config/onex-shiva-integration.v1.json"
},
{
"chainId": 900002,

View File

@@ -0,0 +1,110 @@
{
"$schema": "OMNL SWIFT FIN / ISO 20022 listener — Chain 138 + Office 24 production",
"schemaVersion": "1.0.0",
"chain138": {
"chainId": 138,
"names": ["chain138", "chain 138", "defi oracle meta", "dbis", "omnl"],
"rpcHint": "RPC_URL_138",
"configSources": [
"config/ei-matrix-138-assets-and-pools.v1.json",
"config/chain138-iso20022-attestation.v1.json"
]
},
"supportedSwiftFinTypes": ["MT103", "MT102", "MT202", "MT910", "MT940", "MT942", "MT950"],
"supportedIso20022Types": [
"pain.001",
"pacs.008",
"pacs.009",
"pacs.002",
"camt.052",
"camt.053",
"camt.054"
],
"cToCwSymbolMapping": {
"USD": "cWUSD",
"USDC": "cWUSDC",
"USDT": "cWUSDT",
"EUR": "cWEUR",
"GBP": "cWGBP"
},
"matchHints": {
"instructionIdPrefixes": ["HOSPITALLERS", "CHAIN138", "OMNL", "HYBX", "ZARDASHT", "Z-BANK", "O24"],
"remittanceKeywords": [
"chain138",
"cWUSDC",
"cWUSDT",
"cWUSD",
"omnl",
"hybx",
"office 24",
"zardasht",
"hospitallers"
]
},
"storage": {
"eventsDir": "reports/swift-listener/events",
"latestStatus": "reports/swift-listener/status-latest.json",
"processedIndex": "reports/swift-listener/processed-index.json",
"inboxDir": "reports/swift-listener/inbox",
"archiveDir": "reports/swift-listener/archive",
"as4InboxDir": "reports/swift-listener/as4-inbox",
"mailInboxDir": "reports/swift-listener/mail-inbox",
"telexInboxDir": "reports/swift-listener/telex-inbox"
},
"adapters": {
"fileInbox": { "enabled": true, "pollIntervalSec": 30, "archiveOnProcess": true },
"webhook": {
"enabled": true,
"port": 8788,
"path": "/inbound",
"healthPath": "/health",
"authBearerEnv": "SWIFT_LISTENER_WEBHOOK_TOKEN",
"webhookSecretEnv": "HYBX_WEBHOOK_SECRET",
"webhookSignatureHeader": "x-hybx-signature"
},
"p2pRail": {
"enabled": false,
"baseUrlEnv": "P2P_BASE_URL",
"pollIntervalSec": 60,
"transactionsPath": "/api/v1/transactions"
},
"finGateway": {
"enabled": false,
"baseUrlEnv": "FIN_GATEWAY_URL",
"fallbackBaseUrlEnv": "ALLIANCE_ACCESS_URL",
"pollIntervalSec": 120,
"messagesPath": "/messages",
"authBearerEnv": "FIN_GATEWAY_TOKEN"
},
"swiftFinTcp": {
"enabled": true,
"host": "0.0.0.0",
"portEnv": "SWIFT_FIN_TCP_PORT",
"defaultPort": 5001
},
"as4Inbox": { "enabled": false, "pollIntervalSec": 60, "archiveOnProcess": true },
"mailAttachmentInbox": { "enabled": false, "pollIntervalSec": 120, "archiveOnProcess": true },
"telexInbox": { "enabled": false, "pollIntervalSec": 120, "archiveOnProcess": true }
},
"outbound": {
"omnlForward": {
"enabled": true,
"mode": "local_and_api",
"apiPath": "/api/v1/omnl/iso20022/messages",
"tokenAggregationUrlEnv": "TOKEN_AGGREGATION_URL",
"apiKeyEnv": "OMNL_API_KEY",
"localStoreDir": "config/iso20022-omnl/messages"
},
"intakeGateway": {
"enabled": true,
"gatewayAddressEnv": "ISO20022_INTAKE_GATEWAY_MAINNET",
"gatewayAddressFallback": "0x66C89F91A4e830d87fcE4E5F86aFA71A96FbDa3d",
"rpcUrlEnv": "RPC_URL_138",
"privateKeyEnv": "PRIVATE_KEY",
"executeEnv": "SWIFT_LISTENER_GATEWAY_EXECUTE",
"dryRunDefault": true,
"autoExecuteWhenKeyPresent": true,
"chainId": 138
}
}
}

View File

@@ -0,0 +1,72 @@
{
"$schema": "OMNL SWIFT MT catalog — Central Bank, Office 24, Z Online Bank",
"version": "1.0.0",
"settlementOfficeId": 24,
"bic": "OMNLUS33",
"rails": {
"outbound": "POST /settlement/external-transfer (rail SWIFT)",
"inbound": "POST /settlement/swift/inbound",
"listener": "SWIFT listener webhook POST /inbound :8788"
},
"messageTypes": [
{
"mt": "MT103",
"name": "Single Customer Credit Transfer",
"use": "Z Bank wire, Office 24 outbound, correspondent payments",
"moneyLayers": ["M0", "M1", "M2", "M4"],
"portal": "/central-bank#z-settlement-protocols",
"settlementField": "swiftMessageKind: MT103"
},
{
"mt": "MT102",
"name": "Multiple Customer Credit Transfer",
"use": "Bulk payroll, DLC/LS batch legs",
"moneyLayers": ["M1", "M2", "M3", "M4"],
"portal": "/central-bank#z-settlement-protocols"
},
{
"mt": "MT202",
"name": "General Financial Institution Transfer",
"use": "SBLC cover payment, interbank settlement",
"moneyLayers": ["M4"],
"tradeFinance": ["SBLC"]
},
{
"mt": "MT910",
"name": "Confirmation of Credit",
"use": "Credit advice to beneficiary bank",
"moneyLayers": ["M4"]
},
{
"mt": "MT940",
"name": "Customer Statement",
"use": "End-of-day account statement (Fineract reconcile)",
"moneyLayers": ["M0", "M1", "M2"]
},
{
"mt": "MT942",
"name": "Interim Statement",
"use": "Intraday statement / liquidity monitoring",
"moneyLayers": ["M0", "M1"]
},
{
"mt": "MT950",
"name": "Statement Message",
"use": "Legacy statement (Fineract GL export)",
"moneyLayers": ["M0", "M1", "M2"]
}
],
"iso20022": [
{ "type": "pacs.008", "swiftFin": "MT103", "use": "FI-to-FI customer credit" },
{ "type": "pacs.009", "swiftFin": "MT202", "use": "FI-to-FI institution transfer" },
{ "type": "camt.054", "swiftFin": "MT910", "use": "Credit notification" },
{ "type": "camt.053", "swiftFin": "MT940", "use": "Account statement" },
{ "type": "camt.052", "swiftFin": "MT942", "use": "Account report" }
],
"tradeFinance": {
"SBLC": { "swiftMessages": ["MT103", "MT202"], "endpoint": "POST /settlement/trade-finance/SBLC" },
"DLC": { "swiftMessages": ["MT103", "MT102"], "endpoint": "POST /settlement/trade-finance/DLC" },
"BG": { "swiftMessages": ["MT103"], "endpoint": "POST /settlement/trade-finance/BG" },
"LS": { "swiftMessages": ["MT102", "MT103"], "endpoint": "POST /settlement/trade-finance/LS" }
}
}

View File

@@ -143,7 +143,6 @@ forge script script/DeployTransactionMirror.s.sol \
3. **RPC Providers**:
- Alchemy: https://www.alchemy.com/
- Infura: https://www.infura.io/
- QuickNode: https://www.quicknode.com/
4. **Verification**: Contracts will be automatically verified on Etherscan if `ETHERSCAN_API_KEY` is set
---

View File

@@ -46,9 +46,6 @@ Switch to Alchemy or another provider:
# Alchemy
ETHEREUM_MAINNET_RPC=https://eth-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY
# QuickNode
ETHEREUM_MAINNET_RPC=https://your-endpoint.quiknode.pro/YOUR_API_KEY
# Public RPC (not recommended for production)
ETHEREUM_MAINNET_RPC=https://eth.llamarpc.com
```

View File

@@ -62,11 +62,6 @@ Update `.env` line 18 with alternative provider:
ETHEREUM_MAINNET_RPC=https://eth-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY
```
**QuickNode**:
```bash
ETHEREUM_MAINNET_RPC=https://your-endpoint.quiknode.pro/YOUR_API_KEY
```
**Public RPC** (not recommended for production):
```bash
ETHEREUM_MAINNET_RPC=https://eth.llamarpc.com

View File

@@ -3,10 +3,13 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OMNL Mission Control - Chain 138</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#141a16" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<title>DBIS — Chain 138</title>
<style>
html, body { margin: 0; min-height: 100%; background: #0f2847; color: #e8f4ff; }
html, body { margin: 0; min-height: 100%; background: #141a16; color: #eaecef; }
#root { min-height: 100vh; }
.omnl-boot {
min-height: 100vh;
@@ -14,26 +17,19 @@
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
gap: 0.75rem;
font-family: Inter, system-ui, sans-serif;
background:
radial-gradient(ellipse at 20% 0%, rgba(45, 107, 184, 0.45) 0%, transparent 55%),
linear-gradient(180deg, #0f2847 0%, #0a1a30 100%);
background: #141a16;
}
.omnl-boot__title {
font-size: 1.1rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #00d4ff;
}
.omnl-boot__hint { color: #9bb8d4; font-size: 0.9rem; }
.omnl-boot__title { font-size: 1.125rem; font-weight: 600; color: #eaecef; }
.omnl-boot__hint { color: #848e9c; font-size: 0.875rem; }
</style>
</head>
<body>
<div id="root">
<div class="omnl-boot" role="status" aria-live="polite">
<div class="omnl-boot__title">OMNL Mission Control</div>
<div class="omnl-boot__hint">Loading dashboard...</div>
<div class="omnl-boot__title">DBIS</div>
<div class="omnl-boot__hint">Loading</div>
</div>
</div>
<script type="module" src="/src/main.tsx"></script>

View File

@@ -3,8 +3,11 @@
"version": "1.0.0",
"type": "module",
"scripts": {
"predev": "node scripts/sync-omnl-consoles.mjs",
"dev": "vite",
"prebuild": "node scripts/sync-omnl-consoles.mjs",
"build": "node --max-old-space-size=4096 ./node_modules/vite/bin/vite.js build",
"serve:portal": "node scripts/portal-serve.mjs",
"build:check": "tsc && NODE_OPTIONS='--max-old-space-size=4096' vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
@@ -13,6 +16,9 @@
"test:coverage": "vitest --coverage"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@lit/reactive-element": "^2.1.2",
"@safe-global/api-kit": "^4.0.1",
"@safe-global/protocol-kit": "^1.3.0",
"@tanstack/react-query": "^5.90.21",
@@ -22,6 +28,9 @@
"@walletconnect/ethereum-provider": "^2.23.5",
"autoprefixer": "^10.4.24",
"ethers": "^5.8.0",
"lit": "^3.3.3",
"lit-element": "^4.2.2",
"lit-html": "^3.3.3",
"postcss": "^8.5.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -47,6 +56,7 @@
"eslint-plugin-react-refresh": "^0.4.26",
"identity-obj-proxy": "^3.0.0",
"jsdom": "^23.2.0",
"serve": "^14.2.6",
"ts-jest": "^29.4.6",
"typescript": "^5.9.3",
"vite": "^5.4.21",

View File

@@ -1,4 +1,4 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ThirdwebProvider } from '@thirdweb-dev/react'
@@ -14,8 +14,8 @@ import ReservePage from './pages/ReservePage'
import HistoryPage from './pages/HistoryPage'
import AdminPanel from './pages/AdminPanel'
import DocsPage from './pages/DocsPage'
import WalletsDemoPage from './pages/WalletsDemoPage'
import HubPage from './pages/HubPage'
import WalletsDemoPage from './pages/WalletsDemoPage'
import Layout from './components/layout/Layout'
import OmnlProductLayout from './components/layout/OmnlProductLayout'
import ToastProvider from './components/ui/ToastProvider'
@@ -63,7 +63,6 @@ function App() {
</ThirdwebProvider>
}
>
<Route path="/" element={<BridgePage />} />
<Route path="/bridge" element={<BridgePage />} />
<Route path="/reserve" element={<ReservePage />} />
<Route path="/history" element={<HistoryPage />} />
@@ -71,6 +70,7 @@ function App() {
<Route path="/docs" element={<DocsPage />} />
<Route path="/wallets" element={<WalletsDemoPage />} />
</Route>
<Route path="/" element={<Navigate to="/hub" replace />} />
</Routes>
</AdminProvider>
</BrowserRouter>

View File

@@ -99,7 +99,7 @@ export default function AdminDashboard() {
}, [publicClient, address])
return (
<div className="space-y-6 nasa-mission-root">
<div className="space-y-6 app-shell">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<DashboardStatCard label="Total actions" value={String(stats.totalActions)} accent="blue" icon="admin" />
<DashboardStatCard label="Pending" value={String(stats.pending)} accent="gold" icon="alert" />

View File

@@ -73,7 +73,7 @@ export default function AuditLogViewer() {
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search logs..."
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
<div className="flex gap-2">
{(['all', 'success', 'failure'] as const).map((f) => (

View File

@@ -147,7 +147,7 @@ export default function FunctionPermissions() {
<select
value={selectedContract}
onChange={(e) => setSelectedContract(e.target.value)}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
>
<option value={CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER}>MainnetTether</option>
<option value={CONTRACT_ADDRESSES.mainnet.TRANSACTION_MIRROR}>TransactionMirror</option>
@@ -227,7 +227,7 @@ export default function FunctionPermissions() {
<select
value={selectedRole}
onChange={(e) => setSelectedRole(e.target.value)}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
>
{roles.map((role) => (
<option key={role.id} value={role.id}>
@@ -239,7 +239,7 @@ export default function FunctionPermissions() {
<div className="flex-1">
<label className="block text-white/70 text-sm mb-2">Function</label>
<select
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
>
{contractFunctions.map((func) => (
<option key={func} value={func}>

View File

@@ -92,7 +92,7 @@ export default function GasOptimizer() {
value={estimatedGas}
onChange={(e) => setEstimatedGas(e.target.value)}
placeholder="21000"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
</div>

View File

@@ -105,7 +105,7 @@ export default function ImpersonationMode() {
value={inputAddress}
onChange={(e) => setInputAddress(e.target.value)}
placeholder="0x... or name.eth"
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleEnableImpersonation()

View File

@@ -177,7 +177,7 @@ export default function MainnetTetherAdmin() {
value={newAdmin}
onChange={(e) => setNewAdmin(e.target.value)}
placeholder="0x..."
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
<button
onClick={handleSetAdmin}
@@ -200,7 +200,7 @@ export default function MainnetTetherAdmin() {
value={blockNumber}
onChange={(e) => setBlockNumber(e.target.value)}
placeholder="Block number"
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
<button
onClick={handleCheckBlock}

View File

@@ -72,7 +72,7 @@ export default function MultiChainAdmin() {
<select
value={selectedChain}
onChange={(e) => setSelectedChain(parseInt(e.target.value))}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
>
{CHAIN_CONFIGS.map((chain) => (
<option key={chain.chainId} value={chain.chainId}>

View File

@@ -182,7 +182,7 @@ export default function MultiSigAdmin() {
<select
value={selectedContract}
onChange={(e) => setSelectedContract(e.target.value)}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
>
<option value={contractAddress}>MainnetTether ({contractAddress.slice(0, 10)}...)</option>
<option value={CONTRACT_ADDRESSES.mainnet.TRANSACTION_MIRROR}>

View File

@@ -145,7 +145,7 @@ export default function OwnerManagement() {
<select
value={selectedWallet}
onChange={(e) => setSelectedWallet(e.target.value)}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
>
<option value="">Select a wallet...</option>
{wallets.map((wallet) => (
@@ -189,14 +189,14 @@ export default function OwnerManagement() {
value={newOwner}
onChange={(e) => setNewOwner(e.target.value)}
placeholder="0x..."
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500 font-mono text-sm"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500 font-mono text-sm"
/>
<input
type="text"
value={ownerLabel}
onChange={(e) => setOwnerLabel(e.target.value)}
placeholder="Owner label (optional)"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
<button
onClick={addOwner}

View File

@@ -134,7 +134,7 @@ export default function Pagination({
<select
value={itemsPerPage}
onChange={(e) => onItemsPerPageChange(Number(e.target.value))}
className="px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
>
<option value={10}>10</option>
<option value={25}>25</option>

View File

@@ -151,7 +151,7 @@ export default function RealtimeMonitor() {
onChange={(e) => setWsUrl(e.target.value)}
placeholder="wss://your-monitoring-server.com/ws"
disabled={monitoring}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500 font-mono text-sm disabled:opacity-50"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500 font-mono text-sm disabled:opacity-50"
/>
<p className="text-white/60 text-xs mt-1">
Leave empty to use polling fallback (checks every 10 seconds)

View File

@@ -139,12 +139,12 @@ export default function RoleBasedAccess() {
value={selectedAddress}
onChange={(e) => setSelectedAddress(e.target.value)}
placeholder="0x..."
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500 font-mono text-sm"
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500 font-mono text-sm"
/>
<select
value={selectedRole}
onChange={(e) => setSelectedRole(e.target.value as AdminRole)}
className="px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
>
<option value={AdminRole.SUPER_ADMIN}>Super Admin</option>
<option value={AdminRole.OPERATOR}>Operator</option>

View File

@@ -153,7 +153,7 @@ export default function ScheduledActions() {
value={newAction.name}
onChange={(e) => setNewAction({ ...newAction, name: e.target.value })}
placeholder="Daily State Check"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
</div>
@@ -162,7 +162,7 @@ export default function ScheduledActions() {
<select
value={newAction.contractAddress}
onChange={(e) => setNewAction({ ...newAction, contractAddress: e.target.value as `0x${string}` })}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
>
<option value={CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER}>MainnetTether</option>
<option value={CONTRACT_ADDRESSES.mainnet.TRANSACTION_MIRROR}>TransactionMirror</option>
@@ -176,7 +176,7 @@ export default function ScheduledActions() {
value={newAction.functionName}
onChange={(e) => setNewAction({ ...newAction, functionName: e.target.value })}
placeholder="pause"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
</div>
@@ -185,7 +185,7 @@ export default function ScheduledActions() {
<select
value={newAction.schedule}
onChange={(e) => setNewAction({ ...newAction, schedule: e.target.value })}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>

View File

@@ -107,7 +107,7 @@ export default function TimeLockedActions() {
value={delayMinutes}
onChange={(e) => setDelayMinutes(parseInt(e.target.value) || 0)}
min="0"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
/>
</div>

View File

@@ -231,7 +231,7 @@ export default function TransactionMirrorAdmin() {
value={newAdmin}
onChange={(e) => setNewAdmin(e.target.value)}
placeholder="0x..."
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
<button
onClick={handleSetAdmin}
@@ -254,7 +254,7 @@ export default function TransactionMirrorAdmin() {
value={txHash}
onChange={(e) => setTxHash(e.target.value)}
placeholder="Transaction hash (0x...)"
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500 font-mono text-sm"
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500 font-mono text-sm"
/>
<button
onClick={handleCheckTransaction}

View File

@@ -89,7 +89,7 @@ export default function TransactionPreview() {
<select
value={contractAddress}
onChange={(e) => setContractAddress(e.target.value as `0x${string}`)}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
>
<option value={CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER}>MainnetTether</option>
<option value={CONTRACT_ADDRESSES.mainnet.TRANSACTION_MIRROR}>TransactionMirror</option>
@@ -103,7 +103,7 @@ export default function TransactionPreview() {
value={functionName}
onChange={(e) => setFunctionName(e.target.value)}
placeholder="pause"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
</div>
@@ -114,7 +114,7 @@ export default function TransactionPreview() {
value={args}
onChange={(e) => setArgs(e.target.value)}
placeholder='[] or ["0x..."]'
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500 font-mono text-sm"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500 font-mono text-sm"
/>
</div>

View File

@@ -183,7 +183,7 @@ export default function TransactionTemplates() {
value={customTemplate.name}
onChange={(e) => setCustomTemplate({ ...customTemplate, name: e.target.value })}
placeholder="My Custom Template"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
</div>
<div>
@@ -193,7 +193,7 @@ export default function TransactionTemplates() {
value={customTemplate.description}
onChange={(e) => setCustomTemplate({ ...customTemplate, description: e.target.value })}
placeholder="What this template does"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
</div>
<div>
@@ -203,7 +203,7 @@ export default function TransactionTemplates() {
value={customTemplate.contractAddress}
onChange={(e) => setCustomTemplate({ ...customTemplate, contractAddress: e.target.value })}
placeholder="0x..."
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500 font-mono text-sm"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500 font-mono text-sm"
/>
</div>
<div>
@@ -213,7 +213,7 @@ export default function TransactionTemplates() {
value={customTemplate.functionName}
onChange={(e) => setCustomTemplate({ ...customTemplate, functionName: e.target.value })}
placeholder="pause"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
</div>
<div>
@@ -223,7 +223,7 @@ export default function TransactionTemplates() {
value={customTemplate.args}
onChange={(e) => setCustomTemplate({ ...customTemplate, args: e.target.value })}
placeholder='[] or ["0x..."]'
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500 font-mono text-sm"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500 font-mono text-sm"
/>
</div>
<div>
@@ -231,7 +231,7 @@ export default function TransactionTemplates() {
<select
value={customTemplate.category}
onChange={(e) => setCustomTemplate({ ...customTemplate, category: e.target.value as any })}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
>
<option value="maintenance">Maintenance</option>
<option value="emergency">Emergency</option>

View File

@@ -200,7 +200,7 @@ export default function TwoWayBridgeAdmin() {
value={newAdmin}
onChange={(e) => setNewAdmin(e.target.value)}
placeholder="0x..."
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
<button
onClick={handleSetAdmin}
@@ -220,14 +220,14 @@ export default function TwoWayBridgeAdmin() {
value={chainSelector}
onChange={(e) => setChainSelector(e.target.value)}
placeholder="Chain Selector (e.g., 16015286601757825753)"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
<input
type="text"
value={l2Bridge}
onChange={(e) => setL2Bridge(e.target.value)}
placeholder="L2 Bridge Address (0x...)"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
<button
onClick={handleAddDestination}
@@ -247,7 +247,7 @@ export default function TwoWayBridgeAdmin() {
value={feeToken}
onChange={(e) => setFeeToken(e.target.value)}
placeholder="0x..."
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
<button
onClick={handleUpdateFeeToken}

View File

@@ -96,7 +96,7 @@ export default function WalletBackup() {
value={backupPassword}
onChange={(e) => setBackupPassword(e.target.value)}
placeholder="Enter password"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
</div>
<button
@@ -119,7 +119,7 @@ export default function WalletBackup() {
onChange={(e) => setImportData(e.target.value)}
placeholder="Paste encrypted backup data..."
rows={6}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500 font-mono text-sm"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500 font-mono text-sm"
/>
</div>
<div>
@@ -129,7 +129,7 @@ export default function WalletBackup() {
value={backupPassword}
onChange={(e) => setBackupPassword(e.target.value)}
placeholder="Enter password"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
</div>
<button

View File

@@ -57,7 +57,7 @@ export default function WalletBalance() {
value={newWallet}
onChange={(e) => setNewWallet(e.target.value)}
placeholder="0x..."
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500 font-mono text-sm"
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500 font-mono text-sm"
/>
<button
onClick={addWallet}

View File

@@ -103,7 +103,7 @@ export default function WalletDeployment() {
value={config.name}
onChange={(e) => setConfig({ ...config, name: e.target.value })}
placeholder="My Admin Wallet"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
</div>
@@ -132,7 +132,7 @@ export default function WalletDeployment() {
value={newOwner}
onChange={(e) => setNewOwner(e.target.value)}
placeholder="0x..."
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500 font-mono text-sm"
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500 font-mono text-sm"
/>
<button
onClick={addOwner}
@@ -159,7 +159,7 @@ export default function WalletDeployment() {
}
min="1"
max={config.owners.length}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
/>
<p className="text-white/60 text-xs mt-1">
Requires {config.threshold} of {config.owners.length} owners to approve transactions

View File

@@ -144,7 +144,7 @@ export default function WalletDeploymentEnhanced() {
value={config.name}
onChange={(e) => setConfig({ ...config, name: e.target.value })}
placeholder="My Admin Wallet"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500"
/>
</div>
@@ -173,7 +173,7 @@ export default function WalletDeploymentEnhanced() {
value={newOwner}
onChange={(e) => setNewOwner(e.target.value)}
placeholder="0x..."
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-blue-500 font-mono text-sm"
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-green-500 font-mono text-sm"
/>
<button
onClick={addOwner}
@@ -200,7 +200,7 @@ export default function WalletDeploymentEnhanced() {
}
min="1"
max={config.owners.length}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-blue-500"
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:border-green-500"
/>
<p className="text-white/60 text-xs mt-1">
Requires {config.threshold} of {config.owners.length} owners to approve transactions

View File

@@ -6,9 +6,9 @@
import { useState, useEffect } from 'react';
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';
import DashboardSection from '../../../features/omnl-dashboard/DashboardSection';
import NasaIcon, { StatusIcon } from '../../icons/NasaIcon';
import NasaPageHeader from '../../../features/omnl-dashboard/NasaPageHeader';
interface ChainMetadata {
chainId: number;
@@ -80,8 +80,8 @@ export default function ChainManagementDashboard() {
if (!isConnected) {
return (
<div className="dash-card nasa-mission-root">
<p className="text-[#7a9bb8] flex items-center gap-2">
<div className="dash-card app-shell text-center">
<p className="text-[#848e9c] flex items-center justify-center gap-2">
<NasaIcon name="wallet" size={16} className="nasa-icon--telemetry" />
Please connect your wallet to manage chains.
</p>
@@ -90,10 +90,9 @@ export default function ChainManagementDashboard() {
}
return (
<div className="space-y-6 nasa-mission-root">
<div className="space-y-6 app-page-shell app-shell">
<NasaPageHeader
icon="network"
missionId="NET-ADM"
title="Chain Management"
subtitle="Supported chains, adapters, and deployment registry"
/>
@@ -116,7 +115,7 @@ export default function ChainManagementDashboard() {
<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">
<a href={chain.explorerUrl} target="_blank" rel="noopener noreferrer" className="text-[#4ade80] hover:underline inline-flex items-center gap-1">
<NasaIcon name="link" size={12} />
{chain.explorerUrl}
</a>

View File

@@ -77,7 +77,7 @@ export default function SwapBridgeSwapQuoteForm() {
placeholder="Source token address"
value={sourceToken}
onChange={(e) => setSourceToken(e.target.value)}
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-teal-500/50 placeholder:text-[#A0A0A0]"
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-green-500/50 placeholder:text-[#A0A0A0]"
/>
<label htmlFor="quote-destination-token" className="sr-only">Destination token address</label>
<input
@@ -87,7 +87,7 @@ export default function SwapBridgeSwapQuoteForm() {
placeholder="Destination token address"
value={destinationToken}
onChange={(e) => setDestinationToken(e.target.value)}
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-teal-500/50 placeholder:text-[#A0A0A0]"
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-green-500/50 placeholder:text-[#A0A0A0]"
/>
<label htmlFor="quote-source-chain" className="sr-only">Source chain ID</label>
<input
@@ -97,7 +97,7 @@ export default function SwapBridgeSwapQuoteForm() {
placeholder="Source chain ID"
value={sourceChainId}
onChange={(e) => setSourceChainId(e.target.value)}
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-teal-500/50 placeholder:text-[#A0A0A0]"
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-green-500/50 placeholder:text-[#A0A0A0]"
/>
<label htmlFor="quote-destination-chain" className="sr-only">Destination chain ID</label>
<input
@@ -107,7 +107,7 @@ export default function SwapBridgeSwapQuoteForm() {
placeholder="Destination chain ID"
value={destinationChainId}
onChange={(e) => setDestinationChainId(e.target.value)}
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-teal-500/50 placeholder:text-[#A0A0A0]"
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-green-500/50 placeholder:text-[#A0A0A0]"
/>
<label htmlFor="quote-amount" className="sr-only">Amount (wei or decimal)</label>
<input
@@ -117,14 +117,14 @@ export default function SwapBridgeSwapQuoteForm() {
placeholder="Amount (wei or decimal)"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-teal-500/50 placeholder:text-[#A0A0A0] md:col-span-2"
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-green-500/50 placeholder:text-[#A0A0A0] md:col-span-2"
/>
</div>
<button
type="button"
onClick={handleGetQuote}
disabled={loading}
className="px-4 py-2 rounded-lg bg-teal-600 text-white font-medium hover:bg-teal-500 disabled:opacity-50"
className="px-4 py-2 rounded-lg bg-green-600 text-white font-medium hover:bg-green-500 disabled:opacity-50"
>
{loading ? 'Getting quote...' : 'Get full path quote'}
</button>

View File

@@ -23,7 +23,12 @@ export type NasaIconName =
| 'globe'
| 'money'
| 'rail'
| 'admin';
| 'admin'
| 'dashboard'
| 'code'
| 'shield'
| 'terminal'
| 'plus';
type Props = SVGProps<SVGSVGElement> & {
name: NasaIconName;
@@ -170,6 +175,39 @@ const paths: Record<NasaIconName, ReactNode> = {
<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" />
</>
),
dashboard: (
<>
<rect x="3" y="3" width="7" height="9" rx="1" />
<rect x="14" y="3" width="7" height="5" rx="1" />
<rect x="14" y="12" width="7" height="9" rx="1" />
<rect x="3" y="16" width="7" height="5" rx="1" />
</>
),
code: (
<>
<path d="m16 18 6-6-6-6" />
<path d="m8 6-6 6 6 6" />
</>
),
shield: (
<>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
<path d="m9 12 2 2 4-4" />
</>
),
terminal: (
<>
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="m6 10 4 4-4 4" />
<path d="M12 18h6" />
</>
),
plus: (
<>
<path d="M12 5v14" />
<path d="M5 12h14" />
</>
),
};
export default function NasaIcon({ name, size = 18, className = '', ...props }: Props) {

View File

@@ -1,88 +1,96 @@
import { Link, useLocation } from 'react-router-dom'
import { useRef } from 'react'
import { Link, Outlet, useLocation } from 'react-router-dom'
import { useRef, useState } from 'react'
import WalletConnect from '../wallet/WalletConnect'
import WalletDisconnectNotice from '../wallet/WalletDisconnectNotice'
import { defaultFrontendExplorerUrl } from '../../config/networks'
import MobileNavDrawer from './MobileNavDrawer'
import MobileBottomNav from './MobileBottomNav'
interface LayoutProps {
children: React.ReactNode
}
export default function Layout({ children }: LayoutProps) {
export default function Layout() {
const location = useLocation()
const userInitiatedDisconnectRef = useRef(false)
const [menuOpen, setMenuOpen] = useState(false)
const isActive = (path: string) => location.pathname === path
const navItems = [
{ to: '/hub', label: 'DBIS Dashboard', active: isActive('/hub') },
{ to: '/bridge', label: 'Bridge', active: isActive('/bridge') },
{ to: '/swap', label: 'Swap', active: isActive('/swap') },
{ to: '/reserve', label: 'Reserve', active: isActive('/reserve') },
{ to: '/history', label: 'History', active: isActive('/history') },
{ to: '/wallets', label: 'Wallets', active: isActive('/wallets') },
{ to: '/admin', label: 'Admin', active: isActive('/admin') },
{ to: '/docs', label: 'Developers', active: isActive('/docs') },
{ to: '/central-bank', label: 'Z Online Bank', active: isActive('/central-bank') },
{ to: '/office-24', label: 'O24 Central Bank', active: isActive('/office-24') },
{ to: '/trade', label: 'DBIS Trade', active: isActive('/trade') },
{ href: defaultFrontendExplorerUrl, label: 'Explorer' },
]
return (
<div className="min-h-screen relative bg-[#1a1d24] flex flex-col">
<div className="min-h-screen relative bg-[#141a16] flex flex-col dbis-has-bottom-nav">
<WalletDisconnectNotice userInitiatedDisconnectRef={userInitiatedDisconnectRef} />
<nav className="bg-[#252830] border-b border-white/10 sticky top-0 z-50">
<div className="container mx-auto px-4 max-w-7xl">
<div className="flex justify-between items-center h-14 gap-4">
<div className="flex items-center gap-6 min-w-0">
<Link to="/" className="text-xl font-semibold text-white whitespace-nowrap flex items-center gap-2">
<span className="text-teal-400 font-bold">DBIS</span>
<nav className="mobile-nav-bar bg-[#1a241c] border-b border-green-500/20 sticky top-0 z-50 safe-area-top">
<div className="container mx-auto px-3 sm:px-4 max-w-7xl">
<div className="flex justify-between items-center h-14 gap-2">
<div className="flex items-center gap-2 min-w-0 flex-1">
<button
type="button"
className="sm:hidden shrink-0 text-white p-2 min-h-[44px] min-w-[44px] rounded-lg hover:bg-white/10"
aria-label="Open menu"
aria-expanded={menuOpen}
onClick={() => setMenuOpen(true)}
>
</button>
<Link to="/hub" className="text-lg sm:text-xl font-semibold text-white whitespace-nowrap flex items-center gap-2">
<span className="text-green-400 font-bold">DBIS</span>
</Link>
<div className="hidden sm:flex gap-1">
<Link
to="/bridge"
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
isActive('/bridge') || isActive('/') ? 'bg-teal-600 text-white' : 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
Bridge
</Link>
<Link
to="/swap"
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
isActive('/swap') ? 'bg-teal-600 text-white' : 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
Swap
</Link>
<Link
to="/history"
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
isActive('/history') ? 'bg-teal-600 text-white' : 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
History
</Link>
<Link
to="/wallets"
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
isActive('/wallets') ? 'bg-teal-600 text-white' : 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
Wallets
</Link>
<div className="hidden sm:flex gap-1 ml-2 overflow-x-auto">
{navItems.filter((i) => i.to && !['/central-bank', '/office-24', '/trade', '/docs', '/admin', '/hub'].includes(i.to)).map((item) => (
<Link
key={item.to}
to={item.to!}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap min-h-[44px] flex items-center ${
item.active ? 'bg-green-600 text-white' : 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
{item.label}
</Link>
))}
<a
href={defaultFrontendExplorerUrl}
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 rounded-lg text-sm font-medium text-[#A0A0A0] hover:text-white hover:bg-white/5 transition-colors"
className="px-3 py-2 rounded-lg text-sm font-medium text-[#A0A0A0] hover:text-white hover:bg-white/5 transition-colors whitespace-nowrap min-h-[44px] flex items-center"
>
Explorer
</a>
</div>
</div>
<div className="flex-shrink-0 w-full sm:w-auto max-w-[200px] sm:max-w-none">
<div className="flex-shrink-0 max-w-[50%] sm:max-w-none">
<WalletConnect onBeforeDisconnect={() => { userInitiatedDisconnectRef.current = true }} />
</div>
</div>
</div>
</nav>
<main className="flex-1">{children}</main>
<footer className="border-t border-white/10 py-6 mt-auto">
<MobileNavDrawer open={menuOpen} onClose={() => setMenuOpen(false)} title="DBIS Navigation" items={navItems} />
<MobileBottomNav onOpenMenu={() => setMenuOpen(true)} />
<main className="flex-1 w-full min-w-0 overflow-x-hidden">
<Outlet />
</main>
<footer className="border-t border-white/10 py-6 mt-auto safe-area-bottom">
<div className="container mx-auto px-4 max-w-7xl">
<div className="flex flex-wrap justify-center items-center gap-x-6 gap-y-2 text-sm text-[#A0A0A0]">
<div className="flex flex-wrap justify-center items-center gap-x-4 gap-y-2 text-xs sm:text-sm text-[#A0A0A0]">
<span> Contracts verified</span>
<span> Powered by CCIP</span>
<span> Secure cross-chain messaging</span>
<span> Trackable on Explorer</span>
<Link to="/wallets" className="hover:text-teal-400 transition-colors">Wallets</Link>
<Link to="/docs" className="hover:text-teal-400 transition-colors">Developers</Link>
<span className="hidden sm:inline"> Secure cross-chain messaging</span>
<Link to="/hub" className="hover:text-green-400 transition-colors min-h-[44px] flex items-center">DBIS Dashboard</Link>
<Link to="/wallets" className="hover:text-green-400 transition-colors min-h-[44px] flex items-center">Wallets</Link>
<Link to="/docs" className="hover:text-green-400 transition-colors min-h-[44px] flex items-center">Developers</Link>
</div>
</div>
</footer>

View File

@@ -1,122 +1,92 @@
import { Link, Outlet, useLocation } from 'react-router-dom';
import { useRef } from 'react';
import { useRef, useState } from 'react';
import WalletConnect from '../wallet/WalletConnect';
import WalletDisconnectNotice from '../wallet/WalletDisconnectNotice';
import NasaIcon, { type NasaIconName } from '../icons/NasaIcon';
import MobileNavDrawer from './MobileNavDrawer';
import MobileBottomNav from './MobileBottomNav';
const PRODUCTS: { path: string; label: string; icon: NasaIconName }[] = [
{ path: '/hub', label: 'Hub', icon: 'satellite' },
{ 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' },
{ path: '/hub', label: 'DBIS Dashboard', icon: 'dashboard' },
{ path: '/central-bank', label: 'Z Online Bank', icon: 'central-bank' },
{ path: '/office-24', label: 'O24 Central Bank', icon: 'office' },
{ path: '/trade', label: 'Trade', icon: 'trade' },
{ path: '/swap', label: 'Swap', icon: 'swap' },
];
export default function OmnlProductLayout() {
const location = useLocation();
const userInitiatedDisconnectRef = useRef(false);
const [menuOpen, setMenuOpen] = useState(false);
const isActive = (path: string) =>
path === '/hub' ? location.pathname === '/hub' : location.pathname.startsWith(path);
const drawerItems = [
...PRODUCTS.map((p) => ({ to: p.path, label: p.label, active: isActive(p.path) })),
{ to: '/bridge', label: 'Bridge', active: location.pathname === '/bridge' },
{ to: '/admin', label: 'Admin', active: location.pathname === '/admin' },
{ to: '/wallets', label: 'Wallets', active: location.pathname === '/wallets' },
];
const isActive = (path: string) => location.pathname.startsWith(path);
const isBanking = location.pathname === '/central-bank';
return (
<div className="omnl-app min-h-screen flex flex-col">
<div className={`omnl-app min-h-screen flex flex-col dbis-has-bottom-nav${isBanking ? ' omnl-banking-theme' : ''}`}>
<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="/hub" className="flex items-center gap-2 shrink-0">
<span className="w-8 h-8 border border-[#2d6bb8] bg-[#163a6b] text-[#00d4ff] font-bold flex items-center justify-center text-sm">
<NasaIcon name="satellite" size={16} className="nasa-icon--telemetry" />
<header className="omnl-app-header mobile-nav-bar sticky top-0 z-50 border-b border-green-500/20 safe-area-top bg-[#1a241c]">
<div className="max-w-[1600px] mx-auto px-3 sm:px-4 h-14 flex items-center gap-2 sm:gap-4">
<button
type="button"
className="md:hidden shrink-0 text-white p-2 min-h-[44px] min-w-[44px] rounded-lg hover:bg-white/10"
aria-label="Open menu"
aria-expanded={menuOpen}
onClick={() => setMenuOpen(true)}
>
</button>
<Link to="/hub" className="flex items-center gap-2 shrink-0 min-w-0">
<span className="w-8 h-8 rounded-lg bg-green-600 text-white font-bold flex items-center justify-center text-sm shrink-0">
<NasaIcon name="central-bank" size={16} />
</span>
<span className="nasa-nav-brand font-semibold hidden sm:inline text-sm">OMNL Mission Control</span>
<span className="font-semibold hidden sm:inline text-sm truncate text-white">DBIS OMNL</span>
</Link>
<nav className="flex items-center gap-1 flex-1 overflow-x-auto" aria-label="OMNL products">
<nav className="hidden md:flex items-center gap-1 flex-1 overflow-x-auto min-w-0" aria-label="OMNL products">
{PRODUCTS.map(({ path, label, icon }) => (
<Link
key={path}
to={path}
className={`nasa-nav-link shrink-0 ${isActive(path) ? 'nasa-nav-link--active' : ''}`}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap min-h-[40px] flex items-center gap-1.5 ${
isActive(path) ? 'bg-green-600 text-white' : 'text-[#848e9c] hover:text-white hover:bg-white/5'
}`}
>
<NasaIcon name={icon} size={14} />
{label}
<span className="hidden lg:inline">{label}</span>
<span className="lg:hidden">{label.split(' ')[0]}</span>
</Link>
))}
</nav>
<Link
to="/bridge"
className="nasa-nav-link hidden md:inline shrink-0 !border-transparent"
className="hidden lg:inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium text-[#848e9c] hover:text-white hover:bg-white/5 min-h-[40px]"
>
<NasaIcon name="bridge" size={14} />
Bridge
</Link>
<WalletConnect onBeforeDisconnect={() => { userInitiatedDisconnectRef.current = true; }} />
<div className="shrink-0 max-w-[45%] sm:max-w-none ml-auto">
<WalletConnect onBeforeDisconnect={() => { userInitiatedDisconnectRef.current = true; }} />
</div>
</div>
</header>
<main className="flex-1">
<MobileNavDrawer open={menuOpen} onClose={() => setMenuOpen(false)} title="DBIS OMNL" items={drawerItems} />
<MobileBottomNav onOpenMenu={() => setMenuOpen(true)} />
<main className="flex-1 w-full min-w-0 overflow-x-hidden">
<Outlet />
</main>
</div>
);
}

View File

@@ -34,7 +34,7 @@ export default function ToastProvider() {
},
loading: {
iconTheme: {
primary: '#3b82f6',
primary: '#22c55e',
secondary: '#fff',
},
},

View File

@@ -128,7 +128,7 @@ export default function WalletConnect({ onBeforeDisconnect }: WalletConnectProps
<>
<button
onClick={() => setShowConnectModal(true)}
className="px-6 py-3 bg-teal-600 text-white font-medium rounded-xl hover:bg-teal-500 transition-colors focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-[#252830] min-h-[44px] w-full md:w-auto"
className="px-6 py-3 bg-green-600 text-white font-medium rounded-xl hover:bg-green-500 transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 focus:ring-offset-[#252830] min-h-[44px] w-full md:w-auto"
>
Connect Wallet
</button>
@@ -152,7 +152,7 @@ export default function WalletConnect({ onBeforeDisconnect }: WalletConnectProps
>
<span>{connector.name}</span>
{isPending && (
<svg className="animate-spin h-5 w-5 text-teal-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<svg className="animate-spin h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>

View File

@@ -13,6 +13,7 @@ export const CHAIN_ICON_URLS: Record<number, string> = {
100: `${TW}/xdai/info/logo.png`,
137: `${TW}/polygon/info/logo.png`,
138: ETH_DIAMOND, // Chain 138 (DBIS) - from chain-138.json
900001: ETH_DIAMOND, // Shiva Blockchain (OneX)
42161: `${TW}/arbitrum/info/logo.png`,
43114: `${TW}/avalanchec/info/logo.png`,
8453: `${TW}/base/info/logo.png`,

View File

@@ -9,28 +9,29 @@ function prodOrLocal(envValue: string | undefined, prodPath: string, localUrl: s
export const TOKEN_AGGREGATION_URL = prodOrLocal(
import.meta.env.VITE_TOKEN_AGGREGATION_URL,
'/api/v1',
'https://explorer.d-bis.org/api/v1',
'http://localhost:3000',
);
export const DBIS_EXCHANGE_URL = prodOrLocal(
import.meta.env.VITE_DBIS_EXCHANGE_URL,
'/exchange',
'https://exchange.omdnl.org/exchange',
'http://localhost:3012',
);
export const SETTLEMENT_MIDDLEWARE_URL = prodOrLocal(
import.meta.env.VITE_SETTLEMENT_MIDDLEWARE_URL,
'/settlement',
'https://secure.omdnl.org/settlement',
'http://localhost:3011',
);
/** Production nginx maps /settlement/* → settlement-middleware /api/v1/settlement/* */
/** Production nginx maps /settlement/* and /exchange/* to middleware; bare localhost needs /api/v1/... */
function proxiedServiceUrl(base: string, servicePrefix: string, path: string): string {
const root = base.replace(/\/$/, '');
const suffix = path.startsWith('/') ? path : `/${path}`;
if (root.startsWith('/')) {
return `${root}${suffix}`;
}
return `${root}${servicePrefix}${suffix}`;
const nginxPortal = /\/(settlement|exchange)$/.test(root);
return nginxPortal ? `${root}${suffix}` : `${root}${servicePrefix}${suffix}`;
}
export function settlementApi(path: string): string {
@@ -41,11 +42,10 @@ export function exchangeApi(path: string): string {
return proxiedServiceUrl(DBIS_EXCHANGE_URL, '/api/v1/exchange', path);
}
/** Bearer auth for OMNL mutating APIs — production portals inject via nginx; never embed operator keys in browser. */
/** Bearer auth for OMNL mutating APIs — set VITE_OMNL_API_KEY at build for tunnel deploy; nginx injects in prod portals. */
export function omnlApiHeaders(json = true): Record<string, string> {
const headers: Record<string, string> = {};
if (json) headers['Content-Type'] = 'application/json';
if (import.meta.env.PROD) return headers;
const key = import.meta.env.VITE_OMNL_API_KEY?.trim();
if (key) headers.Authorization = `Bearer ${key}`;
return headers;

View File

@@ -13,6 +13,13 @@ import {
optimism,
} from 'wagmi/chains'
import {
SHIVA_ONEX_CHAIN_ID,
shivaOnexExplorerUrl,
shivaOnexRpcUrl,
} from './onexShiva'
const rpcUrlShivaOneX = shivaOnexRpcUrl
const rpcUrl138 = import.meta.env.VITE_RPC_URL_138 || 'https://rpc-http-pub.d-bis.org'
const rpcUrl651940 = import.meta.env.VITE_RPC_URL_651940 || 'https://mainnet-rpc.alltra.global'
const rpcUrl42793 = import.meta.env.VITE_RPC_URL_42793 || 'https://node.mainnet.etherlink.com'
@@ -74,6 +81,30 @@ export const chain138 = defineChain({
},
})
/** Chain 900001 - Shiva Blockchain (OneX Wallet / OneX Bank) */
export const shivaOneXChain = defineChain({
id: SHIVA_ONEX_CHAIN_ID,
name: 'Shiva Blockchain (OneX)',
network: 'shiva-onex',
nativeCurrency: {
decimals: 18,
name: 'Shiva',
symbol: 'SHIVA',
},
rpcUrls: {
default: { http: [rpcUrlShivaOneX] },
public: { http: [rpcUrlShivaOneX] },
},
blockExplorers: {
default: {
name: 'Shiva OneX Explorer',
url: shivaOnexExplorerUrl,
},
},
})
export const shivaOneXChainId = shivaOneXChain.id
/** Chain 651940 - ALL Mainnet */
export const allMainnet = defineChain({
id: 651940,
@@ -165,6 +196,7 @@ export const gnosis = defineChain({
const customChainsWithout2138 = [
chain138,
shivaOneXChain,
allMainnet,
etherlink,
bsc,
@@ -175,6 +207,7 @@ const customChainsWithout2138 = [
const customChainsWith2138 = [
chain138,
shivaOneXChain,
chain2138Testnet,
allMainnet,
etherlink,
@@ -191,8 +224,8 @@ export const customChains = chain2138TestnetEnabled
/** Source chains the frontend can intentionally target for wallet UX. */
export const frontendSourceChains = chain2138TestnetEnabled
? [chain138, chain2138Testnet] as const
: [chain138] as const
? ([chain138, shivaOneXChain, chain2138Testnet] as const)
: ([chain138, shivaOneXChain] as const)
/** Shared source-chain IDs for connect/switch prompts. */
export const frontendSourceChainIds: number[] = frontendSourceChains.map((chain) => chain.id)
@@ -217,6 +250,7 @@ export const allNetworks = [...customChains, ...standardChains] as const
/** Chain IDs we support (for CowSwap, Jumper, UI filters) - all deployed contract networks */
export const supportedChainIds = [
chain138.id,
shivaOneXChain.id,
...(chain2138TestnetEnabled ? [chain2138Testnet.id] : []),
allMainnet.id,
etherlink.id,
@@ -235,6 +269,7 @@ export const supportedChainIds = [
export const chainRpcUrls: Record<number, string> = {
[mainnet.id]: import.meta.env.VITE_MAINNET_RPC_URL || 'https://ethereum.publicnode.com',
[chain138.id]: rpcUrl138,
[shivaOneXChain.id]: rpcUrlShivaOneX,
...(chain2138TestnetEnabled ? { [chain2138Testnet.id]: rpcUrl2138 } : {}),
[allMainnet.id]: rpcUrl651940,
[etherlink.id]: rpcUrl42793,

View File

@@ -48,14 +48,11 @@ export default function CentralBankDashboard() {
return (
<div className="dash-page cb-page max-w-[1400px] mx-auto px-4 py-6">
<div className="dash-page cb-page app-page-shell px-4 py-6">
<NasaPageHeader
icon="central-bank"
missionId="CB-138"
title="Central Bank Operations"
subtitle={`Live money supply for ${office?.name ?? 'HOSPITALLERS — Zardasht (Iraq / Iran Operations)'} (Office ${office?.officeId ?? 24}) · settlement · SWIFT · HYBX · Chain 138`}
@@ -116,7 +113,7 @@ export default function CentralBankDashboard() {
<section id="z-full-settlement" className="scroll-mt-20">
<h2 className="dash-section-title mb-1 flex items-center gap-2">
<h2 className="dash-section-title mb-1 flex items-center justify-center gap-2">
<NasaIcon name="money" size={14} className="nasa-icon--telemetry" />
@@ -160,7 +157,7 @@ export default function CentralBankDashboard() {
subtitle="Middleware health and refresh"
icon="satellite"
icon="chart"
action={
@@ -258,13 +255,13 @@ export default function CentralBankDashboard() {
<p className="dash-section-subtitle mt-4 flex flex-wrap gap-3">
<Link to="/office-24" className="text-[#00d4ff] hover:underline inline-flex items-center gap-1">
<Link to="/office-24" className="text-[#4ade80] hover:underline inline-flex items-center gap-1">
<NasaIcon name="office" size={12} /> Office 24 dashboard
</Link>
<Link to="/trade" className="text-[#00d4ff] hover:underline inline-flex items-center gap-1">
<Link to="/trade" className="text-[#4ade80] hover:underline inline-flex items-center gap-1">
<NasaIcon name="trade" size={12} /> DBIS Trade

View File

@@ -1,109 +1,214 @@
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 id="z-settlement-protocols-chains" className="dash-card cb-card lg:col-span-2 scroll-mt-20">
<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}
type="button"
onClick={() => setFilter(f)}
className={`px-3 py-1 text-xs capitalize font-mono border ${
filter === f
? 'border-[#00d4ff] text-[#00d4ff] bg-[#00d4ff]/10'
: 'border-[#1e4a8a] text-[#7a9bb8]'
}`}
>
{f}
</button>
))}
</div>
<div className="overflow-x-auto max-h-[360px] overflow-y-auto">
<table className="w-full text-sm nasa-table">
<thead className="sticky top-0">
<tr className="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}>
<td className="py-1.5 pr-3 font-mono text-[#00d4ff]">{c.chainId}</td>
<td className="py-1.5 pr-3 text-[#e8f4ff]">{c.name}</td>
<td className="py-1.5 pr-3 text-xs text-[#7a9bb8]">{c.omnlRole}</td>
<td className="py-1.5 pr-3">
<span
className={`text-xs px-1.5 py-0.5 font-mono uppercase ${
c.status === 'active'
? 'text-[#3dff8a] border border-[#3dff8a]/35'
: c.status === 'staged'
? 'text-[#ffb347] border border-[#ffb347]/35'
: 'text-[#7a9bb8] border border-[#7a9bb8]/35'
}`}
>
{c.status}
</span>
</td>
<td className="py-1.5 pr-3"><StatusIcon ok={!!c.settlement?.swapEnabled} /></td>
<td className="py-1.5"><StatusIcon ok={!!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 justify-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}
type="button"
onClick={() => setFilter(f)}
className={`px-3 py-1 text-xs capitalize rounded border ${
filter === f
? 'border-green-500 text-green-400 bg-green-500/10'
: 'border-white/10 text-[#848e9c]'
}`}
>
{f}
</button>
))}
</div>
<div className="overflow-x-auto max-h-[360px] overflow-y-auto">
<table className="w-full text-sm nasa-table">
<thead className="sticky top-0">
<tr className="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}>
<td className="py-1.5 pr-3 font-mono text-[#4ade80]">{c.chainId}</td>
<td className="py-1.5 pr-3 text-[#e8f4ff]">{c.name}</td>
<td className="py-1.5 pr-3 text-xs text-[#7a9bb8]">{c.omnlRole}</td>
<td className="py-1.5 pr-3">
<span
className={`text-xs px-1.5 py-0.5 font-mono uppercase ${
c.status === 'active'
? 'text-[#3dff8a] border border-[#3dff8a]/35'
: c.status === 'staged'
? 'text-[#ffb347] border border-[#ffb347]/35'
: 'text-[#7a9bb8] border border-[#7a9bb8]/35'
}`}
>
{c.status}
</span>
</td>
<td className="py-1.5 pr-3"><StatusIcon ok={!!c.settlement?.swapEnabled} /></td>
<td className="py-1.5"><StatusIcon ok={!!c.settlement?.bridgeEnabled} /></td>
</tr>
))}
</tbody>
</table>
</div>
</section>
);
}

View File

@@ -67,14 +67,9 @@ export function useCentralBank() {
setOffice(o);
setTokenCount(Array.isArray(m2?.tokens) ? m2.tokens.length : 0);
setMonitor(mon);
setMoneySupply(
ms ??
(h?.status === 'ok'
? { fineractConnected: true, fineractLive: false, officeId: 24 }
: null),
);
setMoneySupply(ms);
if (!h && !o) {
setError('Settlement middleware unreachable');
setError('Settlement middleware unreachable — check production API at explorer.d-bis.org');
}
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to load central bank data');

View File

@@ -16,6 +16,12 @@ import NasaIcon, { StatusIcon } from '../../components/icons/NasaIcon';
import { formatUsd } from '../../utils/formatMoney';
import ChainMatrixPanel from '../central-bank/ChainMatrixPanel';
import Office24BankCorridors from './Office24BankCorridors';
import Office24EcosystemSubjects from './Office24EcosystemSubjects';
export default function Office24Dashboard() {
@@ -44,43 +50,38 @@ export default function Office24Dashboard() {
return (
<div className="dash-page o24-page max-w-[1400px] mx-auto px-4 py-6">
<div className="dash-page o24-page app-page-shell px-4 py-6">
<div className="flex flex-wrap items-start justify-between gap-4 mb-2">
<div className="flex flex-col items-center gap-4 mb-2 text-center">
<div className="flex-1 min-w-0">
<NasaPageHeader
<NasaPageHeader
icon="central-bank"
title={profile?.name ?? 'Office 24 Central Bank — HOSPITALLERS Zardasht'}
icon="office"
subtitle={profile?.externalId ?? 'HOSPITALLERS-ZARDASHT-IRAQ-IRAN · IPSAS Office 24'}
missionId="O24-138"
badges={
title={profile?.name ?? 'HOSPITALLERS — Zardasht (Iraq / Iran Operations)'}
<>
subtitle={profile?.externalId ?? 'HOSPITALLERS-ZARDASHT-IRAQ-IRAN'}
<span className="nasa-badge nasa-badge--nominal">Office 24 Central Bank</span>
badges={
<span className="nasa-badge nasa-badge--caution">128-bank corridors</span>
<>
{profile?.locked && <span className="nasa-badge nasa-badge--caution">Single office · locked</span>}
<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">
<div className="flex flex-wrap gap-2 justify-center">
<Link to="/central-bank" className="nasa-btn inline-flex items-center gap-1">
<NasaIcon name="central-bank" size={12} /> Central Bank
<NasaIcon name="globe" size={12} /> Z Online Bank
</Link>
@@ -104,7 +105,7 @@ export default function Office24Dashboard() {
<h2 className="dash-section-title mb-1 flex items-center gap-2">
<h2 className="dash-section-title mb-1 flex items-center justify-center gap-2">
<NasaIcon name="ledger" size={14} className="nasa-icon--telemetry" />
@@ -146,7 +147,7 @@ export default function Office24Dashboard() {
action={
<button type="button" onClick={() => refresh()} className="text-xs text-[#00d4ff] hover:underline inline-flex items-center gap-1">
<button type="button" onClick={() => refresh()} className="text-xs text-[#4ade80] hover:underline inline-flex items-center gap-1">
<NasaIcon name="refresh" size={12} className={loading ? 'nasa-icon--spin' : ''} />
@@ -232,7 +233,7 @@ export default function Office24Dashboard() {
<DashboardSection title="M2 token registry" subtitle="Tradable lines on Chain 138" icon="rocket" className="lg:col-span-2">
<div className="overflow-x-auto max-h-[420px] overflow-y-auto rounded border border-[#1e4a8a]">
<div className="overflow-x-auto max-h-[420px] overflow-y-auto rounded border border-white/10">
<table className="w-full text-sm nasa-table">
@@ -298,6 +299,16 @@ export default function Office24Dashboard() {
</div>
<div className="grid grid-cols-1 gap-6 mb-8">
<Office24BankCorridors />
<ChainMatrixPanel />
</div>
<Office24EcosystemSubjects />
</div>
);

View File

@@ -49,12 +49,7 @@ export function useOffice24() {
setOffice(o);
setTokens(Array.isArray(t?.tokens) ? t.tokens : []);
setHealth(h);
setMoneySupply(
ms ??
(o
? { fineractConnected: true, fineractLive: false, officeId: 24 }
: null),
);
setMoneySupply(ms);
} finally {
setLoading(false);
}

View File

@@ -1,35 +1,74 @@
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;
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;
}) {
return (
<section className={`dash-card ${className}`}>
<div className="flex flex-col items-center text-center gap-3 mb-4">
<div>
<h2 className="dash-section-title flex items-center justify-center gap-2">
{icon && <NasaIcon name={icon} size={14} className="nasa-icon--telemetry" />}
{title}
</h2>
{subtitle && <p className="dash-section-subtitle">{subtitle}</p>}
</div>
{action}
</div>
<div className="dash-section-body text-left">
{children}
</div>
</section>
);
}

View File

@@ -1,36 +1,67 @@
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,
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-green-500',
green: 'border-l-green-500',
gold: 'border-l-amber-500',
neutral: 'border-l-gray-500',
};
export default function DashboardStatCard({
label,
value,
hint,
accent = 'neutral',
icon,
}: {
label: string;
value: string;
hint?: string;
accent?: Accent;
icon?: NasaIconName;
}) {
return (
<div className={`dash-card border-l-4 ${ACCENT[accent]} text-center`}>
{icon && (
<div className="flex justify-center mb-2">
<NasaIcon name={icon} size={18} className="nasa-icon--telemetry" />
</div>
)}
<p className="dash-stat-label justify-center">{label}</p>
<p className="dash-stat-value">{value}</p>
{hint && <p className="dash-stat-hint">{hint}</p>}
</div>
);
}

View File

@@ -1,48 +1,33 @@
import { useEffect, useState, type ReactNode } from 'react';
import 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;
centered?: boolean;
};
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);
}, []);
export default function NasaPageHeader({ icon, title, subtitle, badges, centered = true }: Props) {
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>
<header className={`nasa-header mb-6${centered ? ' nasa-header--centered' : ''}`}>
<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" />
<NasaIcon name={icon} size={22} 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>}
{badges && (
<div className={`flex flex-wrap items-center gap-2${centered ? ' justify-center w-full' : ''}`}>
{badges}
</div>
)}
</div>
</header>
);

View File

@@ -1,7 +1,7 @@
import { Link } from 'react-router-dom';
import { Link } from 'react-router-dom';
import NasaIcon from '../../components/icons/NasaIcon';
/** Same-origin portal surfaces no FQDNs (see /docs). */
/** Same-origin portal surfaces ÔÇö no FQDNs (see /docs). */
const LINKS = [
{ label: 'Central Bank', to: '/central-bank', icon: 'central-bank' as const },
{ label: 'Office 24', to: '/office-24', icon: 'office' as const },

View File

@@ -42,12 +42,12 @@ function SidePanel({
children,
}: {
title: string;
icon: 'ledger' | 'rocket' | 'rail' | 'chart' | 'satellite';
icon: 'ledger' | 'rocket' | 'rail' | 'chart';
children: ReactNode;
}) {
return (
<div className="dash-card">
<h3 className="dash-section-title mb-3 flex items-center gap-2">
<h3 className="dash-section-title mb-3 flex items-center justify-center gap-2">
<NasaIcon name={icon} size={14} className="nasa-icon--telemetry" />
{title}
</h3>
@@ -75,13 +75,12 @@ export default function DBISSwapPanel() {
: '—';
return (
<div className="nasa-page px-4 py-6 max-w-6xl mx-auto">
<div className="app-page-shell nasa-page px-4 py-6 w-full">
<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>}
badges={<span className="dash-badge dash-badge--green">Chain 138</span>}
/>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
@@ -116,16 +115,16 @@ export default function DBISSwapPanel() {
/>
</label>
<div className="flex items-center gap-3 text-sm font-mono">
<span className="text-[#7a9bb8] uppercase text-xs">Slippage</span>
<div className="flex items-center gap-3 text-sm">
<span className="text-[#848e9c] text-xs font-medium">Slippage</span>
{[25, 50, 100].map((bps) => (
<button
key={bps}
type="button"
className={`px-3 py-1 text-xs border font-mono ${
className={`px-3 py-1 text-xs rounded border ${
swap.slippageBps === bps
? 'border-[#00d4ff] text-[#00d4ff] bg-[#00d4ff]/10'
: 'border-[#1e4a8a] text-[#7a9bb8]'
? 'border-green-500 text-green-400 bg-green-500/10'
: 'border-white/10 text-[#848e9c]'
}`}
onClick={() => swap.setSlippageBps(bps)}
>
@@ -137,10 +136,10 @@ export default function DBISSwapPanel() {
<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>
{outDisplay} <span className="text-base text-green-400">{swap.tokenOut.symbol}</span>
</p>
{swap.plan?.plannerResponse?.routePlan?.legs && (
<p className="text-xs text-[#7a9bb8] mt-2 font-mono">
<p className="text-xs text-[#848e9c] mt-2">
Route:{' '}
{swap.plan.plannerResponse.routePlan.legs
.map((l: { provider?: string }) => l.provider)
@@ -157,9 +156,9 @@ export default function DBISSwapPanel() {
</p>
)}
{swap.txHash && (
<p className="text-xs text-[#7a9bb8] break-all font-mono">
<p className="text-xs text-[#848e9c] break-all">
Tx:{' '}
<a href={`https://explorer.d-bis.org/tx/${swap.txHash}`} target="_blank" rel="noreferrer" className="text-[#00d4ff] hover:underline">
<a href={`https://explorer.d-bis.org/tx/${swap.txHash}`} target="_blank" rel="noreferrer" className="text-green-400 hover:underline">
{swap.txHash}
</a>
</p>
@@ -186,16 +185,16 @@ export default function DBISSwapPanel() {
<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>
<p className="text-sm text-[#848e9c]">Connect wallet for crypto balances</p>
) : swap.ledger ? (
<div className="text-xs space-y-2 font-mono">
<div className="text-xs space-y-2">
<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>
<p>M3: {(swap.ledger as { fundLedger?: { M3?: { tokenLiabilities?: string } } }).fundLedger?.M3?.tokenLiabilities ?? '—'}</p>
<p>M4: {(swap.ledger as { fundLedger?: { M4?: { nearMoney?: string } } }).fundLedger?.M4?.nearMoney ?? '—'}</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>
<div className="mt-2 pt-2 border-t border-white/10">
<p className="text-[#848e9c] mb-1">Wallet balances</p>
{Object.entries(
(swap.ledger as { cryptoLedger: { balances: Record<string, string> } }).cryptoLedger.balances,
).map(([sym, bal]) => (
@@ -212,7 +211,7 @@ export default function DBISSwapPanel() {
</SidePanel>
<SidePanel title="M2→M3 token load" icon="rocket">
<p className="text-xs text-[#7a9bb8] mb-3">GL 2200 GL 2300 · on-chain M3</p>
<p className="text-xs text-[#848e9c] mb-3">GL 2200 GL 2300 · on-chain M3</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}
@@ -232,8 +231,8 @@ export default function DBISSwapPanel() {
</SidePanel>
{swap.lastSettlement && (
<SidePanel title="Last settlement" icon="satellite">
<p className="text-xs text-[#7a9bb8] font-mono break-all">
<SidePanel title="Last settlement" icon="chart">
<p className="text-xs text-[#848e9c] break-all">
{(swap.lastSettlement as { phase?: string }).phase ?? 'OK'} · {(swap.lastSettlement as { settlementId?: string }).settlementId ?? '—'}
</p>
</SidePanel>
@@ -241,16 +240,16 @@ export default function DBISSwapPanel() {
<SidePanel title="Swap monitor" icon="chart">
{swap.monitor ? (
<div className="text-xs space-y-2 font-mono">
<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}</p>
<ul className="max-h-40 overflow-y-auto space-y-1 text-[#7a9bb8]">
<ul className="max-h-40 overflow-y-auto space-y-1 text-[#848e9c]">
{((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>
<p className="text-xs text-[#848e9c]">Loading</p>
)}
</SidePanel>
</div>

View File

@@ -25,12 +25,12 @@ function MarketRow({
active ? 'trade-market-row--active' : ''
}`}
>
<span className="font-medium text-[#e8f4ff] flex items-center gap-1.5">
<NasaIcon name="money" size={12} className="nasa-icon--telemetry" />
<span className="font-medium text-[#eaecef] flex items-center gap-1.5">
<NasaIcon name="money" size={12} className="text-green-500" />
{token.symbol}
<span className="text-[#7a9bb8]">/{quote}</span>
<span className="text-[#848e9c]">/{quote}</span>
</span>
<span className="text-[#3dff8a] text-xs font-mono">M2</span>
<span className="text-green-500 text-xs">M2</span>
</button>
);
}
@@ -44,7 +44,7 @@ function OrderBookMock({ price }: { price: string }) {
});
return (
<div className="trade-orderbook text-xs font-mono">
<div className="flex justify-between text-[#7a9bb8] px-2 py-1 border-b border-[#1e4a8a]">
<div className="flex justify-between text-[#848e9c] px-2 py-1 border-b border-white/10">
<span>Price</span>
<span>Amount</span>
</div>
@@ -115,11 +115,37 @@ export default function BinanceTradePanel() {
: '—';
return (
<div className="trade-layout nasa-mission-root">
{/* Markets sidebar */}
<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" />
<div className="trade-layout app-shell">
{/* Mobile market picker */}
<div className="lg:hidden col-span-full border-b border-white/10 p-3 bg-[var(--nasa-panel)] space-y-2">
<label className="block text-xs text-[#848e9c] font-medium">Market</label>
<input
type="search"
placeholder="Search token"
value={marketSearch}
onChange={(e) => setMarketSearch(e.target.value)}
className="nasa-input !min-h-[44px] !text-base w-full"
/>
<select
className="w-full bg-[#2b3139] border border-[#474d57] rounded px-3 py-3 text-base min-h-[44px]"
value={swap.tokenOut.address}
onChange={(e) => {
const t = markets.find((x) => x.address === e.target.value) ?? swap.tokenOut;
selectMarket(t);
}}
>
{markets.map((t) => (
<option key={t.address} value={t.address}>
{t.symbol} / {quoteToken?.symbol ?? 'USDT'}
</option>
))}
</select>
</div>
{/* Markets sidebar — desktop */}
<aside className="trade-markets hidden lg:flex flex-col border-r border-white/10">
<div className="p-3 border-b border-white/10 flex items-center gap-2">
<NasaIcon name="search" size={14} className="text-green-500 shrink-0" />
<input
type="search"
placeholder="Search markets"
@@ -129,9 +155,9 @@ export default function BinanceTradePanel() {
/>
</div>
<div className="flex-1 overflow-y-auto">
<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 className="px-3 py-2 text-xs text-[#848e9c] font-medium flex items-center gap-1.5">
<NasaIcon name="trade" size={12} className="text-green-500" />
{markets.length} markets
</p>
{markets.map((t) => (
<MarketRow
@@ -147,29 +173,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-[#1e4a8a]">
<div className="trade-pair-header flex flex-wrap items-center gap-4 px-4 py-3 border-b border-white/10">
<div className="flex items-center gap-2">
<TokenIcon symbol={swap.tokenOut.symbol} size={28} />
<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">
<p className="text-xs text-[#848e9c]">Spot</p>
<h1 className="text-xl font-bold text-[#eaecef]">
{swap.tokenOut.symbol}/{quoteToken?.symbol ?? 'USDT'}
</h1>
</div>
</div>
<span className="text-2xl font-semibold text-[#3dff8a] font-mono">{displayPrice}</span>
<span className="nasa-badge nasa-badge--nominal text-xs">Office 24 · Chain 138</span>
<span className="text-xs text-[#7a9bb8] font-mono flex items-center gap-1">
<NasaIcon name="satellite" size={12} />
37 M2 tokens · tradable &amp; swappable
<span className="text-2xl font-semibold text-green-400 tabular-nums">{displayPrice}</span>
<span className="dash-badge dash-badge--green text-xs">Chain 138</span>
<span className="text-xs text-[#848e9c]">
{markets.length} tradable tokens
</span>
</div>
<div className="flex-1 grid grid-cols-1 xl:grid-cols-[1fr_280px] min-h-[320px]">
<div className="trade-chart border-b xl:border-b-0 xl:border-r border-[#2d6bb8] p-4">
<div className="trade-chart border-b xl:border-b-0 xl:border-r border-white/10 p-4">
<div className="trade-chart__canvas h-full flex items-end justify-around px-2 pb-2 gap-1">
{Array.from({ length: 24 }, (_, i) => (
<div
@@ -184,15 +206,23 @@ export default function BinanceTradePanel() {
</div>
<p className="text-xs text-[#848e9c] mt-2 text-center">Live chart · connect WebSocket feed for production</p>
</div>
<div className="trade-book xl:hidden p-2 border-t border-white/10">
<details className="text-xs">
<summary className="text-[#848e9c] px-2 py-2 cursor-pointer min-h-[44px] flex items-center font-medium">
Order book
</summary>
<OrderBookMock price={displayPrice} />
</details>
</div>
<div className="trade-book hidden xl:block p-2">
<p className="text-xs text-[#848e9c] uppercase px-2 py-1">Order book</p>
<p className="text-xs text-[#848e9c] px-2 py-1 font-medium">Order book</p>
<OrderBookMock price={displayPrice} />
</div>
</div>
<div className="trade-recent border-t border-[#2d6bb8] p-3 max-h-[140px] overflow-y-auto">
<p className="text-xs text-[#848e9c] uppercase mb-2">Recent trades</p>
<div className="grid grid-cols-3 gap-2 text-xs font-mono text-[#848e9c]">
<div className="trade-recent border-t border-white/10 p-3 max-h-[140px] overflow-y-auto">
<p className="text-xs text-[#848e9c] mb-2 font-medium">Recent trades</p>
<div className="grid grid-cols-3 gap-2 text-xs tabular-nums text-[#848e9c]">
{((swap.monitor as { swaps?: { status: string; amountIn: string; createdAt: string }[] })?.swaps ?? [])
.slice(0, 6)
.map((s, i) => (
@@ -207,8 +237,8 @@ export default function BinanceTradePanel() {
</section>
{/* Trade panel */}
<aside className="trade-panel w-full lg:w-[320px] shrink-0 border-l border-[#2d6bb8] flex flex-col">
<div className="flex border-b border-[#2d6bb8]">
<aside className="trade-panel w-full lg:w-[320px] shrink-0 border-l border-white/10 flex flex-col">
<div className="flex border-b border-white/10">
<button
type="button"
className={`flex-1 py-3 text-sm font-semibold ${side === 'buy' ? 'text-[#0ecb81] border-b-2 border-[#0ecb81]' : 'text-[#848e9c]'}`}

View File

@@ -11,7 +11,7 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #1a1d24;
background: #141a16;
min-height: 100vh;
}
@@ -50,10 +50,10 @@ body {
@keyframes pulse-glow {
0%, 100% {
box-shadow: 0 0 5px rgba(59, 130, 246, 0.5);
box-shadow: 0 0 5px rgba(34, 197, 94, 0.5);
}
50% {
box-shadow: 0 0 20px rgba(59, 130, 246, 0.8), 0 0 30px rgba(147, 51, 234, 0.6);
box-shadow: 0 0 20px rgba(34, 197, 94, 0.8), 0 0 30px rgba(74, 222, 128, 0.6);
}
}
@@ -89,12 +89,12 @@ body {
}
::-webkit-scrollbar-thumb {
background: linear-gradient(to bottom, #3b82f6, #8b5cf6);
background: linear-gradient(to bottom, #22c55e, #16a34a);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(to bottom, #2563eb, #7c3aed);
background: linear-gradient(to bottom, #16a34a, #15803d);
}
/* Glass morphism effect */
@@ -107,7 +107,7 @@ body {
/* Gradient text */
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #22c55e 0%, #059669 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@@ -115,13 +115,13 @@ body {
/* Button hover effects */
.btn-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #22c55e 0%, #059669 100%);
transition: all 0.3s ease;
}
.btn-gradient:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
box-shadow: 0 10px 25px rgba(34, 197, 94, 0.4);
}
/* Card hover effect */
@@ -193,7 +193,7 @@ body {
/* Focus styles for accessibility */
*:focus-visible {
outline: 2px solid #3b82f6;
outline: 2px solid #22c55e;
outline-offset: 2px;
border-radius: 4px;
}
@@ -224,7 +224,7 @@ textarea:focus {
/* Loading spinner */
.spinner {
border: 3px solid rgba(59, 130, 246, 0.1);
border-top-color: #3b82f6;
border-top-color: #22c55e;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@@ -253,181 +253,19 @@ textarea:focus {
/* Gradient backgrounds */
.bg-gradient-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #22c55e 0%, #059669 100%);
}
.bg-gradient-secondary {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
background: linear-gradient(135deg, #4ade80 0%, #16a34a 100%);
}
.bg-gradient-success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
background: linear-gradient(135deg, #34d399 0%, #22c55e 100%);
}
/* Text selection */
::selection {
background-color: rgba(59, 130, 246, 0.3);
background-color: rgba(34, 197, 94, 0.3);
color: inherit;
}
/* Portal Styling Enhancements */
/* Animated grid pattern for portal effect */
@keyframes gridMove {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(60px, 60px);
}
}
.portal-grid {
background-image:
linear-gradient(rgba(34, 211, 238, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(34, 211, 238, 0.1) 1px, transparent 1px);
background-size: 60px 60px;
animation: gridMove 20s linear infinite;
}
/* Portal entrance animation */
@keyframes portalOpen {
0% {
opacity: 0;
transform: scale(0.8) rotateY(90deg);
filter: blur(10px);
}
100% {
opacity: 1;
transform: scale(1) rotateY(0deg);
filter: blur(0px);
}
}
.portal-entrance {
animation: portalOpen 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Holographic shimmer effect */
@keyframes holographic {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.holographic-text {
background: linear-gradient(
90deg,
#06b6d4 0%,
#8b5cf6 25%,
#ec4899 50%,
#8b5cf6 75%,
#06b6d4 100%
);
background-size: 200% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: holographic 3s linear infinite;
}
/* Energy flow animation */
@keyframes energyFlow {
0% {
transform: translateX(-200%) translateY(-50%);
}
100% {
transform: translateX(200%) translateY(-50%);
}
}
.energy-flow::before {
content: '';
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(
90deg,
transparent,
rgba(34, 211, 238, 0.8),
rgba(168, 85, 247, 0.8),
rgba(236, 72, 153, 0.8),
transparent
);
animation: energyFlow 2s ease-in-out infinite;
}
/* Portal glow pulse */
@keyframes portalGlow {
0%, 100% {
box-shadow:
0 0 20px rgba(34, 211, 238, 0.4),
0 0 40px rgba(168, 85, 247, 0.3),
inset 0 0 20px rgba(34, 211, 238, 0.1);
}
50% {
box-shadow:
0 0 40px rgba(34, 211, 238, 0.6),
0 0 80px rgba(168, 85, 247, 0.5),
inset 0 0 30px rgba(34, 211, 238, 0.2);
}
}
.portal-glow {
animation: portalGlow 3s ease-in-out infinite;
}
/* Floating particles */
@keyframes float {
0%, 100% {
transform: translateY(0) translateX(0);
opacity: 0.3;
}
50% {
transform: translateY(-20px) translateX(10px);
opacity: 0.8;
}
}
.floating-particle {
animation: float 4s ease-in-out infinite;
}
/* Neon border effect */
.neon-border {
border: 2px solid;
border-image: linear-gradient(
45deg,
rgba(34, 211, 238, 0.5),
rgba(168, 85, 247, 0.5),
rgba(236, 72, 153, 0.5),
rgba(34, 211, 238, 0.5)
) 1;
box-shadow:
0 0 10px rgba(34, 211, 238, 0.3),
inset 0 0 10px rgba(34, 211, 238, 0.1);
}
/* Portal ring effect */
@keyframes portalRing {
0%, 100% {
opacity: 0.3;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.05);
}
}
.portal-ring {
animation: portalRing 2s ease-in-out infinite;
}

View File

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

View File

@@ -114,33 +114,48 @@ export default function AdminPanel() {
<div className="container mx-auto px-4 py-8 max-w-7xl">
<div className="bg-white/10 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 overflow-hidden">
{/* Header */}
<div className="border-b border-white/20 p-6 bg-gradient-to-r from-blue-600/20 via-purple-600/20 to-pink-600/20">
<div className="border-b border-white/20 p-6 bg-gradient-to-r from-blue-600/20 via-purple-600/20 to-pink-600/20 text-center">
<h1 className="text-3xl font-bold text-white mb-2">Admin Panel</h1>
<p className="text-white/70 text-sm">
Connected as: <span className="font-mono text-white/90">{address}</span>
</p>
</div>
{/* Tabs */}
<div className="flex border-b border-white/20 bg-black/20">
{/* Tabs — mobile select + desktop scroll */}
<div className="sm:hidden p-3 border-b border-white/20 bg-black/30">
<label htmlFor="admin-tab-select" className="block text-xs text-white/60 mb-1">Section</label>
<select
id="admin-tab-select"
value={activeTab}
onChange={(e) => setActiveTab(e.target.value as TabType)}
className="w-full bg-[#1a1d24] text-white border border-white/20 rounded-lg px-3 py-3 text-sm min-h-[44px]"
>
{tabs.map((tab) => (
<option key={tab.id} value={tab.id}>
{tab.icon} {tab.label}
</option>
))}
</select>
</div>
<div className="admin-tabs hidden sm:flex border-b border-white/20 bg-black/20 overflow-x-auto">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex-1 px-6 py-4 font-semibold transition-all duration-300 ${
className={`shrink-0 px-4 sm:px-6 py-3 sm:py-4 font-semibold transition-all duration-300 whitespace-nowrap min-h-[44px] ${
activeTab === tab.id
? 'bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 text-white border-b-2 border-white'
: 'text-white/70 hover:text-white hover:bg-white/10'
}`}
>
<span className="mr-2">{tab.icon}</span>
{tab.label}
<span className="mr-1 sm:mr-2">{tab.icon}</span>
<span className="text-xs sm:text-sm">{tab.label}</span>
</button>
))}
</div>
{/* Content */}
<div className="p-6">
<div className="p-4 sm:p-6">
{activeTab === 'dashboard' && <AdminDashboard />}
{activeTab === 'mainnet-tether' && <MainnetTetherAdmin />}
{activeTab === 'transaction-mirror' && <TransactionMirrorAdmin />}

View File

@@ -49,17 +49,17 @@ export default function BridgePage() {
</div>
{/* Tabs — text only, no icons */}
<div className="mb-6">
<nav className="flex flex-wrap gap-1 bg-[#252830] rounded-xl p-1.5 border border-white/10" role="tablist">
<div className="mb-6 -mx-1 px-1 overflow-x-auto">
<nav className="flex flex-nowrap sm:flex-wrap gap-1 bg-[#252830] rounded-xl p-1.5 border border-white/10 min-w-min sm:min-w-0" role="tablist">
<button
onClick={() => setActiveTab('custom')}
role="tab"
aria-selected={activeTab === 'custom'}
aria-controls="custom-tabpanel"
id="custom-tab"
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-teal-500/50 ${
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-green-500/50 shrink-0 whitespace-nowrap min-h-[44px] ${
activeTab === 'custom'
? 'bg-teal-600 text-white'
? 'bg-green-600 text-white'
: 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
@@ -71,9 +71,9 @@ export default function BridgePage() {
aria-selected={activeTab === 'trustless'}
aria-controls="trustless-tabpanel"
id="trustless-tab"
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-teal-500/50 ${
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-green-500/50 shrink-0 whitespace-nowrap min-h-[44px] ${
activeTab === 'trustless'
? 'bg-teal-600 text-white'
? 'bg-green-600 text-white'
: 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
@@ -85,9 +85,9 @@ export default function BridgePage() {
aria-selected={activeTab === 'evm'}
aria-controls="evm-tabpanel"
id="evm-tab"
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-teal-500/50 ${
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-green-500/50 shrink-0 whitespace-nowrap min-h-[44px] ${
activeTab === 'evm'
? 'bg-teal-600 text-white'
? 'bg-green-600 text-white'
: 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
@@ -99,9 +99,9 @@ export default function BridgePage() {
aria-selected={activeTab === 'xrpl'}
aria-controls="xrpl-tabpanel"
id="xrpl-tab"
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-teal-500/50 ${
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-green-500/50 shrink-0 whitespace-nowrap min-h-[44px] ${
activeTab === 'xrpl'
? 'bg-teal-600 text-white'
? 'bg-green-600 text-white'
: 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
@@ -113,9 +113,9 @@ export default function BridgePage() {
aria-selected={activeTab === 'track'}
aria-controls="track-tabpanel"
id="track-tab"
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-teal-500/50 ${
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-green-500/50 shrink-0 whitespace-nowrap min-h-[44px] ${
activeTab === 'track'
? 'bg-teal-600 text-white'
? 'bg-green-600 text-white'
: 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
@@ -148,7 +148,7 @@ export default function BridgePage() {
<select
value={ccipDestinationSelector}
onChange={(e) => setCcipDestinationSelector(e.target.value)}
className="px-3 py-2 rounded-lg bg-[#1a1d24] text-white border border-white/20 min-w-[200px] focus:ring-2 focus:ring-teal-500/50 focus:border-teal-500"
className="px-3 py-2 rounded-lg bg-[#1a1d24] text-white border border-white/20 min-w-[200px] focus:ring-2 focus:ring-green-500/50 focus:border-green-500"
>
{CCIP_DESTINATIONS.map((d) => (
<option key={d.chainId} value={d.selector}>{d.name}</option>

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import CentralBankDashboard from '../features/central-bank/CentralBankDashboard';
import OnlineBankApp from '../features/online-bank/OnlineBankApp';
export default function CentralBankPage() {
const { hash } = useLocation();
@@ -12,5 +12,5 @@ export default function CentralBankPage() {
el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, [hash]);
return <CentralBankDashboard />;
return <OnlineBankApp />;
}

View File

@@ -1,5 +1,5 @@
/**
* Developer / API documentation same-origin internal path map.
/**
* Developer / API documentation ÔÇö same-origin internal path map.
*/
import { Link } from 'react-router-dom';
import {
@@ -27,7 +27,7 @@ export default function DocsPage() {
<div className="container mx-auto px-4 py-12 max-w-4xl">
<h1 className="text-2xl font-bold text-white mb-2">Developer reference</h1>
<p className="text-[#A0A0A0] text-sm mb-8">
Internal links use <strong className="text-white font-medium">same-origin paths only</strong> no FQDNs,
Internal links use <strong className="text-white font-medium">same-origin paths only</strong> ÔÇö no FQDNs,
localhost, or 127.0.0.1. Institutional share URLs live in operator manifests.
</p>
@@ -99,13 +99,13 @@ export default function DocsPage() {
<td className="py-2 pr-4">{entry.label}</td>
)}
{group.id !== 'spa' && group.id !== 'omnl-html' && (
<td className="py-2 pr-4 font-mono text-xs">{entry.method ?? ''}</td>
<td className="py-2 pr-4 font-mono text-xs">{entry.method ?? 'ÔÇö'}</td>
)}
<td className="py-2 pr-4">
<PathCode path={entry.path} />
</td>
{group.id !== 'spa' && group.id !== 'omnl-html' && (
<td className="py-2 pr-4 text-xs">{entry.auth ?? ''}</td>
<td className="py-2 pr-4 text-xs">{entry.auth ?? 'ÔÇö'}</td>
)}
{group.id !== 'spa' && group.id !== 'omnl-html' && (
<td className="py-2 text-xs text-[#888]">{entry.notes ?? ''}</td>
@@ -150,7 +150,7 @@ export default function DocsPage() {
</section>
<p className="text-xs text-[#666]">
SSOT: <code>config/omnl-internal-paths.v1.json</code> · Operator map:{' '}
SSOT: <code>config/omnl-internal-paths.v1.json</code> À Operator map:{' '}
<code>docs/04-configuration/ZARDASHT_ENDPOINT_MAP.md</code>
</p>
</div>

View File

@@ -1,9 +1,50 @@
export default function HistoryPage() {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Transaction History</h1>
<p className="text-gray-600">Transaction history coming soon...</p>
</div>
)
import { useSearchParams } from 'react-router-dom';
import TransferTracking from '../components/bridge/TransferTracking';
import NasaPageHeader from '../features/omnl-dashboard/NasaPageHeader';
import DashboardSection from '../features/omnl-dashboard/DashboardSection';
const RECENT_KEY = 'dbis-recent-transfer-ids';
function loadRecentIds(): string[] {
try {
const raw = localStorage.getItem(RECENT_KEY);
const parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? parsed.filter((x) => typeof x === 'string').slice(0, 8) : [];
} catch {
return [];
}
}
export default function HistoryPage() {
const [searchParams] = useSearchParams();
const transferIdFromUrl = searchParams.get('transferId') ?? undefined;
const recent = loadRecentIds();
return (
<div className="app-page-shell nasa-page history-page px-3 sm:px-4 py-6 sm:py-8 w-full min-w-0">
<NasaPageHeader
icon="ledger"
title="Transaction History"
subtitle="Look up bridge transfer status by ID. Recent IDs are stored in this browser."
/>
<DashboardSection title="Track transfer" subtitle="Polls /api/bridge/status/:id">
<TransferTracking transferId={transferIdFromUrl} />
</DashboardSection>
{recent.length > 0 && (
<DashboardSection title="Recent in this browser" className="mt-6">
<ul className="space-y-2">
{recent.map((id) => (
<li key={id}>
<a href={`/history?transferId=${encodeURIComponent(id)}`} className="text-[var(--nasa-telemetry)] hover:underline font-mono text-sm break-all">
{id}
</a>
</li>
))}
</ul>
</DashboardSection>
)}
</div>
);
}

View File

@@ -1,9 +1,78 @@
export default function ReservePage() {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Reserve Status</h1>
<p className="text-gray-600">Reserve backing and peg status coming soon...</p>
</div>
)
}
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { OPERATOR_CONSOLE_LINKS } from '../config/ecosystemLinks';
import { TOKEN_AGGREGATION_URL } from '../config/dex';
import NasaPageHeader from '../features/omnl-dashboard/NasaPageHeader';
import DashboardSection from '../features/omnl-dashboard/DashboardSection';
import DashboardStatCard from '../features/omnl-dashboard/DashboardStatCard';
import NasaIcon from '../components/icons/NasaIcon';
type IntegrationStatus = Record<string, unknown> | null;
export default function ReservePage() {
const [status, setStatus] = useState<IntegrationStatus>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
(async () => {
try {
const base = TOKEN_AGGREGATION_URL.replace(/\/$/, '');
const res = await fetch(`${base}/omnl/integration-status`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (!cancelled) setStatus(data);
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : 'API unavailable');
}
})();
return () => { cancelled = true; };
}, []);
const reserveLinks = OPERATOR_CONSOLE_LINKS.filter((l) =>
['reserve-dash', 'compliance', 'omnl-dash'].includes(l.id),
);
return (
<div className="omnl-app min-h-screen">
<div className="app-page-shell dash-page px-3 sm:px-4 py-6">
<NasaPageHeader
icon="money"
title="Reserve & Attestation"
subtitle="Reserve coverage, HO nostro liquidity, and on-chain evidence consoles."
/>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-8">
<DashboardStatCard label="Integration API" value={error ? 'Offline' : status ? 'Live' : '…'} accent={error ? 'gold' : 'green'} hint={error ?? 'token-aggregation'} />
<DashboardStatCard label="Fineract" value={status?.fineract === true ? 'Configured' : '—'} accent="blue" hint="HO ledger" />
<DashboardStatCard label="Reserve store" value={status?.reserveStore138 === true ? 'On-chain' : '—'} accent="green" hint="Chain 138" />
</div>
<DashboardSection title="Reserve consoles" subtitle="Open full operator UIs">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{reserveLinks.map((link) => (
<a
key={link.id}
href={link.href}
className="dash-card flex flex-col items-center text-center gap-2 hover:border-[var(--nasa-telemetry)] transition-colors no-underline"
>
<NasaIcon name={link.icon} size={20} className="nasa-icon--telemetry shrink-0" />
<div>
<div className="text-sm font-semibold text-[var(--nasa-text)]">{link.label}</div>
<div className="text-xs text-[var(--nasa-muted)]">{link.description}</div>
</div>
</a>
))}
</div>
</DashboardSection>
<p className="mt-6 text-sm text-[var(--nasa-muted)] text-center">
<Link to="/hub" className="text-[var(--nasa-telemetry)] hover:underline inline-flex items-center gap-1">
<NasaIcon name="network" size={14} />
DBIS Dashboard
</Link>
</p>
</div>
</div>
);
}

View File

@@ -1,10 +1,18 @@
import { Link } from 'react-router-dom';
import DBISSwapPanel from '../features/swap/DBISSwapPanel';
export default function SwapPage() {
return (
import DBISSwapPanel from '../features/swap/DBISSwapPanel';
export default function SwapPage() {
return (
<div className="nasa-page app-page-shell">
<DBISSwapPanel />
</div>
);
}

View File

@@ -1,5 +1,9 @@
import BinanceTradePanel from '../features/trade/BinanceTradePanel';
export default function TradePage() {
return <BinanceTradePanel />;
return (
<div className="nasa-page trade-page w-full">
<BinanceTradePanel />
</div>
);
}

View File

@@ -11,7 +11,10 @@ import {
defaultFrontendChainId,
defaultFrontendExplorerUrl,
chainRpcUrls,
shivaOneXChain,
shivaOneXChainId,
} from '../config/networks'
import { onexShiva, onexWalletEnabled } from '../config/onexShiva'
const clientId = import.meta.env.VITE_THIRDWEB_CLIENT_ID || '542981292d51ec610388ba8985f027d7'
const client = createThirdwebClient({ clientId })
@@ -26,13 +29,24 @@ const defaultThirdwebChain = defineChain({
},
})
const shivaThirdwebChain = defineChain({
id: shivaOneXChain.id,
name: shivaOneXChain.name,
rpc: chainRpcUrls[shivaOneXChainId] || shivaOneXChain.blockExplorers.default.url,
nativeCurrency: {
name: shivaOneXChain.nativeCurrency.name,
symbol: shivaOneXChain.nativeCurrency.symbol,
decimals: shivaOneXChain.nativeCurrency.decimals,
},
})
const wallets = [
inAppWallet({
auth: {
options: ['email', 'google', 'apple', 'passkey'],
options: onexShiva.wallet.supportedAuth as ('email' | 'google' | 'apple' | 'passkey')[],
},
metadata: {
name: 'DBIS Bridge',
name: onexWalletEnabled ? onexShiva.brand.wallet : 'DBIS Bridge',
image: {
src: `${defaultFrontendExplorerUrl}/favicon.ico`,
width: 32,
@@ -49,17 +63,24 @@ function WalletsDemoContent() {
chain: defaultThirdwebChain,
address: account?.address,
})
const { data: shivaBalance, isLoading: shivaBalanceLoading } = useWalletBalance({
client,
chain: shivaThirdwebChain,
address: account?.address,
})
return (
<div className="max-w-2xl mx-auto p-6 space-y-8">
<div className="app-page-shell max-w-2xl mx-auto p-3 sm:p-6 space-y-6 sm:space-y-8 wallets-page w-full min-w-0 text-center">
<div>
<h1 className="text-2xl font-bold text-white mb-2">Thirdweb Wallets</h1>
<h1 className="text-2xl font-bold text-white mb-2">{onexShiva.brand.wallet}</h1>
<p className="text-[#A0A0A0] text-sm">
Connect with email, Google, Apple, passkey, or an external wallet (MetaMask, WalletConnect, etc.) on {defaultFrontendChain.name}.
{onexShiva.brand.bank} on {onexShiva.brand.blockchain} (chain {shivaOneXChainId}). Connect with email, Google,
Apple, passkey, or external wallet on Chain 138 or Shiva.
</p>
</div>
<div className="flex flex-col items-center gap-6 p-6 bg-[#252830] rounded-xl border border-white/10">
<p className="text-xs uppercase tracking-wide text-teal-400/90">Chain 138 · DBIS</p>
<ConnectButton
client={client}
chain={defaultThirdwebChain}
@@ -94,6 +115,44 @@ function WalletsDemoContent() {
)}
</div>
{onexWalletEnabled && (
<div className="flex flex-col items-center gap-6 p-6 bg-[#252830] rounded-xl border border-violet-500/20">
<p className="text-xs uppercase tracking-wide text-violet-400/90">Shiva · OneX</p>
<ConnectButton
client={client}
chain={shivaThirdwebChain}
wallets={wallets}
theme="dark"
connectButton={{
label: `Connect ${onexShiva.brand.wallet}`,
style: {
backgroundColor: '#7c3aed',
color: 'white',
borderRadius: '0.75rem',
padding: '0.75rem 1.5rem',
},
}}
/>
{account && (
<div className="w-full space-y-2 text-sm">
{shivaBalanceLoading ? (
<p className="text-[#A0A0A0]">Loading Shiva balance</p>
) : shivaBalance ? (
<p className="text-[#A0A0A0]">
Balance ({shivaOneXChain.name}):{' '}
<span className="text-white">
{shivaBalance.displayValue} {shivaBalance.symbol}
</span>
</p>
) : null}
<p className="text-[#A0A0A0] text-xs">
Fineract office {onexShiva.fineract.officeId} · {onexShiva.fineract.hybxWalletId}
</p>
</div>
)}
</div>
)}
<div className="text-sm text-[#A0A0A0] space-y-2">
<p>
This page uses thirdweb SDK v5 and the{' '}
@@ -101,7 +160,7 @@ function WalletsDemoContent() {
href="https://portal.thirdweb.com/wallets"
target="_blank"
rel="noopener noreferrer"
className="text-teal-400 hover:underline"
className="text-green-400 hover:underline"
>
thirdweb Wallets
</a>{' '}

View File

@@ -1,34 +1,56 @@
/**
* Mobile Responsive Styles - Additional mobile optimizations
* Mobile responsive styles — touch targets, safe areas, scrollable regions
*/
/* Mobile-specific adjustments */
.safe-area-top {
padding-top: env(safe-area-inset-top, 0);
}
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom, 0);
}
.safe-area-pad {
padding-top: env(safe-area-inset-top, 0);
padding-bottom: env(safe-area-inset-bottom, 0);
padding-left: env(safe-area-inset-left, 0);
}
/* Prevent horizontal page scroll */
html,
body,
#root {
overflow-x: hidden;
max-width: 100vw;
}
@media (max-width: 768px) {
/* Navigation */
nav {
padding: 0.5rem 1rem;
.dbis-dashboard__inner {
padding-bottom: calc(4.5rem + env(safe-area-inset-bottom, 0));
}
nav .container {
flex-direction: column;
gap: 0.5rem;
.dbis-section-fold + .dbis-section-fold {
margin-top: 0.5rem;
}
nav .flex {
flex-wrap: wrap;
gap: 0.25rem;
.dash-page,
.o24-page,
.cb-page {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
nav a, nav button {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
.mobile-nav-bar .container {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
/* Admin Panel Tabs */
/* Admin panel tabs */
.admin-tabs {
flex-wrap: wrap;
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
}
.admin-tabs button {
@@ -37,90 +59,91 @@
font-size: 0.75rem;
}
/* Tables - Stack on mobile */
/* Tables — horizontal scroll within container */
.nasa-table,
table {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Cards */
.bg-black\/20 {
padding: 1rem;
.overflow-x-auto {
-webkit-overflow-scrolling: touch;
}
/* Forms */
input, select, textarea {
font-size: 16px; /* Prevents zoom on iOS */
/* Forms — prevent iOS zoom */
input,
select,
textarea {
font-size: 16px;
}
/* Buttons */
button {
min-height: 44px; /* Touch target size */
min-width: 44px;
/* Touch targets */
button,
a.nasa-btn,
.nasa-nav-link {
min-height: 44px;
}
/* Modals/Dialogs */
/* Bridge tab bar */
[role='tablist'] {
overflow-x: auto;
flex-wrap: nowrap;
-webkit-overflow-scrolling: touch;
}
[role='tablist'] button {
flex-shrink: 0;
white-space: nowrap;
}
/* Trade terminal stacks cleanly */
.trade-pair-header {
gap: 0.5rem;
}
.trade-pair-header h1 {
font-size: 1rem;
}
.trade-panel input,
.trade-panel select {
font-size: 16px;
min-height: 44px;
}
/* Modals / toasts */
.modal {
padding: 1rem;
margin: 0.5rem;
max-height: calc(100vh - 1rem);
max-height: calc(100dvh - 1rem);
overflow-y: auto;
}
/* Toast notifications */
.toast {
margin: 0.5rem;
max-width: calc(100vw - 1rem);
.nasa-page {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
/* Transaction lists */
.transaction-item {
.history-page,
.wallets-page {
padding: 0.75rem;
}
/* Wallet connection */
.wallet-connect {
flex-direction: column;
width: 100%;
}
.wallet-connect button {
width: 100%;
margin-bottom: 0.5rem;
max-width: 100%;
overflow-x: hidden;
}
}
/* Touch-friendly interactions */
@media (hover: none) and (pointer: coarse) {
/* Larger touch targets */
a, button {
min-height: 44px;
min-width: 44px;
}
/* Remove hover effects on touch devices */
button:hover {
transform: none;
}
/* Better focus indicators */
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible {
outline: 2px solid #3b82f6;
outline: 2px solid #22c55e;
outline-offset: 2px;
}
}
/* Landscape mobile */
@media (max-width: 1024px) and (orientation: landscape) {
.admin-tabs {
flex-wrap: nowrap;
}
}
/* Very small screens */
@media (max-width: 375px) {
.text-3xl {
font-size: 1.5rem;
@@ -130,12 +153,8 @@
font-size: 1.25rem;
}
.text-xl {
font-size: 1.125rem;
}
button, a {
padding: 0.5rem;
font-size: 0.875rem;
.dash-quick-links__chip {
font-size: 0.75rem;
padding: 0.35rem 0.6rem;
}
}

View File

@@ -1,445 +0,0 @@
@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: #0f2847;
--nasa-bg-deep: #0a1a30;
--nasa-panel: #163a6b;
--nasa-panel-elevated: #1c4680;
--nasa-panel-border: #2d6bb8;
--nasa-telemetry: #00d4ff;
--nasa-nominal: #3dff8a;
--nasa-caution: #ffb347;
--nasa-critical: #fc3d21;
--nasa-text: #e8f4ff;
--nasa-muted: #9bb8d4;
--nasa-mono: 'Share Tech Mono', ui-monospace, monospace;
}
html:has(.omnl-app),
html:has(.omnl-app) body {
background: var(--nasa-bg);
min-height: 100%;
}
.omnl-app {
background:
radial-gradient(ellipse at 15% 0%, rgba(45, 107, 184, 0.45) 0%, transparent 50%),
radial-gradient(ellipse at 85% 100%, rgba(0, 212, 255, 0.12) 0%, transparent 45%),
linear-gradient(180deg, var(--nasa-bg) 0%, var(--nasa-bg-deep) 100%);
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(28, 70, 128, 0.92) 0%, rgba(22, 58, 107, 0.96) 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(22, 58, 107, 0.55);
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(22, 58, 107, 0.85);
}
.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(22, 58, 107, 0.75);
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: var(--nasa-panel-elevated);
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(15, 40, 71, 0.55);
border: 1px solid rgba(45, 107, 184, 0.55);
padding: 1rem;
}
.omnl-app-header {
background: rgba(15, 40, 71, 0.92) !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: transparent;
}
.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: rgba(15, 40, 71, 0.35);
}
.trade-chart {
background: var(--nasa-panel) !important;
border-color: var(--nasa-panel-border) !important;
}
.trade-chart__canvas {
min-height: 240px;
border-radius: 2px;
border: 1px solid var(--nasa-panel-border);
background: linear-gradient(180deg, rgba(22, 58, 107, 0.95) 0%, rgba(15, 40, 71, 0.98) 100%);
}
.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; }
}

View File

@@ -36,13 +36,12 @@
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
letter-spacing: 0.01em;
}
.dash-badge--blue {
background: rgba(56, 97, 251, 0.18);
color: #3861fb;
background: rgba(34, 197, 94, 0.18);
color: #22c55e;
}
.dash-badge--green {
@@ -106,10 +105,9 @@
}
.dash-stat-label {
font-size: 0.7rem;
font-size: 0.75rem;
color: #848e9c;
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 500;
margin-bottom: 0.35rem;
}
@@ -168,9 +166,8 @@
}
.dash-quick-links__label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.06em;
font-size: 0.75rem;
font-weight: 500;
color: #848e9c;
margin-bottom: 0.5rem;
}
@@ -189,8 +186,8 @@
.dash-quick-links__chip:hover {
background: #474d57;
border-color: #f0b90b;
color: #f0b90b;
border-color: #22c55e;
color: #4ade80;
}
.dash-btn {
@@ -245,7 +242,7 @@
.trade-market-row--active {
background: #2b3139;
border-left: 2px solid #f0b90b;
border-left: 2px solid #22c55e;
}
.trade-orderbook {

View File

@@ -1,12 +1,19 @@
import colors from 'tailwindcss/colors'
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
extend: {
colors: {
primary: colors.green,
teal: colors.green,
cyan: colors.emerald,
},
},
},
plugins: [],
}

View File

@@ -37,6 +37,21 @@ export default defineConfig({
},
server: {
port: 3002,
proxy: {
'/api': { target: 'http://localhost:3000', changeOrigin: true },
'/settlement': { target: 'https://secure.omdnl.org', changeOrigin: true },
'/exchange': { target: 'https://exchange.omdnl.org', changeOrigin: true },
'/omnl': {
target: 'http://localhost:3000',
changeOrigin: true,
bypass(req) {
const url = req.url ?? '';
if (/^\/omnl\/(compliance|dashboard|terminal)\/?(\?.*)?$/.test(url)) return url;
if (url.startsWith('/omnl/static/')) return url;
},
},
'/reserve': { target: 'http://localhost:3000', changeOrigin: true },
},
},
optimizeDeps: {
// Exclude problematic packages from optimization

View File

@@ -2,11 +2,11 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="description" content="Multi-Cloud Orchestration Portal - Manage and orchestrate deployments across multiple cloud providers and on-premises infrastructure">
<meta name="keywords" content="multi-cloud, orchestration, kubernetes, cloud management, infrastructure">
<meta name="author" content="Multi-Cloud Orchestration Portal">
<meta name="theme-color" content="#667eea">
<meta name="theme-color" content="#16a34a">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">

View File

@@ -60,6 +60,12 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
} catch (error) {
console.warn('Failed to load panel states from localStorage:', error);
}
if (typeof window !== 'undefined' && window.innerWidth < 768) {
setLeftPanelCollapsed(true);
setRightPanelCollapsed(true);
setBottomPanelCollapsed(true);
}
}, []);
// Save panel collapsed states

View File

@@ -1,13 +1,34 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
const Header: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const [notificationCount] = useState(3);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const location = useLocation();
const menuItems = [
{ path: '/', label: 'Dashboard', icon: 'fas fa-home' },
{ path: '/health', label: 'Health', icon: 'fas fa-heartbeat' },
{ path: '/costs', label: 'Costs', icon: 'fas fa-dollar-sign' },
{ path: '/monitoring', label: 'Monitoring', icon: 'fas fa-chart-line' },
{ path: '/admin', label: 'Admin', icon: 'fas fa-cog' },
];
return (
<>
<header className="app-header" role="banner" aria-label="Main navigation">
<div className="header-content">
<div className="header-left">
<button
type="button"
className="header-btn header-menu-btn md:hidden"
title="Open navigation menu"
aria-label="Open navigation menu"
onClick={() => setMobileMenuOpen(true)}
>
<i className="fas fa-bars" aria-hidden="true" />
</button>
<h1 className="header-title">
<i className="fas fa-cloud mr-2" aria-hidden="true"></i>
<span>Multi-Cloud Orchestration Portal</span>
@@ -81,6 +102,32 @@ const Header: React.FC = () => {
</div>
</div>
</header>
{mobileMenuOpen && (
<div className="mobile-nav-overlay md:hidden" role="dialog" aria-modal="true" aria-label="Navigation menu">
<button type="button" className="mobile-nav-backdrop" aria-label="Close menu" onClick={() => setMobileMenuOpen(false)} />
<nav className="mobile-nav-drawer">
<div className="mobile-nav-drawer-header">
<span>Navigation</span>
<button type="button" className="header-btn" aria-label="Close menu" onClick={() => setMobileMenuOpen(false)}>
<i className="fas fa-times" aria-hidden="true" />
</button>
</div>
{menuItems.map((item) => (
<Link
key={item.path}
to={item.path}
className={`mobile-nav-drawer-link ${location.pathname === item.path ? 'active' : ''}`}
onClick={() => setMobileMenuOpen(false)}
>
<i className={`${item.icon}`} aria-hidden="true" />
<span>{item.label}</span>
</Link>
))}
</nav>
</div>
)}
</>
);
};

View File

@@ -56,6 +56,13 @@ const ResizablePanel: React.FC<Props> = ({
setInternalCollapsed(true);
onCollapsedChange?.(true);
}
if (typeof window !== 'undefined' && window.innerWidth < 768) {
if (controlledCollapsed === undefined) {
setInternalCollapsed(true);
}
onCollapsedChange?.(true);
}
} catch (error) {
console.warn('Failed to load panel state from localStorage:', error);
}

View File

@@ -870,6 +870,94 @@
width: var(--touch-target-comfortable);
height: var(--touch-target-comfortable);
}
/* Side panels become overlay drawers; collapse toggles stay on screen edge */
.resizable-panel.collapsed {
overflow: visible;
}
.resizable-panel-left:not(.collapsed) {
width: min(85vw, 280px) !important;
box-shadow: var(--shadow-xl);
z-index: calc(var(--z-panels) + 10);
}
.resizable-panel-right:not(.collapsed) {
width: min(85vw, 280px) !important;
box-shadow: var(--shadow-xl);
z-index: calc(var(--z-panels) + 10);
}
.resizable-panel-bottom:not(.collapsed) {
height: min(50vh, 320px) !important;
left: 0 !important;
right: 0 !important;
z-index: calc(var(--z-panels) + 10);
}
.resize-handle {
display: none;
}
.main-content {
margin-left: 0 !important;
margin-right: 0 !important;
padding: 1rem !important;
}
.mobile-nav-overlay {
position: fixed;
inset: 0;
z-index: calc(var(--z-panels) + 50);
display: flex;
}
.mobile-nav-backdrop {
flex: 1;
background: rgba(0, 0, 0, 0.55);
border: none;
cursor: pointer;
}
.mobile-nav-drawer {
width: min(85vw, 280px);
background: white;
box-shadow: var(--shadow-xl);
display: flex;
flex-direction: column;
padding-top: env(safe-area-inset-top, 0);
padding-bottom: env(safe-area-inset-bottom, 0);
}
.mobile-nav-drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-color, #e5e7eb);
font-weight: 600;
}
.mobile-nav-drawer-link {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.875rem 1rem;
min-height: 44px;
color: var(--text-color, #1f2937);
text-decoration: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.mobile-nav-drawer-link.active {
background: rgba(102, 126, 234, 0.12);
color: var(--color-primary-600, #16a34a);
font-weight: 600;
}
.header-menu-btn {
margin-right: 0.25rem;
}
}
@media (max-width: 640px) {

View File

@@ -1,17 +1,15 @@
/**
* CSS Variables for Theming and Consistency
* Centralized design tokens for easy customization
* DBIS green UI — orchestration portal design tokens
*/
:root {
/* Colors - Primary */
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
/* Colors - Primary (green) */
--color-primary-500: #22c55e;
--color-primary-600: #16a34a;
--color-primary-700: #15803d;
/* Colors - Gradients */
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--gradient-ai: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
--gradient-primary: linear-gradient(135deg, #22c55e 0%, #059669 100%);
--gradient-ai: linear-gradient(135deg, #4ade80 0%, #16a34a 100%);
/* Colors - Grays */
--color-gray-50: #f9fafb;
@@ -29,7 +27,7 @@
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-info: #3b82f6;
--color-info: #22c55e;
/* Layout - Spacing */
--spacing-xs: 0.25rem;
@@ -40,11 +38,11 @@
--spacing-2xl: 3rem;
/* Layout - Header & Footer */
--header-height: 64px; /* Standard header height, ensures consistent layout */
--header-height-mobile: 56px; /* Smaller on mobile for more screen space */
--header-height: 64px;
--header-height-mobile: 56px;
--footer-height: 15px;
/* Touch Targets - WCAG 2.1 Level AAA requires 44x44px minimum */
/* Touch Targets */
--touch-target-min: 44px;
--touch-target-comfortable: 48px;
@@ -91,10 +89,8 @@
--breakpoint-2xl: 1536px;
}
/* Dark mode support (future) */
@media (prefers-color-scheme: dark) {
:root {
/* Dark mode variables can be added here */
}
}

View File

@@ -128,6 +128,12 @@ onMounted(() => {
} catch (error) {
console.warn('Failed to load panel states from localStorage:', error);
}
if (typeof window !== 'undefined' && window.innerWidth < 768) {
leftPanelCollapsed.value = true;
rightPanelCollapsed.value = true;
bottomPanelCollapsed.value = true;
}
});
// Save panel collapsed states
@@ -154,18 +160,32 @@ watch([leftPanelCollapsed, rightPanelCollapsed, bottomPanelCollapsed], () => {
flex: 1;
padding: 2rem;
overflow-y: auto;
overflow-x: hidden;
background: #f9fafb;
min-height: calc(100vh - 79px);
overscroll-behavior: contain;
}
@media (max-width: 768px) {
.main-content {
padding: 1rem;
margin-left: 0 !important;
margin-right: 0 !important;
margin-bottom: 1rem !important;
}
.app-footer .footer-links {
display: none;
}
}
.app-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #22c55e 0%, #059669 100%);
z-index: 1000;
display: flex;
align-items: center;

View File

@@ -2,6 +2,15 @@
<header class="app-header" role="banner" aria-label="Main navigation">
<div class="header-content">
<div class="header-left">
<button
type="button"
class="header-btn header-menu-btn md:hidden"
title="Open navigation menu"
aria-label="Open navigation menu"
@click="mobileMenuOpen = true"
>
<i class="fas fa-bars" aria-hidden="true"></i>
</button>
<h1 class="header-title">
<i class="fas fa-cloud mr-2" aria-hidden="true"></i>
<span>Multi-Cloud Orchestration Portal</span>
@@ -73,6 +82,29 @@
</div>
</div>
</header>
<div v-if="mobileMenuOpen" class="mobile-nav-overlay md:hidden" role="dialog" aria-modal="true" aria-label="Navigation menu">
<button type="button" class="mobile-nav-backdrop" aria-label="Close menu" @click="mobileMenuOpen = false" />
<nav class="mobile-nav-drawer">
<div class="mobile-nav-drawer-header">
<span>Navigation</span>
<button type="button" class="header-btn" aria-label="Close menu" @click="mobileMenuOpen = false">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
<router-link
v-for="item in menuItems"
:key="item.path"
:to="item.path"
class="mobile-nav-drawer-link"
active-class="active"
@click="mobileMenuOpen = false"
>
<i :class="item.icon" aria-hidden="true"></i>
<span>{{ item.label }}</span>
</router-link>
</nav>
</div>
</template>
<script setup lang="ts">
@@ -80,6 +112,15 @@ import { ref } from 'vue';
const searchQuery = ref('');
const notificationCount = ref(3);
const mobileMenuOpen = ref(false);
const menuItems = [
{ path: '/', label: 'Dashboard', icon: 'fas fa-home' },
{ path: '/health', label: 'Health', icon: 'fas fa-heartbeat' },
{ path: '/costs', label: 'Costs', icon: 'fas fa-dollar-sign' },
{ path: '/monitoring', label: 'Monitoring', icon: 'fas fa-chart-line' },
{ path: '/admin', label: 'Admin', icon: 'fas fa-cog' },
];
</script>
<style scoped>

View File

@@ -247,6 +247,11 @@ onMounted(() => {
isCollapsed.value = true;
emit('update:collapsed', true);
}
if (typeof window !== 'undefined' && window.innerWidth < 768 && !isCollapsed.value) {
isCollapsed.value = true;
emit('update:collapsed', true);
}
});
onUnmounted(() => {

View File

@@ -1,23 +1,23 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./client/**/*.{vue,js,ts,jsx,tsx}",
"./templates/**/*.html",
'./client/**/*.{vue,js,ts,jsx,tsx}',
'./templates/**/*.html',
],
theme: {
extend: {
colors: {
primary: {
50: '#f0f4ff',
100: '#e0e9ff',
200: '#c7d7fe',
300: '#a5b8fc',
400: '#818cf8',
500: '#667eea',
600: '#5568d3',
700: '#4854b0',
800: '#3f4891',
900: '#3a4076',
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
},
},
fontFamily: {
@@ -30,4 +30,3 @@ export default {
require('@tailwindcss/typography'),
],
};

View File

@@ -14,7 +14,7 @@
}
:root {
--primary: #667eea;
--primary: #16a34a;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
@@ -32,7 +32,7 @@
}
.navbar {
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
background: linear-gradient(135deg, var(--primary) 0%, #059669 100%);
color: white;
padding: 1rem 2rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
@@ -137,6 +137,14 @@
font-weight: 600;
color: var(--primary);
}
@media (max-width: 768px) {
.navbar { flex-wrap: wrap; gap: 0.5rem; padding: 0.75rem 1rem; }
.container { padding: 1rem !important; }
.grid, .stats-grid { grid-template-columns: 1fr !important; }
table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.navbar a { min-height: 44px; display: inline-flex; align-items: center; }
}
</style>
</head>
<body>
@@ -223,7 +231,7 @@
datasets: [{
label: 'Daily Cost (USD)',
data: values,
borderColor: '#667eea',
borderColor: '#16a34a',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
tension: 0.4,
fill: true

View File

@@ -14,8 +14,8 @@
}
:root {
--primary: #667eea;
--primary-dark: #5568d3;
--primary: #16a34a;
--primary-dark: #15803d;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
@@ -35,7 +35,7 @@
}
.navbar {
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
background: linear-gradient(135deg, var(--primary) 0%, #059669 100%);
color: white;
padding: 1rem 2rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
@@ -364,6 +364,52 @@
margin-bottom: 1rem;
opacity: 0.5;
}
@media (max-width: 768px) {
.navbar {
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem 1rem;
}
.navbar h1 {
font-size: 1.1rem;
}
.navbar nav {
flex-wrap: wrap;
gap: 0.5rem 1rem;
width: 100%;
}
.navbar a {
min-height: 44px;
display: inline-flex;
align-items: center;
}
.container,
.dashboard-container {
padding: 1rem !important;
}
.stats-grid,
.grid {
grid-template-columns: 1fr !important;
}
.env-card,
.card {
padding: 1rem;
}
table {
display: block;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
</style>
</head>
<body>

View File

@@ -18,7 +18,7 @@
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #16a34a 0%, #059669 100%);
color: white;
padding: 2rem;
}
@@ -38,7 +38,7 @@
}
.card h2 {
color: #667eea;
color: #16a34a;
margin-bottom: 1rem;
}
@@ -69,7 +69,7 @@
.btn {
padding: 0.75rem 1.5rem;
background: #667eea;
background: #16a34a;
color: white;
border: none;
border-radius: 4px;
@@ -79,7 +79,13 @@
}
.btn:hover {
background: #5568d3;
background: #15803d;
}
@media (max-width: 768px) {
.container { padding: 1rem !important; margin: 1rem auto !important; }
.header { padding: 1rem !important; }
.btn { width: 100%; min-height: 44px; }
}
</style>
</head>

View File

@@ -14,7 +14,7 @@
}
:root {
--primary: #667eea;
--primary: #16a34a;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
@@ -32,7 +32,7 @@
}
.navbar {
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
background: linear-gradient(135deg, var(--primary) 0%, #059669 100%);
color: white;
padding: 1rem 2rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
@@ -164,6 +164,14 @@
background: #fee2e2;
color: #991b1b;
}
@media (max-width: 768px) {
.navbar { flex-wrap: wrap; gap: 0.5rem; padding: 0.75rem 1rem; }
.container { padding: 1rem !important; }
.grid, .meta { grid-template-columns: 1fr !important; }
table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.navbar a { min-height: 44px; display: inline-flex; align-items: center; }
}
</style>
</head>
<body>
@@ -295,7 +303,7 @@
datasets: [{
label: 'CPU Usage %',
data: cpuData,
borderColor: '#667eea',
borderColor: '#16a34a',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
tension: 0.4
}, {

View File

@@ -14,7 +14,7 @@
}
:root {
--primary: #667eea;
--primary: #16a34a;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
@@ -32,7 +32,7 @@
}
.navbar {
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
background: linear-gradient(135deg, var(--primary) 0%, #059669 100%);
color: white;
padding: 1rem 2rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
@@ -165,6 +165,14 @@
background: #fee2e2;
color: #991b1b;
}
@media (max-width: 768px) {
.navbar { flex-wrap: wrap; gap: 0.5rem; padding: 0.75rem 1rem; }
.container { padding: 1rem !important; }
.grid, .stats-grid { grid-template-columns: 1fr !important; }
table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.navbar a { min-height: 44px; display: inline-flex; align-items: center; }
}
</style>
</head>
<body>

View File

@@ -18,7 +18,7 @@
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #16a34a 0%, #059669 100%);
color: white;
padding: 2rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
@@ -54,7 +54,7 @@
}
.stat-card h3 {
color: #667eea;
color: #16a34a;
font-size: 0.9rem;
text-transform: uppercase;
margin-bottom: 0.5rem;
@@ -74,14 +74,14 @@
background: white;
padding: 1rem 1.5rem;
border-radius: 8px 8px 0 0;
border-bottom: 2px solid #667eea;
border-bottom: 2px solid #16a34a;
display: flex;
justify-content: space-between;
align-items: center;
}
.provider-header h2 {
color: #667eea;
color: #16a34a;
font-size: 1.5rem;
}
@@ -161,12 +161,12 @@
}
.btn-primary {
background: #667eea;
background: #16a34a;
color: white;
}
.btn-primary:hover {
background: #5568d3;
background: #15803d;
}
.btn-secondary {
@@ -177,6 +177,14 @@
.btn-secondary:hover {
background: #e0e0e0;
}
@media (max-width: 768px) {
.header { padding: 1.5rem 1rem !important; }
.container { padding: 1rem !important; }
.stats { grid-template-columns: 1fr !important; }
.environments { grid-template-columns: 1fr !important; }
.btn { min-height: 44px; }
}
</style>
</head>
<body>

View File

@@ -34,6 +34,24 @@ export declare function m3TokenLoadAmount(requested: string, m2Broad: string, m3
};
/** All settlement layers M0M4 for production routing. */
export declare const ALL_MONEY_LAYERS: MoneyLayer[];
export type ZBankGlProfile = {
glM0: string;
glM1: string;
glM2: string;
glM3: string;
glM4NearMoney: string;
};
/** IPSAS journal pairs for Z Online Bank full-stack settlement (office 24 matrix). */
export declare function resolveZBankLayerJournal(from: MoneyLayer, to: MoneyLayer, gl: ZBankGlProfile): {
debitGl: string;
creditGl: string;
narrative: string;
} | null;
/** Ordered layer hops for Z Online Bank M0→M4 settlement. */
export declare function zBankM0ToM4Hops(includeM3: boolean): {
from: MoneyLayer;
to: MoneyLayer;
}[];
/** M4 outbound SWIFT/RTGS: move M1/M2 into GL 1000 settlement suspense while in flight. */
export declare function resolveM4OutboundJournal(moneyLayers: MoneyLayer[], gl: {
glM1: string;

View File

@@ -5,6 +5,8 @@ exports.aggregateMetaFiatLayers = aggregateMetaFiatLayers;
exports.buildMoneySupplySnapshot = buildMoneySupplySnapshot;
exports.m2FiatToTokenLoadAmount = m2FiatToTokenLoadAmount;
exports.m3TokenLoadAmount = m3TokenLoadAmount;
exports.resolveZBankLayerJournal = resolveZBankLayerJournal;
exports.zBankM0ToM4Hops = zBankM0ToM4Hops;
exports.resolveM4OutboundJournal = resolveM4OutboundJournal;
/** IPSAS GL mapping for OMNL central-bank money aggregates M0M4 */
exports.GL_CODES = {
@@ -105,6 +107,51 @@ function m3TokenLoadAmount(requested, m2Broad, m3Outstanding) {
}
/** All settlement layers M0M4 for production routing. */
exports.ALL_MONEY_LAYERS = ['M0', 'M1', 'M2', 'M3', 'M4'];
/** IPSAS journal pairs for Z Online Bank full-stack settlement (office 24 matrix). */
function resolveZBankLayerJournal(from, to, gl) {
const pairs = {
'M0-M1': {
debitGl: gl.glM0 || exports.GL_CODES.M0_RESERVE,
creditGl: gl.glM1 || exports.GL_CODES.M1_CIRCULATING,
narrative: 'Z Online Bank · M0→M1 reserve to circulating',
},
'M1-M2': {
debitGl: gl.glM1 || exports.GL_CODES.M1_CIRCULATING,
creditGl: gl.glM2 || exports.GL_CODES.M2_BROAD,
narrative: 'Z Online Bank · M1→M2 broad-money conversion',
},
'M2-M3': {
debitGl: gl.glM2 || exports.GL_CODES.M2_BROAD,
creditGl: gl.glM3 || exports.GL_CODES.M3_TOKEN_LIABILITY,
narrative: 'Z Online Bank · M2→M3 on-chain token liability',
},
'M3-M4': {
debitGl: gl.glM3 || exports.GL_CODES.M3_TOKEN_LIABILITY,
creditGl: gl.glM4NearMoney || exports.GL_CODES.SETTLEMENT_SUSPENSE,
narrative: 'Z Online Bank · M3→M4 settlement suspense',
},
'M2-M4': {
debitGl: gl.glM2 || exports.GL_CODES.M2_BROAD,
creditGl: gl.glM4NearMoney || exports.GL_CODES.SETTLEMENT_SUSPENSE,
narrative: 'Z Online Bank · M2→M4 SWIFT/RTGS outbound suspense',
},
};
return pairs[`${from}-${to}`] ?? null;
}
/** Ordered layer hops for Z Online Bank M0→M4 settlement. */
function zBankM0ToM4Hops(includeM3) {
const hops = [
{ from: 'M0', to: 'M1' },
{ from: 'M1', to: 'M2' },
];
if (includeM3) {
hops.push({ from: 'M2', to: 'M3' }, { from: 'M3', to: 'M4' });
}
else {
hops.push({ from: 'M2', to: 'M4' });
}
return hops;
}
/** M4 outbound SWIFT/RTGS: move M1/M2 into GL 1000 settlement suspense while in flight. */
function resolveM4OutboundJournal(moneyLayers, gl) {
if (!moneyLayers.includes('M4'))

View File

@@ -62,6 +62,28 @@ export type TransferRequest = {
remittanceInfo?: string;
currency?: string;
};
export type ZBankLayerStep = {
from: MoneyLayer;
to: MoneyLayer;
debitGl: string;
creditGl: string;
journalRef?: string;
status: 'pending' | 'posted' | 'skipped' | 'failed';
};
export type ZBankM0M4Request = {
idempotencyKey: string;
officeId?: number;
valueDate?: string;
currency: string;
amount: string;
creditorIban: string;
creditorBic?: string;
beneficiaryName?: string;
remittanceInfo?: string;
/** When set, M2→M3 hop posts GL + requests Chain 138 mint */
recipientAddress?: string;
tokenLineId?: string;
};
export type SettlementRecord = {
settlementId: string;
idempotencyKey: string;
@@ -72,6 +94,7 @@ export type SettlementRecord = {
iso20022MessageId?: string;
chainTxHash?: string;
verbiageDocument?: string;
layerSteps?: ZBankLayerStep[];
errors: string[];
createdAt: string;
updatedAt: string;

View File

@@ -117,6 +117,64 @@ export function m3TokenLoadAmount(requested: string, m2Broad: string, m3Outstand
/** All settlement layers M0M4 for production routing. */
export const ALL_MONEY_LAYERS: MoneyLayer[] = ['M0', 'M1', 'M2', 'M3', 'M4'];
export type ZBankGlProfile = {
glM0: string;
glM1: string;
glM2: string;
glM3: string;
glM4NearMoney: string;
};
/** IPSAS journal pairs for Z Online Bank full-stack settlement (office 24 matrix). */
export function resolveZBankLayerJournal(
from: MoneyLayer,
to: MoneyLayer,
gl: ZBankGlProfile,
): { debitGl: string; creditGl: string; narrative: string } | null {
const pairs: Record<string, { debitGl: string; creditGl: string; narrative: string }> = {
'M0-M1': {
debitGl: gl.glM0 || GL_CODES.M0_RESERVE,
creditGl: gl.glM1 || GL_CODES.M1_CIRCULATING,
narrative: 'Z Online Bank · M0→M1 reserve to circulating',
},
'M1-M2': {
debitGl: gl.glM1 || GL_CODES.M1_CIRCULATING,
creditGl: gl.glM2 || GL_CODES.M2_BROAD,
narrative: 'Z Online Bank · M1→M2 broad-money conversion',
},
'M2-M3': {
debitGl: gl.glM2 || GL_CODES.M2_BROAD,
creditGl: gl.glM3 || GL_CODES.M3_TOKEN_LIABILITY,
narrative: 'Z Online Bank · M2→M3 on-chain token liability',
},
'M3-M4': {
debitGl: gl.glM3 || GL_CODES.M3_TOKEN_LIABILITY,
creditGl: gl.glM4NearMoney || GL_CODES.SETTLEMENT_SUSPENSE,
narrative: 'Z Online Bank · M3→M4 settlement suspense',
},
'M2-M4': {
debitGl: gl.glM2 || GL_CODES.M2_BROAD,
creditGl: gl.glM4NearMoney || GL_CODES.SETTLEMENT_SUSPENSE,
narrative: 'Z Online Bank · M2→M4 SWIFT/RTGS outbound suspense',
},
};
return pairs[`${from}-${to}`] ?? null;
}
/** Ordered layer hops for Z Online Bank M0→M4 settlement. */
export function zBankM0ToM4Hops(includeM3: boolean): { from: MoneyLayer; to: MoneyLayer }[] {
const hops: { from: MoneyLayer; to: MoneyLayer }[] = [
{ from: 'M0', to: 'M1' },
{ from: 'M1', to: 'M2' },
];
if (includeM3) {
hops.push({ from: 'M2', to: 'M3' }, { from: 'M3', to: 'M4' });
} else {
hops.push({ from: 'M2', to: 'M4' });
}
return hops;
}
/** M4 outbound SWIFT/RTGS: move M1/M2 into GL 1000 settlement suspense while in flight. */
export function resolveM4OutboundJournal(moneyLayers: MoneyLayer[], gl: {
glM1: string;

View File

@@ -88,6 +88,30 @@ export type TransferRequest = {
currency?: string;
};
export type ZBankLayerStep = {
from: MoneyLayer;
to: MoneyLayer;
debitGl: string;
creditGl: string;
journalRef?: string;
status: 'pending' | 'posted' | 'skipped' | 'failed';
};
export type ZBankM0M4Request = {
idempotencyKey: string;
officeId?: number;
valueDate?: string;
currency: string;
amount: string;
creditorIban: string;
creditorBic?: string;
beneficiaryName?: string;
remittanceInfo?: string;
/** When set, M2→M3 hop posts GL + requests Chain 138 mint */
recipientAddress?: string;
tokenLineId?: string;
};
export type SettlementRecord = {
settlementId: string;
idempotencyKey: string;
@@ -98,6 +122,7 @@ export type SettlementRecord = {
iso20022MessageId?: string;
chainTxHash?: string;
verbiageDocument?: string;
layerSteps?: ZBankLayerStep[];
errors: string[];
createdAt: string;
updatedAt: string;

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Activate full SWIFT stack for Central Bank + Office 24 + Z Online Bank (production).
set -euo pipefail
REPO_DIR="${OMNL_BANK_ROOT:-$HOME/smom-dbis-138}"
ENV_FILE="${OMNL_BANK_ENV:-$REPO_DIR/.env}"
LISTENER_URL="${SWIFT_LISTENER_URL:-http://127.0.0.1:8788}"
SETTLEMENT_BASE="${SETTLEMENT_HEALTH_URL:-https://secure.omdnl.org/settlement}"
log() { echo "[$(date -Iseconds)] swift-activate $*"; }
fail() { log "FAIL: $*"; exit 1; }
[[ -f "$ENV_FILE" ]] || fail "Missing $ENV_FILE — load zardasht handoff first (chmod 600)"
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
: "${OMNL_FINERACT_BASE_URL:?Set OMNL_FINERACT_BASE_URL}"
: "${OMNL_FINERACT_PASSWORD:?Set OMNL_FINERACT_PASSWORD}"
: "${OMNL_SUPER_ADMIN_OFFICE24_KEY:?Set OMNL_SUPER_ADMIN_OFFICE24_KEY}"
export SETTLEMENT_MIDDLEWARE_CONFIG="${SETTLEMENT_MIDDLEWARE_CONFIG:-$REPO_DIR/config/settlement-middleware.production.v1.json}"
export SWIFT_LISTENER_URL="${SWIFT_LISTENER_URL:-$LISTENER_URL}"
export SETTLEMENT_ALLOW_HYBX_PRODUCTION="${SETTLEMENT_ALLOW_HYBX_PRODUCTION:-1}"
export OMNL_PUBLIC_MONEY_SUPPLY="${OMNL_PUBLIC_MONEY_SUPPLY:-1}"
log "1/5 Start SWIFT listener..."
bash "$REPO_DIR/scripts/deployment/start-swift-listener-production.sh"
log "2/5 Settlement health + Office 24..."
health="$(curl -sS "${SETTLEMENT_BASE}/health")"
echo "$health" | grep -q '"officeId":24' || fail "Office 24 not bound on settlement health"
echo "$health" | grep -q '"status":"ok"' || fail "Settlement health not ok"
log "3/5 Public money supply (Fineract live)..."
ms="$(curl -sS "${SETTLEMENT_BASE}/public/money-supply")"
echo "$ms" | grep -q 'fineractLive' || log "WARN: fineractLive not in money-supply response"
log "4/5 SWIFT auth probe (empty external-transfer — expect validation error)..."
code="$(curl -sS -o /tmp/swift-probe.json -w '%{http_code}' \
-X POST "${SETTLEMENT_BASE}/external-transfer" \
-H "Authorization: Bearer ${OMNL_SUPER_ADMIN_OFFICE24_KEY}" \
-H "Content-Type: application/json" \
-d '{}')"
[[ "$code" == "400" || "$code" == "500" ]] || fail "external-transfer auth probe HTTP $code (expected 400/500)"
grep -qi idempotencyKey /tmp/swift-probe.json || fail "Auth probe missing validation message"
log "5/5 Inbound SWIFT stub (MT103 → listener)..."
MT103='{1:F01OMNLUS33XXXX}{2:I103BANKUS33XXXXN}{4:
:20:SWIFT-ACTIVATE-TEST
:32A:260703USD1,00
:59:/IQ98ZARD00000000000000000001
Test Beneficiary
:70:OMNL SWIFT activate probe chain138 cWUSDC
-}'
curl -sS -o /dev/null -w "listener=%{http_code}\n" \
-X POST "${SWIFT_LISTENER_URL}/inbound" \
-H "Content-Type: text/plain" \
${SWIFT_LISTENER_WEBHOOK_TOKEN:+-H "Authorization: Bearer ${SWIFT_LISTENER_WEBHOOK_TOKEN}"} \
--data-binary "$MT103"
log "SWIFT production stack active"
log " Listener: ${SWIFT_LISTENER_URL}/health"
log " Settlement: ${SETTLEMENT_BASE}/health"
log " Portal: /central-bank#z-settlement-protocols (MT102/MT103/SBLC/BG)"
log " Catalog: config/swift-mt-catalog.v1.json"
log ""
log "MT types: MT103 MT102 MT202 MT910 MT940 MT942 MT950 + ISO20022 pacs/camt"
log "Redeploy banking LXCs: bash scripts/deployment/deploy-omnl-bank-production.sh"

View File

@@ -84,7 +84,8 @@ build_pkg() {
}
for dir in packages/integration-foundation packages/settlement-core \
services/token-aggregation services/settlement-middleware services/dbis-exchange; do
services/token-aggregation services/settlement-middleware services/dbis-exchange \
services/swift-listener; do
build_pkg "$dir"
done
@@ -139,7 +140,7 @@ start_svc() {
OMNL_ALLOW_CHAIN_MINT_EXECUTE="${OMNL_ALLOW_CHAIN_MINT_EXECUTE:-1}" \
TOKEN_AGGREGATION_URL="${TOKEN_AGGREGATION_URL:-http://127.0.0.1:3000}" \
HYBX_MIFOS_FINERACT_SIDECAR_URL="${HYBX_MIFOS_FINERACT_SIDECAR_URL:-}" \
SWIFT_LISTENER_URL="${SWIFT_LISTENER_URL:-}" \
SWIFT_LISTENER_URL="${SWIFT_LISTENER_URL:-http://127.0.0.1:8788}" \
RPC_URL_138="${RPC_URL_138:-${CHAIN_138_RPC_URL:-}}" \
CHAIN_138_RPC_URL="${CHAIN_138_RPC_URL:-${RPC_URL_138:-}}" \
OMNL_MINT_OPERATOR_PRIVATE_KEY="${OMNL_MINT_OPERATOR_PRIVATE_KEY:-}" \
@@ -154,6 +155,11 @@ sleep 2
start_svc dbis-exchange services/dbis-exchange 3012 "npm run start"
sleep 2
if [[ "${OMNL_SWIFT_LISTENER_START:-1}" == "1" ]]; then
log "Starting SWIFT listener..."
bash "$REPO_DIR/scripts/deployment/start-swift-listener-production.sh" || log "WARN: SWIFT listener start failed"
fi
FRONTEND_PORT="${OMNL_BANK_FRONTEND_PORT:-3002}"
pkill -f "serve -s.*$FRONTEND_PORT" 2>/dev/null || true
cd "$REPO_DIR/frontend-dapp"
@@ -169,6 +175,7 @@ echo
curl -sf "http://127.0.0.1:3012/api/v1/exchange/health" | head -c 200 || true
echo
curl -sf -o /dev/null -w "frontend=%{http_code}\n" "http://127.0.0.1:$FRONTEND_PORT/central-bank"
curl -sf "http://127.0.0.1:8788/health" 2>/dev/null | head -c 80 || log "WARN: SWIFT listener :8788 not reachable"
if [[ -n "${DATABASE_URL:-}" ]] && [[ "${SEED_SUPER_ADMIN_USERS:-1}" == "1" ]]; then
log "Seeding super-admin control-panel users..."

View File

@@ -1,92 +1,184 @@
#!/usr/bin/env bash
# Start OMNL services from /srv/zardasht-portal inside PCT
set -euo pipefail
ROOT="${OMNL_PORTAL_ROOT:-/srv/zardasht-portal}"
LOG_DIR="${OMNL_BANK_LOG_DIR:-/var/log/omnl-bank}"
NGINX_TEMPLATE="${ROOT}/deploy/nginx/omnl-lxc-portal.conf"
ENV_FILE="${OMNL_BANK_ENV:-${ROOT}/.env}"
mkdir -p "$LOG_DIR"
if [[ -f "$ENV_FILE" ]]; then
set -a
set +u
# shellcheck disable=SC1090
source "$ENV_FILE"
set -u
set +a
fi
export SETTLEMENT_MIDDLEWARE_CONFIG="${ROOT}/config/settlement-middleware.production.v1.json"
export DBIS_EXCHANGE_CONFIG="${ROOT}/config/dbis-exchange.production.v1.json"
export OMNL_SUPPORTED_CHAINS_CONFIG="${ROOT}/config/omnl-supported-chains.v1.json"
export OMNL_M2_TOKEN_REGISTRY="${ROOT}/config/omnl-m2-token-registry.v1.json"
export OMNL_BANK_ROOT="$ROOT"
start() {
local name="$1" dir="$2" cmd="$3"
cd "${ROOT}/${dir}"
pkill -f "${ROOT}/${dir}" 2>/dev/null || true
nohup env \
SETTLEMENT_MIDDLEWARE_CONFIG="$SETTLEMENT_MIDDLEWARE_CONFIG" \
DBIS_EXCHANGE_CONFIG="$DBIS_EXCHANGE_CONFIG" \
OMNL_SUPPORTED_CHAINS_CONFIG="$OMNL_SUPPORTED_CHAINS_CONFIG" \
OMNL_M2_TOKEN_REGISTRY="$OMNL_M2_TOKEN_REGISTRY" \
OMNL_BANK_ROOT="$ROOT" \
OMNL_API_KEY="${OMNL_API_KEY:-}" \
OMNL_SUPER_ADMIN_OFFICE24_KEY="${OMNL_SUPER_ADMIN_OFFICE24_KEY:-}" \
OMNL_SUPER_ADMIN_EXCHANGE_KEY="${OMNL_SUPER_ADMIN_EXCHANGE_KEY:-}" \
OMNL_SUPER_ADMIN_SECURE_KEY="${OMNL_SUPER_ADMIN_SECURE_KEY:-}" \
OMNL_SUPER_ADMIN_FOREX_KEY="${OMNL_SUPER_ADMIN_FOREX_KEY:-}" \
OMNL_PORTAL_INTERNAL_SECRET="${OMNL_PORTAL_INTERNAL_SECRET:-}" \
OMNL_CUSTOMER_API_KEYS="${OMNL_CUSTOMER_API_KEYS:-}" \
OMNL_CUSTOMER_SECURITY_PRODUCTION="${OMNL_CUSTOMER_SECURITY_PRODUCTION:-1}" \
OMNL_FINERACT_BASE_URL="${OMNL_FINERACT_BASE_URL:-}" \
OMNL_FINERACT_TENANT="${OMNL_FINERACT_TENANT:-omnl}" \
OMNL_FINERACT_USERNAME="${OMNL_FINERACT_USERNAME:-}" \
#!/usr/bin/env bash
# Start OMNL services from /srv/zardasht-portal inside PCT
set -euo pipefail
ROOT="${OMNL_PORTAL_ROOT:-/srv/zardasht-portal}"
LOG_DIR="${OMNL_BANK_LOG_DIR:-/var/log/omnl-bank}"
NGINX_TEMPLATE="${ROOT}/deploy/nginx/omnl-lxc-portal.conf"
ENV_FILE="${OMNL_BANK_ENV:-${ROOT}/.env}"
mkdir -p "$LOG_DIR"
if [[ -f "$ENV_FILE" ]]; then
set -a
set +u
# shellcheck disable=SC1090
source "$ENV_FILE"
set -u
set +a
fi
export SETTLEMENT_MIDDLEWARE_CONFIG="${ROOT}/config/settlement-middleware.production.v1.json"
export DBIS_EXCHANGE_CONFIG="${ROOT}/config/dbis-exchange.production.v1.json"
export OMNL_SUPPORTED_CHAINS_CONFIG="${ROOT}/config/omnl-supported-chains.v1.json"
export OMNL_M2_TOKEN_REGISTRY="${ROOT}/config/omnl-m2-token-registry.v1.json"
export OMNL_BANK_ROOT="$ROOT"
start() {
local name="$1" dir="$2" cmd="$3"
cd "${ROOT}/${dir}"
pkill -f "${ROOT}/${dir}" 2>/dev/null || true
nohup env \
SETTLEMENT_MIDDLEWARE_CONFIG="$SETTLEMENT_MIDDLEWARE_CONFIG" \
DBIS_EXCHANGE_CONFIG="$DBIS_EXCHANGE_CONFIG" \
OMNL_SUPPORTED_CHAINS_CONFIG="$OMNL_SUPPORTED_CHAINS_CONFIG" \
OMNL_M2_TOKEN_REGISTRY="$OMNL_M2_TOKEN_REGISTRY" \
OMNL_BANK_ROOT="$ROOT" \
OMNL_API_KEY="${OMNL_API_KEY:-}" \
OMNL_SUPER_ADMIN_OFFICE24_KEY="${OMNL_SUPER_ADMIN_OFFICE24_KEY:-}" \
OMNL_SUPER_ADMIN_EXCHANGE_KEY="${OMNL_SUPER_ADMIN_EXCHANGE_KEY:-}" \
OMNL_SUPER_ADMIN_SECURE_KEY="${OMNL_SUPER_ADMIN_SECURE_KEY:-}" \
OMNL_SUPER_ADMIN_FOREX_KEY="${OMNL_SUPER_ADMIN_FOREX_KEY:-}" \
OMNL_PORTAL_INTERNAL_SECRET="${OMNL_PORTAL_INTERNAL_SECRET:-}" \
OMNL_CUSTOMER_API_KEYS="${OMNL_CUSTOMER_API_KEYS:-}" \
OMNL_CUSTOMER_SECURITY_PRODUCTION="${OMNL_CUSTOMER_SECURITY_PRODUCTION:-1}" \
OMNL_FINERACT_BASE_URL="${OMNL_FINERACT_BASE_URL:-}" \
OMNL_FINERACT_TENANT="${OMNL_FINERACT_TENANT:-omnl}" \
OMNL_FINERACT_USERNAME="${OMNL_FINERACT_USERNAME:-}" \
OMNL_FINERACT_USER="${OMNL_FINERACT_USER:-}" \
OMNL_FINERACT_PASSWORD="${OMNL_FINERACT_PASSWORD:-}" \
OMNL_PUBLIC_MONEY_SUPPLY="${OMNL_PUBLIC_MONEY_SUPPLY:-1}" \
DATABASE_URL="${DATABASE_URL:-}" \
SETTLEMENT_ALLOW_HYBX_PRODUCTION="${SETTLEMENT_ALLOW_HYBX_PRODUCTION:-1}" \
SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE="${SETTLEMENT_ALLOW_CHAIN_MINT_EXECUTE:-1}" \
OMNL_ALLOW_CHAIN_MINT_EXECUTE="${OMNL_ALLOW_CHAIN_MINT_EXECUTE:-1}" \
TOKEN_AGGREGATION_URL="${TOKEN_AGGREGATION_URL:-http://127.0.0.1:3000}" \
HYBX_MIFOS_FINERACT_SIDECAR_URL="${HYBX_MIFOS_FINERACT_SIDECAR_URL:-}" \
SWIFT_LISTENER_URL="${SWIFT_LISTENER_URL:-}" \
RPC_URL_138="${RPC_URL_138:-${CHAIN_138_RPC_URL:-}}" \
CHAIN_138_RPC_URL="${CHAIN_138_RPC_URL:-${RPC_URL_138:-}}" \
OMNL_MINT_OPERATOR_PRIVATE_KEY="${OMNL_MINT_OPERATOR_PRIVATE_KEY:-}" \
INFURA_PROJECT_ID="${INFURA_PROJECT_ID:-}" \
INFURA_PROJECT_SECRET="${INFURA_PROJECT_SECRET:-}" \
bash -c "$cmd" >"${LOG_DIR}/${name}.log" 2>&1 &
echo $! >"${LOG_DIR}/${name}.pid"
}
start token-aggregation services/token-aggregation "npm run start"
sleep 2
start settlement-middleware services/settlement-middleware "npm run start"
sleep 2
start dbis-exchange services/dbis-exchange "npm run start"
sleep 2
FRONTEND_PORT="${OMNL_BANK_FRONTEND_PORT:-3002}"
pkill -f "serve -s.*${FRONTEND_PORT}" 2>/dev/null || true
if [ -f "${LOG_DIR}/nginx.pid" ]; then
nginx -s quit -g "pid ${LOG_DIR}/nginx.pid;" 2>/dev/null || true
fi
fuser -k "${FRONTEND_PORT}/tcp" 2>/dev/null || true
sleep 1
if command -v nginx >/dev/null 2>&1 && [ -f "$NGINX_TEMPLATE" ]; then
sed "s|/srv/zardasht-portal|${ROOT}|g; s|/srv/ali-portal|${ROOT}|g" "$NGINX_TEMPLATE" > "${LOG_DIR}/nginx.conf"
nginx -t -c "${LOG_DIR}/nginx.conf"
nohup nginx -c "${LOG_DIR}/nginx.conf" >"${LOG_DIR}/frontend.log" 2>&1 &
echo $! >"${LOG_DIR}/frontend.pid"
echo "OMNL banking portal started under ${ROOT} (nginx :${FRONTEND_PORT})"
else
cd "${ROOT}/frontend-dapp"
nohup npx --yes serve -s dist -l "$FRONTEND_PORT" >"${LOG_DIR}/frontend.log" 2>&1 &
echo $! >"${LOG_DIR}/frontend.pid"
echo "OMNL banking portal started under ${ROOT} (serve :${FRONTEND_PORT})"
fi

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Start OMNL SWIFT listener (FIN webhook :8788 + file inbox + optional FIN TCP).
# Run on dev VM or banking LXC host after .env is loaded (chmod 600).
set -euo pipefail
REPO_DIR="${OMNL_BANK_ROOT:-$HOME/smom-dbis-138}"
ENV_FILE="${OMNL_BANK_ENV:-$REPO_DIR/.env}"
LOG_DIR="${OMNL_BANK_LOG_DIR:-$HOME/omnl-bank/logs}"
PORT="${SWIFT_LISTENER_PORT:-8788}"
log() { echo "[$(date -Iseconds)] swift-listener $*"; }
mkdir -p "$LOG_DIR"
if [[ -f "$ENV_FILE" ]]; then
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
fi
export SWIFT_LISTENER_WEBHOOK="${SWIFT_LISTENER_WEBHOOK:-1}"
export SWIFT_LISTENER_FILE_INBOX="${SWIFT_LISTENER_FILE_INBOX:-1}"
export SWIFT_LISTENER_OMNL_FORWARD="${SWIFT_LISTENER_OMNL_FORWARD:-1}"
export SWIFT_LISTENER_INTAKE_GATEWAY="${SWIFT_LISTENER_INTAKE_GATEWAY:-1}"
export SWIFT_LISTENER_GATEWAY_EXECUTE="${SWIFT_LISTENER_GATEWAY_EXECUTE:-0}"
export TOKEN_AGGREGATION_URL="${TOKEN_AGGREGATION_URL:-http://127.0.0.1:3000}"
export OMNL_API_KEY="${OMNL_API_KEY:-${OMNL_SUPER_ADMIN_OFFICE24_KEY:-}}"
export RPC_URL_138="${RPC_URL_138:-${CHAIN_138_RPC_URL:-http://192.168.11.211:8545}}"
cd "$REPO_DIR/services/swift-listener"
if [[ ! -d node_modules ]]; then
log "npm install..."
npm install --no-fund --no-audit
fi
if [[ ! -f dist/cli.js ]]; then
log "build..."
npm run build
fi
pkill -f "swift-listener/dist/cli.js" 2>/dev/null || true
fuser -k "${PORT}/tcp" 2>/dev/null || true
sleep 1
log "Starting on :${PORT} (POST /inbound, GET /health)"
nohup env \
OMNL_BANK_ROOT="$REPO_DIR" \
SWIFT_LISTENER_WEBHOOK="$SWIFT_LISTENER_WEBHOOK" \
SWIFT_LISTENER_FILE_INBOX="$SWIFT_LISTENER_FILE_INBOX" \
SWIFT_LISTENER_OMNL_FORWARD="$SWIFT_LISTENER_OMNL_FORWARD" \
SWIFT_LISTENER_INTAKE_GATEWAY="$SWIFT_LISTENER_INTAKE_GATEWAY" \
SWIFT_LISTENER_GATEWAY_EXECUTE="$SWIFT_LISTENER_GATEWAY_EXECUTE" \
SWIFT_LISTENER_WEBHOOK_TOKEN="${SWIFT_LISTENER_WEBHOOK_TOKEN:-}" \
HYBX_WEBHOOK_SECRET="${HYBX_WEBHOOK_SECRET:-}" \
TOKEN_AGGREGATION_URL="$TOKEN_AGGREGATION_URL" \
OMNL_API_KEY="$OMNL_API_KEY" \
RPC_URL_138="$RPC_URL_138" \
PRIVATE_KEY="${PRIVATE_KEY:-}" \
node dist/cli.js --project-root "$REPO_DIR" \
>"$LOG_DIR/swift-listener.log" 2>&1 &
echo $! >"$LOG_DIR/swift-listener.pid"
sleep 2
if curl -sf "http://127.0.0.1:${PORT}/health" | head -c 120; then
echo
log "OK — set SWIFT_LISTENER_URL=http://127.0.0.1:${PORT} in .env for settlement-middleware"
else
log "WARN: health check failed — see $LOG_DIR/swift-listener.log"
exit 1
fi

View File

@@ -10,6 +10,7 @@ const transfer_1 = require("../../workflows/transfer");
const office_scope_1 = require("../../workflows/office-scope");
const settlement_store_1 = require("../../store/settlement-store");
const swift_inbound_1 = require("../../workflows/swift-inbound");
const z_bank_m0_m4_1 = require("../../workflows/z-bank-m0-m4");
const fineract_1 = require("../../adapters/fineract");
const auth_1 = require("../../middleware/auth");
async function respondPublicMoneySupply(res, officeId) {
@@ -164,6 +165,27 @@ function createSettlementRouter() {
});
}
});
router.post('/z-bank/settle-m0-m4', async (req, res) => {
if (!(0, auth_1.requireApiKey)(req, res))
return;
try {
const body = req.body;
if (!body.idempotencyKey || !body.creditorIban || !body.amount) {
res.status(400).json({ error: 'idempotencyKey, creditorIban, amount required' });
return;
}
const record = await (0, z_bank_m0_m4_1.processZBankM0ToM4)({
...body,
officeId: (0, config_1.settlementOfficeId)(body.officeId),
currency: body.currency ?? 'USD',
beneficiaryName: body.beneficiaryName ?? 'Z Online Bank',
});
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
}
catch (e) {
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
}
});
router.post('/swift/inbound', async (req, res) => {
if (!(0, auth_1.requireApiKey)(req, res))
return;

View File

@@ -54,3 +54,45 @@ ${fields.beneficiary}
:70:${fields.remittance}
-}`;
}
/** MT202 — interbank cover / institution transfer (SBLC correspondent leg). */
export function buildMt202Stub(fields: {
senderRef: string;
relatedRef: string;
currency: string;
amount: string;
valueDate: string;
orderingInstitution?: string;
accountWithInstitution?: string;
beneficiaryInstitution?: string;
remittance: string;
}): string {
const ord = fields.orderingInstitution ? `:52A:${fields.orderingInstitution}\n` : '';
const acct = fields.accountWithInstitution ? `:57A:${fields.accountWithInstitution}\n` : '';
const ben = fields.beneficiaryInstitution ? `:58A:${fields.beneficiaryInstitution}\n` : '';
return `{1:F01OMNLUS33XXXX0000000000}{2:I202BANKUS33XXXXN}{4:
:20:${fields.senderRef}
:21:${fields.relatedRef}
:32A:${fields.valueDate.replace(/-/g, '').slice(2)}${fields.currency}${fields.amount.replace('.', ',')}
${ord}${acct}${ben}:70:${fields.remittance}
-}`;
}
/** MT910 — confirmation of credit (advice to beneficiary bank). */
export function buildMt910Stub(fields: {
transactionRef: string;
relatedRef: string;
currency: string;
amount: string;
valueDate: string;
accountWithInstitution?: string;
remittance: string;
}): string {
const acct = fields.accountWithInstitution ? `:57A:${fields.accountWithInstitution}\n` : '';
return `{1:F01OMNLUS33XXXX0000000000}{2:I910BANKUS33XXXXN}{4:
:20:${fields.transactionRef}
:21:${fields.relatedRef}
:32A:${fields.valueDate.replace(/-/g, '').slice(2)}${fields.currency}${fields.amount.replace('.', ',')}
${acct}:72:${fields.remittance}
-}`;
}

View File

@@ -1,5 +1,5 @@
import { Router, type Request, type Response } from 'express';
import type { ExternalTransferRequest, TradeFinanceInstrument, TokenLoadRequest, TransferRequest } from '@dbis/settlement-core';
import type { ExternalTransferRequest, TradeFinanceInstrument, TokenLoadRequest, TransferRequest, ZBankM0M4Request } from '@dbis/settlement-core';
import { buildMoneySupplySnapshot, loadOmnlChainRegistry, getActiveChains } from '@dbis/settlement-core';
import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../../config';
import {
@@ -12,6 +12,7 @@ import { processTransfer } from '../../workflows/transfer';
import { bindOffice24 } from '../../workflows/office-scope';
import { settlementStore } from '../../store/settlement-store';
import { processSwiftInbound } from '../../workflows/swift-inbound';
import { processZBankM0ToM4 } from '../../workflows/z-bank-m0-m4';
import { fetchGlBalances, fineractOfficeReachable } from '../../adapters/fineract';
import { requireApiKey } from '../../middleware/auth';
@@ -171,6 +172,26 @@ export function createSettlementRouter(): Router {
}
});
router.post('/z-bank/settle-m0-m4', async (req, res) => {
if (!requireApiKey(req, res)) return;
try {
const body = req.body as ZBankM0M4Request;
if (!body.idempotencyKey || !body.creditorIban || !body.amount) {
res.status(400).json({ error: 'idempotencyKey, creditorIban, amount required' });
return;
}
const record = await processZBankM0ToM4({
...body,
officeId: settlementOfficeId(body.officeId),
currency: body.currency ?? 'USD',
beneficiaryName: body.beneficiaryName ?? 'Z Online Bank',
});
res.status(record.phase === 'FAILED' ? 422 : 200).json(record);
} catch (e) {
res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
}
});
router.post('/swift/inbound', async (req, res) => {
if (!requireApiKey(req, res)) return;
try {

View File

@@ -16,7 +16,7 @@ import { loadConfig, loadOfficeProfile, settlementOfficeId } from '../config';
import { fetchGlBalances, postJournalEntry, resolveGlAccountId } from '../adapters/fineract';
import { HybxProductionRail } from '../adapters/hybx-production';
import { archiveIso20022, requestTokenMint } from '../adapters/omnl';
import { buildMt102Stub, buildMt103Stub, forwardSwiftToListener } from '../adapters/swift';
import { buildMt102Stub, buildMt103Stub, buildMt202Stub, buildMt910Stub, forwardSwiftToListener } from '../adapters/swift';
import { settlementStore } from '../store/settlement-store';
export async function processExternalTransfer(
@@ -130,6 +130,29 @@ export async function processExternalTransfer(
if (cfg.rails.swift.enabled) {
await forwardSwiftToListener(swiftRaw);
if (req.tradeFinance?.instrument === 'SBLC') {
const cover = buildMt202Stub({
senderRef: `${req.idempotencyKey}-COV`,
relatedRef: req.idempotencyKey,
currency: req.currency,
amount: req.amount,
valueDate: req.valueDate,
orderingInstitution: 'OMNLUS33',
beneficiaryInstitution: req.creditorBic ?? 'BANKUS33',
remittance: `SBLC cover ${req.tradeFinance.reference ?? req.idempotencyKey}`,
});
await forwardSwiftToListener(cover);
}
const advice = buildMt910Stub({
transactionRef: `${req.idempotencyKey}-ADV`,
relatedRef: req.idempotencyKey,
currency: req.currency,
amount: req.amount,
valueDate: req.valueDate,
accountWithInstitution: req.creditorBic,
remittance: `Credit confirmation ${req.beneficiaryName}`,
});
await forwardSwiftToListener(advice);
}
const iso = await archiveIso20022({
messageType: swiftKind === 'MT102' ? 'pacs.008' : 'pacs.008',

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Token Aggregation Control Panel</title>
</head>
<body>

Some files were not shown because too many files have changed in this diff Show More