feat: clarify Central Bank and Office 24 dashboard layout
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m21s
CI/CD Pipeline / Security Scanning (push) Successful in 2m42s
CI/CD Pipeline / Lint and Format (push) Failing after 52s
CI/CD Pipeline / Terraform Validation (push) Failing after 28s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 44s
Validation / validate-genesis (push) Successful in 31s
Validation / validate-terraform (push) Failing after 28s
Validation / validate-kubernetes (push) Failing after 12s
Validation / validate-smart-contracts (push) Failing after 12s
Validation / validate-security (push) Failing after 1m21s
Validation / validate-documentation (push) Failing after 20s
Verify Deployment / Verify Deployment (push) Failing after 1m20s
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m21s
CI/CD Pipeline / Security Scanning (push) Successful in 2m42s
CI/CD Pipeline / Lint and Format (push) Failing after 52s
CI/CD Pipeline / Terraform Validation (push) Failing after 28s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 44s
Validation / validate-genesis (push) Successful in 31s
Validation / validate-terraform (push) Failing after 28s
Validation / validate-kubernetes (push) Failing after 12s
Validation / validate-smart-contracts (push) Failing after 12s
Validation / validate-security (push) Failing after 1m21s
Validation / validate-documentation (push) Failing after 20s
Verify Deployment / Verify Deployment (push) Failing after 1m20s
Add status banners, grouped money-supply sections, public portal links, and shared dashboard components for clearer operator views. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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 (
|
||||
<div className={`cb-card border-l-4 ${border}`}>
|
||||
<p className="text-xs text-[#848e9c] uppercase tracking-wide mb-1">{label}</p>
|
||||
<p className="text-2xl font-semibold text-[#eaecef]">{value}</p>
|
||||
{sub && <p className="text-xs text-[#848e9c] mt-1">{sub}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CentralBankDashboard() {
|
||||
const cb = useCentralBank();
|
||||
const office = cb.office as {
|
||||
@@ -35,157 +17,172 @@ export default function CentralBankDashboard() {
|
||||
} | null;
|
||||
|
||||
return (
|
||||
<div className="cb-page max-w-[1400px] mx-auto px-4 py-6">
|
||||
<header className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="px-2 py-0.5 rounded text-xs font-medium bg-[#3861fb]/20 text-[#3861fb]">
|
||||
OMNL Central Bank
|
||||
</span>
|
||||
<span className="px-2 py-0.5 rounded text-xs font-medium bg-[#0ecb81]/20 text-[#0ecb81]">
|
||||
Production · 128 chains
|
||||
</span>
|
||||
<span className="text-xs text-[#848e9c]">Meta fiat · M0 / M1 / M2</span>
|
||||
<div className="dash-page cb-page max-w-[1400px] mx-auto px-4 py-6">
|
||||
<header className="mb-6">
|
||||
<div className="flex flex-wrap items-center gap-2 mb-3">
|
||||
<span className="dash-badge dash-badge--blue">OMNL Central Bank</span>
|
||||
<span className="dash-badge dash-badge--green">Production</span>
|
||||
<span className="dash-badge dash-badge--gold">128 chains</span>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-[#eaecef]">Central Bank Operations</h1>
|
||||
<p className="text-[#848e9c] mt-1">
|
||||
Settlement orchestration · SWIFT · HYBX · Chain 138 token loading
|
||||
<h1 className="dash-page-title">Central Bank Operations</h1>
|
||||
<p className="dash-page-subtitle">
|
||||
Live money supply for {office?.name ?? 'HOSPITALLERS Ali Iraq-Iran'} (Office{' '}
|
||||
{office?.officeId ?? 24}) · settlement · SWIFT · HYBX · Chain 138
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<QuickPortalLinks />
|
||||
|
||||
{cb.error && (
|
||||
<div className="mb-4 p-3 rounded bg-[#f6465d]/10 text-[#f6465d] text-sm">{cb.error}</div>
|
||||
<div className="dash-banner dash-banner--error mb-6">
|
||||
<p className="dash-banner__title">Failed to load dashboard</p>
|
||||
<p className="dash-banner__detail">{cb.error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FineractLedgerBanner
|
||||
moneySupply={cb.moneySupply}
|
||||
loading={cb.loading}
|
||||
officeName={office?.name}
|
||||
/>
|
||||
|
||||
<h2 className="dash-section-title mb-1">Money supply</h2>
|
||||
<p className="dash-section-subtitle mb-4">
|
||||
Meta-fiat layers from Fineract general ledger · USD
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<StatCard
|
||||
label="M0 Reserve (GL 1050)"
|
||||
<DashboardStatCard
|
||||
label="M0 reserve"
|
||||
value={formatUsd(cb.moneySupply?.M0?.gl1050)}
|
||||
sub={`Backing ${cb.moneySupply?.M0?.backingRatio ?? 1.2}×`}
|
||||
hint={`GL 1050 · ${cb.moneySupply?.M0?.backingRatio ?? 1.2}× backing policy`}
|
||||
accent="blue"
|
||||
/>
|
||||
<StatCard
|
||||
label="M1 Circulating (GL 2100)"
|
||||
<DashboardStatCard
|
||||
label="M1 circulating"
|
||||
value={formatUsd(cb.moneySupply?.M1?.circulating)}
|
||||
sub="Narrow money · eMoney"
|
||||
hint="GL 2100 · narrow money / eMoney"
|
||||
accent="green"
|
||||
/>
|
||||
<StatCard
|
||||
label="M2 Broad (GL 2200)"
|
||||
<DashboardStatCard
|
||||
label="M2 broad money"
|
||||
value={formatUsd(cb.moneySupply?.M2?.broadMoney)}
|
||||
sub="Token load source"
|
||||
hint="GL 2200 · token-load source"
|
||||
accent="gold"
|
||||
/>
|
||||
<StatCard
|
||||
label="M2 tokens registered"
|
||||
value={String(cb.tokenCount)}
|
||||
sub="Swappable · convertible · transferable"
|
||||
accent="gold"
|
||||
<DashboardStatCard
|
||||
label="Due from head office"
|
||||
value={formatUsd(cb.moneySupply?.interoffice?.dueFromHeadOffice)}
|
||||
hint="GL 1410 · inter-office receivable"
|
||||
accent="blue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{cb.moneySupply?.fineractConnected && !cb.moneySupply?.fineractLive && (
|
||||
<p className="text-xs text-[#f0b90b] mb-6">
|
||||
Fineract connected for Office 24 — no opening journal yet. Post Dr 1410 / Cr 2100 via{' '}
|
||||
<code className="text-[#f0b90b]">seed-office-24-opening-journal.mjs</code>.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{cb.moneySupply?.fineractConnected === false && (
|
||||
<p className="text-xs text-[#f6465d] mb-6">
|
||||
Fineract unreachable — check OMNL_FINERACT_* on settlement middleware.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{cb.moneySupply?.asOf && (
|
||||
<p className="text-xs text-[#848e9c] mb-6">
|
||||
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)}`
|
||||
: ''}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!cb.moneySupply && (
|
||||
<p className="text-xs text-[#848e9c] mb-6">
|
||||
Money supply loads from{' '}
|
||||
<code className="text-[#f0b90b]">/money-supply/public</code> when online bank mode is enabled.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<section className="cb-card">
|
||||
<h2 className="text-lg font-semibold text-[#eaecef] mb-4">Settlement service</h2>
|
||||
<dl className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[#848e9c]">Status</dt>
|
||||
<dd className="text-[#0ecb81] font-medium">{(cb.health as { status?: string })?.status ?? '—'}</dd>
|
||||
<DashboardSection
|
||||
title="Settlement service"
|
||||
subtitle="Middleware health and refresh"
|
||||
action={
|
||||
<button
|
||||
type="button"
|
||||
className="dash-btn !w-auto px-4"
|
||||
onClick={() => cb.refresh()}
|
||||
disabled={cb.loading}
|
||||
>
|
||||
{cb.loading ? 'Refreshing…' : 'Refresh'}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<dl>
|
||||
<div className="dash-kv">
|
||||
<dt>Status</dt>
|
||||
<dd className="dash-kv-value--ok">{(cb.health as { status?: string })?.status ?? '—'}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[#848e9c]">Role</dt>
|
||||
<dd className="text-[#eaecef] text-right max-w-[60%]">
|
||||
{(cb.health as { role?: string })?.role ?? '—'}
|
||||
<div className="dash-kv">
|
||||
<dt>Office</dt>
|
||||
<dd>
|
||||
{office?.officeId ?? 24} · {office?.externalId ?? 'HOSPITALLERS-ALI-IRAQ-IRAN'}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[#848e9c]">API</dt>
|
||||
<dd className="text-[#f0b90b] text-xs truncate max-w-[60%]">{SETTLEMENT_MIDDLEWARE_URL}</dd>
|
||||
<div className="dash-kv">
|
||||
<dt>M2 tokens registered</dt>
|
||||
<dd>{cb.tokenCount}</dd>
|
||||
</div>
|
||||
<div className="dash-kv">
|
||||
<dt>Active chains</dt>
|
||||
<dd>
|
||||
{(cb.health as { chainSupport?: { active?: number; total?: number } })?.chainSupport
|
||||
?.active ?? '—'}
|
||||
{' / '}
|
||||
{(cb.health as { chainSupport?: { active?: number; total?: number } })?.chainSupport
|
||||
?.total ?? 128}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-4 w-full py-2 rounded bg-[#2b3139] text-[#eaecef] text-sm hover:bg-[#474d57]"
|
||||
onClick={() => cb.refresh()}
|
||||
>
|
||||
{cb.loading ? 'Refreshing…' : 'Refresh'}
|
||||
</button>
|
||||
</section>
|
||||
</DashboardSection>
|
||||
|
||||
<section className="cb-card">
|
||||
<h2 className="text-lg font-semibold text-[#eaecef] mb-4">Money layers (Office {office?.officeId ?? 24})</h2>
|
||||
<DashboardSection
|
||||
title="General ledger map"
|
||||
subtitle={`Money layers · Office ${office?.officeId ?? 24}`}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{(office?.settlement?.moneyLayers ?? ['M0', 'M1', 'M2']).map((layer) => (
|
||||
<span key={layer} className="px-3 py-1 rounded-full bg-[#2b3139] text-[#f0b90b] text-xs font-medium">
|
||||
<span
|
||||
key={layer}
|
||||
className="px-3 py-1 rounded-full bg-[#2b3139] text-[#f0b90b] text-xs font-semibold"
|
||||
>
|
||||
{layer}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<dl className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[#848e9c]">GL M0</dt>
|
||||
<dd>{office?.settlement?.glM0 ?? '1050'}</dd>
|
||||
<dl>
|
||||
<div className="dash-kv">
|
||||
<dt>M0 reserve</dt>
|
||||
<dd>GL {office?.settlement?.glM0 ?? '1050'}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[#848e9c]">GL M1</dt>
|
||||
<dd>{office?.settlement?.glM1 ?? '2100'}</dd>
|
||||
<div className="dash-kv">
|
||||
<dt>M1 liability</dt>
|
||||
<dd>GL {office?.settlement?.glM1 ?? '2100'}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-[#848e9c]">GL M2</dt>
|
||||
<dd>{office?.settlement?.glM2 ?? '2200'}</dd>
|
||||
<div className="dash-kv">
|
||||
<dt>M2 broad</dt>
|
||||
<dd>GL {office?.settlement?.glM2 ?? '2200'}</dd>
|
||||
</div>
|
||||
<div className="dash-kv">
|
||||
<dt>Inter-office (HO)</dt>
|
||||
<dd>GL 1410</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
<p className="dash-section-subtitle mt-4">
|
||||
<Link to="/office-24" className="text-[#f0b90b] hover:underline">
|
||||
Open Office 24 dashboard →
|
||||
</Link>
|
||||
{' · '}
|
||||
<Link to="/trade" className="text-[#f0b90b] hover:underline">
|
||||
DBIS Trade →
|
||||
</Link>
|
||||
</p>
|
||||
</DashboardSection>
|
||||
|
||||
<section className="cb-card lg:col-span-2">
|
||||
<h2 className="text-lg font-semibold text-[#eaecef] mb-4">Recent exchange activity</h2>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<DashboardSection title="Recent exchange activity" subtitle="Last 8 swaps on DBIS Exchange" className="lg:col-span-2">
|
||||
<ul className="space-y-0 text-sm">
|
||||
{((cb.monitor as { swaps?: { status: string; tokenIn: string; createdAt: string }[] })?.swaps ?? [])
|
||||
.slice(0, 8)
|
||||
.map((s, i) => (
|
||||
<li key={i} className="flex justify-between py-2 border-b border-[#2b3139] last:border-0">
|
||||
<span className="text-[#848e9c]">{s.status}</span>
|
||||
<span className="text-[#eaecef] font-mono text-xs truncate max-w-[50%]">{s.tokenIn?.slice(0, 10)}…</span>
|
||||
<span className="text-[#848e9c] text-xs">{s.createdAt?.slice(0, 16)}</span>
|
||||
<li
|
||||
key={i}
|
||||
className="flex justify-between gap-3 py-2.5 border-b border-[#2b3139] last:border-0 text-sm"
|
||||
>
|
||||
<span className="text-[#848e9c] shrink-0">{s.status}</span>
|
||||
<span className="text-[#eaecef] font-mono text-xs truncate">{s.tokenIn?.slice(0, 12)}…</span>
|
||||
<span className="text-[#848e9c] text-xs shrink-0">{s.createdAt?.slice(0, 16)}</span>
|
||||
</li>
|
||||
))}
|
||||
{!((cb.monitor as { swaps?: unknown[] })?.swaps?.length) && (
|
||||
<li className="text-[#848e9c]">No swaps recorded yet</li>
|
||||
<li className="text-[#848e9c] py-3">No swaps recorded yet</li>
|
||||
)}
|
||||
</ul>
|
||||
<p className="text-xs text-[#848e9c] mt-4">
|
||||
DBIS Exchange: {DBIS_EXCHANGE_URL} · Token-load & transfers via settlement middleware
|
||||
</p>
|
||||
</section>
|
||||
</DashboardSection>
|
||||
|
||||
<ChainMatrixPanel />
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function ChainMatrixPanel() {
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="cb-card lg:col-span-2">
|
||||
<section className="dash-card cb-card lg:col-span-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[#eaecef]">128-chain network matrix</h2>
|
||||
|
||||
@@ -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 (
|
||||
<div className="o24-page max-w-[1400px] mx-auto px-4 py-6">
|
||||
<header className="mb-8 flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="dash-page o24-page max-w-[1400px] mx-auto px-4 py-6">
|
||||
<header className="mb-6 flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="px-2 py-0.5 rounded text-xs font-medium bg-[#0ecb81]/20 text-[#0ecb81]">
|
||||
Office 24 · Locked
|
||||
</span>
|
||||
{profile?.locked && <span className="text-xs text-[#848e9c]">Single-office enforcement</span>}
|
||||
<div className="flex flex-wrap items-center gap-2 mb-3">
|
||||
<span className="dash-badge dash-badge--green">Office 24</span>
|
||||
{profile?.locked && <span className="dash-badge dash-badge--gold">Single office · locked</span>}
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-[#eaecef]">{profile?.name ?? 'HOSPITALLERS Ali Iraq-Iran'}</h1>
|
||||
<p className="text-[#848e9c] mt-1 font-mono text-sm">{profile?.externalId ?? 'HOSPITALLERS-ALI-IRAQ-IRAN'}</p>
|
||||
<h1 className="dash-page-title">{profile?.name ?? 'HOSPITALLERS Ali Iraq-Iran'}</h1>
|
||||
<p className="dash-page-subtitle font-mono text-sm">{profile?.externalId ?? 'HOSPITALLERS-ALI-IRAQ-IRAN'}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Link
|
||||
to="/central-bank"
|
||||
className="px-4 py-2.5 rounded-md border border-[#474d57] text-[#eaecef] text-sm font-medium hover:border-[#f0b90b] hover:text-[#f0b90b]"
|
||||
>
|
||||
Central Bank
|
||||
</Link>
|
||||
<Link
|
||||
to="/trade"
|
||||
className="px-4 py-2.5 rounded-md bg-[#f0b90b] text-[#181a20] font-semibold text-sm hover:bg-[#fcd535]"
|
||||
>
|
||||
DBIS Trade
|
||||
</Link>
|
||||
</div>
|
||||
<Link
|
||||
to="/trade"
|
||||
className="px-5 py-2.5 rounded bg-[#f0b90b] text-[#181a20] font-semibold text-sm hover:bg-[#fcd535]"
|
||||
>
|
||||
Open DBIS Trade →
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||
<div className="o24-card">
|
||||
<p className="text-xs text-[#848e9c] uppercase">Office ID</p>
|
||||
<p className="text-2xl font-bold text-[#0ecb81]">{profile?.officeId ?? 24}</p>
|
||||
</div>
|
||||
<div className="o24-card">
|
||||
<p className="text-xs text-[#848e9c] uppercase">M1 circulating (GL 2100)</p>
|
||||
<p className="text-2xl font-bold text-[#eaecef]">
|
||||
{formatUsd(moneySupply?.M1?.circulating)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="o24-card">
|
||||
<p className="text-xs text-[#848e9c] uppercase">Due from HO (GL 1410)</p>
|
||||
<p className="text-2xl font-bold text-[#3861fb]">
|
||||
{formatUsd(moneySupply?.interoffice?.dueFromHeadOffice)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="o24-card">
|
||||
<p className="text-xs text-[#848e9c] uppercase">M2 tradable tokens</p>
|
||||
<p className="text-2xl font-bold text-[#f0b90b]">{tokens.length}</p>
|
||||
</div>
|
||||
<QuickPortalLinks />
|
||||
|
||||
<FineractLedgerBanner moneySupply={moneySupply} loading={loading} officeName={profile?.name} />
|
||||
|
||||
<h2 className="dash-section-title mb-1">Office balances</h2>
|
||||
<p className="dash-section-subtitle mb-4">
|
||||
Live Fineract · {profile?.settlement?.defaultCurrency ?? 'USD'}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<DashboardStatCard
|
||||
label="Office ID"
|
||||
value={String(profile?.officeId ?? 24)}
|
||||
hint="Fineract office · HOSPITALLERS Ali"
|
||||
accent="green"
|
||||
/>
|
||||
<DashboardStatCard
|
||||
label="M1 circulating"
|
||||
value={formatUsd(moneySupply?.M1?.circulating)}
|
||||
hint="GL 2100 · spendable M1 liability"
|
||||
accent="green"
|
||||
/>
|
||||
<DashboardStatCard
|
||||
label="Due from head office"
|
||||
value={formatUsd(moneySupply?.interoffice?.dueFromHeadOffice)}
|
||||
hint="GL 1410 · HO allocation receivable"
|
||||
accent="blue"
|
||||
/>
|
||||
<DashboardStatCard
|
||||
label="M2 tokens"
|
||||
value={String(tokens.length)}
|
||||
hint="Registered · swap · convert · transfer"
|
||||
accent="gold"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{moneySupply?.fineractConnected && !moneySupply?.fineractLive && (
|
||||
<p className="text-sm text-[#f0b90b] mb-6">
|
||||
Fineract connected — post opening M1: Dr 1410 / Cr 2100 via{' '}
|
||||
<code className="text-[#f0b90b]">scripts/deployment/seed-office-24-opening-journal.mjs</code>
|
||||
</p>
|
||||
)}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||
<section className="o24-card lg:col-span-1">
|
||||
<h2 className="font-semibold text-[#eaecef] mb-4">Settlement rails</h2>
|
||||
<ul className="space-y-3 text-sm">
|
||||
<li className="flex justify-between">
|
||||
<span className="text-[#848e9c]">SWIFT</span>
|
||||
<span className={profile?.rails?.swift?.enabled ? 'text-[#0ecb81]' : 'text-[#848e9c]'}>
|
||||
{profile?.rails?.swift?.enabled ? 'Enabled' : 'Off'}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex justify-between">
|
||||
<span className="text-[#848e9c]">HYBX</span>
|
||||
<span className={profile?.rails?.hybx?.enabled ? 'text-[#0ecb81]' : 'text-[#848e9c]'}>
|
||||
{profile?.rails?.hybx?.enabled ? 'Enabled' : 'Off'}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex justify-between">
|
||||
<span className="text-[#848e9c]">Chain 138</span>
|
||||
<span className={profile?.rails?.chain138?.enabled ? 'text-[#0ecb81]' : 'text-[#848e9c]'}>
|
||||
{profile?.rails?.chain138?.enabled ? 'Enabled' : 'Off'}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex justify-between">
|
||||
<span className="text-[#848e9c]">Exchange</span>
|
||||
<span className="text-[#0ecb81]">{(health as { status?: string })?.status ?? 'ok'}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-xs text-[#848e9c] mt-4">Tenant: {profile?.tenantUser ?? 'ali_hospitallers_tenant'}</p>
|
||||
</section>
|
||||
|
||||
<section className="o24-card lg:col-span-2">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-semibold text-[#eaecef]">M2 token registry — swap · convert · transfer</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => refresh()}
|
||||
className="text-xs text-[#f0b90b] hover:underline"
|
||||
>
|
||||
{loading ? '…' : 'Refresh'}
|
||||
<DashboardSection
|
||||
title="Settlement rails"
|
||||
subtitle="Enabled payment and chain paths"
|
||||
action={
|
||||
<button type="button" onClick={() => refresh()} className="text-xs text-[#f0b90b] hover:underline">
|
||||
{loading ? 'Refreshing…' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto max-h-[420px] overflow-y-auto">
|
||||
}
|
||||
>
|
||||
<dl>
|
||||
<div className="dash-kv">
|
||||
<dt>SWIFT</dt>
|
||||
<dd className={profile?.rails?.swift?.enabled ? 'dash-kv-value--ok' : undefined}>
|
||||
{profile?.rails?.swift?.enabled ? 'Enabled' : 'Off'}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="dash-kv">
|
||||
<dt>HYBX</dt>
|
||||
<dd className={profile?.rails?.hybx?.enabled ? 'dash-kv-value--ok' : undefined}>
|
||||
{profile?.rails?.hybx?.enabled ? 'Enabled' : 'Off'}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="dash-kv">
|
||||
<dt>Chain 138</dt>
|
||||
<dd className={profile?.rails?.chain138?.enabled ? 'dash-kv-value--ok' : undefined}>
|
||||
{profile?.rails?.chain138?.enabled ? 'Enabled' : 'Off'}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="dash-kv">
|
||||
<dt>DBIS Exchange</dt>
|
||||
<dd className="dash-kv-value--ok">{(health as { status?: string })?.status ?? 'ok'}</dd>
|
||||
</div>
|
||||
<div className="dash-kv">
|
||||
<dt>Fineract tenant</dt>
|
||||
<dd className="text-xs">{profile?.tenantUser ?? 'ali_hospitallers_tenant'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</DashboardSection>
|
||||
|
||||
<DashboardSection
|
||||
title="M2 token registry"
|
||||
subtitle="Tradable lines on Chain 138"
|
||||
className="lg:col-span-2"
|
||||
>
|
||||
<div className="overflow-x-auto max-h-[420px] overflow-y-auto rounded-md border border-[#2b3139]">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 bg-[#1e2329]">
|
||||
<thead className="sticky top-0 bg-[#181a20]">
|
||||
<tr className="text-[#848e9c] text-left text-xs uppercase">
|
||||
<th className="py-2 pr-4">Symbol</th>
|
||||
<th className="py-2 pr-4">Line</th>
|
||||
<th className="py-2 pr-4">Swap</th>
|
||||
<th className="py-2 pr-4">Convert</th>
|
||||
<th className="py-2">Xfer</th>
|
||||
<th className="py-2.5 px-3">Symbol</th>
|
||||
<th className="py-2.5 px-3">Line</th>
|
||||
<th className="py-2.5 px-3">Swap</th>
|
||||
<th className="py-2.5 px-3">Convert</th>
|
||||
<th className="py-2.5 px-3">Transfer</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tokens.map((t) => (
|
||||
<tr key={t.address} className="border-t border-[#2b3139] hover:bg-[#2b3139]/50">
|
||||
<td className="py-2 pr-4 font-medium text-[#eaecef]">{t.symbol}</td>
|
||||
<td className="py-2 pr-4 text-[#848e9c] text-xs">{t.omnlLine ?? 'M2'}</td>
|
||||
<td className="py-2 pr-4">{t.swappable !== false ? '✓' : '—'}</td>
|
||||
<td className="py-2 pr-4">{t.convertible !== false ? '✓' : '—'}</td>
|
||||
<td className="py-2 text-[#0ecb81]">
|
||||
{t.transferableInternal && t.transferableExternal ? 'Int+Ext' : '✓'}
|
||||
<tr key={t.address} className="border-t border-[#2b3139] hover:bg-[#2b3139]/40">
|
||||
<td className="py-2.5 px-3 font-medium text-[#eaecef]">{t.symbol}</td>
|
||||
<td className="py-2.5 px-3 text-[#848e9c] text-xs">{t.omnlLine ?? 'M2'}</td>
|
||||
<td className="py-2.5 px-3 text-[#0ecb81]">{t.swappable !== false ? 'Yes' : '—'}</td>
|
||||
<td className="py-2.5 px-3 text-[#0ecb81]">{t.convertible !== false ? 'Yes' : '—'}</td>
|
||||
<td className="py-2.5 px-3 text-[#0ecb81]">
|
||||
{t.transferableInternal && t.transferableExternal ? 'Internal + external' : 'Yes'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!tokens.length && (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-6 px-3 text-center text-[#848e9c]">
|
||||
No M2 tokens loaded
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</DashboardSection>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<section className={`dash-card ${className}`}>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3 mb-4">
|
||||
<div>
|
||||
<h2 className="dash-section-title">{title}</h2>
|
||||
{subtitle && <p className="dash-section-subtitle">{subtitle}</p>}
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
type Accent = 'gold' | 'green' | 'blue' | 'neutral';
|
||||
|
||||
const ACCENT: Record<Accent, string> = {
|
||||
blue: 'border-l-[#3861fb]',
|
||||
green: 'border-l-[#0ecb81]',
|
||||
gold: 'border-l-[#f0b90b]',
|
||||
neutral: 'border-l-[#848e9c]',
|
||||
};
|
||||
|
||||
export default function DashboardStatCard({
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
accent = 'neutral',
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
hint?: string;
|
||||
accent?: Accent;
|
||||
}) {
|
||||
return (
|
||||
<div className={`dash-card border-l-4 ${ACCENT[accent]}`}>
|
||||
<p className="dash-stat-label">{label}</p>
|
||||
<p className="dash-stat-value">{value}</p>
|
||||
{hint && <p className="dash-stat-hint">{hint}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="dash-banner dash-banner--loading mb-6">
|
||||
<p className="dash-banner__title">Loading live ledger balances…</p>
|
||||
<p className="dash-banner__detail">Fetching Fineract data for Office 24</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!moneySupply) {
|
||||
return (
|
||||
<div className="dash-banner dash-banner--warn mb-6">
|
||||
<p className="dash-banner__title">Ledger data not available</p>
|
||||
<p className="dash-banner__detail">
|
||||
The money-supply API did not respond. Check settlement middleware on port 3011.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const asOf = formatAsOf(moneySupply.asOf);
|
||||
const officeLabel = officeName ?? `Office ${moneySupply.officeId ?? 24}`;
|
||||
|
||||
if (moneySupply.fineractConnected === false) {
|
||||
return (
|
||||
<div className="dash-banner dash-banner--error mb-6">
|
||||
<p className="dash-banner__title">Fineract offline</p>
|
||||
<p className="dash-banner__detail">
|
||||
Cannot reach Fineract for {officeLabel}. Verify OMNL_FINERACT_* credentials on the
|
||||
settlement service.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (moneySupply.fineractConnected && !moneySupply.fineractLive) {
|
||||
return (
|
||||
<div className="dash-banner dash-banner--warn mb-6">
|
||||
<p className="dash-banner__title">Fineract connected — opening journal pending</p>
|
||||
<p className="dash-banner__detail">
|
||||
{officeLabel} has no M1 allocation yet. Post the opening entry: Debit GL 1410 (Due from
|
||||
Head Office) · Credit GL 2100 (M1 liabilities).
|
||||
{asOf ? ` · Checked ${asOf}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dash-banner dash-banner--ok mb-6">
|
||||
<p className="dash-banner__title">Live Fineract ledger · {officeLabel}</p>
|
||||
<p className="dash-banner__detail">
|
||||
M0 / M1 / M2 balances below are read from Fineract in real time.
|
||||
{asOf ? ` Last updated ${asOf}.` : ''}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="dash-quick-links mb-8">
|
||||
<p className="dash-quick-links__label">Public portals</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{LINKS.map(({ label, href }) => (
|
||||
<a
|
||||
key={href}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="dash-quick-links__chip"
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user