diff --git a/frontend-dapp/src/components/layout/OmnlProductLayout.tsx b/frontend-dapp/src/components/layout/OmnlProductLayout.tsx index 74141d6..6239a2d 100644 --- a/frontend-dapp/src/components/layout/OmnlProductLayout.tsx +++ b/frontend-dapp/src/components/layout/OmnlProductLayout.tsx @@ -7,6 +7,7 @@ const PRODUCTS = [ { path: '/central-bank', label: 'Central Bank', short: 'CB' }, { path: '/office-24', label: 'Office 24', short: 'O24' }, { path: '/trade', label: 'DBIS Trade', short: 'Trade' }, + { path: '/swap', label: 'Swap', short: 'Swap' }, ] as const; export default function OmnlProductLayout() { diff --git a/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx b/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx index 45603c1..2d46eb6 100644 --- a/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx +++ b/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx @@ -1,30 +1,12 @@ +import { Link } from 'react-router-dom'; import { useCentralBank } from './useCentralBank'; import ChainMatrixPanel from './ChainMatrixPanel'; -import { SETTLEMENT_MIDDLEWARE_URL, DBIS_EXCHANGE_URL } from '../../config/dex'; +import FineractLedgerBanner from '../omnl-dashboard/FineractLedgerBanner'; +import DashboardStatCard from '../omnl-dashboard/DashboardStatCard'; +import DashboardSection from '../omnl-dashboard/DashboardSection'; +import QuickPortalLinks from '../omnl-dashboard/QuickPortalLinks'; import { formatUsd } from '../../utils/formatMoney'; -function StatCard({ - label, - value, - sub, - accent, -}: { - label: string; - value: string; - sub?: string; - accent?: 'gold' | 'green' | 'blue'; -}) { - const border = - accent === 'gold' ? 'border-l-[#f0b90b]' : accent === 'green' ? 'border-l-[#0ecb81]' : 'border-l-[#3861fb]'; - return ( -
-

{label}

-

{value}

- {sub &&

{sub}

} -
- ); -} - export default function CentralBankDashboard() { const cb = useCentralBank(); const office = cb.office as { @@ -35,157 +17,172 @@ export default function CentralBankDashboard() { } | null; return ( -
-
-
- - OMNL Central Bank - - - Production · 128 chains - - Meta fiat · M0 / M1 / M2 +
+
+
+ OMNL Central Bank + Production + 128 chains
-

Central Bank Operations

-

- Settlement orchestration · SWIFT · HYBX · Chain 138 token loading +

Central Bank Operations

+

+ Live money supply for {office?.name ?? 'HOSPITALLERS Ali Iraq-Iran'} (Office{' '} + {office?.officeId ?? 24}) · settlement · SWIFT · HYBX · Chain 138

+ + {cb.error && ( -
{cb.error}
+
+

Failed to load dashboard

+

{cb.error}

+
)} + + +

Money supply

+

+ Meta-fiat layers from Fineract general ledger · USD +

+
- - - -
- {cb.moneySupply?.fineractConnected && !cb.moneySupply?.fineractLive && ( -

- Fineract connected for Office 24 — no opening journal yet. Post Dr 1410 / Cr 2100 via{' '} - seed-office-24-opening-journal.mjs. -

- )} - - {cb.moneySupply?.fineractConnected === false && ( -

- Fineract unreachable — check OMNL_FINERACT_* on settlement middleware. -

- )} - - {cb.moneySupply?.asOf && ( -

- Live Fineract · Office {cb.moneySupply.officeId ?? 24} · as of{' '} - {cb.moneySupply.asOf.slice(0, 19).replace('T', ' ')} UTC - {cb.moneySupply.interoffice?.dueFromHeadOffice - ? ` · Due from HO (GL 1410): ${formatUsd(cb.moneySupply.interoffice.dueFromHeadOffice)}` - : ''} -

- )} - - {!cb.moneySupply && ( -

- Money supply loads from{' '} - /money-supply/public when online bank mode is enabled. -

- )} -
-
-

Settlement service

-
-
-
Status
-
{(cb.health as { status?: string })?.status ?? '—'}
+ cb.refresh()} + disabled={cb.loading} + > + {cb.loading ? 'Refreshing…' : 'Refresh'} + + } + > +
+
+
Status
+
{(cb.health as { status?: string })?.status ?? '—'}
-
-
Role
-
- {(cb.health as { role?: string })?.role ?? '—'} +
+
Office
+
+ {office?.officeId ?? 24} · {office?.externalId ?? 'HOSPITALLERS-ALI-IRAQ-IRAN'}
-
-
API
-
{SETTLEMENT_MIDDLEWARE_URL}
+
+
M2 tokens registered
+
{cb.tokenCount}
+
+
+
Active chains
+
+ {(cb.health as { chainSupport?: { active?: number; total?: number } })?.chainSupport + ?.active ?? '—'} + {' / '} + {(cb.health as { chainSupport?: { active?: number; total?: number } })?.chainSupport + ?.total ?? 128} +
- -
+ -
-

Money layers (Office {office?.officeId ?? 24})

+
{(office?.settlement?.moneyLayers ?? ['M0', 'M1', 'M2']).map((layer) => ( - + {layer} ))}
-
-
-
GL M0
-
{office?.settlement?.glM0 ?? '1050'}
+
+
+
M0 reserve
+
GL {office?.settlement?.glM0 ?? '1050'}
-
-
GL M1
-
{office?.settlement?.glM1 ?? '2100'}
+
+
M1 liability
+
GL {office?.settlement?.glM1 ?? '2100'}
-
-
GL M2
-
{office?.settlement?.glM2 ?? '2200'}
+
+
M2 broad
+
GL {office?.settlement?.glM2 ?? '2200'}
+
+
+
Inter-office (HO)
+
GL 1410
-
+

+ + Open Office 24 dashboard → + + {' · '} + + DBIS Trade → + +

+ -
-

Recent exchange activity

-
    + +
      {((cb.monitor as { swaps?: { status: string; tokenIn: string; createdAt: string }[] })?.swaps ?? []) .slice(0, 8) .map((s, i) => ( -
    • - {s.status} - {s.tokenIn?.slice(0, 10)}… - {s.createdAt?.slice(0, 16)} +
    • + {s.status} + {s.tokenIn?.slice(0, 12)}… + {s.createdAt?.slice(0, 16)}
    • ))} {!((cb.monitor as { swaps?: unknown[] })?.swaps?.length) && ( -
    • No swaps recorded yet
    • +
    • No swaps recorded yet
    • )}
    -

    - DBIS Exchange: {DBIS_EXCHANGE_URL} · Token-load & transfers via settlement middleware -

    -
+
diff --git a/frontend-dapp/src/features/central-bank/ChainMatrixPanel.tsx b/frontend-dapp/src/features/central-bank/ChainMatrixPanel.tsx index c354aa3..e34366b 100644 --- a/frontend-dapp/src/features/central-bank/ChainMatrixPanel.tsx +++ b/frontend-dapp/src/features/central-bank/ChainMatrixPanel.tsx @@ -24,7 +24,7 @@ export default function ChainMatrixPanel() { }); return ( -
+

128-chain network matrix

diff --git a/frontend-dapp/src/features/office24/Office24Dashboard.tsx b/frontend-dapp/src/features/office24/Office24Dashboard.tsx index 34f60ed..8a11d6c 100644 --- a/frontend-dapp/src/features/office24/Office24Dashboard.tsx +++ b/frontend-dapp/src/features/office24/Office24Dashboard.tsx @@ -1,5 +1,9 @@ import { Link } from 'react-router-dom'; import { useOffice24 } from './useOffice24'; +import FineractLedgerBanner from '../omnl-dashboard/FineractLedgerBanner'; +import DashboardStatCard from '../omnl-dashboard/DashboardStatCard'; +import DashboardSection from '../omnl-dashboard/DashboardSection'; +import QuickPortalLinks from '../omnl-dashboard/QuickPortalLinks'; import { formatUsd } from '../../utils/formatMoney'; export default function Office24Dashboard() { @@ -15,123 +19,147 @@ export default function Office24Dashboard() { } | null; return ( -
-
+
+
-
- - Office 24 · Locked - - {profile?.locked && Single-office enforcement} +
+ Office 24 + {profile?.locked && Single office · locked}
-

{profile?.name ?? 'HOSPITALLERS Ali Iraq-Iran'}

-

{profile?.externalId ?? 'HOSPITALLERS-ALI-IRAQ-IRAN'}

+

{profile?.name ?? 'HOSPITALLERS Ali Iraq-Iran'}

+

{profile?.externalId ?? 'HOSPITALLERS-ALI-IRAQ-IRAN'}

+
+
+ + Central Bank + + + DBIS Trade +
- - Open DBIS Trade → -
-
-
-

Office ID

-

{profile?.officeId ?? 24}

-
-
-

M1 circulating (GL 2100)

-

- {formatUsd(moneySupply?.M1?.circulating)} -

-
-
-

Due from HO (GL 1410)

-

- {formatUsd(moneySupply?.interoffice?.dueFromHeadOffice)} -

-
-
-

M2 tradable tokens

-

{tokens.length}

-
+ + + + +

Office balances

+

+ Live Fineract · {profile?.settlement?.defaultCurrency ?? 'USD'} +

+ +
+ + + +
- {moneySupply?.fineractConnected && !moneySupply?.fineractLive && ( -

- Fineract connected — post opening M1: Dr 1410 / Cr 2100 via{' '} - scripts/deployment/seed-office-24-opening-journal.mjs -

- )}
-
-

Settlement rails

-
    -
  • - SWIFT - - {profile?.rails?.swift?.enabled ? 'Enabled' : 'Off'} - -
  • -
  • - HYBX - - {profile?.rails?.hybx?.enabled ? 'Enabled' : 'Off'} - -
  • -
  • - Chain 138 - - {profile?.rails?.chain138?.enabled ? 'Enabled' : 'Off'} - -
  • -
  • - Exchange - {(health as { status?: string })?.status ?? 'ok'} -
  • -
-

Tenant: {profile?.tenantUser ?? 'ali_hospitallers_tenant'}

-
- -
-
-

M2 token registry — swap · convert · transfer

- -
-
+ } + > +
+
+
SWIFT
+
+ {profile?.rails?.swift?.enabled ? 'Enabled' : 'Off'} +
+
+
+
HYBX
+
+ {profile?.rails?.hybx?.enabled ? 'Enabled' : 'Off'} +
+
+
+
Chain 138
+
+ {profile?.rails?.chain138?.enabled ? 'Enabled' : 'Off'} +
+
+
+
DBIS Exchange
+
{(health as { status?: string })?.status ?? 'ok'}
+
+
+
Fineract tenant
+
{profile?.tenantUser ?? 'ali_hospitallers_tenant'}
+
+
+ + + +
- + - - - - - + + + + + {tokens.map((t) => ( - - - - - - + + + + + ))} + {!tokens.length && ( + + + + )}
SymbolLineSwapConvertXferSymbolLineSwapConvertTransfer
{t.symbol}{t.omnlLine ?? 'M2'}{t.swappable !== false ? '✓' : '—'}{t.convertible !== false ? '✓' : '—'} - {t.transferableInternal && t.transferableExternal ? 'Int+Ext' : '✓'} +
{t.symbol}{t.omnlLine ?? 'M2'}{t.swappable !== false ? 'Yes' : '—'}{t.convertible !== false ? 'Yes' : '—'} + {t.transferableInternal && t.transferableExternal ? 'Internal + external' : 'Yes'}
+ No M2 tokens loaded +
-
+
); diff --git a/frontend-dapp/src/features/omnl-dashboard/DashboardSection.tsx b/frontend-dapp/src/features/omnl-dashboard/DashboardSection.tsx new file mode 100644 index 0000000..d2fc89f --- /dev/null +++ b/frontend-dapp/src/features/omnl-dashboard/DashboardSection.tsx @@ -0,0 +1,28 @@ +import type { ReactNode } from 'react'; + +export default function DashboardSection({ + title, + subtitle, + action, + children, + className = '', +}: { + title: string; + subtitle?: string; + action?: React.ReactNode; + children: ReactNode; + className?: string; +}) { + return ( +
+
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+ {action} +
+ {children} +
+ ); +} diff --git a/frontend-dapp/src/features/omnl-dashboard/DashboardStatCard.tsx b/frontend-dapp/src/features/omnl-dashboard/DashboardStatCard.tsx new file mode 100644 index 0000000..c4a6154 --- /dev/null +++ b/frontend-dapp/src/features/omnl-dashboard/DashboardStatCard.tsx @@ -0,0 +1,28 @@ +type Accent = 'gold' | 'green' | 'blue' | 'neutral'; + +const ACCENT: Record = { + blue: 'border-l-[#3861fb]', + green: 'border-l-[#0ecb81]', + gold: 'border-l-[#f0b90b]', + neutral: 'border-l-[#848e9c]', +}; + +export default function DashboardStatCard({ + label, + value, + hint, + accent = 'neutral', +}: { + label: string; + value: string; + hint?: string; + accent?: Accent; +}) { + return ( +
+

{label}

+

{value}

+ {hint &&

{hint}

} +
+ ); +} diff --git a/frontend-dapp/src/features/omnl-dashboard/FineractLedgerBanner.tsx b/frontend-dapp/src/features/omnl-dashboard/FineractLedgerBanner.tsx new file mode 100644 index 0000000..f2fc766 --- /dev/null +++ b/frontend-dapp/src/features/omnl-dashboard/FineractLedgerBanner.tsx @@ -0,0 +1,72 @@ +import type { MoneySupply } from '../central-bank/useCentralBank'; + +type Props = { + moneySupply: MoneySupply | null; + loading?: boolean; + officeName?: string; +}; + +function formatAsOf(asOf?: string) { + if (!asOf) return null; + return asOf.slice(0, 19).replace('T', ' ') + ' UTC'; +} + +export default function FineractLedgerBanner({ moneySupply, loading, officeName }: Props) { + if (loading && !moneySupply) { + return ( +
+

Loading live ledger balances…

+

Fetching Fineract data for Office 24

+
+ ); + } + + if (!moneySupply) { + return ( +
+

Ledger data not available

+

+ The money-supply API did not respond. Check settlement middleware on port 3011. +

+
+ ); + } + + const asOf = formatAsOf(moneySupply.asOf); + const officeLabel = officeName ?? `Office ${moneySupply.officeId ?? 24}`; + + if (moneySupply.fineractConnected === false) { + return ( +
+

Fineract offline

+

+ Cannot reach Fineract for {officeLabel}. Verify OMNL_FINERACT_* credentials on the + settlement service. +

+
+ ); + } + + if (moneySupply.fineractConnected && !moneySupply.fineractLive) { + return ( +
+

Fineract connected — opening journal pending

+

+ {officeLabel} has no M1 allocation yet. Post the opening entry: Debit GL 1410 (Due from + Head Office) · Credit GL 2100 (M1 liabilities). + {asOf ? ` · Checked ${asOf}` : ''} +

+
+ ); + } + + return ( +
+

Live Fineract ledger · {officeLabel}

+

+ M0 / M1 / M2 balances below are read from Fineract in real time. + {asOf ? ` Last updated ${asOf}.` : ''} +

+
+ ); +} diff --git a/frontend-dapp/src/features/omnl-dashboard/QuickPortalLinks.tsx b/frontend-dapp/src/features/omnl-dashboard/QuickPortalLinks.tsx new file mode 100644 index 0000000..67a032c --- /dev/null +++ b/frontend-dapp/src/features/omnl-dashboard/QuickPortalLinks.tsx @@ -0,0 +1,28 @@ +const LINKS = [ + { label: 'Central Bank', href: 'https://secure.omdnl.org/central-bank' }, + { label: 'Online banking', href: 'https://online.omdnl.org/central-bank' }, + { label: 'Office 24', href: 'https://office24.omdnl.org/office-24' }, + { label: 'DBIS Trade', href: 'https://exchange.omdnl.org/trade' }, + { label: 'Digital swap', href: 'https://digital.omdnl.org/swap' }, +] as const; + +export default function QuickPortalLinks() { + return ( +
+

Public portals

+
+ {LINKS.map(({ label, href }) => ( + + {label} + + ))} +
+
+ ); +} diff --git a/frontend-dapp/src/styles/omnl-apps.css b/frontend-dapp/src/styles/omnl-apps.css index 63e00d1..c419871 100644 --- a/frontend-dapp/src/styles/omnl-apps.css +++ b/frontend-dapp/src/styles/omnl-apps.css @@ -1,18 +1,218 @@ /* OMNL product UIs — Central Bank, Office 24, Binance-style trade */ .cb-page, -.o24-page { +.o24-page, +.dash-page { min-height: calc(100vh - 56px); } .cb-card, -.o24-card { +.o24-card, +.dash-card { background: #1e2329; border: 1px solid #2b3139; - border-radius: 4px; + border-radius: 8px; padding: 1.25rem; } +.dash-page-title { + font-size: 1.875rem; + font-weight: 700; + color: #eaecef; + line-height: 1.2; +} + +.dash-page-subtitle { + margin-top: 0.35rem; + color: #848e9c; + font-size: 0.95rem; + max-width: 42rem; +} + +.dash-badge { + display: inline-flex; + align-items: center; + padding: 0.2rem 0.55rem; + border-radius: 4px; + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; +} + +.dash-badge--blue { + background: rgba(56, 97, 251, 0.18); + color: #3861fb; +} + +.dash-badge--green { + background: rgba(14, 203, 129, 0.18); + color: #0ecb81; +} + +.dash-badge--gold { + background: rgba(240, 185, 11, 0.18); + color: #f0b90b; +} + +.dash-banner { + border-radius: 8px; + padding: 1rem 1.15rem; + border: 1px solid #2b3139; +} + +.dash-banner__title { + font-size: 0.95rem; + font-weight: 600; + color: #eaecef; + margin-bottom: 0.25rem; +} + +.dash-banner__detail { + font-size: 0.85rem; + color: #848e9c; + line-height: 1.45; +} + +.dash-banner--ok { + background: rgba(14, 203, 129, 0.08); + border-color: rgba(14, 203, 129, 0.35); +} + +.dash-banner--ok .dash-banner__title { + color: #0ecb81; +} + +.dash-banner--warn { + background: rgba(240, 185, 11, 0.08); + border-color: rgba(240, 185, 11, 0.35); +} + +.dash-banner--warn .dash-banner__title { + color: #f0b90b; +} + +.dash-banner--error { + background: rgba(246, 70, 93, 0.08); + border-color: rgba(246, 70, 93, 0.35); +} + +.dash-banner--error .dash-banner__title { + color: #f6465d; +} + +.dash-banner--loading { + background: #1e2329; +} + +.dash-stat-label { + font-size: 0.7rem; + color: #848e9c; + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: 0.35rem; +} + +.dash-stat-value { + font-size: 1.65rem; + font-weight: 700; + color: #eaecef; + font-variant-numeric: tabular-nums; +} + +.dash-stat-hint { + margin-top: 0.35rem; + font-size: 0.75rem; + color: #848e9c; +} + +.dash-section-title { + font-size: 1.05rem; + font-weight: 600; + color: #eaecef; +} + +.dash-section-subtitle { + margin-top: 0.2rem; + font-size: 0.8rem; + color: #848e9c; +} + +.dash-kv { + display: flex; + justify-content: space-between; + gap: 1rem; + padding: 0.45rem 0; + font-size: 0.875rem; + border-bottom: 1px solid #2b3139; +} + +.dash-kv:last-child { + border-bottom: none; +} + +.dash-kv dt { + color: #848e9c; + flex-shrink: 0; +} + +.dash-kv dd { + color: #eaecef; + text-align: right; + word-break: break-word; +} + +.dash-kv .dash-kv-value--ok { + color: #0ecb81; + font-weight: 500; +} + +.dash-quick-links__label { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: #848e9c; + margin-bottom: 0.5rem; +} + +.dash-quick-links__chip { + display: inline-block; + padding: 0.35rem 0.75rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 500; + color: #eaecef; + background: #2b3139; + border: 1px solid #474d57; + transition: background 0.15s, border-color 0.15s; +} + +.dash-quick-links__chip:hover { + background: #474d57; + border-color: #f0b90b; + color: #f0b90b; +} + +.dash-btn { + width: 100%; + padding: 0.55rem 1rem; + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; + background: #2b3139; + color: #eaecef; + transition: background 0.15s; +} + +.dash-btn:hover { + background: #474d57; +} + +.dash-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + /* Binance-style trade terminal */ .trade-layout { display: grid;