Files
crank/apps/ui/js/logs.js
T
github-ops 8b8f2fc6c5
CI / UI Checks (push) Has been cancelled
CI / Frontend E2E (push) Has been cancelled
CI / Deployment Manifests (push) Has been cancelled
CI / Deploy (push) Has been cancelled
CI / Rust Checks (push) Has been cancelled
Expose approval requests in admin logs
2026-06-24 13:51:54 +00:00

590 lines
18 KiB
JavaScript

document.addEventListener('DOMContentLoaded', function () {
var state = {
logs: [],
details: {},
level: 'all',
search: '',
period: '7d',
openId: null,
liveMode: true,
timer: null,
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');
var liveDot = document.querySelector('.live-dot');
var liveLabel = document.querySelector('.live-label');
function tKey(key) {
return window.t ? t(key) : key;
}
function currentWorkspaceId() {
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
return workspace ? workspace.id : null;
}
function durationLabel(durationMs) {
if (!durationMs) {
return '0ms';
}
if (durationMs >= 1000) {
return (durationMs / 1000).toFixed(1) + 's';
}
return durationMs + 'ms';
}
function formatJson(value) {
if (value === null || value === undefined) {
return '';
}
if (typeof value === 'string') {
return value;
}
return JSON.stringify(value, null, 2);
}
function formatTime(timestamp) {
var date = new Date(timestamp);
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;
if (text !== undefined && text !== null) node.textContent = text;
return node;
}
function statusClass(statusCode) {
if (!statusCode) {
return '';
}
if (statusCode < 300) {
return 'ok';
}
if (statusCode < 500) {
return 'warn';
}
return 'err';
}
function normalizeLog(record) {
var log = record.log;
var agent = window.localizeDemoAgent
? window.localizeDemoAgent({
slug: record.agent_slug,
display_name: record.agent_display_name,
description: '',
})
: null;
var operation = window.localizeDemoOperation
? window.localizeDemoOperation({
name: record.operation_name,
operation_display_name: record.operation_display_name,
})
: null;
return {
id: log.id,
createdAt: log.created_at,
level: log.level,
source: log.source,
status: log.status,
statusCode: log.status_code,
durationMs: log.duration_ms,
toolName: log.tool_name,
message: log.message,
operationName: record.operation_name,
operationDisplayName: operation ? operation.operation_display_name : record.operation_display_name,
agentSlug: record.agent_slug,
agentDisplayName: agent ? agent.display_name : record.agent_display_name,
requestPreview: log.request_preview,
responsePreview: log.response_preview,
errorKind: log.error_kind,
requestId: log.request_id,
};
}
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');
empty.appendChild(element('div', 'empty-state-title', title));
empty.appendChild(element('div', 'empty-state-text', message));
logList.appendChild(empty);
}
function renderLogs() {
if (state.loading && state.logs.length === 0) {
renderEmpty(tKey('logs.loading.title'), tKey('logs.loading.sub'));
return;
}
if (state.loadError) {
renderEmpty(tKey('logs.error.title'), state.loadError);
return;
}
if (!state.logs.length) {
renderEmpty(
tKey('logs.empty.title'),
state.search || state.level !== 'all'
? tKey('logs.empty.filtered')
: tKey('logs.empty.initial')
);
return;
}
var fragment = document.createDocumentFragment();
state.logs.forEach(function (item) {
var row = document.createElement('div');
row.className = 'log-entry';
row.setAttribute('data-id', item.id);
var timeEl = document.createElement('span');
timeEl.className = 'log-time';
timeEl.textContent = formatTime(item.createdAt);
row.appendChild(timeEl);
var levelEl = document.createElement('span');
levelEl.className = 'log-level ' + item.level;
levelEl.textContent = item.level.toUpperCase();
row.appendChild(levelEl);
var msgDiv = document.createElement('div');
msgDiv.className = 'log-msg';
var opSpan = document.createElement('span');
opSpan.className = 'log-op-name';
opSpan.textContent = item.operationDisplayName || item.operationName;
msgDiv.appendChild(opSpan);
msgDiv.appendChild(document.createTextNode('\u00a0\u00a0'));
if (item.statusCode) {
var statusEl = document.createElement('span');
statusEl.className = 'log-status ' + statusClass(item.statusCode);
statusEl.textContent = item.statusCode;
msgDiv.appendChild(statusEl);
msgDiv.appendChild(document.createTextNode(' \u00b7 '));
}
msgDiv.appendChild(document.createTextNode(item.message));
row.appendChild(msgDiv);
var durationEl = document.createElement('span');
var durationClass = item.level === 'error' ? ' error' : (item.durationMs >= 1000 ? ' slow' : '');
durationEl.className = 'log-duration' + durationClass;
durationEl.textContent = durationLabel(item.durationMs);
row.appendChild(durationEl);
fragment.appendChild(row);
var expanded = document.createElement('div');
expanded.className = 'log-entry-expanded' + (item.id === state.openId ? ' open' : '');
expanded.setAttribute('data-exp', item.id);
if (item.id === state.openId) {
var detail = state.details[item.id] || item;
if (detail.agentDisplayName || detail.agentSlug) {
var agentLabel = document.createElement('div');
agentLabel.className = 'log-detail-label';
agentLabel.textContent = tKey('logs.detail.agent');
expanded.appendChild(agentLabel);
var agentPre = document.createElement('pre');
agentPre.className = 'log-detail-block';
agentPre.textContent = detail.agentDisplayName || detail.agentSlug;
expanded.appendChild(agentPre);
}
var requestLabel = document.createElement('div');
requestLabel.className = 'log-detail-label';
requestLabel.textContent = tKey('logs.detail.request');
expanded.appendChild(requestLabel);
var requestPre = document.createElement('pre');
requestPre.className = 'log-detail-block';
requestPre.textContent = formatJson(detail.requestPreview);
expanded.appendChild(requestPre);
var responseLabel = document.createElement('div');
responseLabel.className = 'log-detail-label';
responseLabel.textContent = tKey('logs.detail.response');
expanded.appendChild(responseLabel);
var responsePre = document.createElement('pre');
responsePre.className = 'log-detail-block';
responsePre.textContent = formatJson(detail.responsePreview);
expanded.appendChild(responsePre);
if (detail.errorKind || detail.requestId) {
var metaLabel = document.createElement('div');
metaLabel.className = 'log-detail-label';
metaLabel.textContent = tKey('logs.detail.meta');
expanded.appendChild(metaLabel);
var metaPre = document.createElement('pre');
metaPre.className = 'log-detail-block';
metaPre.textContent = formatJson({
request_id: detail.requestId || null,
error_kind: detail.errorKind || null,
source: detail.source,
status: detail.status,
});
expanded.appendChild(metaPre);
}
}
fragment.appendChild(expanded);
});
logList.innerHTML = '';
logList.appendChild(fragment);
logList.querySelectorAll('.log-entry').forEach(function (row) {
row.addEventListener('click', async function () {
var id = this.getAttribute('data-id');
state.openId = state.openId === id ? null : id;
renderLogs();
if (state.openId) {
await loadLogDetail(id);
}
});
});
}
function queryParams() {
var params = {
period: state.period,
limit: 100,
};
if (state.level !== 'all') {
params.level = state.level;
}
if (state.search) {
params.search = state.search;
}
return params;
}
async function loadLogs() {
if (!window.CrankApi) {
state.loadError = tKey('logs.error.api');
renderLogs();
return;
}
state.workspaceId = currentWorkspaceId();
if (!state.workspaceId) {
state.loadError = tKey('logs.error.workspace');
renderLogs();
return;
}
state.loading = true;
state.loadError = '';
renderLogs();
try {
var response = await window.CrankApi.listLogs(state.workspaceId, queryParams());
state.logs = (response && response.items ? response.items : []).map(normalizeLog);
if (state.openId && !state.logs.some(function (item) { return item.id === state.openId; })) {
state.openId = null;
}
} catch (error) {
state.loadError = error.message || tKey('logs.error.load');
} finally {
state.loading = false;
renderLogs();
}
}
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;
}
try {
var record = await window.CrankApi.getLog(state.workspaceId, logId);
state.details[logId] = normalizeLog(record);
if (state.openId === logId) {
renderLogs();
}
} catch (_error) {
}
}
function setLiveState() {
if (liveDot) {
liveDot.classList.toggle('is-paused', !state.liveMode);
}
if (liveLabel) {
liveLabel.textContent = state.liveMode ? tKey('logs.live') : tKey('logs.paused');
liveLabel.classList.toggle('is-paused', !state.liveMode);
}
}
function stopPolling() {
if (state.timer) {
clearInterval(state.timer);
state.timer = null;
}
}
function startPolling() {
stopPolling();
if (!state.liveMode) {
return;
}
state.timer = setInterval(refreshOperationalData, 4000);
}
function toggleLive() {
state.liveMode = !state.liveMode;
setLiveState();
startPolling();
if (window.CrankUi) {
window.CrankUi.info(
state.liveMode ? tKey('logs.live.on.body') : tKey('logs.live.off.body'),
state.liveMode ? tKey('logs.live.on.title') : tKey('logs.live.off.title')
);
}
}
document.querySelectorAll('.filter-chip[data-level]').forEach(function (button) {
button.addEventListener('click', function () {
state.level = this.getAttribute('data-level');
document.querySelectorAll('.filter-chip[data-level]').forEach(function (item) {
item.classList.remove('active');
});
this.classList.add('active');
loadLogs();
});
});
if (logSearch) {
logSearch.addEventListener('input', function () {
state.search = this.value.trim();
loadLogs();
});
}
if (refreshBtn) {
refreshBtn.addEventListener('click', function () {
loadLogs().then(function () {
if (!state.loadError && window.CrankUi) {
window.CrankUi.info(tKey('logs.refresh.body'), tKey('logs.refresh.title'));
}
});
});
}
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 () {
state.period = this.value;
loadLogs();
});
}
if (liveDot) {
liveDot.addEventListener('click', toggleLive);
}
if (liveLabel) {
liveLabel.addEventListener('click', toggleLive);
}
window.addEventListener('crank:workspacechange', function () {
state.details = {};
state.openId = null;
refreshOperationalData();
});
setLiveState();
startPolling();
if (window.whenWorkspacesReady) {
window.whenWorkspacesReady().finally(refreshOperationalData);
} else {
refreshOperationalData();
}
});