Expose approval requests in admin logs
This commit is contained in:
@@ -327,6 +327,12 @@
|
||||
getLog: function(workspaceId, logId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/logs/' + encodeURIComponent(logId));
|
||||
},
|
||||
listApprovals: function(workspaceId, params) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/approvals' + query(params));
|
||||
},
|
||||
getApproval: function(workspaceId, approvalId) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/approvals/' + encodeURIComponent(approvalId));
|
||||
},
|
||||
getUsageOverview: function(workspaceId, params) {
|
||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/usage' + query(params));
|
||||
},
|
||||
|
||||
@@ -281,6 +281,27 @@ var TRANSLATIONS = {
|
||||
'logs.live.off.body': 'Automatic polling is paused.',
|
||||
'logs.refresh.title': 'Logs refreshed',
|
||||
'logs.refresh.body': 'The latest invocation records were loaded for the current workspace.',
|
||||
'approvals.title': 'Human confirmations',
|
||||
'approvals.subtitle': 'Requests waiting for an external user decision and recent results.',
|
||||
'approvals.refresh': 'Refresh',
|
||||
'approvals.refresh.title': 'Confirmations refreshed',
|
||||
'approvals.refresh.body': 'The latest confirmation requests were loaded.',
|
||||
'approvals.loading': 'Loading confirmation requests…',
|
||||
'approvals.empty': 'There are no confirmation requests yet.',
|
||||
'approvals.error.load': 'Failed to load confirmation requests',
|
||||
'approvals.untitled': 'Confirmation request',
|
||||
'approvals.operation': 'Operation',
|
||||
'approvals.agent': 'Agent',
|
||||
'approvals.expires_at': 'Expires',
|
||||
'approvals.updated_at': 'Updated',
|
||||
'approvals.request': 'Request',
|
||||
'approvals.response': 'Result',
|
||||
'approvals.status.pending': 'Pending',
|
||||
'approvals.status.approved': 'Approved',
|
||||
'approvals.status.denied': 'Denied',
|
||||
'approvals.status.expired': 'Expired',
|
||||
'approvals.status.completed': 'Completed',
|
||||
'approvals.status.failed': 'Failed',
|
||||
|
||||
// Usage page
|
||||
'usage.title': 'Usage',
|
||||
@@ -1127,6 +1148,27 @@ var TRANSLATIONS = {
|
||||
'logs.live.off.body': 'Автоматический опрос остановлен.',
|
||||
'logs.refresh.title': 'Логи обновлены',
|
||||
'logs.refresh.body': 'Получены последние записи вызовов для текущего воркспейса.',
|
||||
'approvals.title': 'Подтверждения человеком',
|
||||
'approvals.subtitle': 'Заявки, которые ожидают решения пользователя, и последние результаты.',
|
||||
'approvals.refresh': 'Обновить',
|
||||
'approvals.refresh.title': 'Подтверждения обновлены',
|
||||
'approvals.refresh.body': 'Получены последние заявки на подтверждение.',
|
||||
'approvals.loading': 'Загрузка заявок на подтверждение…',
|
||||
'approvals.empty': 'Заявок на подтверждение пока нет.',
|
||||
'approvals.error.load': 'Не удалось загрузить заявки на подтверждение',
|
||||
'approvals.untitled': 'Заявка на подтверждение',
|
||||
'approvals.operation': 'Операция',
|
||||
'approvals.agent': 'Агент',
|
||||
'approvals.expires_at': 'Истекает',
|
||||
'approvals.updated_at': 'Обновлено',
|
||||
'approvals.request': 'Запрос',
|
||||
'approvals.response': 'Результат',
|
||||
'approvals.status.pending': 'Ожидает',
|
||||
'approvals.status.approved': 'Подтверждено',
|
||||
'approvals.status.denied': 'Отклонено',
|
||||
'approvals.status.expired': 'Истекло',
|
||||
'approvals.status.completed': 'Выполнено',
|
||||
'approvals.status.failed': 'Ошибка',
|
||||
|
||||
// Usage page
|
||||
'usage.title': 'Использование',
|
||||
|
||||
+174
-4
@@ -11,9 +11,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
workspaceId: null,
|
||||
loading: false,
|
||||
loadError: '',
|
||||
approvals: [],
|
||||
approvalsLoading: false,
|
||||
approvalsError: '',
|
||||
};
|
||||
|
||||
var logList = document.getElementById('log-list');
|
||||
var approvalList = document.getElementById('approval-list');
|
||||
var approvalRefreshBtn = document.getElementById('approval-refresh-btn');
|
||||
var logSearch = document.getElementById('log-search');
|
||||
var refreshBtn = document.getElementById('refresh-btn');
|
||||
var timeRangeSel = document.getElementById('time-range');
|
||||
@@ -54,6 +59,19 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
return date.toISOString().slice(11, 23);
|
||||
}
|
||||
|
||||
function formatDateTime(timestamp) {
|
||||
if (!timestamp) {
|
||||
return '';
|
||||
}
|
||||
var date = new Date(timestamp);
|
||||
return date.toLocaleString(window.CrankLocale || undefined, {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function element(tag, className, text) {
|
||||
var node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
@@ -111,6 +129,115 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeApproval(record) {
|
||||
var approval = record.approval || record;
|
||||
return {
|
||||
id: approval.id,
|
||||
agentId: approval.agent_id,
|
||||
operationId: approval.operation_id,
|
||||
operationVersion: approval.operation_version,
|
||||
status: approval.status,
|
||||
riskLevel: approval.risk_level,
|
||||
title: approval.confirmation_title,
|
||||
body: approval.confirmation_body,
|
||||
requestPayload: approval.request_payload,
|
||||
responsePayload: approval.response_payload,
|
||||
createdAt: approval.created_at,
|
||||
expiresAt: approval.expires_at,
|
||||
decidedAt: approval.decided_at,
|
||||
note: approval.decision_note,
|
||||
};
|
||||
}
|
||||
|
||||
function approvalStatusLabel(status) {
|
||||
var key = 'approvals.status.' + status;
|
||||
var translated = tKey(key);
|
||||
return translated === key ? status : translated;
|
||||
}
|
||||
|
||||
function renderApprovals() {
|
||||
if (!approvalList) {
|
||||
return;
|
||||
}
|
||||
|
||||
approvalList.innerHTML = '';
|
||||
|
||||
if (state.approvalsLoading && state.approvals.length === 0) {
|
||||
var loading = element('div', 'approval-empty', tKey('approvals.loading'));
|
||||
approvalList.appendChild(loading);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.approvalsError) {
|
||||
var error = element('div', 'approval-empty approval-empty-error', state.approvalsError);
|
||||
approvalList.appendChild(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.approvals.length) {
|
||||
var empty = element('div', 'approval-empty', tKey('approvals.empty'));
|
||||
approvalList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
var fragment = document.createDocumentFragment();
|
||||
state.approvals.forEach(function (item) {
|
||||
var card = element('article', 'approval-item approval-' + item.status);
|
||||
|
||||
var header = element('div', 'approval-item-header');
|
||||
var titleWrap = element('div', 'approval-item-title-wrap');
|
||||
titleWrap.appendChild(element('div', 'approval-item-title', item.title || tKey('approvals.untitled')));
|
||||
|
||||
var meta = element('div', 'approval-item-meta');
|
||||
meta.textContent = [
|
||||
tKey('approvals.operation') + ': ' + item.operationId + ' v' + item.operationVersion,
|
||||
tKey('approvals.agent') + ': ' + item.agentId,
|
||||
].join(' · ');
|
||||
titleWrap.appendChild(meta);
|
||||
header.appendChild(titleWrap);
|
||||
|
||||
var badge = element('span', 'approval-status approval-status-' + item.status, approvalStatusLabel(item.status));
|
||||
header.appendChild(badge);
|
||||
card.appendChild(header);
|
||||
|
||||
if (item.body) {
|
||||
card.appendChild(element('p', 'approval-item-body', item.body));
|
||||
}
|
||||
|
||||
var timing = element('div', 'approval-timing');
|
||||
timing.textContent = item.status === 'pending'
|
||||
? tKey('approvals.expires_at') + ': ' + formatDateTime(item.expiresAt)
|
||||
: tKey('approvals.updated_at') + ': ' + formatDateTime(item.decidedAt || item.createdAt);
|
||||
card.appendChild(timing);
|
||||
|
||||
var payloadGrid = element('div', 'approval-payload-grid');
|
||||
var requestBlock = element('div', 'approval-payload');
|
||||
requestBlock.appendChild(element('div', 'approval-payload-label', tKey('approvals.request')));
|
||||
var requestPre = element('pre', 'approval-payload-code');
|
||||
requestPre.textContent = formatJson(item.requestPayload);
|
||||
requestBlock.appendChild(requestPre);
|
||||
payloadGrid.appendChild(requestBlock);
|
||||
|
||||
if (item.responsePayload !== null && item.responsePayload !== undefined) {
|
||||
var responseBlock = element('div', 'approval-payload');
|
||||
responseBlock.appendChild(element('div', 'approval-payload-label', tKey('approvals.response')));
|
||||
var responsePre = element('pre', 'approval-payload-code');
|
||||
responsePre.textContent = formatJson(item.responsePayload);
|
||||
responseBlock.appendChild(responsePre);
|
||||
payloadGrid.appendChild(responseBlock);
|
||||
}
|
||||
card.appendChild(payloadGrid);
|
||||
|
||||
if (item.note) {
|
||||
card.appendChild(element('div', 'approval-note', item.note));
|
||||
}
|
||||
|
||||
fragment.appendChild(card);
|
||||
});
|
||||
|
||||
approvalList.appendChild(fragment);
|
||||
}
|
||||
|
||||
function renderEmpty(title, message) {
|
||||
logList.innerHTML = '';
|
||||
var empty = element('div', 'empty-state');
|
||||
@@ -306,6 +433,39 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadApprovals() {
|
||||
if (!window.CrankApi) {
|
||||
state.approvalsError = tKey('logs.error.api');
|
||||
renderApprovals();
|
||||
return;
|
||||
}
|
||||
|
||||
state.workspaceId = currentWorkspaceId();
|
||||
if (!state.workspaceId) {
|
||||
state.approvalsError = tKey('logs.error.workspace');
|
||||
renderApprovals();
|
||||
return;
|
||||
}
|
||||
|
||||
state.approvalsLoading = true;
|
||||
state.approvalsError = '';
|
||||
renderApprovals();
|
||||
|
||||
try {
|
||||
var response = await window.CrankApi.listApprovals(state.workspaceId, { limit: 20 });
|
||||
state.approvals = (response && response.items ? response.items : []).map(normalizeApproval);
|
||||
} catch (error) {
|
||||
state.approvalsError = error.message || tKey('approvals.error.load');
|
||||
} finally {
|
||||
state.approvalsLoading = false;
|
||||
renderApprovals();
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshOperationalData() {
|
||||
await Promise.all([loadLogs(), loadApprovals()]);
|
||||
}
|
||||
|
||||
async function loadLogDetail(logId) {
|
||||
if (!window.CrankApi || !state.workspaceId || state.details[logId]) {
|
||||
return;
|
||||
@@ -343,7 +503,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (!state.liveMode) {
|
||||
return;
|
||||
}
|
||||
state.timer = setInterval(loadLogs, 4000);
|
||||
state.timer = setInterval(refreshOperationalData, 4000);
|
||||
}
|
||||
|
||||
function toggleLive() {
|
||||
@@ -386,6 +546,16 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
}
|
||||
|
||||
if (approvalRefreshBtn) {
|
||||
approvalRefreshBtn.addEventListener('click', function () {
|
||||
loadApprovals().then(function () {
|
||||
if (!state.approvalsError && window.CrankUi) {
|
||||
window.CrankUi.info(tKey('approvals.refresh.body'), tKey('approvals.refresh.title'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (timeRangeSel) {
|
||||
timeRangeSel.value = state.period;
|
||||
timeRangeSel.addEventListener('change', function () {
|
||||
@@ -405,15 +575,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
window.addEventListener('crank:workspacechange', function () {
|
||||
state.details = {};
|
||||
state.openId = null;
|
||||
loadLogs();
|
||||
refreshOperationalData();
|
||||
});
|
||||
|
||||
setLiveState();
|
||||
startPolling();
|
||||
|
||||
if (window.whenWorkspacesReady) {
|
||||
window.whenWorkspacesReady().finally(loadLogs);
|
||||
window.whenWorkspacesReady().finally(refreshOperationalData);
|
||||
} else {
|
||||
loadLogs();
|
||||
refreshOperationalData();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user