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
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:
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
110
config/swift-listener-chain138.v1.json
Normal file
110
config/swift-listener-chain138.v1.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
72
config/swift-mt-catalog.v1.json
Normal file
72
config/swift-mt-catalog.v1.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function ToastProvider() {
|
||||
},
|
||||
loading: {
|
||||
iconTheme: {
|
||||
primary: '#3b82f6',
|
||||
primary: '#22c55e',
|
||||
secondary: '#fff',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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]'}`}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>{' '}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: [],
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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'),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}, {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
18
packages/settlement-core/dist/money-supply.d.ts
vendored
18
packages/settlement-core/dist/money-supply.d.ts
vendored
@@ -34,6 +34,24 @@ export declare function m3TokenLoadAmount(requested: string, m2Broad: string, m3
|
||||
};
|
||||
/** All settlement layers M0–M4 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;
|
||||
|
||||
47
packages/settlement-core/dist/money-supply.js
vendored
47
packages/settlement-core/dist/money-supply.js
vendored
@@ -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 M0–M4 */
|
||||
exports.GL_CODES = {
|
||||
@@ -105,6 +107,51 @@ function m3TokenLoadAmount(requested, m2Broad, m3Outstanding) {
|
||||
}
|
||||
/** All settlement layers M0–M4 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'))
|
||||
|
||||
23
packages/settlement-core/dist/types.d.ts
vendored
23
packages/settlement-core/dist/types.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -117,6 +117,64 @@ export function m3TokenLoadAmount(requested: string, m2Broad: string, m3Outstand
|
||||
/** All settlement layers M0–M4 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
71
scripts/deployment/activate-swift-production.sh
Normal file
71
scripts/deployment/activate-swift-production.sh
Normal 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"
|
||||
@@ -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..."
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
70
scripts/deployment/start-swift-listener-production.sh
Normal file
70
scripts/deployment/start-swift-listener-production.sh
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
-}`;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user