Add scope receipts approval UI

This commit is contained in:
Codex committed 2026-07-12 09:50:43 -04:00
1 parent d500a29186
commit 964fe5b3ba
2 files changed
+152 -25

No files matched your search

+145 -24
View File
@@ -3305,6 +3305,10 @@
<div class="nav-item active" data-page="warroom">
<span class="icon">⚔️</span> War Room
</div>
<div class="nav-item" data-page="receipts">
<span class="icon">🧾</span> Scope Receipts
<span class="badge" id="pendingReceiptCount">0</span>
</div>
<div class="nav-item" data-page="operators">
<span class="icon">🤖</span> Operatives
<span class="badge" id="activeOperatorCount">0</span>
@@ -4173,6 +4177,28 @@
<div id="targetList" style="display: none;"></div>
</div>
<!-- Scope Receipts - explicit approvals for external/live execution -->
<div class="page" id="page-receipts">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:18px;flex-wrap:wrap;">
<div>
<div style="font-size:10px;color:var(--accent);text-transform:uppercase;letter-spacing:2px;margin-bottom:6px;">ScopeGuard</div>
<div style="font-size:24px;font-weight:800;color:var(--text-primary);line-height:1.15;">Scope Receipts</div>
</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<button class="btn btn-secondary btn-sm" onclick="refreshReceiptsPage()">Refresh</button>
<button class="btn btn-secondary btn-sm" onclick="approveAllPendingReceipts()">Approve Pending</button>
</div>
</div>
<div id="receiptsSummary" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;margin-bottom:14px;"></div>
<section style="background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;overflow:hidden;">
<div style="padding:12px 14px;border-bottom:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center;gap:10px;">
<div style="font-size:11px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:1px;">Approval Requests</div>
<div id="receiptsCount" style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--accent);">0 receipts</div>
</div>
<div id="receiptsList" style="max-height:min(62vh,680px);overflow:auto;"></div>
</section>
</div>
<!-- Operators - Tactical Unit Command -->
<div class="page" id="page-operators">
<!-- ═══ OPERATIVES ROSTER — click any specialist to view + edit its prompt / tools / config ═══ -->
@@ -6907,13 +6933,106 @@ Correlating findings across agents, identifying patterns, producing actionable i
}));
}
const ReceiptsState = { approvals: [] };
function receiptStatusColor(status) {
const s = String(status || '').toLowerCase();
if (s === 'approved') return '#00ff88';
if (s === 'pending') return '#ffaa00';
if (s === 'rejected' || s === 'expired') return '#ff6666';
return 'var(--text-secondary)';
}
window.refreshReceiptsPage = async function refreshReceiptsPage() {
const data = await T3MP3ST_API.get('/api/approvals');
ReceiptsState.approvals = Array.isArray(data.approvals) ? data.approvals : [];
renderReceiptsPage();
};
window.approveReceipt = async function approveReceipt(id) {
await T3MP3ST_API.post('/api/approvals/' + id + '/approve', {});
await window.refreshReceiptsPage();
if (window.toast) toast('Receipt approved: ' + id, 'success');
};
window.rejectReceipt = async function rejectReceipt(id) {
await T3MP3ST_API.post('/api/approvals/' + id + '/reject', {});
await window.refreshReceiptsPage();
if (window.toast) toast('Receipt rejected: ' + id, 'warning');
};
window.approveAllPendingReceipts = async function approveAllPendingReceipts() {
const pending = ReceiptsState.approvals.filter(function(a){ return a.status === 'pending'; });
await Promise.all(pending.map(function(a){ return T3MP3ST_API.post('/api/approvals/' + a.id + '/approve', {}); }));
await window.refreshReceiptsPage();
if (window.toast) toast('Approved ' + pending.length + ' pending receipt(s)', pending.length ? 'success' : 'info');
};
function renderReceiptsPage() {
const approvals = ReceiptsState.approvals || [];
const pending = approvals.filter(function(a){ return a.status === 'pending'; }).length;
const approved = approvals.filter(function(a){ return a.status === 'approved'; }).length;
const expired = approvals.filter(function(a){ return a.status === 'expired' || a.status === 'rejected'; }).length;
const badge = document.getElementById('pendingReceiptCount');
if (badge) {
badge.textContent = String(pending);
badge.style.background = pending ? '#ffaa00' : 'rgba(255,255,255,0.12)';
badge.style.color = pending ? '#1b1200' : '';
}
const summary = document.getElementById('receiptsSummary');
if (summary) {
const cells = [
['Pending', pending, pending ? '#ffaa00' : 'var(--text-secondary)'],
['Approved', approved, '#00ff88'],
['Closed', expired, expired ? '#ff6666' : 'var(--text-secondary)'],
['Total', approvals.length, '#00aaff']
];
summary.innerHTML = cells.map(function(cell){
return '<div style="background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;padding:12px;min-height:74px;">'
+ '<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;">' + escapeHtml(cell[0]) + '</div>'
+ '<div style="font-family:&quot;JetBrains Mono&quot;,monospace;font-size:18px;color:' + cell[2] + ';">' + escapeHtml(cell[1]) + '</div>'
+ '</div>';
}).join('');
}
const count = document.getElementById('receiptsCount');
if (count) count.textContent = approvals.length + ' receipts';
const list = document.getElementById('receiptsList');
if (!list) return;
if (!approvals.length) {
list.innerHTML = '<div style="padding:18px;color:var(--text-muted);font-size:12px;">No receipts yet. Live external actions will create target-scoped approval requests here.</div>';
return;
}
list.innerHTML = approvals.map(function(a){
const color = receiptStatusColor(a.status);
const pendingActions = a.status === 'pending'
? '<div style="display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap;"><button class="btn btn-primary btn-sm" onclick="approveReceipt(&quot;' + escapeHtml(a.id) + '&quot;)">Approve</button><button class="btn btn-secondary btn-sm" onclick="rejectReceipt(&quot;' + escapeHtml(a.id) + '&quot;)">Reject</button></div>'
: '';
return '<div style="padding:12px 14px;border-bottom:1px solid rgba(255,255,255,0.06);display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:start;">'
+ '<div style="min-width:0;">'
+ '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:6px;"><span style="font-family:&quot;JetBrains Mono&quot;,monospace;font-size:10px;color:var(--text-muted);">' + escapeHtml(a.id) + '</span><span style="font-family:&quot;JetBrains Mono&quot;,monospace;font-size:10px;color:' + color + ';text-transform:uppercase;">' + escapeHtml(a.status || 'unknown') + '</span></div>'
+ '<div style="font-size:13px;color:var(--text-primary);font-weight:700;overflow-wrap:anywhere;">' + escapeHtml(a.target || '-') + '</div>'
+ '<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">' + escapeHtml(a.action || '-') + ' · ' + escapeHtml(a.requestedBy || '-') + '</div>'
+ '<div style="font-size:11px;color:var(--text-secondary);margin-top:7px;line-height:1.45;overflow-wrap:anywhere;">' + escapeHtml(a.reason || '') + '</div>'
+ (a.expiresAt ? '<div style="font-size:10px;color:var(--text-muted);margin-top:7px;">Expires ' + escapeHtml(new Date(a.expiresAt).toLocaleString()) + '</div>' : '')
+ '</div>'
+ pendingActions
+ '</div>';
}).join('');
}
setInterval(() => {
if (document.getElementById('page-receipts')?.classList.contains('active')) {
window.refreshReceiptsPage?.();
}
}, 4000);
// Navigation
function navigateTo(page) {
document.querySelectorAll('.nav-item').forEach(i => i.classList.toggle('active', i.dataset.page === page));
document.querySelectorAll('.page').forEach(p => p.classList.toggle('active', p.id === `page-${page}`));
const titles = { warroom: 'War Room', operators: 'Operatives', evidence: 'Evidence Vault', arsenal: 'Arsenal', terminal: 'Terminal', benchmarks: 'Benchmarks', configs: 'Config Library', 'ctf-range': 'CTF Range', general: 'Op Admiral', selfimprove: 'Self-Improvement', settings: 'Settings', about: 'About' };
const titles = { warroom: 'War Room', receipts: 'Scope Receipts', operators: 'Operatives', evidence: 'Evidence Vault', arsenal: 'Arsenal', terminal: 'Terminal', benchmarks: 'Benchmarks', configs: 'Config Library', 'ctf-range': 'CTF Range', general: 'Op Admiral', selfimprove: 'Self-Improvement', settings: 'Settings', about: 'About' };
document.getElementById('pageTitle').textContent = titles[page] || 'War Room';
document.getElementById('sidebar').classList.remove('open');
if (page === 'receipts') setTimeout(() => window.refreshReceiptsPage?.(), 50);
if (page === 'operators' && typeof window.loadOperativesRoster === 'function') setTimeout(window.loadOperativesRoster, 80);
if (page === 'evidence' && typeof window.hydrateFindings === 'function') setTimeout(window.hydrateFindings, 60);
if (page === 'selfimprove' && typeof window.renderSelfImprove === 'function') setTimeout(window.renderSelfImprove, 60);
@@ -25147,6 +25266,10 @@ Be specific and actionable. Format as structured JSON.`;
if (typeof updateSitRep === 'function') updateSitRep('reconnaissance', '⚓ Op Admiral is planning the ' + t.label + ' operation on ' + tgt + '…', 'ACTIVE');
if (typeof addIntel === 'function') addIntel('ADMIRAL', '⚓ Brief handed to Op Admiral — planning ' + brief.family + ' on ' + tgt + ' (~30-90s)', 'info');
if (window.toast) toast('⚓ Op Admiral is planning the operation…', 'info');
const admiralApiBase = (typeof getApiBase === 'function')
? getApiBase()
: ((typeof T3MP3ST_API !== 'undefined' && T3MP3ST_API.baseUrl) || '');
const admiralFetch = function(path, opts){ return fetch(admiralApiBase + path, opts); };
const body = function(extra){ return JSON.stringify(Object.assign({ brief: brief, confirmed: true }, bb, extra || {})); };
// Op Admiral planning is a slow LLM call, and on success the SERVER brings up a LIVE
// mission. The server budgets up to 300s for planning, so the client cap MUST exceed that
@@ -25156,31 +25279,29 @@ Be specific and actionable. Format as structured JSON.`;
// cautious rather than declaring failure.
const launchOpts = function(extra){ return { method:'POST', headers:{'Content-Type':'application/json'}, body: body(extra), signal: AbortSignal.timeout(330000) }; };
try {
let res = await fetch('/api/admiral/launch', launchOpts());
let approvalIds = [];
let res = await admiralFetch('/api/admiral/launch', launchOpts());
let d = await res.json();
// autonomous-execution approval receipt — auto-grant for loopback/lab or Autonomous mode, else require it.
// The server gates this with HTTP 403 (blockForApproval); the HOLD gate below is 409.
if (res.status === 403 && d.approval && d.approval.id) {
if (isLab || autonomous) {
await fetch('/api/approvals/' + d.approval.id + '/approve', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' });
res = await fetch('/api/admiral/launch', launchOpts({ approvalId: d.approval.id }));
d = await res.json();
} else {
throw new Error('Approval required for ' + tgt + ' — choose Autonomous mode, or approve in the receipts panel.');
}
}
if (res.status === 403 && Array.isArray(d.approvals) && d.approvals.length) {
const approvalIds = d.approvals.map(function(a){ return a && a.id; }).filter(Boolean);
const targets = (d.outOfScopeTargets || []).join(', ');
if (isLab || autonomous) {
await Promise.all(approvalIds.map(function(id){
return fetch('/api/approvals/' + id + '/approve', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' });
}));
res = await fetch('/api/admiral/launch', launchOpts({ approvalIds: approvalIds }));
d = await res.json();
} else {
throw new Error('Expanded target approval required for ' + (targets || 'planner-added targets') + ' — approve the receipts or choose Autonomous mode.');
for (let approvalRound = 0; approvalRound < 5 && res.status === 403; approvalRound++) {
const requested = [];
if (d.approval && d.approval.id) requested.push(d.approval);
if (Array.isArray(d.approvals)) d.approvals.forEach(function(a){ if (a && a.id) requested.push(a); });
if (!requested.length) break;
const requestedIds = requested.map(function(a){ return a.id; }).filter(Boolean);
const targets = (d.outOfScopeTargets || requested.map(function(a){ return a.target; }).filter(Boolean)).join(', ');
if (!(isLab || autonomous)) {
addIntel?.('SCOPEGUARD', 'Approval required: ' + (targets || tgt) + ' — open Scope Receipts', 'warning');
navigateTo?.('receipts');
throw new Error((d.error || 'Approval required') + ' — open Scope Receipts to approve ' + (targets || 'pending targets') + '.');
}
await Promise.all(requestedIds.map(function(id){
return admiralFetch('/api/approvals/' + id + '/approve', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' });
}));
approvalIds = approvalIds.concat(requestedIds).filter(function(id, idx, arr){ return id && arr.indexOf(id) === idx; });
addIntel?.('SCOPEGUARD', 'Auto-approved ' + requestedIds.length + ' receipt(s): ' + (targets || requestedIds.join(', ')), 'success');
await window.refreshReceiptsPage?.();
res = await admiralFetch('/api/admiral/launch', launchOpts({ approvalIds: approvalIds }));
d = await res.json();
}
if (res.status === 409 && d.review && d.review.status === 'hold') throw new Error('Op Admiral plan gate is HOLD — ' + ((d.review && d.review.reason) || 'manual review required before execution'));
if (!res.ok || d.error) throw new Error(d.error || ('launch failed (' + res.status + ')'));