Refine OMNL portal and vault controls

This commit is contained in:
defiQUG
2026-07-12 15:34:15 -07:00
parent 7b43908585
commit 6915d5b067
16 changed files with 1002 additions and 2 deletions

View File

@@ -18,6 +18,7 @@ export default function Layout() {
{ to: '/bridge', label: 'Bridge', active: isActive('/bridge') },
{ to: '/swap', label: 'Swap', active: isActive('/swap') },
{ to: '/reserve', label: 'Reserve', active: isActive('/reserve') },
{ href: '/omnl/gru-vault-controls', label: 'Vault Controls' },
{ to: '/history', label: 'History', active: isActive('/history') },
{ to: '/wallets', label: 'Wallets', active: isActive('/wallets') },
{ to: '/admin', label: 'Admin', active: isActive('/admin') },
@@ -55,6 +56,12 @@ export default function Layout() {
{item.label}
</Link>
))}
<a
href="/omnl/gru-vault-controls"
className="px-3 py-2 rounded-lg text-sm font-medium text-[#A0A0A0] hover:text-white hover:bg-white/5 transition-colors whitespace-nowrap min-h-[44px] flex items-center"
>
Vault Controls
</a>
<a
href={defaultFrontendExplorerUrl}
target="_blank"

View File

@@ -56,7 +56,13 @@ export default function MobileNavDrawer({ open, onClose, title, items }: Props)
if (item.href) {
return (
<li key={item.href}>
<a href={item.href} target="_blank" rel="noopener noreferrer" className={className} onClick={onClose}>
<a
href={item.href}
target={item.href.startsWith('http') ? '_blank' : undefined}
rel={item.href.startsWith('http') ? 'noopener noreferrer' : undefined}
className={className}
onClick={onClose}
>
{item.label}
</a>
</li>

View File

@@ -27,6 +27,7 @@ export default function OmnlProductLayout() {
{ to: '/bridge', label: 'Bridge', active: location.pathname === '/bridge' },
{ to: '/admin', label: 'Admin', active: location.pathname === '/admin' },
{ to: '/wallets', label: 'Wallets', active: location.pathname === '/wallets' },
{ href: '/omnl/gru-vault-controls', label: 'Vault Controls' },
];
const isBanking = location.pathname === '/central-bank';
@@ -75,6 +76,13 @@ export default function OmnlProductLayout() {
<NasaIcon name="bridge" size={14} />
Bridge
</Link>
<a
href="/omnl/gru-vault-controls"
className="hidden xl:inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium text-[#848e9c] hover:text-white hover:bg-white/5 min-h-[40px]"
>
<NasaIcon name="admin" size={14} />
Vault Controls
</a>
<div className="shrink-0 max-w-[45%] sm:max-w-none ml-auto">
<WalletConnect onBeforeDisconnect={() => { userInitiatedDisconnectRef.current = true; }} />
</div>

View File

@@ -42,6 +42,7 @@ export const OPERATOR_CONSOLE_LINKS: EcosystemLink[] = [
{ id: 'compliance', label: 'OMNL Compliance Console', description: 'Posture, pending actions, Safe tx', icon: 'shield', href: '/omnl/compliance', accent: 'purple' },
{ id: 'omnl-dash', label: 'OMNL API Snapshot', description: 'Registry and Fineract compare JSON', icon: 'chart', href: '/omnl/dashboard', accent: 'blue' },
{ id: 'reserve-dash', label: 'Reserve Dashboard', description: 'Proof rows, coverage, institutional view', icon: 'money', href: '/reserve', accent: 'green' },
{ id: 'gru-controls', label: 'GRU Vault Controls', description: 'Runtime smart-vault policy knobs', icon: 'admin', href: '/omnl/gru-vault-controls', accent: 'gold' },
{ id: 'settlement-term', label: 'Settlement Terminal', description: 'Operator settlement terminal UI', icon: 'terminal', href: '/omnl/terminal', accent: 'gold' },
{ id: 'token-admin', label: 'Token Aggregation API', description: 'API catalog JSON', icon: 'ledger', href: '/api/v1/omnl/catalog', accent: 'teal' },
];

View File

@@ -22,6 +22,7 @@ const CHAIN138: { path: string; label: string; icon: NasaIconName }[] = [
const OMNL_OPS: { path: string; label: string; icon: NasaIconName }[] = [
{ path: '/omnl/compliance', label: 'OMNL Compliance console', icon: 'ledger' },
{ path: '/omnl/dashboard', label: 'OMNL API snapshot', icon: 'chart' },
{ path: '/omnl/gru-vault-controls', label: 'GRU Vault Controls', icon: 'admin' },
{ path: '/omnl/terminal', label: 'Settlement terminal', icon: 'rail' },
];

View File

@@ -30,7 +30,7 @@ export default function ReservePage() {
}, []);
const reserveLinks = OPERATOR_CONSOLE_LINKS.filter((l) =>
['reserve-dash', 'compliance', 'omnl-dash'].includes(l.id),
['reserve-dash', 'compliance', 'omnl-dash', 'gru-controls'].includes(l.id),
);
return (

View File

@@ -0,0 +1,209 @@
:root {
--bg: #090d12;
--panel: #111822;
--panel-2: #0f1520;
--border: #223146;
--text: #e8eef8;
--muted: #8aa0b8;
--accent: #65d6ff;
--accent-2: #8cffc1;
--warn: #f59e0b;
--danger: #ef4444;
}
* { box-sizing: border-box; }
body {
margin: 0;
background:
radial-gradient(circle at top left, rgba(101, 214, 255, 0.14), transparent 28%),
radial-gradient(circle at top right, rgba(140, 255, 193, 0.12), transparent 24%),
var(--bg);
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
.shell {
max-width: 1180px;
margin: 0 auto;
padding: 24px 18px 40px;
}
.hero {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 16px;
}
.eyebrow {
margin: 0 0 6px;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.18em;
font-size: 12px;
}
h1 {
margin: 0;
font-size: clamp(2rem, 3vw, 3.2rem);
}
.sub {
color: var(--muted);
margin: 8px 0 0;
line-height: 1.5;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
button,
.btn {
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.02);
color: var(--text);
border-radius: 12px;
padding: 11px 14px;
font-size: 14px;
text-decoration: none;
cursor: pointer;
}
button.primary {
background: linear-gradient(135deg, rgba(101, 214, 255, 0.9), rgba(140, 255, 193, 0.86));
color: #04111b;
border-color: transparent;
font-weight: 700;
}
.banner {
border: 1px solid var(--border);
border-radius: 14px;
padding: 14px 16px;
margin-bottom: 18px;
background: rgba(255, 255, 255, 0.03);
}
.banner.loading { color: var(--muted); }
.banner.ok { border-color: rgba(140, 255, 193, 0.35); }
.banner.error { border-color: rgba(239, 68, 68, 0.5); color: #fecaca; }
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.card {
background: linear-gradient(180deg, rgba(17, 24, 34, 0.96), rgba(11, 16, 25, 0.96));
border: 1px solid var(--border);
border-radius: 18px;
padding: 18px;
margin-bottom: 16px;
}
.card h2 {
margin: 0 0 14px;
font-size: 18px;
}
.kv {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.kv div,
.form-grid label {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 14px;
padding: 12px;
}
.kv span,
.form-grid span {
display: block;
color: var(--muted);
font-size: 12px;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.kv strong {
font-size: 16px;
}
.note,
.form-grid small {
color: var(--muted);
display: block;
margin-top: 10px;
line-height: 1.4;
}
.form-grid {
display: grid;
gap: 14px;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
label input,
label select,
label textarea {
width: 100%;
border-radius: 12px;
border: 1px solid var(--border);
background: rgba(5, 8, 13, 0.9);
color: var(--text);
padding: 10px 12px;
font-size: 14px;
}
label textarea {
resize: vertical;
min-height: 118px;
}
label.switch {
display: flex;
align-items: center;
gap: 10px;
}
label.switch input {
width: 18px;
height: 18px;
margin: 0;
}
pre {
background: rgba(5, 8, 13, 0.95);
border: 1px solid var(--border);
border-radius: 14px;
padding: 14px;
overflow: auto;
max-height: 360px;
font-size: 12px;
}
.hidden { display: none; }
@media (max-width: 900px) {
.hero,
.grid,
.form-grid,
.kv {
grid-template-columns: 1fr;
}
.hero {
flex-direction: column;
}
}

View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="omnl-api-base" content="auto" />
<title>GRU Smart Vault Controls</title>
<link rel="stylesheet" href="/omnl/static/gru-vault-controls.css" />
</head>
<body>
<div class="shell">
<header class="hero">
<div>
<p class="eyebrow">Operator dashboard</p>
<h1>GRU Smart Vault Controls</h1>
<p class="sub">
Runtime policy knobs for the smart vault layer. The ladder remains fixed at
<code>1 M00 = 5 M0 = 25 M1</code>.
</p>
</div>
<div class="toolbar">
<button id="btn-refresh" type="button" class="primary">Refresh</button>
<button id="btn-save" type="button">Save</button>
<button id="btn-reset" type="button">Reset defaults</button>
<button id="btn-raw" type="button">Raw JSON</button>
<a class="btn" href="/reserve">Reserve dashboard</a>
<a class="btn" href="/omnl/compliance">Compliance console</a>
</div>
</header>
<div id="status" class="banner loading">Initializing…</div>
<p id="updated-at" class="sub"></p>
<div class="grid">
<section class="card">
<h2>Canonical ladder</h2>
<div class="kv">
<div><span>Expression</span><strong>1 M00 = 5 M0 = 25 M1</strong></div>
<div><span>M00 → M0</span><strong>5x fixed</strong></div>
<div><span>M0 → M1</span><strong>5x fixed</strong></div>
</div>
<p class="note">
The dashboard can tune runtime policy references and component selections, but the canonical
ratio is hard-coded and cannot be changed here.
</p>
</section>
<section class="card">
<h2>Profile</h2>
<label>
<span>Runtime profile</span>
<select id="profile">
<option value="live">Live</option>
<option value="conservative">Conservative</option>
<option value="expanded">Expanded</option>
<option value="emergency">Emergency</option>
</select>
</label>
<label class="switch">
<input id="cstar-enabled" type="checkbox" />
<span>c* settlement tier enabled</span>
</label>
<label class="switch">
<input id="expanded-enabled" type="checkbox" />
<span>Expanded asset classes enabled</span>
</label>
<label class="switch">
<input id="bond-enabled" type="checkbox" />
<span>Approved bond basket enabled</span>
</label>
</section>
</div>
<section class="card">
<h2>Operational tuning</h2>
<div class="form-grid">
<label>
<span>LiXAU withdrawal fraction</span>
<input id="withdraw-fraction" type="number" min="0.05" max="0.5" step="0.01" />
<small>Default 0.20. Display and policy hint only.</small>
</label>
<label>
<span>Reserve reference multiplier</span>
<input id="reserve-multiplier" type="number" min="0.5" max="5" step="0.01" />
<small>Controls reserve reference presentation; archival formula reference remains explicit.</small>
</label>
<label>
<span>Refresh interval (seconds)</span>
<input id="refresh-interval" type="number" min="5" max="300" step="1" />
<small>Dashboard polling interval for the operator page.</small>
</label>
</div>
</section>
<section class="card">
<h2>Component families</h2>
<div class="form-grid">
<label>
<span>M00 Li* base components</span>
<textarea id="li-components" rows="4" spellcheck="false"></textarea>
<small>Comma or newline separated. Example: LiXAU, LiPMG, LiBMG1, LiBMG2, LiBMG3.</small>
</label>
<label>
<span>Approved bond components</span>
<textarea id="bond-components" rows="4" spellcheck="false"></textarea>
<small>Editable bundle for approved bond rows when policy needs to change without redeploy.</small>
</label>
<label>
<span>Expanded asset components</span>
<textarea id="expanded-components" rows="4" spellcheck="false"></textarea>
<small>Future or registry-only classes such as LiCARB / LiRES*.</small>
</label>
</div>
</section>
<section id="raw-section" class="card hidden">
<h2>Raw snapshot</h2>
<pre id="raw-json">{}</pre>
</section>
</div>
<script src="/omnl/static/omnl-api-base.js"></script>
<script src="/omnl/static/gru-vault-controls.js"></script>
</body>
</html>

View File

@@ -0,0 +1,147 @@
(function () {
const apiBaseResolver = window.resolveOmnlApiBase || (() => '/api/v1');
const apiBase = apiBaseResolver().replace(/\/$/, '');
const params = new URLSearchParams(window.location.search);
const accessToken = params.get('access_token')?.trim() || '';
const authQuery = accessToken ? `?access_token=${encodeURIComponent(accessToken)}` : '';
const els = {
status: document.getElementById('status'),
updatedAt: document.getElementById('updated-at'),
profile: document.getElementById('profile'),
withdrawFraction: document.getElementById('withdraw-fraction'),
reserveMultiplier: document.getElementById('reserve-multiplier'),
refreshInterval: document.getElementById('refresh-interval'),
cstarEnabled: document.getElementById('cstar-enabled'),
expandedEnabled: document.getElementById('expanded-enabled'),
bondEnabled: document.getElementById('bond-enabled'),
liComponents: document.getElementById('li-components'),
bondComponents: document.getElementById('bond-components'),
expandedComponents: document.getElementById('expanded-components'),
rawSection: document.getElementById('raw-section'),
rawJson: document.getElementById('raw-json'),
btnRefresh: document.getElementById('btn-refresh'),
btnSave: document.getElementById('btn-save'),
btnReset: document.getElementById('btn-reset'),
btnRaw: document.getElementById('btn-raw'),
};
function listToText(list) {
return Array.isArray(list) ? list.join(', ') : '';
}
function textToList(text) {
return String(text || '')
.split(/[\n,]/)
.map((value) => value.trim())
.filter(Boolean);
}
function setStatus(type, message) {
els.status.className = `banner ${type}`;
els.status.textContent = message;
}
function apiUrl(path) {
return `${apiBase}${path}${authQuery}`;
}
function readForm() {
return {
profile: els.profile.value,
smartVault: {
liXauWithdrawalFraction: Number(els.withdrawFraction.value),
cStarSettlementEnabled: Boolean(els.cstarEnabled.checked),
expandedAssetClassesEnabled: Boolean(els.expandedEnabled.checked),
approvedBondBasketEnabled: Boolean(els.bondEnabled.checked),
},
componentFamilies: {
liIndexComponents: textToList(els.liComponents.value),
approvedBondComponents: textToList(els.bondComponents.value),
expandedAssetComponents: textToList(els.expandedComponents.value),
},
dashboard: {
reserveReferenceMultiplier: Number(els.reserveMultiplier.value),
refreshIntervalSeconds: Number(els.refreshInterval.value),
},
};
}
function fillForm(snapshot) {
const controls = snapshot.controls || snapshot;
els.profile.value = controls.profile || 'live';
els.withdrawFraction.value = controls.smartVault?.liXauWithdrawalFraction ?? 0.2;
els.reserveMultiplier.value = controls.dashboard?.reserveReferenceMultiplier ?? 1.2;
els.refreshInterval.value = controls.dashboard?.refreshIntervalSeconds ?? 30;
els.cstarEnabled.checked = Boolean(controls.smartVault?.cStarSettlementEnabled);
els.expandedEnabled.checked = Boolean(controls.smartVault?.expandedAssetClassesEnabled);
els.bondEnabled.checked = Boolean(controls.smartVault?.approvedBondBasketEnabled);
els.liComponents.value = listToText(controls.componentFamilies?.liIndexComponents);
els.bondComponents.value = listToText(controls.componentFamilies?.approvedBondComponents);
els.expandedComponents.value = listToText(controls.componentFamilies?.expandedAssetComponents);
els.updatedAt.textContent = `Updated: ${controls.updatedAt || snapshot.updatedAt || '—'} · Profile: ${controls.profile || 'live'}`;
els.rawJson.textContent = JSON.stringify(snapshot, null, 2);
}
async function loadControls() {
setStatus('loading', 'Loading GRU vault controls…');
try {
const res = await fetch(apiUrl('/omnl/gru-vault-controls'), { cache: 'no-store' });
const data = await res.json();
if (!res.ok) {
throw new Error(data?.error || `HTTP ${res.status}`);
}
fillForm(data);
setStatus('ok', 'GRU vault controls loaded. Changes are persisted server-side.');
} catch (error) {
setStatus('error', `Load failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async function saveControls() {
setStatus('loading', 'Saving runtime controls…');
try {
const payload = readForm();
const res = await fetch(apiUrl('/omnl/gru-vault-controls'), {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data?.error || `HTTP ${res.status}`);
}
fillForm(data);
setStatus('ok', 'Runtime controls saved without redeploying.');
} catch (error) {
setStatus('error', `Save failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async function resetControls() {
setStatus('loading', 'Resetting controls to defaults…');
try {
const res = await fetch(apiUrl('/omnl/gru-vault-controls/reset'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();
if (!res.ok) {
throw new Error(data?.error || `HTTP ${res.status}`);
}
fillForm(data);
setStatus('ok', 'Controls reset to defaults.');
} catch (error) {
setStatus('error', `Reset failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
els.btnRefresh?.addEventListener('click', loadControls);
els.btnSave?.addEventListener('click', saveControls);
els.btnReset?.addEventListener('click', resetControls);
els.btnRaw?.addEventListener('click', () => {
els.rawSection?.classList.toggle('hidden');
});
loadControls();
})();

View File

@@ -17,6 +17,7 @@
<div class="toolbar">
<button type="button" class="primary" id="btn-refresh">Refresh</button>
<button type="button" id="btn-toggle-raw">Raw JSON</button>
<a class="btn" href="/omnl/gru-vault-controls">Vault controls</a>
<a class="btn" href="/api/v1/reserve/proof-index">Proof index API</a>
<a class="btn" href="/omnl/compliance">Compliance console</a>
</div>

View File

@@ -4,3 +4,10 @@
{"id":"4d21094d-56fd-4324-a75b-1075f9a5dd59","ts":"2026-07-06T00:23:58.822Z","category":"reconcile","action":"triple_state_reconcile","traceId":"0xfefb7411285a639b00be06bbffe897eb802ee817983aa57facf5bde6a1b52ff4","status":"error","metadata":{"breakCount":3,"aligned":false}}
{"id":"9b28b030-b396-4e07-9aa8-47854797096c","ts":"2026-07-06T00:28:58.771Z","category":"reconcile","action":"triple_state_reconcile","traceId":"0xfefb7411285a639b00be06bbffe897eb802ee817983aa57facf5bde6a1b52ff4","status":"error","metadata":{"breakCount":3,"aligned":false}}
{"id":"d7ea5f0c-5827-4795-ae98-23f958b92646","ts":"2026-07-06T00:29:30.710Z","category":"reconcile","action":"triple_state_reconcile","traceId":"0xfefb7411285a639b00be06bbffe897eb802ee817983aa57facf5bde6a1b52ff4","status":"error","metadata":{"breakCount":3,"aligned":false}}
{"id":"c284835f-23c1-4f7f-9648-0558da9fd540","ts":"2026-07-12T22:29:52.477Z","category":"api","action":"GET /omnl/gru-vault-controls","sourceSystem":"http","traceId":"e9598ce7-448f-44e0-bbea-0366d9d4108f","status":"ok","metadata":{"statusCode":200,"durationMs":9,"query":{}}}
{"id":"93b1b6c2-58c5-4379-841c-8568f8c85524","ts":"2026-07-12T22:29:52.578Z","category":"api","action":"update_gru_vault_controls","actor":"operator","status":"ok","metadata":{"profile":"conservative","liIndexComponents":2,"approvedBondComponents":2,"expandedAssetComponents":2,"liXauWithdrawalFraction":0.15,"reserveReferenceMultiplier":1.35,"refreshIntervalSeconds":45}}
{"id":"e02b9819-3040-458d-828e-9a045d818667","ts":"2026-07-12T22:29:52.582Z","category":"api","action":"PUT /omnl/gru-vault-controls","sourceSystem":"http","traceId":"6967adc2-e7c8-4d8d-b05a-b727f43cbc82","status":"ok","metadata":{"statusCode":200,"durationMs":10,"query":{}}}
{"id":"b85e62ba-1cd1-41e7-a3ce-f7b32db46808","ts":"2026-07-12T22:29:52.609Z","category":"api","action":"update_gru_vault_controls","actor":"operator","status":"ok","metadata":{"profile":"emergency","liIndexComponents":5,"approvedBondComponents":0,"expandedAssetComponents":4,"liXauWithdrawalFraction":0.3,"reserveReferenceMultiplier":1.2,"refreshIntervalSeconds":30}}
{"id":"0847e9a4-6d0d-44b2-9024-52653806cd02","ts":"2026-07-12T22:29:52.611Z","category":"api","action":"PUT /omnl/gru-vault-controls","sourceSystem":"http","traceId":"399f9edb-15f0-4dad-9b96-faf579336f4e","status":"ok","metadata":{"statusCode":200,"durationMs":3,"query":{}}}
{"id":"fbc5fc45-7ded-4174-b316-49a990b01120","ts":"2026-07-12T22:29:52.624Z","category":"api","action":"reset_gru_vault_controls","actor":"operator","status":"ok","metadata":{"profile":"live"}}
{"id":"420b3ed9-88e6-4748-a9b8-2697589e8c1c","ts":"2026-07-12T22:29:52.626Z","category":"api","action":"POST /omnl/gru-vault-controls/reset","sourceSystem":"http","traceId":"ae0ec963-aca9-461c-833c-b7d2dab1c7db","status":"ok","metadata":{"statusCode":200,"durationMs":4,"query":{}}}

View File

@@ -0,0 +1,149 @@
import { createServer } from 'http';
import express from 'express';
import fs from 'fs/promises';
import path from 'path';
import gruVaultControlsRoutes from './gru-vault-controls';
jest.mock('../middleware/cache');
function createApp() {
const app = express();
app.use(express.json());
app.use('/api/v1', gruVaultControlsRoutes);
return app;
}
async function startServer(app: express.Application): Promise<{ server: ReturnType<typeof createServer>; baseUrl: string }> {
const server = createServer(app);
await new Promise<void>((resolve) => server.listen(0, () => resolve()));
const port = (server.address() as { port: number }).port;
return { server, baseUrl: `http://127.0.0.1:${port}` };
}
describe('GRU vault controls API', () => {
let server: ReturnType<typeof createServer>;
let baseUrl: string;
const originalApiKey = process.env.OMNL_API_KEY;
const originalControlsPath = process.env.GRU_VAULT_CONTROLS_PATH;
beforeAll(async () => {
process.env.OMNL_API_KEY = 'test-omnl-key';
process.env.GRU_VAULT_CONTROLS_PATH = path.join('/tmp', `gru-vault-controls-${Date.now()}.json`);
const started = await startServer(createApp());
server = started.server;
baseUrl = started.baseUrl;
});
afterEach(async () => {
await fs.rm(process.env.GRU_VAULT_CONTROLS_PATH || '', { force: true }).catch(() => undefined);
process.env.GRU_VAULT_CONTROLS_PATH = path.join('/tmp', `gru-vault-controls-${Date.now()}-${Math.random()}.json`);
});
afterAll((done) => {
if (originalApiKey === undefined) {
delete process.env.OMNL_API_KEY;
} else {
process.env.OMNL_API_KEY = originalApiKey;
}
if (originalControlsPath === undefined) {
delete process.env.GRU_VAULT_CONTROLS_PATH;
} else {
process.env.GRU_VAULT_CONTROLS_PATH = originalControlsPath;
}
server.close(done);
});
it('returns canonical defaults when no runtime file exists', async () => {
const res = await fetch(`${baseUrl}/api/v1/omnl/gru-vault-controls`, {
headers: { Authorization: 'Bearer test-omnl-key' },
});
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.controls.canonicalLadder).toEqual({
expression: '1 M00 = 5 M0 = 25 M1',
m00ToM0Ratio: 5,
m0ToM1Ratio: 5,
});
expect(body.controls.smartVault.liXauWithdrawalFraction).toBeCloseTo(0.2);
expect(body.controls.componentFamilies.liIndexComponents).toEqual(
expect.arrayContaining(['LiXAU', 'LiPMG', 'LiBMG1', 'LiBMG2', 'LiBMG3'])
);
});
it('persists runtime adjustments without changing the canonical ladder', async () => {
const payload = {
profile: 'conservative',
smartVault: {
liXauWithdrawalFraction: 0.15,
cStarSettlementEnabled: false,
expandedAssetClassesEnabled: true,
approvedBondBasketEnabled: true,
},
componentFamilies: {
liIndexComponents: ['LiXAU', 'LiPMG'],
approvedBondComponents: ['Bond-A', 'Bond-B'],
expandedAssetComponents: ['LiCARB', 'LiRES1'],
},
dashboard: {
reserveReferenceMultiplier: 1.35,
refreshIntervalSeconds: 45,
},
canonicalLadder: {
expression: 'bad',
m00ToM0Ratio: 7,
m0ToM1Ratio: 9,
},
};
const res = await fetch(`${baseUrl}/api/v1/omnl/gru-vault-controls`, {
method: 'PUT',
headers: {
Authorization: 'Bearer test-omnl-key',
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.controls.profile).toBe('conservative');
expect(body.controls.smartVault.liXauWithdrawalFraction).toBeCloseTo(0.15);
expect(body.controls.smartVault.cStarSettlementEnabled).toBe(false);
expect(body.controls.componentFamilies.liIndexComponents).toEqual(['LiXAU', 'LiPMG']);
expect(body.controls.canonicalLadder).toEqual({
expression: '1 M00 = 5 M0 = 25 M1',
m00ToM0Ratio: 5,
m0ToM1Ratio: 5,
});
const stored = JSON.parse(await fs.readFile(process.env.GRU_VAULT_CONTROLS_PATH || '', 'utf8')) as Record<string, any>;
expect(stored.profile).toBe('conservative');
expect(stored.canonicalLadder).toEqual({
expression: '1 M00 = 5 M0 = 25 M1',
m00ToM0Ratio: 5,
m0ToM1Ratio: 5,
});
});
it('can reset controls to defaults', async () => {
await fetch(`${baseUrl}/api/v1/omnl/gru-vault-controls`, {
method: 'PUT',
headers: {
Authorization: 'Bearer test-omnl-key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
profile: 'emergency',
smartVault: { liXauWithdrawalFraction: 0.3 },
}),
});
const res = await fetch(`${baseUrl}/api/v1/omnl/gru-vault-controls/reset`, {
method: 'POST',
headers: { Authorization: 'Bearer test-omnl-key' },
});
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.controls.profile).toBe('live');
expect(body.controls.smartVault.liXauWithdrawalFraction).toBeCloseTo(0.2);
});
});

View File

@@ -0,0 +1,80 @@
import { Router, Request, Response } from 'express';
import { omnlRateLimiter } from '../middleware/rate-limit';
import { omnlAuditMiddleware } from '../middleware/omnl-audit-middleware';
import { omnlSensitiveRouteGuard, omnlRequireApiKeyInProduction } from '../middleware/omnl-guards';
import {
getDefaultGruVaultControls,
getGruVaultControlsPath,
loadGruVaultControls,
resetGruVaultControls,
saveGruVaultControls,
} from '../../services/gru-vault-controls';
import { appendOmnlAudit } from '../../services/omnl-audit-log';
const router = Router();
router.use(omnlRateLimiter);
router.use(omnlAuditMiddleware);
router.use(omnlRequireApiKeyInProduction);
function extractBody(req: Request): unknown {
const body = req.body as Record<string, unknown> | undefined;
if (!body) return {};
if (body.controls && typeof body.controls === 'object') return body.controls;
return body;
}
router.get('/omnl/gru-vault-controls', omnlSensitiveRouteGuard, (_req: Request, res: Response) => {
res.json(loadGruVaultControls());
});
router.get('/omnl/gru-vault-controls/defaults', omnlSensitiveRouteGuard, (_req: Request, res: Response) => {
res.json({
path: getGruVaultControlsPath(),
controls: getDefaultGruVaultControls(),
});
});
router.put('/omnl/gru-vault-controls', omnlSensitiveRouteGuard, (req: Request, res: Response) => {
try {
const snapshot = saveGruVaultControls(extractBody(req));
appendOmnlAudit({
category: 'api',
action: 'update_gru_vault_controls',
actor: String(req.headers['x-omnl-actor'] ?? req.headers['x-forwarded-user'] ?? 'operator'),
status: 'ok',
metadata: {
profile: snapshot.controls.profile,
liIndexComponents: snapshot.controls.componentFamilies.liIndexComponents.length,
approvedBondComponents: snapshot.controls.componentFamilies.approvedBondComponents.length,
expandedAssetComponents: snapshot.controls.componentFamilies.expandedAssetComponents.length,
liXauWithdrawalFraction: snapshot.controls.smartVault.liXauWithdrawalFraction,
reserveReferenceMultiplier: snapshot.controls.dashboard.reserveReferenceMultiplier,
refreshIntervalSeconds: snapshot.controls.dashboard.refreshIntervalSeconds,
},
});
res.json(snapshot);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(400).json({ error: msg });
}
});
router.post('/omnl/gru-vault-controls/reset', omnlSensitiveRouteGuard, (_req: Request, res: Response) => {
try {
const snapshot = resetGruVaultControls();
appendOmnlAudit({
category: 'api',
action: 'reset_gru_vault_controls',
actor: String(_req.headers['x-omnl-actor'] ?? _req.headers['x-forwarded-user'] ?? 'operator'),
status: 'ok',
metadata: { profile: snapshot.controls.profile },
});
res.json(snapshot);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
res.status(400).json({ error: msg });
}
});
export default router;

View File

@@ -26,6 +26,7 @@ import omnlIpsasRoutes from './routes/omnl-ipsas';
import omnlPayoutJournalRoutes from './routes/omnl-payout-journal-routes';
import omnlComplianceRoutes from './routes/omnl-compliance-routes';
import omnlTerminalRoutes from './routes/omnl-terminal-routes';
import gruVaultControlsRoutes from './routes/gru-vault-controls';
import zWalletRoutes from './routes/z-wallet';
import zBotRoutes from './routes/z-bot';
import checkpointRoutes from './routes/checkpoint';
@@ -193,6 +194,7 @@ export class ApiServer {
const settlementTerminalPath = path.join(__dirname, '../../public/omnl-settlement-terminal.html');
const reserveDashboardPath = path.join(__dirname, '../../public/reserve-dashboard.html');
const reserveInstitutionalPath = path.join(__dirname, '../../public/reserve-institutional.html');
const gruVaultControlsPath = path.join(__dirname, '../../public/gru-vault-controls.html');
const authorizeOmnlHtml = (req: Request, res: Response): boolean => {
const tok = process.env.OMNL_DASHBOARD_TOKEN?.trim();
@@ -252,6 +254,15 @@ export class ApiServer {
res.type('html').send(readFileSync(file, 'utf8'));
});
this.app.get('/omnl/gru-vault-controls', (req: Request, res: Response) => {
if (!authorizeOmnlHtml(req, res)) return;
if (!existsSync(gruVaultControlsPath)) {
res.status(404).type('text/plain').send('gru-vault-controls.html missing');
return;
}
res.type('html').send(readFileSync(gruVaultControlsPath, 'utf8'));
});
// Public API catalog (register before routers so GET /api/v1 is not swallowed by middleware-only mounts)
const sendApiV1Catalog = (_req: Request, res: Response) => {
res.json({
@@ -321,6 +332,7 @@ export class ApiServer {
this.app.use('/api/v1/prices/metamask', metamaskPriceRoutes);
this.app.use('/api/v1', omnlComplianceRoutes);
this.app.use('/api/v1', omnlTerminalRoutes);
this.app.use('/api/v1', gruVaultControlsRoutes);
this.app.use('/api/v1', omnlRoutes);
this.app.use('/api/v1', omnlIpsasRoutes);
this.app.use('/api/v1', omnlPayoutJournalRoutes);

View File

@@ -0,0 +1,243 @@
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
import { dirname, resolve } from 'path';
export type GruVaultProfile = 'live' | 'conservative' | 'expanded' | 'emergency';
export interface GruVaultControls {
schemaVersion: '1.0.0';
updatedAt: string;
profile: GruVaultProfile;
canonicalLadder: {
expression: '1 M00 = 5 M0 = 25 M1';
m00ToM0Ratio: 5;
m0ToM1Ratio: 5;
};
smartVault: {
liXauWithdrawalFraction: number;
cStarSettlementEnabled: boolean;
expandedAssetClassesEnabled: boolean;
approvedBondBasketEnabled: boolean;
};
componentFamilies: {
liIndexComponents: string[];
approvedBondComponents: string[];
expandedAssetComponents: string[];
};
dashboard: {
reserveReferenceMultiplier: number;
refreshIntervalSeconds: number;
notes: string;
};
}
export interface GruVaultControlsSnapshot {
source: 'defaults' | 'file';
path: string;
controls: GruVaultControls;
}
const DEFAULT_CONTROLS: GruVaultControls = {
schemaVersion: '1.0.0',
updatedAt: new Date(0).toISOString(),
profile: 'live',
canonicalLadder: {
expression: '1 M00 = 5 M0 = 25 M1',
m00ToM0Ratio: 5,
m0ToM1Ratio: 5,
},
smartVault: {
liXauWithdrawalFraction: 0.2,
cStarSettlementEnabled: true,
expandedAssetClassesEnabled: false,
approvedBondBasketEnabled: true,
},
componentFamilies: {
liIndexComponents: ['LiXAU', 'LiPMG', 'LiBMG1', 'LiBMG2', 'LiBMG3'],
approvedBondComponents: [],
expandedAssetComponents: ['LiCARB', 'LiRES1', 'LiRES2', 'LiRES3'],
},
dashboard: {
reserveReferenceMultiplier: 1.2,
refreshIntervalSeconds: 30,
notes: 'Runtime policy knobs for the smart-vault dashboard; canonical ladder remains fixed.',
},
};
const ALLOWED_PROFILES: GruVaultProfile[] = ['live', 'conservative', 'expanded', 'emergency'];
function projectRoot(): string {
return (
process.env.PROXMOX_ROOT?.trim() ||
process.env.PHOENIX_REPO_ROOT?.trim() ||
resolve(__dirname, '../../../../../..')
);
}
function resolveControlsPath(): string {
const configured = process.env.GRU_VAULT_CONTROLS_PATH?.trim();
if (configured) return resolve(configured);
return resolve(projectRoot(), 'reports/status/gru-vault-controls-latest.json');
}
function cloneDefaults(): GruVaultControls {
return JSON.parse(JSON.stringify(DEFAULT_CONTROLS)) as GruVaultControls;
}
function parseStringList(value: unknown): string[] {
if (Array.isArray(value)) {
return value
.map((item) => String(item).trim())
.filter((item) => item.length > 0);
}
if (typeof value === 'string') {
return value
.split(/[\n,]/)
.map((item) => item.trim())
.filter((item) => item.length > 0);
}
return [];
}
function clampNumber(value: unknown, fallback: number, min: number, max: number): number {
const n = Number(value);
if (!Number.isFinite(n)) return fallback;
return Math.min(max, Math.max(min, n));
}
function parseBoolean(value: unknown, fallback: boolean): boolean {
if (typeof value === 'boolean') return value;
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
}
return fallback;
}
function normalizeProfile(value: unknown, fallback: GruVaultProfile): GruVaultProfile {
if (typeof value === 'string' && ALLOWED_PROFILES.includes(value as GruVaultProfile)) {
return value as GruVaultProfile;
}
return fallback;
}
function normalizeControls(raw: unknown, updatedAt?: string): GruVaultControls {
const defaults = cloneDefaults();
const input = (raw && typeof raw === 'object' ? raw : {}) as Partial<GruVaultControls> & {
smartVault?: Partial<GruVaultControls['smartVault']>;
componentFamilies?: Partial<GruVaultControls['componentFamilies']>;
dashboard?: Partial<GruVaultControls['dashboard']>;
};
const controls: GruVaultControls = {
...defaults,
profile: normalizeProfile(input.profile, defaults.profile),
updatedAt: updatedAt || defaults.updatedAt,
canonicalLadder: defaults.canonicalLadder,
smartVault: {
liXauWithdrawalFraction: clampNumber(
input.smartVault?.liXauWithdrawalFraction,
defaults.smartVault.liXauWithdrawalFraction,
0.05,
0.5,
),
cStarSettlementEnabled: parseBoolean(
input.smartVault?.cStarSettlementEnabled,
defaults.smartVault.cStarSettlementEnabled,
),
expandedAssetClassesEnabled: parseBoolean(
input.smartVault?.expandedAssetClassesEnabled,
defaults.smartVault.expandedAssetClassesEnabled,
),
approvedBondBasketEnabled: parseBoolean(
input.smartVault?.approvedBondBasketEnabled,
defaults.smartVault.approvedBondBasketEnabled,
),
},
componentFamilies: {
liIndexComponents: parseStringList(input.componentFamilies?.liIndexComponents).length
? parseStringList(input.componentFamilies?.liIndexComponents)
: defaults.componentFamilies.liIndexComponents,
approvedBondComponents: parseStringList(input.componentFamilies?.approvedBondComponents),
expandedAssetComponents: parseStringList(input.componentFamilies?.expandedAssetComponents).length
? parseStringList(input.componentFamilies?.expandedAssetComponents)
: defaults.componentFamilies.expandedAssetComponents,
},
dashboard: {
reserveReferenceMultiplier: clampNumber(
input.dashboard?.reserveReferenceMultiplier,
defaults.dashboard.reserveReferenceMultiplier,
0.5,
5,
),
refreshIntervalSeconds: Math.round(
clampNumber(input.dashboard?.refreshIntervalSeconds, defaults.dashboard.refreshIntervalSeconds, 5, 300),
),
notes:
typeof input.dashboard?.notes === 'string' && input.dashboard.notes.trim().length > 0
? input.dashboard.notes.trim()
: defaults.dashboard.notes,
},
};
if (!controls.smartVault.cStarSettlementEnabled) {
controls.smartVault.cStarSettlementEnabled = false;
}
return controls;
}
export function getGruVaultControlsPath(): string {
return resolveControlsPath();
}
export function getDefaultGruVaultControls(): GruVaultControls {
return cloneDefaults();
}
export function loadGruVaultControls(): GruVaultControlsSnapshot {
const path = resolveControlsPath();
if (!existsSync(path)) {
return {
source: 'defaults',
path,
controls: cloneDefaults(),
};
}
try {
const raw = JSON.parse(readFileSync(path, 'utf8')) as unknown;
const updatedAt = typeof (raw as { updatedAt?: unknown })?.updatedAt === 'string'
? String((raw as { updatedAt?: unknown }).updatedAt)
: new Date().toISOString();
return {
source: 'file',
path,
controls: normalizeControls(raw, updatedAt),
};
} catch {
return {
source: 'defaults',
path,
controls: cloneDefaults(),
};
}
}
export function saveGruVaultControls(raw: unknown): GruVaultControlsSnapshot {
const path = resolveControlsPath();
const controls = normalizeControls(raw, new Date().toISOString());
mkdirSync(dirname(path), { recursive: true });
const tmp = `${path}.tmp`;
writeFileSync(tmp, `${JSON.stringify(controls, null, 2)}\n`, 'utf8');
renameSync(tmp, path);
return {
source: 'file',
path,
controls,
};
}
export function resetGruVaultControls(): GruVaultControlsSnapshot {
return saveGruVaultControls(cloneDefaults());
}

View File

@@ -68,6 +68,10 @@ export function getOmnlApiCatalog(): {
{ method: 'POST', path: '/omnl/terminal/alchemy/rpc', description: 'Alchemy JSON-RPC (read/estimate methods)', body: '{ network, method, params }', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/omnl/terminal/alchemy/prices', description: 'Alchemy token prices by address', body: '{ network, addresses[] }', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/omnl/terminal/alchemy/tx-prep', description: 'Prepare unsigned ERC-20 transfer calldata via Alchemy (dry-run default)', body: '{ network, from, to, tokenAddress, amount, dryRun }', auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/omnl/gru-vault-controls', description: 'GRU smart-vault runtime control snapshot', auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/omnl/gru-vault-controls/defaults', description: 'GRU smart-vault control defaults', auth: 'OMNL_API_KEY' },
{ method: 'PUT', path: '/omnl/gru-vault-controls', description: 'Persist GRU smart-vault runtime controls', body: '{ profile, smartVault, componentFamilies, dashboard }', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/omnl/gru-vault-controls/reset', description: 'Reset GRU smart-vault runtime controls to defaults', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/journal-entries', description: 'Create payout journal entry draft (IPSAS-validated pair)', auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/journal-entries/:id', description: 'Get journal entry by id', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/journal-entries/:id/post', description: 'Post journal entry to ledger path', auth: 'OMNL_API_KEY' },