903 lines
28 KiB
JavaScript
903 lines
28 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function () {
|
|
var state = {
|
|
logs: [],
|
|
details: {},
|
|
level: 'all',
|
|
status: 'all',
|
|
outcomeGroup: 'all',
|
|
operationId: '',
|
|
agentId: '',
|
|
search: '',
|
|
period: '7d',
|
|
openId: null,
|
|
nextCursor: null,
|
|
liveMode: true,
|
|
timer: null,
|
|
searchTimer: null,
|
|
refreshPromise: null,
|
|
workspaceId: null,
|
|
loading: false,
|
|
loadError: '',
|
|
approvals: [],
|
|
approvalsLoading: false,
|
|
approvalsError: '',
|
|
approvalActionBusy: {},
|
|
approvalsEpoch: 0,
|
|
logsEpoch: 0,
|
|
exporting: false,
|
|
};
|
|
|
|
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 exportLogsBtn = document.getElementById('export-logs-btn');
|
|
var loadMoreBtn = document.getElementById('load-more-logs-btn');
|
|
var timeRangeSel = document.getElementById('time-range');
|
|
var statusFilter = document.getElementById('status-filter');
|
|
var outcomeFilter = document.getElementById('outcome-filter');
|
|
var operationFilter = document.getElementById('operation-filter');
|
|
var agentFilter = document.getElementById('agent-filter');
|
|
var liveDot = document.querySelector('.live-dot');
|
|
var liveLabel = document.querySelector('.live-label');
|
|
var initialParams = new URLSearchParams(window.location.search);
|
|
var deepLinkLogId = /^[A-Za-z0-9_-]{1,128}$/.test(initialParams.get('log_id') || '') ? initialParams.get('log_id') : '';
|
|
var deepLinkAgentId = /^[A-Za-z0-9_-]{1,128}$/.test(initialParams.get('agent_id') || '') ? initialParams.get('agent_id') : '';
|
|
var deepLinkOperationId = /^[A-Za-z0-9_-]{1,128}$/.test(initialParams.get('operation_id') || '') ? initialParams.get('operation_id') : '';
|
|
state.agentId = deepLinkAgentId;
|
|
state.operationId = deepLinkOperationId;
|
|
|
|
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,
|
|
operationVersion: log.operation_version,
|
|
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,
|
|
executionStage: log.execution_stage,
|
|
executionErrorCode: log.execution_error_code,
|
|
requestId: log.request_id,
|
|
traceId: log.trace_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,
|
|
requestPayload: approval.request_payload,
|
|
responsePayload: approval.response_payload,
|
|
requestId: approval.request_id,
|
|
traceId: approval.trace_id,
|
|
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', tKey('approvals.untitled') + ' ' + item.id));
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
if (item.requestId || item.traceId) {
|
|
var correlation = element('div', 'approval-correlation');
|
|
if (item.requestId) {
|
|
correlation.appendChild(element('span', 'approval-correlation-id', tKey('approvals.request_id') + ': ' + item.requestId));
|
|
}
|
|
if (item.traceId) {
|
|
correlation.appendChild(element('span', 'approval-correlation-id', tKey('approvals.trace_id') + ': ' + item.traceId));
|
|
}
|
|
card.appendChild(correlation);
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
if (item.status === 'pending') {
|
|
var actions = element('div', 'approval-actions');
|
|
var busy = Boolean(state.approvalActionBusy[item.id]);
|
|
var approve = element('button', 'btn btn-primary btn-sm approval-approve', busy ? tKey('approvals.action.busy') : tKey('approvals.action.approve'));
|
|
approve.type = 'button';
|
|
approve.disabled = busy;
|
|
approve.dataset.approvalId = item.id;
|
|
approve.dataset.action = 'approve';
|
|
var deny = element('button', 'btn btn-secondary btn-sm approval-deny', busy ? tKey('approvals.action.busy') : tKey('approvals.action.deny'));
|
|
deny.type = 'button';
|
|
deny.disabled = busy;
|
|
deny.dataset.approvalId = item.id;
|
|
deny.dataset.action = 'deny';
|
|
actions.appendChild(approve);
|
|
actions.appendChild(deny);
|
|
card.appendChild(actions);
|
|
}
|
|
|
|
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' || state.status !== 'all' || state.outcomeGroup !== 'all' || state.operationId || state.agentId
|
|
? tKey('logs.empty.filtered')
|
|
: tKey('logs.empty.initial')
|
|
);
|
|
if (loadMoreBtn) {
|
|
loadMoreBtn.hidden = true;
|
|
}
|
|
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 || detail.traceId || detail.executionStage || detail.executionErrorCode || detail.operationVersion) {
|
|
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,
|
|
trace_id: detail.traceId || null,
|
|
operation_version: detail.operationVersion || null,
|
|
error_kind: detail.errorKind || null,
|
|
execution_stage: detail.executionStage || null,
|
|
execution_error_code: detail.executionErrorCode || null,
|
|
source: detail.source,
|
|
status: detail.status,
|
|
});
|
|
expanded.appendChild(metaPre);
|
|
}
|
|
}
|
|
|
|
fragment.appendChild(expanded);
|
|
});
|
|
|
|
logList.innerHTML = '';
|
|
logList.appendChild(fragment);
|
|
if (loadMoreBtn) {
|
|
loadMoreBtn.hidden = !state.nextCursor;
|
|
loadMoreBtn.disabled = state.loading;
|
|
}
|
|
|
|
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 invalidateLogsRequest() {
|
|
state.logsEpoch += 1;
|
|
state.nextCursor = null;
|
|
}
|
|
|
|
function queryParams(options) {
|
|
options = options || {};
|
|
var params = {
|
|
period: state.period,
|
|
};
|
|
if (options.includeLimit !== false) {
|
|
params.limit = 100;
|
|
}
|
|
if (state.level !== 'all') {
|
|
params.level = state.level;
|
|
}
|
|
if (state.search) {
|
|
params.search = state.search;
|
|
}
|
|
if (state.status !== 'all') {
|
|
params.status = state.status;
|
|
}
|
|
if (state.outcomeGroup !== 'all') {
|
|
params.outcome_group = state.outcomeGroup;
|
|
}
|
|
if (state.operationId) {
|
|
params.operation_id = state.operationId;
|
|
}
|
|
if (state.agentId) {
|
|
params.agent_id = state.agentId;
|
|
}
|
|
return params;
|
|
}
|
|
|
|
function bindTextFilter(input, key) {
|
|
if (!input) {
|
|
return;
|
|
}
|
|
input.value = state[key] || '';
|
|
input.addEventListener('input', function () {
|
|
state[key] = this.value.trim();
|
|
invalidateLogsRequest();
|
|
if (state.searchTimer) {
|
|
clearTimeout(state.searchTimer);
|
|
}
|
|
state.searchTimer = setTimeout(function () {
|
|
state.searchTimer = null;
|
|
loadLogs();
|
|
}, 250);
|
|
});
|
|
}
|
|
|
|
async function loadLogs(options) {
|
|
options = options || {};
|
|
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;
|
|
}
|
|
|
|
var workspaceId = state.workspaceId;
|
|
var epoch = ++state.logsEpoch;
|
|
var append = Boolean(options.append && state.nextCursor);
|
|
var params = queryParams();
|
|
if (append) {
|
|
params.cursor = state.nextCursor;
|
|
}
|
|
|
|
state.loading = true;
|
|
state.loadError = '';
|
|
renderLogs();
|
|
|
|
try {
|
|
var response = await window.CrankApi.listLogs(workspaceId, params);
|
|
if (epoch !== state.logsEpoch || workspaceId !== currentWorkspaceId()) {
|
|
return;
|
|
}
|
|
var loaded = (response && response.items ? response.items : []).map(normalizeLog);
|
|
state.logs = append ? state.logs.concat(loaded) : loaded;
|
|
state.nextCursor = response && response.next_cursor ? response.next_cursor : null;
|
|
if (state.openId && !state.logs.some(function (item) { return item.id === state.openId; })) {
|
|
state.openId = null;
|
|
}
|
|
} catch (error) {
|
|
if (epoch !== state.logsEpoch) {
|
|
return;
|
|
}
|
|
state.loadError = error.message || tKey('logs.error.load');
|
|
} finally {
|
|
if (epoch === state.logsEpoch) {
|
|
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;
|
|
}
|
|
|
|
var epoch = ++state.approvalsEpoch;
|
|
var workspaceId = state.workspaceId;
|
|
state.approvalsLoading = true;
|
|
state.approvalsError = '';
|
|
renderApprovals();
|
|
|
|
try {
|
|
var response = await window.CrankApi.listApprovals(workspaceId, { limit: 20 });
|
|
if (epoch !== state.approvalsEpoch || workspaceId !== currentWorkspaceId()) {
|
|
return;
|
|
}
|
|
state.approvals = (response && response.items ? response.items : []).map(normalizeApproval);
|
|
} catch (error) {
|
|
if (epoch !== state.approvalsEpoch) {
|
|
return;
|
|
}
|
|
state.approvalsError = error.message || tKey('approvals.error.load');
|
|
} finally {
|
|
if (epoch === state.approvalsEpoch) {
|
|
state.approvalsLoading = false;
|
|
renderApprovals();
|
|
}
|
|
}
|
|
}
|
|
|
|
async function decideApproval(approvalId, action) {
|
|
if (!window.CrankApi || !state.workspaceId || !approvalId) return;
|
|
if (state.approvalActionBusy[approvalId]) return;
|
|
var confirmKey = action === 'approve' ? 'approvals.confirm.approve' : 'approvals.confirm.deny';
|
|
if (!window.confirm(tKey(confirmKey))) return;
|
|
var workspaceId = state.workspaceId;
|
|
var epoch = ++state.approvalsEpoch;
|
|
state.approvalActionBusy[approvalId] = true;
|
|
state.approvalsError = '';
|
|
renderApprovals();
|
|
try {
|
|
if (action === 'approve') {
|
|
await window.CrankApi.approveApproval(workspaceId, approvalId, { approve: 'yes' });
|
|
} else {
|
|
await window.CrankApi.denyApproval(workspaceId, approvalId, { approve: 'no' });
|
|
}
|
|
if (workspaceId !== currentWorkspaceId() || epoch !== state.approvalsEpoch) return;
|
|
state.approvalActionBusy[approvalId] = false;
|
|
await loadApprovals();
|
|
} catch (error) {
|
|
if (workspaceId !== currentWorkspaceId() || epoch !== state.approvalsEpoch) return;
|
|
state.approvalsError = error.message || tKey('approvals.error.decision');
|
|
state.approvalActionBusy[approvalId] = false;
|
|
renderApprovals();
|
|
}
|
|
}
|
|
|
|
async function refreshOperationalData() {
|
|
if (state.refreshPromise) {
|
|
return state.refreshPromise;
|
|
}
|
|
state.refreshPromise = Promise.all([loadLogs(), loadApprovals()]).finally(function () {
|
|
state.refreshPromise = null;
|
|
});
|
|
return state.refreshPromise;
|
|
}
|
|
|
|
async function loadLogDetail(logId) {
|
|
if (!window.CrankApi || !state.workspaceId || state.details[logId]) {
|
|
return;
|
|
}
|
|
|
|
var workspaceId = state.workspaceId;
|
|
var epoch = state.logsEpoch;
|
|
try {
|
|
var record = await window.CrankApi.getLog(workspaceId, logId);
|
|
if (epoch !== state.logsEpoch || workspaceId !== currentWorkspaceId()) {
|
|
return;
|
|
}
|
|
state.details[logId] = normalizeLog(record);
|
|
if (state.openId === logId) {
|
|
renderLogs();
|
|
}
|
|
} catch (_error) {
|
|
}
|
|
}
|
|
|
|
async function loadExactLog(logId) {
|
|
var workspaceId = currentWorkspaceId();
|
|
var epoch = ++state.logsEpoch;
|
|
state.workspaceId = workspaceId;
|
|
state.loading = true;
|
|
state.loadError = '';
|
|
renderLogs();
|
|
try {
|
|
var record = await window.CrankApi.getLog(workspaceId, logId);
|
|
if (epoch !== state.logsEpoch || workspaceId !== currentWorkspaceId()) return;
|
|
var detail = normalizeLog(record);
|
|
state.logs = [detail];
|
|
state.details[logId] = detail;
|
|
state.openId = logId;
|
|
state.nextCursor = null;
|
|
} catch (error) {
|
|
if (epoch !== state.logsEpoch) return;
|
|
state.loadError = error.message || tKey('logs.error.load');
|
|
} finally {
|
|
if (epoch === state.logsEpoch) {
|
|
state.loading = false;
|
|
renderLogs();
|
|
}
|
|
}
|
|
}
|
|
|
|
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 || document.hidden) {
|
|
return;
|
|
}
|
|
state.timer = setTimeout(async function poll() {
|
|
state.timer = null;
|
|
await refreshOperationalData();
|
|
startPolling();
|
|
}, 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')
|
|
);
|
|
}
|
|
}
|
|
|
|
async function exportLogsCsv() {
|
|
if (!window.CrankApi || state.exporting) {
|
|
return;
|
|
}
|
|
var workspaceId = currentWorkspaceId();
|
|
if (!workspaceId) {
|
|
if (window.CrankUi) {
|
|
window.CrankUi.error(tKey('logs.error.workspace'), tKey('logs.export.error_title'));
|
|
}
|
|
return;
|
|
}
|
|
state.exporting = true;
|
|
if (exportLogsBtn) {
|
|
exportLogsBtn.disabled = true;
|
|
}
|
|
try {
|
|
var csv = await window.CrankApi.exportLogsCsv(workspaceId, queryParams({ includeLimit: false }));
|
|
if (workspaceId !== currentWorkspaceId()) {
|
|
return;
|
|
}
|
|
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
|
var url = URL.createObjectURL(blob);
|
|
var link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = 'crank-invocation-history.csv';
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
if (window.CrankUi) {
|
|
window.CrankUi.success(tKey('logs.export.done_body'), tKey('logs.export.done_title'));
|
|
}
|
|
} catch (error) {
|
|
if (window.CrankUi) {
|
|
window.CrankUi.error(error.message || tKey('logs.export.error_body'), tKey('logs.export.error_title'));
|
|
}
|
|
} finally {
|
|
state.exporting = false;
|
|
if (exportLogsBtn) {
|
|
exportLogsBtn.disabled = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll('.filter-chip[data-level]').forEach(function (button) {
|
|
button.addEventListener('click', function () {
|
|
state.level = this.getAttribute('data-level');
|
|
invalidateLogsRequest();
|
|
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();
|
|
invalidateLogsRequest();
|
|
if (state.searchTimer) {
|
|
clearTimeout(state.searchTimer);
|
|
}
|
|
state.searchTimer = setTimeout(function () {
|
|
state.searchTimer = null;
|
|
loadLogs();
|
|
}, 250);
|
|
});
|
|
}
|
|
|
|
if (statusFilter) {
|
|
statusFilter.value = state.status;
|
|
statusFilter.addEventListener('change', function () {
|
|
state.status = this.value || 'all';
|
|
invalidateLogsRequest();
|
|
loadLogs();
|
|
});
|
|
}
|
|
|
|
if (outcomeFilter) {
|
|
outcomeFilter.value = state.outcomeGroup;
|
|
outcomeFilter.addEventListener('change', function () {
|
|
state.outcomeGroup = this.value || 'all';
|
|
invalidateLogsRequest();
|
|
loadLogs();
|
|
});
|
|
}
|
|
|
|
bindTextFilter(operationFilter, 'operationId');
|
|
bindTextFilter(agentFilter, 'agentId');
|
|
|
|
if (refreshBtn) {
|
|
refreshBtn.addEventListener('click', function () {
|
|
invalidateLogsRequest();
|
|
loadLogs().then(function () {
|
|
if (!state.loadError && window.CrankUi) {
|
|
window.CrankUi.info(tKey('logs.refresh.body'), tKey('logs.refresh.title'));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
if (exportLogsBtn) {
|
|
exportLogsBtn.addEventListener('click', exportLogsCsv);
|
|
}
|
|
|
|
if (loadMoreBtn) {
|
|
loadMoreBtn.addEventListener('click', function () {
|
|
loadLogs({ append: true });
|
|
});
|
|
}
|
|
|
|
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 (approvalList) {
|
|
approvalList.addEventListener('click', function (event) {
|
|
var button = event.target.closest('button[data-approval-id][data-action]');
|
|
if (!button) return;
|
|
decideApproval(button.dataset.approvalId, button.dataset.action);
|
|
});
|
|
}
|
|
|
|
if (timeRangeSel) {
|
|
timeRangeSel.value = state.period;
|
|
timeRangeSel.addEventListener('change', function () {
|
|
state.period = this.value;
|
|
invalidateLogsRequest();
|
|
loadLogs();
|
|
});
|
|
}
|
|
|
|
if (liveDot) {
|
|
liveDot.addEventListener('click', toggleLive);
|
|
}
|
|
|
|
if (liveLabel) {
|
|
liveLabel.addEventListener('click', toggleLive);
|
|
}
|
|
|
|
window.addEventListener('crank:workspacechange', function () {
|
|
state.logsEpoch += 1;
|
|
state.details = {};
|
|
state.openId = null;
|
|
state.nextCursor = null;
|
|
refreshOperationalData();
|
|
});
|
|
|
|
document.addEventListener('visibilitychange', function () {
|
|
if (document.hidden) {
|
|
stopPolling();
|
|
return;
|
|
}
|
|
if (state.liveMode) {
|
|
refreshOperationalData().finally(startPolling);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('pagehide', function () {
|
|
stopPolling();
|
|
if (state.searchTimer) {
|
|
clearTimeout(state.searchTimer);
|
|
state.searchTimer = null;
|
|
}
|
|
});
|
|
|
|
setLiveState();
|
|
startPolling();
|
|
|
|
if (window.whenWorkspacesReady) {
|
|
window.whenWorkspacesReady().finally(function() {
|
|
if (deepLinkLogId) return Promise.all([loadExactLog(deepLinkLogId), loadApprovals()]);
|
|
return refreshOperationalData();
|
|
});
|
|
} else {
|
|
if (deepLinkLogId) Promise.all([loadExactLog(deepLinkLogId), loadApprovals()]);
|
|
else refreshOperationalData();
|
|
}
|
|
});
|