diff --git a/config/ecosystem-links.v1.json b/config/ecosystem-links.v1.json
new file mode 100644
index 0000000..63699bf
--- /dev/null
+++ b/config/ecosystem-links.v1.json
@@ -0,0 +1,128 @@
+{
+ "$schema": "OMNL ecosystem link map — canonical URLs for portals, APIs, and local dev",
+ "version": "1.2.0",
+ "updated": "2026-07-03",
+ "notes": [
+ "Public nginx exposes /settlement/* and /exchange/* without /api/v1/... prefix",
+ "Tunnel save file logs/public-url-{operator}.txt stores primary portal URL (Z Online Bank)",
+ "/omnl/* operator UIs ship in frontend dist (public/omnl) — portal-serve proxies /api to explorer",
+ "POST /z-bank/settle-m0-m4 exists in settlement-middleware source; production may use POST /settlement/external-transfer until deployed",
+ "SPA anchors #z-full-settlement and #z-settlement-protocols exist on /central-bank"
+ ],
+ "tunnel": {
+ "ephemeral": true,
+ "saveFile": "logs/public-url-ali.txt",
+ "primaryPath": "/central-bank",
+ "apps": {
+ "bridge": "/bridge",
+ "hub": "/hub",
+ "zOnlineBank": "/central-bank",
+ "zFullSettlement": "/central-bank#z-full-settlement",
+ "wireProtocols": "/central-bank#z-settlement-protocols",
+ "office24": "/office-24",
+ "omnlCompliance": "/omnl/compliance",
+ "omnlDashboard": "/omnl/dashboard",
+ "omnlTerminal": "/omnl/terminal",
+ "trade": "/trade",
+ "swap": "/swap",
+ "reserve": "/reserve",
+ "history": "/history",
+ "wallets": "/wallets",
+ "admin": "/admin",
+ "docs": "/docs"
+ },
+ "nginxOn3002": {
+ "settlementHealth": "/settlement/health",
+ "exchangeHealth": "/exchange/health",
+ "apiCatalog": "/api/v1/omnl/catalog"
+ },
+ "brokenOnServeOnly": []
+ },
+ "subjects": [
+ { "id": "bridge", "label": "Bridge", "path": "/bridge", "group": "app" },
+ { "id": "hub", "label": "DBIS Hub", "path": "/hub", "group": "app" },
+ { "id": "zOnlineBank", "label": "Z Online Bank", "path": "/central-bank", "group": "app" },
+ { "id": "zFullSettlement", "label": "Z M0-M4 settlement", "path": "/central-bank#z-full-settlement", "group": "app" },
+ { "id": "wireProtocols", "label": "Wire protocols", "path": "/central-bank#z-settlement-protocols", "group": "app" },
+ { "id": "office24", "label": "Office 24 Central Bank", "path": "/office-24", "group": "app" },
+ { "id": "trade", "label": "DBIS Trade", "path": "/trade", "group": "app" },
+ { "id": "swap", "label": "Digital Swap", "path": "/swap", "group": "app" },
+ { "id": "reserve", "label": "Reserve", "path": "/reserve", "group": "app" },
+ { "id": "history", "label": "History", "path": "/history", "group": "app" },
+ { "id": "wallets", "label": "Wallets", "path": "/wallets", "group": "app" },
+ { "id": "admin", "label": "Admin", "path": "/admin", "group": "app" },
+ { "id": "docs", "label": "Developers", "path": "/docs", "group": "app" },
+ { "id": "omnlCompliance", "label": "OMNL Compliance", "path": "/omnl/compliance", "group": "console" },
+ { "id": "omnlDashboard", "label": "OMNL API snapshot", "path": "/omnl/dashboard", "group": "console" },
+ { "id": "omnlTerminal", "label": "Settlement terminal", "path": "/omnl/terminal", "group": "console" },
+ { "id": "apiCatalog", "label": "API catalog", "path": "/api/v1/omnl/catalog", "group": "api" },
+ { "id": "gazetteNotices", "label": "GUOSMM gazette registry", "path": "/api/v1/omnl/gazette/notices", "group": "api" },
+ { "id": "integrationStatus", "label": "Integration status", "path": "/api/v1/omnl/integration-status", "group": "api" },
+ { "id": "settlementHealth", "label": "Settlement health", "path": "/settlement/health", "group": "api" },
+ { "id": "exchangeHealth", "label": "Exchange health", "path": "/exchange/health", "group": "api" }
+ ],
+ "local": {
+ "frontend": {
+ "port": 3002,
+ "bridge": "http://localhost:3002/bridge",
+ "centralBank": "http://localhost:3002/central-bank",
+ "hub": "http://localhost:3002/hub",
+ "office24": "http://localhost:3002/office-24",
+ "trade": "http://localhost:3002/trade",
+ "swap": "http://localhost:3002/swap"
+ },
+ "backend": {
+ "omnlCatalog": "http://localhost:3000/api/v1/omnl/catalog",
+ "compliance": "http://localhost:3000/omnl/compliance",
+ "omnlDashboard": "http://localhost:3000/omnl/dashboard",
+ "settlementTerminal": "http://localhost:3000/omnl/terminal",
+ "settlementHealth": "http://localhost:3011/api/v1/settlement/health",
+ "settlementChains": "http://localhost:3011/api/v1/settlement/chains",
+ "moneySupplyPublic": "http://localhost:3011/api/v1/settlement/money-supply/public",
+ "exchangeHealth": "http://localhost:3012/api/v1/exchange/health"
+ },
+ "nginx3002": {
+ "settlementHealth": "http://localhost:3002/settlement/health",
+ "exchangeHealth": "http://localhost:3002/exchange/health",
+ "apiCatalog": "http://localhost:3002/api/v1/omnl/catalog"
+ }
+ },
+ "productionPortals": {
+ "zOnlineBank": "https://online.omdnl.org/central-bank",
+ "centralBankSecure": "https://secure.omdnl.org/central-bank",
+ "office24": "https://office24.omdnl.org/office-24",
+ "exchangeTrade": "https://exchange.omdnl.org/trade",
+ "digitalSwap": "https://digital.omdnl.org/swap",
+ "forexTrade": "https://forex.omdnl.org/trade",
+ "omnlBankHybx": "https://bank.omnl.hybxfinance.io",
+ "exchangeDbisMirror": "https://exchange.d-bis.org/trade",
+ "centralBankDbisMirror": "https://secure.d-bis.org/central-bank",
+ "forexDbisMirror": "https://forex.d-bis.org/trade"
+ },
+ "productionApis": {
+ "explorerRoot": "https://explorer.d-bis.org/api/v1",
+ "catalog": "https://explorer.d-bis.org/api/v1/omnl/catalog",
+ "gazetteNotices": "https://explorer.d-bis.org/api/v1/omnl/gazette/notices",
+ "complianceUi": "https://explorer.d-bis.org/omnl/compliance",
+ "omnlDashboardUi": "https://explorer.d-bis.org/omnl/dashboard",
+ "settlementTerminalUi": "https://explorer.d-bis.org/omnl/terminal",
+ "settlementHealth": "https://secure.omdnl.org/settlement/health",
+ "settlementChains": "https://secure.omdnl.org/settlement/chains",
+ "moneySupplyPublic": "https://secure.omdnl.org/settlement/public/money-supply",
+ "externalTransfer": "https://secure.omdnl.org/settlement/external-transfer",
+ "exchangeHealth": "https://exchange.omdnl.org/exchange/health",
+ "exchangeTokens": "https://exchange.omdnl.org/exchange/tokens",
+ "chain138Rpc": "https://rpc-http-pub.d-bis.org",
+ "chain138Explorer": "https://explorer.d-bis.org",
+ "fineractSwagger": "https://omnl.hybxfinance.io/fineract-provider/swagger-ui/index.html",
+ "swiftListenerPublic": "https://swift-listener.d-bis.org"
+ },
+ "infra": {
+ "gitea": "https://gitea.d-bis.org",
+ "smomRepo": "https://gitea.d-bis.org/d-bis/smom-dbis-138.git",
+ "guosmmGazette": "https://gazette.shosjj.org/notices/2026",
+ "swiftListenerLan": "http://192.168.11.114:8788",
+ "fineractSidecarLan": "http://192.168.11.89:8080",
+ "chain138RpcLan": "http://192.168.11.211:8545"
+ }
+}
diff --git a/config/guosmm-notices.v1.json b/config/guosmm-notices.v1.json
new file mode 100644
index 0000000..0a77eab
--- /dev/null
+++ b/config/guosmm-notices.v1.json
@@ -0,0 +1,115 @@
+{
+ "$schema": "GUOSMM official gazette notices — OMNL / ISB member banks",
+ "version": "1.0.0",
+ "gazetteBase": "https://gazette.shosjj.org/notices/2026",
+ "updated": "2026-07-03",
+ "notices": [
+ {
+ "documentId": "GUOSMM-B-2026-001",
+ "series": "B",
+ "title": "Central Bank Charter — OMNL",
+ "documentType": "Central Bank Charter",
+ "url": "https://gazette.shosjj.org/notices/2026/GUOSMM-B-2026-001",
+ "publishedAt": "2026-06-12T12:00:00Z",
+ "effectiveAt": "2026-06-13T00:00:00Z",
+ "signedAt": "2026-07-02T00:45:01.965Z",
+ "signingAuthority": "Sovereign Military Order of Malta",
+ "contentSha256": "685ebdbfd6a9c1b2c0b7963b7771bf96e96d002e822107207df66f7f593b8032",
+ "republication": "v2",
+ "omnlLei": "98450070C57395F6B906",
+ "scope": ["omnl", "emcb", "central-bank", "office-24", "settlement"],
+ "enablingActs": ["GUOSMM-A-2026-001"],
+ "notes": "EMCB model — Treasury issues SCUDO; OMNL administers; not issuer"
+ },
+ {
+ "documentId": "GUOSMM-C-2026-003",
+ "series": "C",
+ "title": "zBank — ISB Membership Admission and Operations Charter",
+ "documentType": "ISB Membership Admission and Operations Charter",
+ "url": "https://gazette.shosjj.org/notices/2026/GUOSMM-C-2026-003",
+ "publishedAt": "2026-07-01T20:00:00Z",
+ "effectiveAt": "2026-05-02T00:00:00Z",
+ "operationsCommencement": "2026-05-02T00:00:00Z",
+ "signedAt": "2026-07-03T03:24:07.594Z",
+ "signingAuthority": "Sovereign Military Order of Malta",
+ "contentSha256": "c685fd513cbc1e5d20d8f91605f33a8e824680a58b85c67a1a8ef12cf42b71eb",
+ "licenseId": "LICENSE-ZBANK-SHOSJJ-2026-001",
+ "licenseClass": "entity.bank",
+ "entity": "zBank",
+ "fineractOfficeId": 29,
+ "fineractExternalId": "ZBANK-OMNL",
+ "issuingDelegate": "pack-dbis",
+ "prudentialSupervisor": "pack-omnl",
+ "parentActs": [
+ "GUOSMM-A-2026-001",
+ "GUOSMM-A-2026-002",
+ "GUOSMM-A-2026-003",
+ "GUOSMM-B-2026-001",
+ "GUOSMM-B-2026-002",
+ "GUOSMM-C-2026-001",
+ "GUOSMM-C-2026-002"
+ ],
+ "scope": ["z-bank", "onex-bank", "central-bank", "office-29"]
+ },
+ {
+ "documentId": "GUOSMM-C-2026-004",
+ "series": "C",
+ "title": "NovaBank — ISB Membership Admission and Operations Charter",
+ "documentType": "ISB Membership Admission and Operations Charter",
+ "url": "https://gazette.shosjj.org/notices/2026/GUOSMM-C-2026-004",
+ "publishedAt": "2026-07-01T20:00:00Z",
+ "effectiveAt": "2026-05-02T00:00:00Z",
+ "operationsCommencement": "2026-05-02T00:00:00Z",
+ "signedAt": "2026-07-03T03:24:08.276Z",
+ "signingAuthority": "Sovereign Military Order of Malta",
+ "contentSha256": "6c524ad5207ae9ca8d53cd4f847593f175740291ad2f519c554bbb823b304b50",
+ "licenseId": "LICENSE-NOVABANK-SHOSJJ-2026-001",
+ "licenseClass": "entity.bank",
+ "entity": "NovaBank",
+ "fineractOfficeId": 30,
+ "fineractExternalId": "NOVABANK-OMNL",
+ "issuingDelegate": "pack-dbis",
+ "prudentialSupervisor": "pack-omnl",
+ "parentActs": [
+ "GUOSMM-A-2026-001",
+ "GUOSMM-A-2026-002",
+ "GUOSMM-A-2026-003",
+ "GUOSMM-B-2026-001",
+ "GUOSMM-B-2026-002",
+ "GUOSMM-C-2026-001",
+ "GUOSMM-C-2026-002"
+ ],
+ "scope": ["nova-bank", "central-bank", "office-30"]
+ },
+ {
+ "documentId": "GUOSMM-C-2026-005",
+ "series": "C",
+ "title": "AnakaBank — ISB Membership Admission and Operations Charter",
+ "documentType": "ISB Membership Admission and Operations Charter",
+ "url": "https://gazette.shosjj.org/notices/2026/GUOSMM-C-2026-005",
+ "publishedAt": "2026-07-01T20:00:00Z",
+ "effectiveAt": "2026-05-02T00:00:00Z",
+ "operationsCommencement": "2026-05-02T00:00:00Z",
+ "signedAt": "2026-07-03T03:24:08.926Z",
+ "signingAuthority": "Sovereign Military Order of Malta",
+ "contentSha256": "b93879aeb6e1c4791084b1e327682ccbf9b482a2b9b4fbbaff193e6e7a8f5f3e",
+ "licenseId": "LICENSE-ANAKABANK-SHOSJJ-2026-001",
+ "licenseClass": "entity.bank",
+ "entity": "AnakaBank",
+ "fineractOfficeId": 31,
+ "fineractExternalId": "ANAKABANK-OMNL",
+ "issuingDelegate": "pack-dbis",
+ "prudentialSupervisor": "pack-omnl",
+ "parentActs": [
+ "GUOSMM-A-2026-001",
+ "GUOSMM-A-2026-002",
+ "GUOSMM-A-2026-003",
+ "GUOSMM-B-2026-001",
+ "GUOSMM-B-2026-002",
+ "GUOSMM-C-2026-001",
+ "GUOSMM-C-2026-002"
+ ],
+ "scope": ["anaka-bank", "central-bank", "office-31"]
+ }
+ ]
+}
diff --git a/config/hybx-omnl-cross-chain-lines.json b/config/hybx-omnl-cross-chain-lines.json
index 3971e0e..d1d1098 100644
--- a/config/hybx-omnl-cross-chain-lines.json
+++ b/config/hybx-omnl-cross-chain-lines.json
@@ -554,6 +554,42 @@
"inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8",
"inboundSettlementType": "gnosis-safe",
"integrationRef": "config/onex-shiva-integration.v1.json"
+ },
+ {
+ "lineId": "0xa1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef12345678",
+ "officeId": 30,
+ "externalId": "NOVABANK-OMNL",
+ "entityName": "NovaBank",
+ "walletId": "HYBX-OFFICE-030",
+ "guosmmNotice": "GUOSMM-C-2026-004",
+ "treasuryWalletMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8",
+ "treasuryWalletChain138": "0xc93522ae4ab7482572cc29edd68fbb6d5ec6a585",
+ "chains": {
+ "138": {
+ "tokenM0": "0x0000000000000000000000000000000000000000",
+ "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b"
+ }
+ },
+ "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8",
+ "inboundSettlementType": "gnosis-safe"
+ },
+ {
+ "lineId": "0xb2c3d4e5f6789012345678901234567890abcdef1234567890abcdef1234567890",
+ "officeId": 31,
+ "externalId": "ANAKABANK-OMNL",
+ "entityName": "AnakaBank",
+ "walletId": "HYBX-OFFICE-031",
+ "guosmmNotice": "GUOSMM-C-2026-005",
+ "treasuryWalletMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8",
+ "treasuryWalletChain138": "0xc93522ae4ab7482572cc29edd68fbb6d5ec6a585",
+ "chains": {
+ "138": {
+ "tokenM0": "0x0000000000000000000000000000000000000000",
+ "tokenM1": "0xf22258f57794CC8E06237084b353Ab30fFfa640b"
+ }
+ },
+ "inboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8",
+ "inboundSettlementType": "gnosis-safe"
}
],
"globalInboundSettlementMainnet": "0xB08502ceE2545eEF782e933CFb80D0663E564da8"
diff --git a/config/offices/office-24-settlement.v1.json b/config/offices/office-24-settlement.v1.json
index 2ee898d..02ff5e2 100644
--- a/config/offices/office-24-settlement.v1.json
+++ b/config/offices/office-24-settlement.v1.json
@@ -27,5 +27,7 @@
"chain138": { "enabled": true, "rpcEnv": "RPC_URL_138", "defaultLineId": "USD-M2" }
},
"omnlLegalName": "ORGANISATION MONDIALE DU NUMERIQUE L.P.B.C.",
- "omnlLei": "98450070C57395F6B906"
+ "omnlLei": "98450070C57395F6B906",
+ "guosmmEnablingAct": "GUOSMM-B-2026-001",
+ "guosmmNoticeUrl": "https://gazette.shosjj.org/notices/2026/GUOSMM-B-2026-001"
}
diff --git a/config/offices/office-29-zbank.v1.json b/config/offices/office-29-zbank.v1.json
new file mode 100644
index 0000000..31fa8ec
--- /dev/null
+++ b/config/offices/office-29-zbank.v1.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "OMNL Office 29 — zBank (OneX Bank) per GUOSMM-C-2026-003",
+ "version": "1.0.0",
+ "officeId": 29,
+ "externalId": "ZBANK-OMNL",
+ "name": "zBank — ISB member (OneX Bank)",
+ "guosmmNotice": "GUOSMM-C-2026-003",
+ "licenseId": "LICENSE-ZBANK-SHOSJJ-2026-001",
+ "fineractTenant": "omnl",
+ "settlement": {
+ "defaultCurrency": "USD",
+ "moneyLayers": ["M0", "M1", "M2", "M3", "M4"],
+ "glM1": "2100",
+ "glM2": "2200",
+ "glM4NearMoney": "1000"
+ },
+ "rails": {
+ "swift": { "enabled": true },
+ "hybx": { "enabled": true },
+ "chain138": { "enabled": true, "defaultLineId": "USD-M2" }
+ }
+}
diff --git a/config/offices/office-30-novabank.v1.json b/config/offices/office-30-novabank.v1.json
new file mode 100644
index 0000000..b57a130
--- /dev/null
+++ b/config/offices/office-30-novabank.v1.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "OMNL Office 30 — NovaBank per GUOSMM-C-2026-004",
+ "version": "1.0.0",
+ "officeId": 30,
+ "externalId": "NOVABANK-OMNL",
+ "name": "NovaBank — ISB member",
+ "guosmmNotice": "GUOSMM-C-2026-004",
+ "licenseId": "LICENSE-NOVABANK-SHOSJJ-2026-001",
+ "fineractTenant": "omnl",
+ "settlement": {
+ "defaultCurrency": "USD",
+ "moneyLayers": ["M0", "M1", "M2", "M3", "M4"],
+ "glM1": "2100",
+ "glM2": "2200",
+ "glM4NearMoney": "1000"
+ },
+ "rails": {
+ "swift": { "enabled": true },
+ "hybx": { "enabled": true },
+ "chain138": { "enabled": true, "defaultLineId": "USD-M2" }
+ }
+}
diff --git a/config/offices/office-31-anakabank.v1.json b/config/offices/office-31-anakabank.v1.json
new file mode 100644
index 0000000..ba054dc
--- /dev/null
+++ b/config/offices/office-31-anakabank.v1.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "OMNL Office 31 — AnakaBank per GUOSMM-C-2026-005",
+ "version": "1.0.0",
+ "officeId": 31,
+ "externalId": "ANAKABANK-OMNL",
+ "name": "AnakaBank — ISB member",
+ "guosmmNotice": "GUOSMM-C-2026-005",
+ "licenseId": "LICENSE-ANAKABANK-SHOSJJ-2026-001",
+ "fineractTenant": "omnl",
+ "settlement": {
+ "defaultCurrency": "USD",
+ "moneyLayers": ["M0", "M1", "M2", "M3", "M4"],
+ "glM1": "2100",
+ "glM2": "2200",
+ "glM4NearMoney": "1000"
+ },
+ "rails": {
+ "swift": { "enabled": true },
+ "hybx": { "enabled": true },
+ "chain138": { "enabled": true, "defaultLineId": "USD-M2" }
+ }
+}
diff --git a/config/settlement-middleware.production.v1.json b/config/settlement-middleware.production.v1.json
index 9af6ed1..27d66c3 100644
--- a/config/settlement-middleware.production.v1.json
+++ b/config/settlement-middleware.production.v1.json
@@ -47,5 +47,10 @@
"allowHybxProduction": true,
"allowChainMintExecute": true,
"onlineBankMode": true
+ },
+ "guosmm": {
+ "emcbCharter": "GUOSMM-B-2026-001",
+ "registryPath": "config/guosmm-notices.v1.json",
+ "gazetteBase": "https://gazette.shosjj.org/notices/2026"
}
}
diff --git a/frontend-dapp/public/omnl/static/omnl-compliance-console.js b/frontend-dapp/public/omnl/static/omnl-compliance-console.js
new file mode 100644
index 0000000..e4b878c
--- /dev/null
+++ b/frontend-dapp/public/omnl/static/omnl-compliance-console.js
@@ -0,0 +1,255 @@
+(function () {
+ const params = new URLSearchParams(window.location.search);
+ const accessToken = params.get('access_token') || '';
+ const apiBase = window.resolveOmnlApiBase
+ ? window.resolveOmnlApiBase()
+ : document.querySelector('meta[name="omnl-api-base"]')?.getAttribute('content')?.trim() || '/api/v1';
+
+ const els = {
+ status: document.getElementById('load-status'),
+ grid: document.getElementById('posture-grid'),
+ actions: document.getElementById('pending-actions'),
+ evidence: document.getElementById('evidence-kv'),
+ safe: document.getElementById('safe-kv'),
+ external: document.getElementById('external-kv'),
+ triple: document.getElementById('triple-summary'),
+ hoLiquidity: document.getElementById('ho-liquidity-kv'),
+ signoffs: document.getElementById('signoffs-summary'),
+ raw: document.getElementById('raw-json'),
+ refreshed: document.getElementById('refreshed-at'),
+ };
+
+ function apiHeaders() {
+ const h = { Accept: 'application/json' };
+ if (accessToken) h.Authorization = 'Bearer ' + accessToken;
+ return h;
+ }
+
+ function apiUrl(path) {
+ const base = apiBase.replace(/\/$/, '');
+ const rel = path.startsWith('/') ? path : '/' + path;
+ const u = new URL(base + rel, window.location.origin);
+ if (accessToken) u.searchParams.set('access_token', accessToken);
+ return u.toString();
+ }
+
+ function badge(ok, okLabel, badLabel) {
+ if (ok === true) return '' + okLabel + '';
+ if (ok === false) return '' + badLabel + '';
+ return 'Unknown';
+ }
+
+ function sevBadge(sev) {
+ const map = { critical: 'bad', high: 'bad', medium: 'warn', low: 'neutral' };
+ return '' + sev + '';
+ }
+
+ function renderPosture(data) {
+ const p = data.posture || {};
+ els.grid.innerHTML = [
+ card('Reporting', badge(p.reportingCompliant, 'Compliant', 'Not compliant'), ''),
+ card('Attestation', badge(!p.attestationStale, 'Fresh', 'Stale'), ''),
+ card('Triple reconcile', badge(p.tripleAligned, 'Aligned', 'Breaks'), ''),
+ card('Notary gate', badge(p.requireNotarizedEvidence, 'Enforced', 'Not enforced'), 'requireNotarizedEvidence'),
+ card('Threshold', '
' + (p.complianceThreshold || '—') + '
', 'IPSAS / IFRS / US GAAP'),
+ card('Policy v', '' + (p.jurisdictionPolicyVersion ?? '—') + '
', 'ID jurisdiction'),
+ ].join('');
+ }
+
+ function card(title, body, sub) {
+ return (
+ '' +
+ title +
+ '
' +
+ body +
+ (sub ? '' + sub + '' : '') +
+ ''
+ );
+ }
+
+ function renderActions(actions) {
+ if (!actions || !actions.length) {
+ els.actions.innerHTML = 'No pending actions.
';
+ return;
+ }
+ els.actions.innerHTML =
+ '';
+ }
+
+ function kv(rows) {
+ return (
+ '' +
+ rows
+ .map(function (r) {
+ return '- ' + esc(r[0]) + '
- ' + esc(String(r[1] ?? '—')) + '
';
+ })
+ .join('') +
+ '
'
+ );
+ }
+
+ function renderEvidence(data) {
+ const pr = data.proofReport;
+ const gate = data.onChainGate || {};
+ const triple = data.tripleReconcile;
+ els.evidence.innerHTML = kv([
+ ['Evidence hash', pr && pr.evidenceHash],
+ ['Merkle root', pr && pr.merkleRoot],
+ ['Package 3-of-3', pr && pr.packageNotarized3of3],
+ ['Reserve 3-of-3', pr && pr.reserveAttested3of3],
+ ['Reserve store', gate.reserveStore],
+ ['Notary registry', gate.notaryRegistry],
+ ['Attestation threshold', gate.attestationThreshold],
+ ['On-chain R', triple && triple.onChain && triple.onChain.r],
+ ]);
+ }
+
+ function renderSafe(data) {
+ const pr = data.proofReport;
+ const g = (pr && pr.gnosisSafe) || {};
+ const sw = data.safeWallet || {};
+ els.safe.innerHTML = kv([
+ ['Admin Safe', g.address || data.web3.deployed.GnosisSafeAdmin],
+ ['Threshold', g.threshold ? g.threshold + '-of-' + g.owners : '—'],
+ ['Safe Wallet registry', sw.npmRegistryHas138 ? 'chain 138 listed' : 'pending PR #1568'],
+ ['Registry status', sw.status],
+ ]);
+ }
+
+ function renderExternal(data) {
+ const ev = data.externalVisibility || {};
+ const guosmm = ev.guosmmGazette || {};
+ const noticeLinks = (guosmm.notices || [])
+ .map(function (n) {
+ return '' + esc(n.documentId) + '';
+ })
+ .join(' · ');
+ els.external.innerHTML =
+ kv([
+ ['GUOSMM gazette', guosmm.base || '—'],
+ ['API registry', guosmm.apiRegistry || '/api/v1/omnl/gazette/notices'],
+ ['Notices', noticeLinks || '—'],
+ ['DefiLlama chain', ev.defiLlama && ev.defiLlama.chainPage],
+ ['Pricing PR', ev.defiLlama && ev.defiLlama.pr12094],
+ ['Bridge TVL PR', ev.defiLlama && ev.defiLlama.pr19451],
+ ['Safe deployments PR', ev.safeDeployments && ev.safeDeployments.pr1568],
+ ]);
+ }
+
+ function renderHoLiquidity(snap) {
+ if (!els.hoLiquidity) return;
+ if (!snap) {
+ els.hoLiquidity.textContent = 'HO liquidity snapshot unavailable.';
+ return;
+ }
+ const cash = snap.cashInBank || {};
+ const cov = snap.reserveCoverage || {};
+ els.hoLiquidity.innerHTML = kv([
+ ['Generated', snap.generatedAt],
+ ['Nostro cash (GL 13010)', cash.cashInBankUsd != null ? '$' + Number(cash.cashInBankUsd).toLocaleString() : '—'],
+ ['Reserve coverage', cov.coverageRatio != null ? cov.coverageRatio.toFixed(2) + '×' : '—'],
+ ['Reserve assets', cov.reserveAssetsUsd != null ? '$' + Number(cov.reserveAssetsUsd).toLocaleString() : '—'],
+ ['Issued c* USD', cov.issuedCStarUsd != null ? '$' + Number(cov.issuedCStarUsd).toLocaleString() : '—'],
+ ['Public API', apiBase + '/omnl/ho-liquidity-snapshot'],
+ ]);
+ }
+
+ function renderTriple(data) {
+ const t = data.tripleReconcile;
+ if (!t) {
+ els.triple.textContent = 'Triple-state reconcile unavailable.';
+ return;
+ }
+ const breaks = (t.breaks || []).length;
+ els.triple.innerHTML =
+ badge(t.aligned, 'Aligned', breaks + ' break(s)') +
+ ' line ' +
+ esc(t.lineId).slice(0, 18) +
+ '…';
+ }
+
+ function renderSignoffs(data) {
+ const s = data.signoffs || {};
+ els.signoffs.innerHTML = '' + esc(JSON.stringify(s, null, 2)) + '
';
+ }
+
+ function esc(s) {
+ return String(s)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+ }
+
+ function setStatus(kind, msg) {
+ els.status.className = 'status-bar ' + kind;
+ els.status.textContent = msg;
+ }
+
+ async function load() {
+ setStatus('loading', 'Loading compliance console…');
+ try {
+ const res = await fetch(apiUrl('/omnl/compliance/console'), { headers: apiHeaders() });
+ const text = await res.text();
+ let data;
+ try {
+ data = JSON.parse(text);
+ } catch {
+ throw new Error(res.status + ' — non-JSON response');
+ }
+ if (!res.ok) throw new Error(data.error || res.statusText);
+
+ renderPosture(data);
+ renderActions(data.pendingActions);
+ renderEvidence(data);
+ renderSafe(data);
+ renderExternal(data);
+ renderTriple(data);
+ try {
+ const hoRes = await fetch(apiUrl('/omnl/ho-liquidity-snapshot'), { headers: apiHeaders() });
+ if (hoRes.ok) renderHoLiquidity(await hoRes.json());
+ } catch (_) {
+ renderHoLiquidity(null);
+ }
+ renderSignoffs(data);
+ els.raw.textContent = JSON.stringify(data, null, 2);
+ els.refreshed.textContent = 'Updated ' + new Date(data.generatedAt).toLocaleString();
+ setStatus('ok', 'Live — ' + data.pendingActions.length + ' pending action(s)');
+ } catch (e) {
+ setStatus('error', 'Failed to load: ' + (e && e.message ? e.message : e));
+ }
+ }
+
+ document.getElementById('btn-refresh').addEventListener('click', load);
+ document.getElementById('btn-download-safe').addEventListener('click', function () {
+ window.location.href = apiUrl('/omnl/compliance/safe-notary-gate-tx');
+ });
+ document.getElementById('btn-toggle-raw').addEventListener('click', function () {
+ document.getElementById('raw-section').classList.toggle('hidden');
+ });
+
+ load();
+})();
diff --git a/frontend-dapp/src/config/guosmmNotices.ts b/frontend-dapp/src/config/guosmmNotices.ts
new file mode 100644
index 0000000..cddf1e5
--- /dev/null
+++ b/frontend-dapp/src/config/guosmmNotices.ts
@@ -0,0 +1,46 @@
+/** GUOSMM gazette notices — SSOT: config/guosmm-notices.v1.json */
+export type GuosmmNoticeLink = {
+ documentId: string;
+ title: string;
+ url: string;
+ series: string;
+ entity?: string;
+ fineractOfficeId?: number;
+};
+
+export const GUOSMM_GAZETTE_BASE = 'https://gazette.shosjj.org/notices/2026';
+
+export const GUOSMM_NOTICES: GuosmmNoticeLink[] = [
+ {
+ documentId: 'GUOSMM-B-2026-001',
+ series: 'B',
+ title: 'Central Bank Charter — OMNL',
+ url: `${GUOSMM_GAZETTE_BASE}/GUOSMM-B-2026-001`,
+ },
+ {
+ documentId: 'GUOSMM-C-2026-003',
+ series: 'C',
+ title: 'zBank — ISB Operations Charter',
+ url: `${GUOSMM_GAZETTE_BASE}/GUOSMM-C-2026-003`,
+ entity: 'zBank',
+ fineractOfficeId: 29,
+ },
+ {
+ documentId: 'GUOSMM-C-2026-004',
+ series: 'C',
+ title: 'NovaBank — ISB Operations Charter',
+ url: `${GUOSMM_GAZETTE_BASE}/GUOSMM-C-2026-004`,
+ entity: 'NovaBank',
+ fineractOfficeId: 30,
+ },
+ {
+ documentId: 'GUOSMM-C-2026-005',
+ series: 'C',
+ title: 'AnakaBank — ISB Operations Charter',
+ url: `${GUOSMM_GAZETTE_BASE}/GUOSMM-C-2026-005`,
+ entity: 'AnakaBank',
+ fineractOfficeId: 31,
+ },
+];
+
+export const GUOSMM_API_CATALOG = '/api/v1/omnl/gazette/notices';
diff --git a/frontend-dapp/src/config/omnlInternalPaths.ts b/frontend-dapp/src/config/omnlInternalPaths.ts
index 2302c1b..617b663 100644
--- a/frontend-dapp/src/config/omnlInternalPaths.ts
+++ b/frontend-dapp/src/config/omnlInternalPaths.ts
@@ -86,6 +86,8 @@ export const INTERNAL_PATH_GROUPS: InternalPathGroup[] = [
label: 'OMNL API',
entries: [
{ method: 'GET', path: '/api/v1/omnl/catalog', auth: 'none' },
+ { method: 'GET', path: '/api/v1/omnl/gazette/notices', auth: 'none', notes: 'GUOSMM official gazette registry' },
+ { method: 'GET', path: '/api/v1/omnl/gazette/notices/:documentId', auth: 'none' },
{ method: 'GET', path: '/api/v1/omnl/openapi.json', auth: 'none' },
{ method: 'GET', path: '/api/v1/omnl/health', auth: 'omnl-key-in-production' },
{ method: 'GET', path: '/api/v1/omnl/ipsas/fineract-health', auth: 'omnl-key-in-production' },
diff --git a/frontend-dapp/src/features/online-bank/GuosmmNoticesPanel.tsx b/frontend-dapp/src/features/online-bank/GuosmmNoticesPanel.tsx
new file mode 100644
index 0000000..b8b4a31
--- /dev/null
+++ b/frontend-dapp/src/features/online-bank/GuosmmNoticesPanel.tsx
@@ -0,0 +1,32 @@
+import { GUOSMM_NOTICES } from '../../config/guosmmNotices';
+
+export default function GuosmmNoticesPanel() {
+ return (
+
+
+ Official gazette (GUOSMM)
+
+
+ Sovereign Military Order of Malta · SHOSJJ official notices governing OMNL EMCB and ISB member banks.
+
+
+
+ );
+}
diff --git a/frontend-dapp/src/features/online-bank/OnlineBankApp.tsx b/frontend-dapp/src/features/online-bank/OnlineBankApp.tsx
new file mode 100644
index 0000000..a46a969
--- /dev/null
+++ b/frontend-dapp/src/features/online-bank/OnlineBankApp.tsx
@@ -0,0 +1,242 @@
+import { Link } from 'react-router-dom';
+import NasaIcon from '../../components/icons/NasaIcon';
+import { formatUsd } from '../../utils/formatMoney';
+import CentralBankDashboard from '../central-bank/CentralBankDashboard';
+import SettlementProtocolsPanel from './SettlementProtocolsPanel';
+import GuosmmNoticesPanel from './GuosmmNoticesPanel';
+import ZBankFullSettlementPanel from './ZBankFullSettlementPanel';
+import { useOnlineBank } from './useOnlineBank';
+
+const ACCOUNTS = [
+ { key: 'main', label: 'USD account', meta: 'Main · M1 eMoney', flag: 'USD', balanceKey: 'primaryBalance' as const, active: true },
+ { key: 'savings', label: 'Savings', meta: 'M2 tokenized', flag: 'M2', balanceKey: 'savingsBalance' as const },
+ { key: 'reserve', label: 'Reserve', meta: 'M0 base money', flag: 'M0', balanceKey: 'reserveBalance' as const },
+ { key: 'fiat', label: 'Fiat rails', meta: 'M4 SWIFT settlement', flag: 'M4', balanceKey: 'm4Balance' as const },
+ { key: 'chain', label: 'On-chain', meta: 'M3 digital', flag: 'M3', balanceKey: 'm3Balance' as const },
+];
+
+function BalanceSkeleton() {
+ return (
+
+ );
+}
+
+export default function OnlineBankApp() {
+ const bank = useOnlineBank();
+
+ return (
+
+
+
+
+
+ Z
+
+
+
Z Online Bank
+
+ Hello · Good {timeOfDay()}, {bank.displayName}
+
+
+
+
+
+ {bank.connected ? 'Live' : 'Offline'}
+
+
+
+ {bank.loading && !bank.primaryBalance ? (
+
+ ) : (
+
+ Total balance
+ {formatUsd(bank.primaryBalance)}
+
+ {bank.officeLabel}
+ {bank.live && Synced with Fineract}
+
+
+ )}
+
+
+
+
+ Your accounts
+
+ {ACCOUNTS.map((acct) => (
+
+
{acct.flag}
+
+
{acct.label}
+
{acct.meta}
+
+
{formatUsd(bank[acct.balanceKey])}
+
+ ))}
+
+
+
+
+
+
bank.refresh()}
+ balances={{
+ m0: bank.reserveBalance,
+ m1: bank.primaryBalance,
+ m2: bank.savingsBalance,
+ m3: bank.m3Balance,
+ m4: bank.m4Balance,
+ }}
+ />
+
+ bank.refresh()} />
+
+ {bank.error && (
+
+ {bank.error}. Showing cached layout — retrying every 30s.
+
+ )}
+
+
+
+
Transactions
+
+
+
+ {bank.activities.length === 0 ? (
+
+
+
+
+
+
No transactions yet
+
Swaps and settlements appear here
+
+
+ ) : (
+ bank.activities.map((item) => (
+
+
+
+
+
+
{item.title}
+
{item.meta}
+
+
+ {item.amount}
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+
+
+ Treasury & operator console
+
+
+
+
+
+
+
+
+ );
+}
+
+function timeOfDay(): string {
+ const h = new Date().getHours();
+ if (h < 12) return 'morning';
+ if (h < 17) return 'afternoon';
+ return 'evening';
+}
diff --git a/frontend-dapp/src/features/online-bank/SettlementProtocolsPanel.tsx b/frontend-dapp/src/features/online-bank/SettlementProtocolsPanel.tsx
new file mode 100644
index 0000000..edc54b8
--- /dev/null
+++ b/frontend-dapp/src/features/online-bank/SettlementProtocolsPanel.tsx
@@ -0,0 +1,366 @@
+import { useEffect, useState } from 'react';
+import { useAccount } from 'wagmi';
+import { useZBankSettlement, type SettlementPhase } from './useZBankSettlement';
+import { formatUsd } from '../../utils/formatMoney';
+
+const PHASE_LABEL: Record = {
+ RECEIVED: 'Received',
+ VALIDATED: 'Validated',
+ VERBIAGE_ROLLED: 'Verbiage rolled',
+ FINERACT_POSTED: 'Fineract posted',
+ HYBX_RAIL_DISPATCHED: 'HYBX dispatched',
+ ISO20022_ARCHIVED: 'ISO 20022 archived',
+ CHAIN_MINT_REQUESTED: 'Chain mint',
+ SETTLED: 'Settled',
+ FAILED: 'Failed',
+};
+
+const DEFAULT_IBAN = 'IQ98ZARD00000000000000000001';
+const BILLION = '1000000000';
+
+type ProtocolGroup = 'classic' | 'digital' | 'trade' | 'corridor';
+
+type ProtocolId =
+ | 'mt103'
+ | 'mt102'
+ | 'mt103-lc'
+ | 'sblc'
+ | 'bg'
+ | 'nova-zbank'
+ | 'onex-zbank'
+ | 'onex-shiva-crypto'
+ | 'o24-zbank'
+ | 'internal'
+ | 'external'
+ | 'token-load'
+ | 'fiat-crypto'
+ | 'full-external';
+
+const GROUPS: { id: ProtocolGroup; label: string; desc: string }[] = [
+ { id: 'classic', label: 'Classic bank', desc: 'SWIFT wires · MT102/MT103 · correspondent banking' },
+ { id: 'trade', label: 'Trade finance', desc: 'SBLC · BG · Documentary LC (MT103_LC)' },
+ { id: 'digital', label: 'Digital bank', desc: 'M2/M3 · token load · fiat→crypto · on-chain' },
+ { id: 'corridor', label: 'Corridor import', desc: 'Nova · OneX Bank · Office 24 · Shiva blockchain' },
+];
+
+const PROTOCOLS: Record = {
+ classic: [
+ { id: 'mt103', label: 'MT103 wire' },
+ { id: 'mt102', label: 'MT102 bulk' },
+ ],
+ trade: [
+ { id: 'mt103-lc', label: 'MT103_LC (DLC)' },
+ { id: 'sblc', label: 'SBLC' },
+ { id: 'bg', label: 'Bank guarantee' },
+ ],
+ digital: [
+ { id: 'internal', label: 'Internal M2/M3' },
+ { id: 'token-load', label: 'M2 token load' },
+ { id: 'fiat-crypto', label: 'Fiat → crypto (138)' },
+ { id: 'onex-shiva-crypto', label: 'Fiat → Shiva (OneX)' },
+ { id: 'external', label: 'M4 SWIFT out' },
+ ],
+ corridor: [
+ { id: 'nova-zbank', label: 'Nova Bank → Z' },
+ { id: 'onex-zbank', label: 'OneX Bank → Z' },
+ { id: 'o24-zbank', label: 'O24 CB → Z' },
+ { id: 'full-external', label: 'Full M0–M4' },
+ ],
+};
+
+export default function SettlementProtocolsPanel({ onSuccess }: { onSuccess?: () => void }) {
+ const { address, isConnected } = useAccount();
+ const s = useZBankSettlement();
+ const [group, setGroup] = useState('classic');
+ const [active, setActive] = useState('mt103');
+ const [amount, setAmount] = useState(BILLION);
+ const [iban, setIban] = useState(DEFAULT_IBAN);
+ const [recipient, setRecipient] = useState('');
+ const [tfReference, setTfReference] = useState('');
+ const [tokenSymbol, setTokenSymbol] = useState('cUSDC');
+ const [tokenAddress, setTokenAddress] = useState('0x71D6687F38b93CCad569Fa6352c876eea967201b');
+ const [lineId, setLineId] = useState('USD-M2');
+
+ useEffect(() => {
+ if (address) setRecipient(address);
+ s.refreshSettlements();
+ }, [address]);
+
+ useEffect(() => {
+ const first = PROTOCOLS[group][0]?.id;
+ if (first) setActive(first);
+ }, [group]);
+
+ async function submit() {
+ try {
+ switch (active) {
+ case 'mt103':
+ await s.swiftMessageTransfer({
+ amount: String(amount),
+ creditorIban: iban,
+ swiftMessageKind: 'MT103',
+ });
+ break;
+ case 'mt102':
+ await s.swiftMessageTransfer({
+ amount: String(amount),
+ creditorIban: iban,
+ swiftMessageKind: 'MT102',
+ });
+ break;
+ case 'mt103-lc':
+ await s.mt103LcTransfer(String(amount), iban, tfReference || undefined);
+ break;
+ case 'sblc':
+ await s.tradeFinanceTransfer({
+ instrument: 'SBLC',
+ amount: String(amount),
+ creditorIban: iban,
+ reference: tfReference || undefined,
+ });
+ break;
+ case 'bg':
+ await s.tradeFinanceTransfer({
+ instrument: 'BG',
+ amount: String(amount),
+ creditorIban: iban,
+ reference: tfReference || undefined,
+ });
+ break;
+ case 'nova-zbank':
+ await s.transferNovaBankToZBank(String(amount), iban);
+ break;
+ case 'onex-zbank':
+ await s.transferOneXBankToZBank(String(amount), iban);
+ break;
+ case 'o24-zbank':
+ await s.transferOffice24ToZBank(String(amount), iban);
+ break;
+ case 'internal':
+ if (!recipient) throw new Error('Connect wallet for recipient');
+ await s.internalTransfer({
+ tokenAddress,
+ tokenSymbol,
+ amount: String(amount),
+ recipientAddress: recipient,
+ senderAddress: address,
+ });
+ break;
+ case 'external':
+ if (!recipient) throw new Error('Recipient address required');
+ await s.externalTransfer({
+ tokenAddress,
+ tokenSymbol,
+ amount: String(amount),
+ recipientAddress: recipient,
+ creditorIban: iban,
+ beneficiaryName: 'Z Online Bank',
+ });
+ break;
+ case 'token-load':
+ if (!recipient) throw new Error('Connect wallet for mint recipient');
+ await s.tokenLoad({
+ lineId,
+ symbol: tokenSymbol,
+ tokenAddress,
+ amount: String(amount),
+ recipientAddress: recipient,
+ });
+ break;
+ case 'fiat-crypto':
+ if (!recipient) throw new Error('Wallet recipient required');
+ await s.fiatToCrypto({
+ amount: String(amount),
+ creditorIban: iban,
+ recipientAddress: recipient,
+ tokenLineId: lineId,
+ });
+ break;
+ case 'onex-shiva-crypto':
+ if (!recipient) throw new Error('OneX wallet recipient required');
+ await s.fiatToCryptoShiva({
+ amount: String(amount),
+ creditorIban: iban,
+ recipientAddress: recipient,
+ tokenLineId: lineId,
+ });
+ break;
+ case 'full-external':
+ await s.fullExternalTransfer({
+ amount: String(amount),
+ creditorIban: iban,
+ beneficiaryName: 'Z Online Bank',
+ });
+ break;
+ }
+ await s.refreshSettlements();
+ onSuccess?.();
+ } catch {
+ /* error in hook */
+ }
+ }
+
+ const needsWallet = ['internal', 'external', 'token-load', 'fiat-crypto', 'onex-shiva-crypto'].includes(active);
+ const needsTfRef = ['mt103-lc', 'sblc', 'bg'].includes(active);
+ const simpleWire = ['mt103', 'mt102', 'mt103-lc', 'sblc', 'bg', 'nova-zbank', 'onex-zbank', 'o24-zbank', 'full-external'].includes(
+ active,
+ );
+
+ return (
+
+ );
+}
diff --git a/frontend-dapp/src/features/online-bank/ZBankFullSettlementPanel.tsx b/frontend-dapp/src/features/online-bank/ZBankFullSettlementPanel.tsx
new file mode 100644
index 0000000..deb27d1
--- /dev/null
+++ b/frontend-dapp/src/features/online-bank/ZBankFullSettlementPanel.tsx
@@ -0,0 +1,204 @@
+import { useEffect, useState } from 'react';
+import { useAccount } from 'wagmi';
+import { useZBankSettlement, type SettlementPhase } from './useZBankSettlement';
+import { formatUsd } from '../../utils/formatMoney';
+
+const PHASE_LABEL: Record = {
+ RECEIVED: 'Received',
+ VALIDATED: 'Validated',
+ VERBIAGE_ROLLED: 'Verbiage rolled',
+ FINERACT_POSTED: 'Fineract posted',
+ HYBX_RAIL_DISPATCHED: 'HYBX dispatched',
+ ISO20022_ARCHIVED: 'ISO 20022 archived',
+ CHAIN_MINT_REQUESTED: 'Chain mint',
+ SETTLED: 'Settled',
+ FAILED: 'Failed',
+};
+
+const DEFAULT_IBAN = 'IQ98ZARD00000000000000000001';
+const LAYERS = [
+ { id: 'M0', label: 'M0 Reserve', gl: '1050', desc: 'Base money · IPSAS reserve' },
+ { id: 'M1', label: 'M1 eMoney', gl: '2100', desc: 'Circulating liabilities' },
+ { id: 'M2', label: 'M2 Broad', gl: '2200', desc: 'Meta-fiat broad money' },
+ { id: 'M3', label: 'M3 On-chain', gl: '2300', desc: 'Token liabilities · Chain 138' },
+ { id: 'M4', label: 'M4 Fiat rails', gl: '1000', desc: 'SWIFT · HYBX · settlement suspense' },
+] as const;
+
+type Props = {
+ onSuccess?: () => void;
+ balances?: {
+ m0?: string;
+ m1?: string;
+ m2?: string;
+ m3?: string;
+ m4?: string;
+ };
+};
+
+export default function ZBankFullSettlementPanel({ onSuccess, balances }: Props) {
+ const { address, isConnected } = useAccount();
+ const s = useZBankSettlement();
+ const [amount, setAmount] = useState('1000000000');
+ const [iban, setIban] = useState(DEFAULT_IBAN);
+ const [recipient, setRecipient] = useState('');
+ const [includeM3, setIncludeM3] = useState(true);
+
+ useEffect(() => {
+ if (address) setRecipient(address);
+ s.refreshSettlements();
+ }, [address]);
+
+ async function submit() {
+ try {
+ await s.zBankSettleM0ToM4({
+ amount: String(amount),
+ creditorIban: iban,
+ recipientAddress: includeM3 ? recipient : undefined,
+ });
+ await s.refreshSettlements();
+ onSuccess?.();
+ } catch {
+ /* hook sets error */
+ }
+ }
+
+ const layerSteps = s.lastResult?.layerSteps ?? [];
+ const activeLayerIdx = layerSteps.filter((st) => st.status === 'posted').length;
+
+ return (
+
+ Full settlement · M0 → M4
+
+ Posts sequential Fineract journals through every OMNL money layer, then dispatches HYBX + SWIFT MT103 to your Z
+ Online Bank IBAN.
+
+
+
+ {LAYERS.map((layer, i) => {
+ const posted = layerSteps.find((st) => st.to === layer.id)?.status === 'posted';
+ const active = s.loading && i === activeLayerIdx;
+ const done = posted || (s.lastResult?.phase === 'SETTLED' && i <= LAYERS.length - 1);
+ return (
+
+ {i > 0 &&
}
+
+ {layer.id}
+ {layer.label}
+ GL {layer.gl}
+ {balances && (
+
+ {formatUsd(
+ layer.id === 'M0'
+ ? balances.m0
+ : layer.id === 'M1'
+ ? balances.m1
+ : layer.id === 'M2'
+ ? balances.m2
+ : layer.id === 'M3'
+ ? balances.m3
+ : balances.m4,
+ )}
+
+ )}
+ {layer.desc}
+
+
+ );
+ })}
+
+
+
+
+
+
+ {includeM3 && (
+
+ )}
+
+
+ {layerSteps.length > 0 && (
+
+
Layer journals
+
+ {layerSteps.map((step) => (
+ -
+
+ {step.from}→{step.to}
+
+
+ Dr {step.debitGl} / Cr {step.creditGl}
+
+
+ {step.status}
+ {step.journalRef ? ` · JE ${step.journalRef}` : ''}
+
+
+ ))}
+
+
+ )}
+
+ {s.error && (
+
+ {s.error}
+
+ )}
+
+ {s.lastResult?.settlementId && (
+
+
+ Settlement: {s.lastResult.settlementId}
+
+
+ Phase: {s.lastResult.phase ? PHASE_LABEL[s.lastResult.phase] : '—'}
+
+ {s.lastResult.fineractJournalRef && (
+
+ Last journal: {s.lastResult.fineractJournalRef}
+
+ )}
+ {s.lastResult.hybxPaymentId && (
+
+ HYBX: {s.lastResult.hybxPaymentId}
+
+ )}
+ {s.lastResult.chainTxHash && (
+
+ Chain mint: {s.lastResult.chainTxHash}
+
+ )}
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend-dapp/src/features/online-bank/useOnlineBank.ts b/frontend-dapp/src/features/online-bank/useOnlineBank.ts
new file mode 100644
index 0000000..8504240
--- /dev/null
+++ b/frontend-dapp/src/features/online-bank/useOnlineBank.ts
@@ -0,0 +1,77 @@
+import { useMemo } from 'react';
+import { useCentralBank } from '../central-bank/useCentralBank';
+import { formatUsd } from '../../utils/formatMoney';
+
+export type BankActivity = {
+ id: string;
+ title: string;
+ meta: string;
+ amount: string;
+ positive?: boolean;
+ kind: 'swap' | 'transfer' | 'system';
+};
+
+export function useOnlineBank() {
+ const cb = useCentralBank();
+
+ const office = cb.office as {
+ officeId?: number;
+ name?: string;
+ externalId?: string;
+ } | null;
+
+ const healthOk = (cb.health as { status?: string } | null)?.status === 'ok';
+ const live = Boolean(cb.moneySupply?.fineractLive);
+ const connected = Boolean(cb.moneySupply?.fineractConnected) || healthOk;
+
+ const primaryBalance = cb.moneySupply?.M1?.circulating ?? cb.moneySupply?.M2?.broadMoney;
+ const savingsBalance = cb.moneySupply?.M2?.broadMoney;
+ const reserveBalance = cb.moneySupply?.M0?.gl1050;
+ const m4Balance = cb.moneySupply?.M4?.nearMoney;
+ const m3Balance = cb.moneySupply?.M3?.tokenLiabilities;
+
+ const activities = useMemo(() => {
+ const items: BankActivity[] = [];
+ const swaps = (cb.monitor as { swaps?: { status: string; tokenIn: string; tokenOut?: string; amountIn?: string; createdAt: string }[] } | null)?.swaps ?? [];
+
+ for (const [i, s] of swaps.slice(0, 12).entries()) {
+ items.push({
+ id: `swap-${i}`,
+ title: `Exchange · ${s.tokenIn?.slice(0, 8) ?? 'token'}`,
+ meta: `${s.status} · ${s.createdAt?.slice(0, 16) ?? 'recent'}`,
+ amount: s.amountIn ? formatUsd(s.amountIn) : '—',
+ positive: s.status?.toLowerCase() === 'completed',
+ kind: 'swap',
+ });
+ }
+
+ if (connected && items.length === 0) {
+ items.push({
+ id: 'system-live',
+ title: 'Z Online Bank connected',
+ meta: live ? 'Fineract ledger live' : 'Settlement middleware online',
+ amount: formatUsd(primaryBalance),
+ positive: true,
+ kind: 'system',
+ });
+ }
+
+ return items;
+ }, [cb.monitor, connected, live, primaryBalance]);
+
+ return {
+ ...cb,
+ office,
+ healthOk,
+ live,
+ connected,
+ primaryBalance,
+ savingsBalance,
+ reserveBalance,
+ m4Balance,
+ m3Balance,
+ activities,
+ displayName: office?.name?.split('—')[0]?.trim() ?? 'DBIS Account',
+ officeLabel: office?.externalId ?? `Office ${office?.officeId ?? 24}`,
+ };
+}
diff --git a/frontend-dapp/src/features/online-bank/useZBankSettlement.ts b/frontend-dapp/src/features/online-bank/useZBankSettlement.ts
new file mode 100644
index 0000000..1aabeaa
--- /dev/null
+++ b/frontend-dapp/src/features/online-bank/useZBankSettlement.ts
@@ -0,0 +1,455 @@
+import { useCallback, useState } from 'react';
+import { exchangeApi, omnlApiHeaders, settlementApi } from '../../config/dex';
+import { onexShiva, SHIVA_ONEX_CHAIN_ID } from '../../config/onexShiva';
+
+export type SettlementPhase =
+ | 'RECEIVED'
+ | 'VALIDATED'
+ | 'VERBIAGE_ROLLED'
+ | 'FINERACT_POSTED'
+ | 'HYBX_RAIL_DISPATCHED'
+ | 'ISO20022_ARCHIVED'
+ | 'CHAIN_MINT_REQUESTED'
+ | 'SETTLED'
+ | 'FAILED';
+
+export type SettlementResult = {
+ settlementId?: string;
+ phase?: SettlementPhase;
+ errors?: string[];
+ fineractJournalRef?: string;
+ hybxPaymentId?: string;
+ chainTxHash?: string;
+ error?: string;
+ layerSteps?: {
+ from: string;
+ to: string;
+ debitGl: string;
+ creditGl: string;
+ journalRef?: string;
+ status: 'pending' | 'posted' | 'skipped' | 'failed';
+ }[];
+};
+
+async function postJson(url: string, body: unknown): Promise<{ ok: boolean; status: number; data: T }> {
+ const res = await fetch(url, {
+ method: 'POST',
+ headers: omnlApiHeaders(),
+ body: JSON.stringify(body),
+ });
+ const data = (await res.json().catch(() => ({ error: res.statusText || `HTTP ${res.status}` }))) as T;
+ return { ok: res.ok, status: res.status, data };
+}
+
+async function getJson(url: string): Promise {
+ try {
+ const res = await fetch(url, { headers: omnlApiHeaders(false) });
+ return res.ok ? ((await res.json()) as T) : null;
+ } catch {
+ return null;
+ }
+}
+
+function idem(prefix: string) {
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
+}
+
+/** Nova Bank Online → Z Online Bank: inbound wire import to M4 fiat */
+export async function novaBankToZBankM4Fiat(params: {
+ amount: string;
+ creditorIban: string;
+ beneficiaryName?: string;
+ remittanceInfo?: string;
+}) {
+ const base = {
+ idempotencyKey: idem('NOVA-Z-M4'),
+ officeId: 24,
+ currency: 'USD',
+ amount: params.amount,
+ creditorIban: params.creditorIban,
+ beneficiaryName: params.beneficiaryName ?? 'Z Online Bank',
+ orderingName: 'Nova Bank Online',
+ creditorBic: 'NOVAGB2LXXX',
+ debtorIban: 'GB29NOVA60161331926819',
+ remittanceInfo: params.remittanceInfo ?? 'Nova Bank Online → Z Online Bank · M4 fiat import',
+ moneyLayers: ['M1', 'M2', 'M4'] as const,
+ rail: 'SWIFT' as const,
+ swiftMessageKind: 'MT103' as const,
+ };
+
+ return postJson(settlementApi('/external-transfer'), base);
+}
+
+/** OneX Bank (Shiva / zBank office 29) → Z Online Bank: inbound wire to M4 */
+export async function onexBankToZBankM4Fiat(params: {
+ amount: string;
+ creditorIban: string;
+ beneficiaryName?: string;
+ remittanceInfo?: string;
+}) {
+ const corridor = onexShiva.bankCorridor;
+ const base = {
+ idempotencyKey: idem('ONEX-Z-M4'),
+ officeId: onexShiva.settlement.zBridgeOfficeId,
+ currency: 'USD',
+ amount: params.amount,
+ creditorIban: params.creditorIban,
+ beneficiaryName: params.beneficiaryName ?? 'Z Online Bank',
+ orderingName: corridor.orderingName,
+ creditorBic: corridor.creditorBic,
+ debtorIban: corridor.debtorIban,
+ remittanceInfo: params.remittanceInfo ?? corridor.remittanceTemplate,
+ moneyLayers: ['M1', 'M2', 'M4'] as const,
+ rail: 'HYBX' as const,
+ swiftMessageKind: corridor.swiftMessageKind as 'MT103',
+ metadata: {
+ sourceOfficeId: onexShiva.fineract.officeId,
+ sourceHybxWallet: onexShiva.fineract.hybxWalletId,
+ targetChainId: SHIVA_ONEX_CHAIN_ID,
+ integration: 'onex-shiva',
+ },
+ };
+
+ return postJson(settlementApi('/external-transfer'), base);
+}
+
+/** Office 24 → Z Online Bank: credit M1 then settle to M4 fiat rails */
+export async function office24ToZBankM4Fiat(params: {
+ amount: string;
+ creditorIban: string;
+ beneficiaryName?: string;
+ remittanceInfo?: string;
+}) {
+ const base = {
+ idempotencyKey: idem('Z-O24-M4'),
+ officeId: 24,
+ currency: 'USD',
+ amount: params.amount,
+ creditorIban: params.creditorIban,
+ beneficiaryName: params.beneficiaryName ?? 'Z Online Bank',
+ orderingName: 'Office 24 Central Bank — HOSPITALLERS',
+ remittanceInfo: params.remittanceInfo ?? 'Office 24 → Z Online Bank · M4 fiat settlement',
+ moneyLayers: ['M1', 'M2', 'M4'] as const,
+ rail: 'SWIFT' as const,
+ swiftMessageKind: 'MT103' as const,
+ };
+
+ return postJson(settlementApi('/external-transfer'), base);
+}
+
+export function useZBankSettlement() {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [lastResult, setLastResult] = useState(null);
+ const [settlements, setSettlements] = useState([]);
+
+ const run = useCallback(async (label: string, fn: () => Promise<{ ok: boolean; status: number; data: SettlementResult }>) => {
+ setLoading(true);
+ setError(null);
+ try {
+ const { ok, data, status } = await fn();
+ setLastResult(data);
+ if (!ok || data.phase === 'FAILED') {
+ const errBody = data as SettlementResult & { message?: string };
+ throw new Error(
+ errBody.error || errBody.errors?.join(', ') || errBody.message || `${label} failed (HTTP ${status})`,
+ );
+ }
+ return data;
+ } catch (e) {
+ const msg = e instanceof Error ? e.message : `${label} failed`;
+ setError(msg);
+ throw e;
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const transferNovaBankToZBank = useCallback(
+ (amount: string, creditorIban: string) =>
+ run('Nova Bank Online → Z Online Bank M4 import', async () => {
+ const r = await novaBankToZBankM4Fiat({ amount, creditorIban });
+ return { ok: r.ok, status: r.status, data: r.data };
+ }),
+ [run],
+ );
+
+ const transferOffice24ToZBank = useCallback(
+ (amount: string, creditorIban: string) =>
+ run('Office 24 → Z Online Bank M4 transfer', async () => {
+ const r = await office24ToZBankM4Fiat({ amount, creditorIban });
+ return { ok: r.ok, status: r.status, data: r.data };
+ }),
+ [run],
+ );
+
+ const transferOneXBankToZBank = useCallback(
+ (amount: string, creditorIban: string) =>
+ run('OneX Bank → Z Online Bank M4 import (Shiva)', async () => {
+ const r = await onexBankToZBankM4Fiat({ amount, creditorIban });
+ return { ok: r.ok, status: r.status, data: r.data };
+ }),
+ [run],
+ );
+
+ const internalTransfer = useCallback(
+ (body: {
+ tokenAddress: string;
+ tokenSymbol: string;
+ amount: string;
+ recipientAddress: string;
+ senderAddress?: string;
+ }) =>
+ run('Internal transfer', async () => {
+ const r = await postJson(exchangeApi('/transfer/internal'), {
+ idempotencyKey: idem('Z-INT'),
+ ...body,
+ moneyLayers: ['M2', 'M3'],
+ });
+ return { ok: r.ok, status: r.status, data: r.data };
+ }),
+ [run],
+ );
+
+ const externalTransfer = useCallback(
+ (body: {
+ tokenAddress: string;
+ tokenSymbol: string;
+ amount: string;
+ recipientAddress: string;
+ creditorIban: string;
+ beneficiaryName?: string;
+ }) =>
+ run('External M4 transfer', async () => {
+ const r = await postJson(exchangeApi('/transfer/external'), {
+ idempotencyKey: idem('Z-EXT'),
+ ...body,
+ moneyLayers: ['M2', 'M4'],
+ rail: 'SWIFT',
+ remittanceInfo: 'Z Online Bank M4 fiat outbound',
+ });
+ return { ok: r.ok, status: r.status, data: r.data };
+ }),
+ [run],
+ );
+
+ const tokenLoad = useCallback(
+ (body: {
+ lineId: string;
+ symbol: string;
+ tokenAddress: string;
+ amount: string;
+ recipientAddress: string;
+ }) =>
+ run('M2 token load', async () => {
+ const r = await postJson(exchangeApi('/token-load'), {
+ idempotencyKey: idem('Z-LOAD'),
+ currency: 'USD',
+ ...body,
+ });
+ return { ok: r.ok, status: r.status, data: r.data };
+ }),
+ [run],
+ );
+
+ const fiatToCrypto = useCallback(
+ (body: {
+ amount: string;
+ creditorIban: string;
+ recipientAddress: string;
+ tokenLineId?: string;
+ targetChainId?: number;
+ }) =>
+ run('Fiat to crypto', async () => {
+ const chainId = body.targetChainId ?? 138;
+ const r = await postJson(settlementApi('/convert/fiat-to-crypto'), {
+ idempotencyKey: idem('Z-F2C'),
+ officeId: 24,
+ currency: 'USD',
+ amount: body.amount,
+ creditorIban: body.creditorIban,
+ beneficiaryName: 'Z Online Bank',
+ recipientAddress: body.recipientAddress,
+ convertToCrypto: {
+ enabled: true,
+ targetChainId: chainId,
+ tokenLineId: body.tokenLineId ?? 'USD-M2',
+ recipientAddress: body.recipientAddress,
+ },
+ moneyLayers: ['M2', 'M3'],
+ });
+ return { ok: r.ok, status: r.status, data: r.data };
+ }),
+ [run],
+ );
+
+ const fiatToCryptoShiva = useCallback(
+ (body: { amount: string; creditorIban: string; recipientAddress: string; tokenLineId?: string }) =>
+ fiatToCrypto({ ...body, targetChainId: SHIVA_ONEX_CHAIN_ID }),
+ [fiatToCrypto],
+ );
+
+ const zBankSettleM0ToM4 = useCallback(
+ (body: { amount: string; creditorIban: string; recipientAddress?: string; tokenLineId?: string }) =>
+ run('Z Online Bank M0→M4 settlement', async () => {
+ const payload = {
+ idempotencyKey: idem('Z-M0M4'),
+ officeId: 24,
+ currency: 'USD',
+ amount: body.amount,
+ creditorIban: body.creditorIban,
+ beneficiaryName: 'Z Online Bank',
+ remittanceInfo: 'Z Online Bank full-stack settlement M0→M1→M2→M3→M4',
+ recipientAddress: body.recipientAddress,
+ tokenLineId: body.tokenLineId ?? 'USD-M2',
+ };
+ let r = await postJson(settlementApi('/z-bank/settle-m0-m4'), payload);
+ if (r.status === 404 || r.status === 405) {
+ r = await postJson(settlementApi('/external-transfer'), {
+ ...payload,
+ moneyLayers: ['M0', 'M1', 'M2', 'M3', 'M4'],
+ rail: 'SWIFT',
+ swiftMessageKind: 'MT103',
+ convertToCrypto: body.recipientAddress
+ ? {
+ enabled: true,
+ targetChainId: 138,
+ tokenLineId: body.tokenLineId ?? 'USD-M2',
+ recipientAddress: body.recipientAddress,
+ }
+ : undefined,
+ });
+ }
+ return { ok: r.ok, status: r.status, data: r.data };
+ }),
+ [run],
+ );
+
+ const tradeFinanceTransfer = useCallback(
+ (body: {
+ instrument: 'SBLC' | 'DLC' | 'BG' | 'LS';
+ amount: string;
+ creditorIban: string;
+ reference?: string;
+ expiryDate?: string;
+ swiftMessageKind?: 'MT102' | 'MT103';
+ }) =>
+ run(`${body.instrument} trade finance`, async () => {
+ const r = await postJson(settlementApi(`/trade-finance/${body.instrument}`), {
+ idempotencyKey: idem(`Z-TF-${body.instrument}`),
+ officeId: 24,
+ currency: 'USD',
+ amount: body.amount,
+ creditorIban: body.creditorIban,
+ beneficiaryName: 'Z Online Bank',
+ orderingName: 'Z Online Bank — OMNL Office 24 Central Bank',
+ remittanceInfo: `Z Online Bank ${body.instrument} · ${body.swiftMessageKind ?? 'MT103'}`,
+ moneyLayers: ['M0', 'M1', 'M2', 'M3', 'M4'],
+ rail: 'SWIFT',
+ swiftMessageKind: body.swiftMessageKind ?? 'MT103',
+ tradeFinance: {
+ instrument: body.instrument,
+ reference: body.reference ?? idem('TF-REF'),
+ expiryDate: body.expiryDate,
+ },
+ });
+ return { ok: r.ok, status: r.status, data: r.data };
+ }),
+ [run],
+ );
+
+ const swiftMessageTransfer = useCallback(
+ (body: {
+ amount: string;
+ creditorIban: string;
+ swiftMessageKind: 'MT102' | 'MT103';
+ moneyLayers?: string[];
+ remittanceInfo?: string;
+ }) =>
+ run(`SWIFT ${body.swiftMessageKind}`, async () => {
+ const layers =
+ body.moneyLayers ??
+ (body.swiftMessageKind === 'MT102' ? ['M1', 'M2', 'M3', 'M4'] : ['M0', 'M1', 'M2', 'M4']);
+ const r = await postJson(settlementApi('/external-transfer'), {
+ idempotencyKey: idem(`Z-${body.swiftMessageKind}`),
+ officeId: 24,
+ currency: 'USD',
+ amount: body.amount,
+ creditorIban: body.creditorIban,
+ beneficiaryName: 'Z Online Bank',
+ orderingName: 'Z Online Bank — OMNL Office 24 Central Bank',
+ remittanceInfo: body.remittanceInfo ?? `Z Online Bank SWIFT ${body.swiftMessageKind}`,
+ moneyLayers: layers,
+ rail: 'SWIFT',
+ swiftMessageKind: body.swiftMessageKind,
+ });
+ return { ok: r.ok, status: r.status, data: r.data };
+ }),
+ [run],
+ );
+
+ const mt103LcTransfer = useCallback(
+ (amount: string, creditorIban: string, reference?: string) =>
+ tradeFinanceTransfer({
+ instrument: 'DLC',
+ amount,
+ creditorIban,
+ reference,
+ swiftMessageKind: 'MT103',
+ }),
+ [tradeFinanceTransfer],
+ );
+
+ const fullExternalTransfer = useCallback(
+ (body: {
+ amount: string;
+ creditorIban: string;
+ beneficiaryName?: string;
+ moneyLayers?: string[];
+ }) =>
+ run('Full external transfer', async () => {
+ const r = await postJson(settlementApi('/external-transfer'), {
+ idempotencyKey: idem('Z-EXT-FULL'),
+ officeId: 24,
+ currency: 'USD',
+ amount: body.amount,
+ creditorIban: body.creditorIban,
+ beneficiaryName: body.beneficiaryName ?? 'Beneficiary',
+ moneyLayers: body.moneyLayers ?? ['M0', 'M1', 'M2', 'M3', 'M4'],
+ rail: 'SWIFT',
+ });
+ return { ok: r.ok, status: r.status, data: r.data };
+ }),
+ [run],
+ );
+
+ const refreshSettlements = useCallback(async () => {
+ const list = await getJson<{ settlements?: SettlementResult[]; items?: SettlementResult[] }>(
+ `${settlementApi('/settlements')}?limit=20`,
+ );
+ const rows = list?.settlements ?? list?.items;
+ if (rows) setSettlements(rows);
+ }, []);
+
+ return {
+ loading,
+ error,
+ lastResult,
+ settlements,
+ transferNovaBankToZBank,
+ transferOffice24ToZBank,
+ transferOneXBankToZBank,
+ internalTransfer,
+ externalTransfer,
+ tokenLoad,
+ fiatToCrypto,
+ fiatToCryptoShiva,
+ swiftMessageTransfer,
+ tradeFinanceTransfer,
+ mt103LcTransfer,
+ zBankSettleM0ToM4,
+ fullExternalTransfer,
+ refreshSettlements,
+ clearError: () => setError(null),
+ };
+}
diff --git a/frontend-dapp/src/pages/HubPage.tsx b/frontend-dapp/src/pages/HubPage.tsx
index d0d694e..4261a09 100644
--- a/frontend-dapp/src/pages/HubPage.tsx
+++ b/frontend-dapp/src/pages/HubPage.tsx
@@ -1,5 +1,6 @@
import { Link } from 'react-router-dom';
import NasaIcon, { type NasaIconName } from '../components/icons/NasaIcon';
+import { GUOSMM_NOTICES } from '../config/guosmmNotices';
const BANKING: { path: string; label: string; icon: NasaIconName }[] = [
{ path: '/central-bank', label: 'Z Online Bank / Central Bank', icon: 'central-bank' },
@@ -71,6 +72,24 @@ export default function HubPage() {
+
+ GUOSMM official gazette
+
+
);
}
diff --git a/frontend-dapp/src/styles/online-bank.css b/frontend-dapp/src/styles/online-bank.css
new file mode 100644
index 0000000..8aa3ce8
--- /dev/null
+++ b/frontend-dapp/src/styles/online-bank.css
@@ -0,0 +1,966 @@
+/**
+ * Z Online Bank — Wise-inspired online banking UI
+ */
+
+.revolut-bank {
+ --rb-bg: #f2f2f2;
+ --rb-surface: #ffffff;
+ --rb-card: #ffffff;
+ --rb-text: #163300;
+ --rb-text-body: #454745;
+ --rb-muted: #6a6a6a;
+ --rb-subject: #454745;
+ --rb-accent: #9fe870;
+ --rb-accent-hover: #8fd960;
+ --rb-accent-text: #163300;
+ --rb-accent-deep: #0e2f0a;
+ --rb-accent-soft: #e8f8e0;
+ --rb-border: #e2e2e2;
+ --rb-border-strong: #d3d3d3;
+ --rb-shadow: 0 1px 3px rgba(22, 51, 0, 0.06);
+ --rb-shadow-md: 0 4px 12px rgba(22, 51, 0, 0.08);
+ --rb-radius: 16px;
+ --rb-radius-sm: 12px;
+ --rb-radius-pill: 9999px;
+ min-height: calc(100dvh - 3.5rem);
+ background: var(--rb-bg);
+ color: var(--rb-text-body);
+ font-family: Inter, 'Segoe UI', system-ui, -apple-system, sans-serif;
+ -webkit-font-smoothing: antialiased;
+}
+
+.omnl-banking-theme .omnl-app-header {
+ background: #ffffff !important;
+ border-bottom: 1px solid var(--rb-border) !important;
+ box-shadow: none !important;
+}
+
+.omnl-banking-theme .omnl-app-header a,
+.omnl-banking-theme .omnl-app-header .font-semibold,
+.omnl-banking-theme .omnl-app-header button {
+ color: var(--rb-text) !important;
+}
+
+.omnl-banking-theme .omnl-app-header nav a {
+ color: var(--rb-muted) !important;
+}
+
+.omnl-banking-theme .omnl-app-header nav a.bg-green-600 {
+ background: var(--rb-accent) !important;
+ color: var(--rb-accent-text) !important;
+}
+
+.omnl-banking-theme .omnl-app-header .bg-green-600 {
+ background: var(--rb-accent-deep) !important;
+}
+
+.omnl-banking-theme .omnl-app-header .text-white {
+ color: var(--rb-text) !important;
+}
+
+.omnl-banking-theme .omnl-app-header .hover\:bg-white\/10:hover {
+ background: var(--rb-bg) !important;
+}
+
+.revolut-bank__inner {
+ max-width: 42rem;
+ margin: 0 auto;
+ padding: 1.25rem 1rem 6rem;
+}
+
+@media (min-width: 768px) {
+ .revolut-bank__inner {
+ max-width: 52rem;
+ padding: 2rem 1.5rem 3rem;
+ }
+}
+
+/* Page header */
+.revolut-bank__page-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-bottom: 1.75rem;
+}
+
+.revolut-bank__logo-row {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ min-width: 0;
+}
+
+.revolut-bank__logo-mark {
+ width: 2.5rem;
+ height: 2.5rem;
+ border-radius: 50%;
+ background: var(--rb-accent);
+ color: var(--rb-accent-text);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 800;
+ font-size: 1rem;
+ flex-shrink: 0;
+}
+
+.revolut-bank__brand-block {
+ min-width: 0;
+}
+
+.revolut-bank__brand-title {
+ font-size: 1.125rem;
+ font-weight: 700;
+ color: var(--rb-text);
+ letter-spacing: -0.02em;
+ margin: 0;
+ line-height: 1.2;
+}
+
+.revolut-bank__greeting-label {
+ font-size: 0.8125rem;
+ color: var(--rb-muted);
+ margin: 0;
+}
+
+.revolut-bank__greeting-name {
+ font-size: 0.8125rem;
+ font-weight: 600;
+ color: var(--rb-text);
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.revolut-bank__status {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ font-size: 0.6875rem;
+ font-weight: 600;
+ padding: 0.35rem 0.65rem;
+ border-radius: var(--rb-radius-pill);
+ background: var(--rb-accent-soft);
+ color: var(--rb-accent-deep);
+ border: 1px solid rgba(159, 232, 112, 0.5);
+ flex-shrink: 0;
+}
+
+.revolut-bank__status--offline {
+ background: #f5f5f5;
+ color: var(--rb-muted);
+ border-color: var(--rb-border);
+}
+
+/* Balance hero */
+.revolut-bank__balance-card {
+ background: transparent;
+ padding: 0 0 1.5rem;
+ margin-bottom: 0.5rem;
+ border: none;
+ box-shadow: none;
+ position: relative;
+}
+
+.revolut-bank__balance-card::before {
+ display: none;
+}
+
+.revolut-bank__balance-label {
+ font-size: 0.875rem;
+ color: var(--rb-muted);
+ margin-bottom: 0.5rem;
+ font-weight: 400;
+}
+
+.revolut-bank__balance-amount {
+ font-size: 2.75rem;
+ font-weight: 700;
+ letter-spacing: -0.04em;
+ line-height: 1.05;
+ color: var(--rb-text);
+}
+
+@media (min-width: 768px) {
+ .revolut-bank__balance-amount {
+ font-size: 3.25rem;
+ }
+}
+
+.revolut-bank__balance-sub {
+ margin-top: 0.65rem;
+ font-size: 0.8125rem;
+ color: var(--rb-muted);
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ border: none;
+ padding: 0;
+}
+
+.revolut-bank__balance-sub span:last-child {
+ color: var(--rb-accent-deep);
+ font-weight: 600;
+}
+
+/* Primary CTAs — Wise Send / Add money */
+.revolut-bank__cta-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0.75rem;
+ margin-bottom: 2rem;
+}
+
+.revolut-bank__cta {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ padding: 0.875rem 1rem;
+ border-radius: var(--rb-radius-pill);
+ font-size: 0.9375rem;
+ font-weight: 600;
+ text-decoration: none;
+ cursor: pointer;
+ border: none;
+ transition: background 0.15s, transform 0.1s;
+ min-height: 48px;
+}
+
+.revolut-bank__cta--primary {
+ background: var(--rb-accent);
+ color: var(--rb-accent-text);
+}
+
+.revolut-bank__cta--primary:hover {
+ background: var(--rb-accent-hover);
+}
+
+.revolut-bank__cta--secondary {
+ background: var(--rb-surface);
+ color: var(--rb-text);
+ border: 1px solid var(--rb-border-strong);
+}
+
+.revolut-bank__cta--secondary:hover {
+ background: #fafafa;
+}
+
+/* Account list — Wise-style rows */
+.revolut-bank__accounts-section {
+ margin-bottom: 2rem;
+}
+
+.revolut-bank__accounts-heading {
+ font-size: 1.125rem;
+ font-weight: 700;
+ color: var(--rb-text);
+ margin: 0 0 0.75rem;
+ letter-spacing: -0.02em;
+}
+
+.revolut-bank__accounts {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ overflow: visible;
+ padding: 0;
+ margin: 0;
+}
+
+.revolut-bank__account-pill {
+ display: flex;
+ align-items: center;
+ gap: 0.875rem;
+ min-width: 0;
+ padding: 1rem 1.125rem;
+ border-radius: var(--rb-radius-sm);
+ background: var(--rb-surface);
+ border: 1px solid var(--rb-border);
+ box-shadow: var(--rb-shadow);
+ cursor: default;
+}
+
+.revolut-bank__account-pill--active {
+ border-color: var(--rb-accent);
+ background: var(--rb-surface);
+ box-shadow: var(--rb-shadow-md);
+}
+
+.revolut-bank__account-flag {
+ width: 2.5rem;
+ height: 2.5rem;
+ border-radius: 50%;
+ background: var(--rb-bg);
+ border: 1px solid var(--rb-border);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.6875rem;
+ font-weight: 800;
+ color: var(--rb-text);
+ flex-shrink: 0;
+ letter-spacing: -0.02em;
+}
+
+.revolut-bank__account-pill--active .revolut-bank__account-flag {
+ background: var(--rb-accent-soft);
+ border-color: rgba(159, 232, 112, 0.6);
+}
+
+.revolut-bank__account-pill-body {
+ flex: 1;
+ min-width: 0;
+}
+
+.revolut-bank__account-pill-label {
+ font-size: 0.9375rem;
+ font-weight: 600;
+ color: var(--rb-text);
+ margin-bottom: 0.1rem;
+ text-transform: none;
+ letter-spacing: 0;
+}
+
+.revolut-bank__account-pill-meta {
+ font-size: 0.75rem;
+ color: var(--rb-muted);
+}
+
+.revolut-bank__account-pill-value {
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--rb-text);
+ flex-shrink: 0;
+ text-align: right;
+}
+
+/* Quick links row */
+.revolut-bank__actions {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 0.5rem;
+ margin-bottom: 2rem;
+ padding: 0;
+ background: transparent;
+ border: none;
+ box-shadow: none;
+}
+
+.revolut-bank__action {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.4rem;
+ padding: 0.5rem 0.25rem;
+ text-decoration: none;
+ color: var(--rb-subject);
+ border: none;
+ background: none;
+ cursor: pointer;
+ font-size: 0.6875rem;
+ font-weight: 600;
+}
+
+.revolut-bank__action-icon {
+ width: 3rem;
+ height: 3rem;
+ border-radius: 50%;
+ background: var(--rb-surface);
+ border: 1px solid var(--rb-border);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--rb-text);
+ transition: background 0.15s, border-color 0.15s;
+ box-shadow: var(--rb-shadow);
+}
+
+.revolut-bank__action:hover .revolut-bank__action-icon {
+ background: var(--rb-accent-soft);
+ border-color: rgba(159, 232, 112, 0.5);
+ transform: none;
+}
+
+/* Sections */
+.revolut-bank__section-title {
+ font-size: 1.125rem;
+ font-weight: 700;
+ margin-bottom: 0.75rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ color: var(--rb-text);
+ letter-spacing: -0.02em;
+}
+
+.revolut-bank__section-title h2,
+.revolut-bank__section-title h3 {
+ color: inherit;
+ font-size: inherit;
+ font-weight: inherit;
+ margin: 0;
+}
+
+.revolut-bank__section-link {
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--rb-accent-deep);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+
+/* Activity */
+.revolut-bank__activity {
+ background: var(--rb-surface);
+ border-radius: var(--rb-radius-sm);
+ border: 1px solid var(--rb-border);
+ box-shadow: var(--rb-shadow);
+ overflow: hidden;
+ margin-bottom: 2rem;
+}
+
+.revolut-bank__error {
+ color: #9a6700;
+ background: #fffbeb;
+ border-color: #fcd34d;
+}
+
+.revolut-bank__activity-item {
+ display: flex;
+ align-items: center;
+ gap: 0.875rem;
+ padding: 1rem 1.125rem;
+ border-bottom: 1px solid var(--rb-border);
+}
+
+.revolut-bank__activity-item:last-child {
+ border-bottom: none;
+}
+
+.revolut-bank__activity-icon {
+ width: 2.5rem;
+ height: 2.5rem;
+ border-radius: 50%;
+ background: var(--rb-bg);
+ border: 1px solid var(--rb-border);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ color: var(--rb-text);
+}
+
+.revolut-bank__activity-icon--swap {
+ background: #eef6ff;
+ border-color: #cce0ff;
+ color: #2563eb;
+}
+
+.revolut-bank__activity-body {
+ flex: 1;
+ min-width: 0;
+}
+
+.revolut-bank__activity-title {
+ font-size: 0.9375rem;
+ font-weight: 600;
+ color: var(--rb-text);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.revolut-bank__activity-meta {
+ font-size: 0.8125rem;
+ color: var(--rb-muted);
+ margin-top: 0.15rem;
+}
+
+.revolut-bank__activity-amount {
+ font-size: 0.9375rem;
+ font-weight: 700;
+ flex-shrink: 0;
+ color: var(--rb-text);
+}
+
+.revolut-bank__activity-amount--positive {
+ color: #0d7a4f;
+}
+
+/* Services grid */
+.revolut-bank__services {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 0.65rem;
+ margin-bottom: 2rem;
+}
+
+@media (min-width: 640px) {
+ .revolut-bank__services {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+
+.revolut-bank__service-card {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ padding: 1rem 1.125rem;
+ border-radius: var(--rb-radius-sm);
+ background: var(--rb-surface);
+ border: 1px solid var(--rb-border);
+ box-shadow: var(--rb-shadow);
+ text-decoration: none;
+ color: var(--rb-text);
+ transition: border-color 0.15s, box-shadow 0.15s;
+ min-height: 5rem;
+}
+
+.revolut-bank__service-card:hover {
+ border-color: var(--rb-border-strong);
+ box-shadow: var(--rb-shadow-md);
+}
+
+.revolut-bank__service-label {
+ font-size: 0.875rem;
+ font-weight: 700;
+ color: var(--rb-text);
+}
+
+.revolut-bank__service-desc {
+ font-size: 0.75rem;
+ color: var(--rb-muted);
+ line-height: 1.4;
+}
+
+/* Treasury accordion */
+.revolut-bank__treasury {
+ margin-top: 0.5rem;
+}
+
+.revolut-bank__treasury summary {
+ list-style: none;
+ cursor: pointer;
+ padding: 1rem 1.125rem;
+ border-radius: var(--rb-radius-sm);
+ background: var(--rb-surface);
+ border: 1px solid var(--rb-border);
+ font-size: 0.9375rem;
+ font-weight: 600;
+ color: var(--rb-text);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-shadow: var(--rb-shadow);
+}
+
+.revolut-bank__treasury summary::-webkit-details-marker {
+ display: none;
+}
+
+.revolut-bank__chevron {
+ color: var(--rb-muted);
+}
+
+.revolut-bank__treasury-body {
+ margin-top: 0.75rem;
+ border-radius: var(--rb-radius-sm);
+ overflow: hidden;
+}
+
+.revolut-bank__skeleton {
+ background: linear-gradient(90deg, #ececec 25%, #e0e0e0 50%, #ececec 75%);
+ background-size: 200% 100%;
+ animation: rb-shimmer 1.2s infinite;
+ border-radius: 8px;
+}
+
+@keyframes rb-shimmer {
+ 0% { background-position: 200% 0; }
+ 100% { background-position: -200% 0; }
+}
+
+/* Settlement panel */
+.revolut-bank__protocols {
+ margin-bottom: 2rem;
+ padding: 1.25rem;
+ border-radius: var(--rb-radius-sm);
+ background: var(--rb-surface);
+ border: 1px solid var(--rb-border);
+ box-shadow: var(--rb-shadow);
+}
+
+.revolut-bank__protocols-hint {
+ font-size: 0.8125rem;
+ color: var(--rb-muted);
+ margin-bottom: 0.75rem;
+ line-height: 1.5;
+}
+
+.revolut-bank__protocol-tabs {
+ display: flex;
+ gap: 0.5rem;
+ overflow-x: auto;
+ margin-bottom: 1rem;
+ scrollbar-width: none;
+ padding-bottom: 0.25rem;
+}
+
+.revolut-bank__protocol-tabs::-webkit-scrollbar {
+ display: none;
+}
+
+.revolut-bank__protocol-tab {
+ flex: 0 0 auto;
+ padding: 0.5rem 0.875rem;
+ border-radius: var(--rb-radius-pill);
+ border: 1px solid var(--rb-border);
+ background: var(--rb-surface);
+ color: var(--rb-subject);
+ font-size: 0.75rem;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.revolut-bank__protocol-tab--active {
+ background: var(--rb-accent);
+ border-color: var(--rb-accent);
+ color: var(--rb-accent-text);
+}
+
+.revolut-bank__protocol-form {
+ display: grid;
+ gap: 0.875rem;
+ margin-bottom: 1rem;
+}
+
+.revolut-bank__field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ font-size: 0.8125rem;
+ color: var(--rb-text);
+ font-weight: 600;
+}
+
+.revolut-bank__input {
+ width: 100%;
+ padding: 0.75rem 0.875rem;
+ border-radius: 10px;
+ border: 1px solid var(--rb-border-strong);
+ background: #fff;
+ color: var(--rb-text);
+ font-size: 1rem;
+ font-weight: 400;
+}
+
+.revolut-bank__input:focus {
+ outline: none;
+ border-color: var(--rb-accent-deep);
+ box-shadow: 0 0 0 3px rgba(159, 232, 112, 0.35);
+}
+
+.revolut-bank__protocol-error {
+ padding: 0.875rem;
+ border-radius: 10px;
+ background: #fef2f2;
+ border: 1px solid #fecaca;
+ color: #b91c1c;
+ font-size: 0.8125rem;
+ margin-bottom: 0.875rem;
+}
+
+.revolut-bank__protocol-result {
+ padding: 0.875rem;
+ border-radius: 10px;
+ background: var(--rb-accent-soft);
+ border: 1px solid rgba(159, 232, 112, 0.45);
+ font-size: 0.8125rem;
+ color: var(--rb-subject);
+ margin-bottom: 0.875rem;
+}
+
+.revolut-bank__protocol-result strong {
+ color: var(--rb-accent-deep);
+}
+
+.revolut-bank__submit {
+ width: 100%;
+ padding: 0.9375rem 1rem;
+ border-radius: var(--rb-radius-pill);
+ border: none;
+ background: var(--rb-accent);
+ color: var(--rb-accent-text);
+ font-weight: 700;
+ font-size: 1rem;
+ cursor: pointer;
+ min-height: 48px;
+ transition: background 0.15s;
+}
+
+.revolut-bank__submit:hover:not(:disabled) {
+ background: var(--rb-accent-hover);
+}
+
+.revolut-bank__submit:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.revolut-bank__settlements-list {
+ margin-top: 1.25rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--rb-border);
+}
+
+.revolut-bank__settlements-list ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.revolut-bank__settlements-list li {
+ display: flex;
+ justify-content: space-between;
+ gap: 0.5rem;
+ padding: 0.65rem 0;
+ border-bottom: 1px solid var(--rb-border);
+ font-size: 0.8125rem;
+ color: var(--rb-muted);
+}
+
+.revolut-bank__panel {
+ background: var(--rb-surface);
+ border: 1px solid var(--rb-border);
+ border-radius: var(--rb-radius-sm);
+ box-shadow: var(--rb-shadow);
+ padding: 1.25rem;
+ margin-bottom: 2rem;
+}
+
+/* M0→M4 full settlement pipeline */
+.zbank-settle {
+ margin-bottom: 2rem;
+ padding: 1.25rem;
+ border-radius: var(--rb-radius-sm);
+ background: var(--rb-surface);
+ border: 1px solid var(--rb-border);
+ box-shadow: var(--rb-shadow);
+}
+
+.zbank-settle__pipeline {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ margin: 1.25rem 0 1.5rem;
+ overflow-x: auto;
+ padding-bottom: 0.25rem;
+}
+
+@media (min-width: 640px) {
+ .zbank-settle__pipeline {
+ flex-direction: row;
+ align-items: stretch;
+ }
+}
+
+.zbank-settle__layer-wrap {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-width: 7.5rem;
+}
+
+@media (min-width: 640px) {
+ .zbank-settle__layer-wrap {
+ flex-direction: row;
+ align-items: stretch;
+ }
+}
+
+.zbank-settle__connector {
+ width: 2px;
+ height: 1rem;
+ background: var(--rb-border-strong);
+ margin: 0 auto;
+}
+
+@media (min-width: 640px) {
+ .zbank-settle__connector {
+ width: 1rem;
+ height: 2px;
+ margin: auto 0;
+ flex-shrink: 0;
+ }
+}
+
+.zbank-settle__connector--done {
+ background: var(--rb-accent);
+}
+
+.zbank-settle__layer {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 0.2rem;
+ padding: 0.85rem 0.75rem;
+ border-radius: var(--rb-radius-sm);
+ border: 1px solid var(--rb-border);
+ background: var(--rb-bg);
+ min-height: 6.5rem;
+}
+
+.zbank-settle__layer--active {
+ border-color: var(--rb-accent);
+ box-shadow: 0 0 0 2px rgba(159, 232, 112, 0.35);
+}
+
+.zbank-settle__layer--done {
+ border-color: rgba(159, 232, 112, 0.6);
+ background: var(--rb-accent-soft);
+}
+
+.zbank-settle__layer-id {
+ font-size: 0.6875rem;
+ font-weight: 800;
+ letter-spacing: 0.06em;
+ color: var(--rb-accent-deep);
+}
+
+.zbank-settle__layer-name {
+ font-size: 0.8125rem;
+ font-weight: 700;
+ color: var(--rb-text);
+}
+
+.zbank-settle__layer-gl {
+ font-size: 0.6875rem;
+ color: var(--rb-muted);
+ font-family: ui-monospace, monospace;
+}
+
+.zbank-settle__layer-bal {
+ font-size: 0.75rem;
+ font-weight: 700;
+ color: var(--rb-text);
+ margin-top: 0.15rem;
+}
+
+.zbank-settle__layer-desc {
+ font-size: 0.625rem;
+ color: var(--rb-muted);
+ line-height: 1.35;
+ margin-top: auto;
+}
+
+.zbank-settle__checkbox {
+ flex-direction: row !important;
+ align-items: center;
+ gap: 0.5rem !important;
+ font-weight: 500 !important;
+}
+
+.zbank-settle__checkbox input {
+ width: 1rem;
+ height: 1rem;
+ accent-color: var(--rb-accent-deep);
+}
+
+.zbank-settle__steps {
+ margin-bottom: 1rem;
+ padding: 0.875rem;
+ border-radius: 10px;
+ background: var(--rb-bg);
+ border: 1px solid var(--rb-border);
+}
+
+.zbank-settle__steps-title {
+ font-size: 0.8125rem;
+ font-weight: 700;
+ color: var(--rb-text);
+ margin: 0 0 0.5rem;
+}
+
+.zbank-settle__steps ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.zbank-settle__steps li {
+ display: grid;
+ grid-template-columns: 3.5rem 1fr auto;
+ gap: 0.5rem;
+ padding: 0.4rem 0;
+ border-bottom: 1px solid var(--rb-border);
+ font-size: 0.75rem;
+ color: var(--rb-muted);
+}
+
+.zbank-settle__steps li:last-child {
+ border-bottom: none;
+}
+
+.zbank-settle__step-status--posted {
+ color: #0d7a4f;
+ font-weight: 600;
+}
+
+.zbank-settle__step-status--failed {
+ color: #b91c1c;
+ font-weight: 600;
+}
+
+.zbank-settle__protocol-groups {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+@media (min-width: 640px) {
+ .zbank-settle__protocol-groups {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+.zbank-settle__protocol-group {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.2rem;
+ padding: 0.75rem 0.875rem;
+ border-radius: var(--rb-radius-sm);
+ border: 1px solid var(--rb-border);
+ background: var(--rb-surface);
+ cursor: pointer;
+ text-align: left;
+}
+
+.zbank-settle__protocol-group--active {
+ border-color: var(--rb-accent);
+ background: var(--rb-accent-soft);
+}
+
+.zbank-settle__protocol-group-label {
+ font-size: 0.8125rem;
+ font-weight: 700;
+ color: var(--rb-text);
+}
+
+.zbank-settle__protocol-group-desc {
+ font-size: 0.6875rem;
+ color: var(--rb-muted);
+ line-height: 1.35;
+}
+
+/* Legacy header elements — hidden when using Wise layout */
+.revolut-bank__header-bar,
+.revolut-bank__brand,
+.revolut-bank__topbar,
+.revolut-bank__avatar {
+ display: none;
+}
diff --git a/scripts/deployment/deploy-omnl-bank-production.sh b/scripts/deployment/deploy-omnl-bank-production.sh
index f1a8c56..a314918 100644
--- a/scripts/deployment/deploy-omnl-bank-production.sh
+++ b/scripts/deployment/deploy-omnl-bank-production.sh
@@ -189,6 +189,7 @@ if [[ -n "${DATABASE_URL:-}" ]] && [[ "${SEED_SUPER_ADMIN_USERS:-1}" == "1" ]];
fi
log "OMNL Bank deploy complete"
+bash "$REPO_DIR/scripts/deployment/sync-guosmm-notices.sh" || log "WARN: GUOSMM sync check failed"
log " Central Bank: http://127.0.0.1:$FRONTEND_PORT/central-bank"
log " Office 24: http://127.0.0.1:$FRONTEND_PORT/office-24"
log " DBIS Trade: http://127.0.0.1:$FRONTEND_PORT/trade"
diff --git a/scripts/deployment/sync-guosmm-notices.sh b/scripts/deployment/sync-guosmm-notices.sh
new file mode 100644
index 0000000..e69eeb6
--- /dev/null
+++ b/scripts/deployment/sync-guosmm-notices.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+# Verify GUOSMM gazette notices are live and propagate config to banking stack.
+set -euo pipefail
+
+REPO_DIR="${OMNL_BANK_ROOT:-$HOME/smom-dbis-138}"
+REGISTRY="${REPO_DIR}/config/guosmm-notices.v1.json"
+log() { echo "[$(date -Iseconds)] guosmm-sync $*"; }
+
+[[ -f "$REGISTRY" ]] || { log "Missing $REGISTRY"; exit 1; }
+
+log "Verifying gazette URLs..."
+while IFS= read -r url; do
+ [[ -n "$url" ]] || continue
+ code="$(curl -sS -o /dev/null -w '%{http_code}' "$url")"
+ log " $url -> HTTP $code"
+ [[ "$code" == "200" ]] || exit 1
+done < <(node -e "
+const r=require('${REGISTRY//\\/\\\\}');
+for (const n of r.notices) console.log(n.url);
+")
+
+log "API catalog probe (local token-aggregation)..."
+curl -sf "http://127.0.0.1:3000/api/v1/omnl/gazette/notices" | head -c 200 || log "WARN: local API not up — redeploy first"
+echo
+
+if [[ "${GUOSMM_SYNC_LXCS:-0}" == "1" ]]; then
+ PROXMOX_ROOT="${PROXMOX_ROOT:-/srv/projects/proxmox}"
+ if [[ -f "${PROXMOX_ROOT}/scripts/deployment/sync-zardasht-omnl-banking-portal-lxcs.sh" ]]; then
+ log "LXC sync..."
+ bash "${PROXMOX_ROOT}/scripts/deployment/sync-zardasht-omnl-banking-portal-lxcs.sh" --apply
+ fi
+fi
+
+log "GUOSMM notices deployed to config + API registry"
+log " Portal: /central-bank and /hub (gazette section)"
+log " API: GET /api/v1/omnl/gazette/notices"
diff --git a/services/token-aggregation/public/omnl-compliance-console.js b/services/token-aggregation/public/omnl-compliance-console.js
index 92566fe..e4b878c 100644
--- a/services/token-aggregation/public/omnl-compliance-console.js
+++ b/services/token-aggregation/public/omnl-compliance-console.js
@@ -141,12 +141,22 @@
function renderExternal(data) {
const ev = data.externalVisibility || {};
- els.external.innerHTML = kv([
- ['DefiLlama chain', ev.defiLlama && ev.defiLlama.chainPage],
- ['Pricing PR', ev.defiLlama && ev.defiLlama.pr12094],
- ['Bridge TVL PR', ev.defiLlama && ev.defiLlama.pr19451],
- ['Safe deployments PR', ev.safeDeployments && ev.safeDeployments.pr1568],
- ]);
+ const guosmm = ev.guosmmGazette || {};
+ const noticeLinks = (guosmm.notices || [])
+ .map(function (n) {
+ return '' + esc(n.documentId) + '';
+ })
+ .join(' · ');
+ els.external.innerHTML =
+ kv([
+ ['GUOSMM gazette', guosmm.base || '—'],
+ ['API registry', guosmm.apiRegistry || '/api/v1/omnl/gazette/notices'],
+ ['Notices', noticeLinks || '—'],
+ ['DefiLlama chain', ev.defiLlama && ev.defiLlama.chainPage],
+ ['Pricing PR', ev.defiLlama && ev.defiLlama.pr12094],
+ ['Bridge TVL PR', ev.defiLlama && ev.defiLlama.pr19451],
+ ['Safe deployments PR', ev.safeDeployments && ev.safeDeployments.pr1568],
+ ]);
}
function renderHoLiquidity(snap) {
diff --git a/services/token-aggregation/src/api/routes/omnl.ts b/services/token-aggregation/src/api/routes/omnl.ts
index 39b4faa..1ed47e1 100644
--- a/services/token-aggregation/src/api/routes/omnl.ts
+++ b/services/token-aggregation/src/api/routes/omnl.ts
@@ -15,6 +15,7 @@ import { omnlComplianceCore138, omnlReserveStore138 } from '../../services/omnl-
import { computeOmnlReconcileAnchor } from '../../services/omnl-reconcile-anchor';
import { getOmnlIntegrationStatus } from '../../services/omnl-integration-status';
import { getOmnlApiCatalog } from '../../services/omnl-api-catalog';
+import { loadGuosmmNotices, getGuosmmNotice } from '../../services/omnl-gazette-notices';
import omnlOpenApi from '../../resources/omnl-openapi.json';
import {
executeErc20Transfer,
@@ -90,6 +91,28 @@ router.get('/omnl/integration-status', (_req: Request, res: Response) => {
});
});
+/**
+ * GET /omnl/gazette/notices — GUOSMM official gazette registry (public disclosure).
+ */
+router.get('/omnl/gazette/notices', (_req: Request, res: Response) => {
+ const reg = loadGuosmmNotices();
+ res.json({
+ generatedAt: new Date().toISOString(),
+ gazetteBase: reg.gazetteBase,
+ count: reg.notices.length,
+ notices: reg.notices,
+ });
+});
+
+router.get('/omnl/gazette/notices/:documentId', (req: Request, res: Response) => {
+ const notice = getGuosmmNotice(req.params.documentId);
+ if (!notice) {
+ res.status(404).json({ error: 'Notice not found' });
+ return;
+ }
+ res.json(notice);
+});
+
/**
* GET /omnl/reconcile-anchor — same SHA-256 as omnl-reconcile-report.mjs (IPSAS + matrix files).
*/
diff --git a/services/token-aggregation/src/services/omnl-api-catalog.ts b/services/token-aggregation/src/services/omnl-api-catalog.ts
index eb8c932..c86a7e8 100644
--- a/services/token-aggregation/src/services/omnl-api-catalog.ts
+++ b/services/token-aggregation/src/services/omnl-api-catalog.ts
@@ -19,6 +19,8 @@ export function getOmnlApiCatalog(): {
endpoints: [
{ method: 'GET', path: '/omnl/openapi.json', description: 'OpenAPI 3.0 JSON (Swagger-compatible)', auth: 'none' },
{ method: 'GET', path: '/omnl/catalog', description: 'This catalog (machine-readable)', auth: 'none' },
+ { method: 'GET', path: '/omnl/gazette/notices', description: 'GUOSMM official gazette notice registry', auth: 'none' },
+ { method: 'GET', path: '/omnl/gazette/notices/:documentId', description: 'Single GUOSMM notice by document ID', auth: 'none' },
{ method: 'GET', path: '/omnl/integration-status', description: 'Which env-backed integrations are configured', auth: 'none' },
{ method: 'GET', path: '/omnl/reconcile-anchor', description: 'SHA-256 of canonical IPSAS registry + journal matrix JSON', auth: 'OMNL_API_KEY when OMNL_REQUIRE_API_KEY=1' },
{ method: 'GET', path: '/omnl/reconcile/triple-state', description: 'Fineract + on-chain + custodian reconcile', query: ['lineId'], auth: 'OMNL_API_KEY' },
diff --git a/services/token-aggregation/src/services/omnl-compliance-console.ts b/services/token-aggregation/src/services/omnl-compliance-console.ts
index fd30396..0031b7c 100644
--- a/services/token-aggregation/src/services/omnl-compliance-console.ts
+++ b/services/token-aggregation/src/services/omnl-compliance-console.ts
@@ -6,6 +6,7 @@ import { getWeb3ComplianceSummary } from './omnl-web3-compliance';
import { getComplianceSignoffsSummary } from './omnl-ifrs-disclosures';
import { omnlReserveStore138 } from './omnl-chain138-addresses';
import { buildSafeNotaryGateTransaction } from './omnl-safe-notary-gate-tx';
+import { loadGuosmmNotices } from './omnl-gazette-notices';
function projectRoot(): string {
return (
@@ -83,6 +84,11 @@ export interface ComplianceConsoleSnapshot {
externalVisibility: {
defiLlama: { chainPage: string; pr12094: string; pr19451: string; note: string };
safeDeployments: { pr1568: string; note: string };
+ guosmmGazette: {
+ base: string;
+ apiRegistry: string;
+ notices: Array<{ documentId: string; title: string; url: string }>;
+ };
};
explorerBase: string;
}
@@ -280,6 +286,15 @@ export async function buildComplianceConsoleSnapshot(
pr1568: 'https://github.com/safe-global/safe-deployments/pull/1568',
note: 'Safe Wallet npm registry for chain 138',
},
+ guosmmGazette: {
+ base: loadGuosmmNotices().gazetteBase,
+ apiRegistry: '/api/v1/omnl/gazette/notices',
+ notices: loadGuosmmNotices().notices.map((n) => ({
+ documentId: n.documentId,
+ title: n.title,
+ url: n.url,
+ })),
+ },
},
explorerBase: process.env.EXPLORER_PUBLIC_URL?.trim() || 'https://explorer.d-bis.org',
};
diff --git a/services/token-aggregation/src/services/omnl-gazette-notices.ts b/services/token-aggregation/src/services/omnl-gazette-notices.ts
new file mode 100644
index 0000000..08f0c9a
--- /dev/null
+++ b/services/token-aggregation/src/services/omnl-gazette-notices.ts
@@ -0,0 +1,49 @@
+import { readFileSync, existsSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+export type GuosmmNotice = {
+ documentId: string;
+ series: string;
+ title: string;
+ documentType: string;
+ url: string;
+ publishedAt: string;
+ effectiveAt: string;
+ signingAuthority: string;
+ contentSha256: string;
+ [key: string]: unknown;
+};
+
+export type GuosmmNoticeRegistry = {
+ version: string;
+ gazetteBase: string;
+ updated: string;
+ notices: GuosmmNotice[];
+};
+
+function repoRoot(): string {
+ return (
+ process.env.OMNL_BANK_ROOT?.trim() ||
+ process.env.PROXMOX_ROOT?.trim() ||
+ resolve(process.cwd(), '../../..')
+ );
+}
+
+export function loadGuosmmNotices(): GuosmmNoticeRegistry {
+ const path = resolve(repoRoot(), 'config/guosmm-notices.v1.json');
+ if (!existsSync(path)) {
+ return { version: '0', gazetteBase: 'https://gazette.shosjj.org/notices/2026', updated: '', notices: [] };
+ }
+ return JSON.parse(readFileSync(path, 'utf8')) as GuosmmNoticeRegistry;
+}
+
+export function getGuosmmNotice(documentId: string): GuosmmNotice | null {
+ const reg = loadGuosmmNotices();
+ return reg.notices.find((n) => n.documentId === documentId) ?? null;
+}
+
+export function listGuosmmNoticesByScope(scope?: string): GuosmmNotice[] {
+ const reg = loadGuosmmNotices();
+ if (!scope) return reg.notices;
+ return reg.notices.filter((n) => Array.isArray(n.scope) && (n.scope as string[]).includes(scope));
+}