Resource: status header (status + runtime_type + host + clickable URL), three-column row of Dependencies (with add-by-UUID form) / Source docs / Runbook pages (filters /api/links/to/resource/:id for from_type=page and relation=runbook, then fetches each page title). Change history card pulls /api/resources/:id/changes. Inbox: groups pending changes by agent. Each row shows entity type badge + action + reason, with a collapsible payload disclosure. Approve calls /api/pending-changes/:id/approve and, if the response carries entity_id, navigates to the resulting detail view. Reject just re-fetches. Both update the shared pending-count emitted to state.js so the sidebar and topbar badges drop immediately. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
94 lines
3.0 KiB
JavaScript
94 lines
3.0 KiB
JavaScript
// Inbox: pending changes grouped by agent. Approve/reject calls the API
|
|
// then re-fetches. On approve, navigate to the resulting entity_id if
|
|
// the response carries one.
|
|
|
|
import { api } from '../api.js';
|
|
import { el, mount, clear } from '../dom.js';
|
|
import { navigate } from '../router.js';
|
|
import { emit } from '../state.js';
|
|
|
|
const ROUTE_BY_TYPE = {
|
|
page: id => '#/page/' + id,
|
|
project: id => '#/project/' + id,
|
|
task: () => '#/',
|
|
ref: id => '#/ref/' + id,
|
|
resource: id => '#/resource/' + id,
|
|
source_doc: () => '#/',
|
|
space: id => '#/space/' + id
|
|
};
|
|
|
|
function diffBlock(diff) {
|
|
if (!diff) return null;
|
|
return el('pre', {}, JSON.stringify(diff, null, 2));
|
|
}
|
|
|
|
function pendingRow(row, onActed) {
|
|
const meta = el('p', { style: { margin: '0 0 6px' } },
|
|
el('span', { class: 'status idle' }, row.entity_type),
|
|
' ',
|
|
el('span', { class: 'muted' }, row.action),
|
|
row.reason ? el('span', { class: 'muted' }, ' — ' + row.reason) : null
|
|
);
|
|
|
|
async function approve() {
|
|
try {
|
|
const res = await api.post(`/api/pending-changes/${row.id}/approve`);
|
|
onActed();
|
|
const route = ROUTE_BY_TYPE[row.entity_type];
|
|
if (res.entity_id && route) navigate(route(res.entity_id));
|
|
} catch (e) { alert('approve failed: ' + e.message); }
|
|
}
|
|
async function reject() {
|
|
try { await api.post(`/api/pending-changes/${row.id}/reject`); onActed(); }
|
|
catch (e) { alert('reject failed: ' + e.message); }
|
|
}
|
|
|
|
return el('div', { class: 'card', style: { marginBottom: '10px' } },
|
|
meta,
|
|
el('details', {},
|
|
el('summary', { class: 'muted', style: { cursor: 'pointer', fontSize: '11px' } }, 'payload'),
|
|
el('pre', {}, JSON.stringify(row.payload, null, 2))
|
|
),
|
|
diffBlock(null),
|
|
el('div', { style: { display: 'flex', gap: '8px', marginTop: '8px' } },
|
|
el('button', { class: 'primary', onclick: approve }, 'Approve'),
|
|
el('button', { class: 'ghost', onclick: reject }, 'Reject')
|
|
)
|
|
);
|
|
}
|
|
|
|
async function refresh(container) {
|
|
let rows = [];
|
|
try { rows = await api.get('/api/pending-changes?limit=200'); }
|
|
catch (e) {
|
|
clear(container);
|
|
container.appendChild(el('p', { class: 'muted' }, 'Could not load: ' + e.message));
|
|
return;
|
|
}
|
|
emit('pending-count', rows.length);
|
|
clear(container);
|
|
if (!rows.length) {
|
|
container.appendChild(el('p', { class: 'muted' }, 'Inbox is empty.'));
|
|
return;
|
|
}
|
|
const byAgent = new Map();
|
|
for (const r of rows) {
|
|
if (!byAgent.has(r.agent_id)) byAgent.set(r.agent_id, []);
|
|
byAgent.get(r.agent_id).push(r);
|
|
}
|
|
for (const [agentId, items] of byAgent) {
|
|
container.appendChild(el('div', { class: 'sb-title' }, 'Agent ' + agentId.slice(0, 8)));
|
|
for (const row of items) container.appendChild(pendingRow(row, () => refresh(container)));
|
|
}
|
|
}
|
|
|
|
export async function render(main) {
|
|
const wrap = el('div');
|
|
mount(main,
|
|
el('h1', { class: 'view-h1' }, 'Inbox'),
|
|
el('p', { class: 'view-sub' }, 'Pending agent suggestions awaiting your call.'),
|
|
wrap
|
|
);
|
|
refresh(wrap);
|
|
}
|