feat: complete Epic 1 production foundation
This commit is contained in:
+297
-14
@@ -3,9 +3,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
logs: [],
|
||||
details: {},
|
||||
level: 'all',
|
||||
status: 'all',
|
||||
outcomeGroup: 'all',
|
||||
operationId: '',
|
||||
agentId: '',
|
||||
search: '',
|
||||
period: '7d',
|
||||
openId: null,
|
||||
nextCursor: null,
|
||||
liveMode: true,
|
||||
timer: null,
|
||||
searchTimer: null,
|
||||
@@ -16,6 +21,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
approvals: [],
|
||||
approvalsLoading: false,
|
||||
approvalsError: '',
|
||||
approvalActionBusy: {},
|
||||
approvalsEpoch: 0,
|
||||
logsEpoch: 0,
|
||||
exporting: false,
|
||||
};
|
||||
|
||||
var logList = document.getElementById('log-list');
|
||||
@@ -23,9 +32,21 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
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;
|
||||
@@ -118,6 +139,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
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,
|
||||
@@ -127,7 +149,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -142,6 +167,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
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,
|
||||
@@ -206,6 +233,17 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
: 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')));
|
||||
@@ -228,6 +266,24 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -256,10 +312,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (!state.logs.length) {
|
||||
renderEmpty(
|
||||
tKey('logs.empty.title'),
|
||||
state.search || state.level !== 'all'
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -347,7 +406,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
responsePre.textContent = formatJson(detail.responsePreview);
|
||||
expanded.appendChild(responsePre);
|
||||
|
||||
if (detail.errorKind || detail.requestId) {
|
||||
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');
|
||||
@@ -357,7 +416,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
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,
|
||||
});
|
||||
@@ -370,6 +433,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
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 () {
|
||||
@@ -383,21 +450,60 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
}
|
||||
|
||||
function queryParams() {
|
||||
function invalidateLogsRequest() {
|
||||
state.logsEpoch += 1;
|
||||
state.nextCursor = null;
|
||||
}
|
||||
|
||||
function queryParams(options) {
|
||||
options = options || {};
|
||||
var params = {
|
||||
period: state.period,
|
||||
limit: 100,
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
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();
|
||||
@@ -411,21 +517,39 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
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(state.workspaceId, queryParams());
|
||||
state.logs = (response && response.items ? response.items : []).map(normalizeLog);
|
||||
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 {
|
||||
state.loading = false;
|
||||
renderLogs();
|
||||
if (epoch === state.logsEpoch) {
|
||||
state.loading = false;
|
||||
renderLogs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,17 +567,54 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
return;
|
||||
}
|
||||
|
||||
var epoch = ++state.approvalsEpoch;
|
||||
var workspaceId = state.workspaceId;
|
||||
state.approvalsLoading = true;
|
||||
state.approvalsError = '';
|
||||
renderApprovals();
|
||||
|
||||
try {
|
||||
var response = await window.CrankApi.listApprovals(state.workspaceId, { limit: 20 });
|
||||
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 {
|
||||
state.approvalsLoading = false;
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -473,8 +634,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
return;
|
||||
}
|
||||
|
||||
var workspaceId = state.workspaceId;
|
||||
var epoch = state.logsEpoch;
|
||||
try {
|
||||
var record = await window.CrankApi.getLog(state.workspaceId, logId);
|
||||
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();
|
||||
@@ -483,6 +649,32 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -524,9 +716,52 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
@@ -538,6 +773,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (logSearch) {
|
||||
logSearch.addEventListener('input', function () {
|
||||
state.search = this.value.trim();
|
||||
invalidateLogsRequest();
|
||||
if (state.searchTimer) {
|
||||
clearTimeout(state.searchTimer);
|
||||
}
|
||||
@@ -548,8 +784,30 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
}
|
||||
|
||||
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'));
|
||||
@@ -558,6 +816,16 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
}
|
||||
|
||||
if (exportLogsBtn) {
|
||||
exportLogsBtn.addEventListener('click', exportLogsCsv);
|
||||
}
|
||||
|
||||
if (loadMoreBtn) {
|
||||
loadMoreBtn.addEventListener('click', function () {
|
||||
loadLogs({ append: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (approvalRefreshBtn) {
|
||||
approvalRefreshBtn.addEventListener('click', function () {
|
||||
loadApprovals().then(function () {
|
||||
@@ -568,10 +836,19 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
@@ -585,8 +862,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}
|
||||
|
||||
window.addEventListener('crank:workspacechange', function () {
|
||||
state.logsEpoch += 1;
|
||||
state.details = {};
|
||||
state.openId = null;
|
||||
state.nextCursor = null;
|
||||
refreshOperationalData();
|
||||
});
|
||||
|
||||
@@ -612,8 +891,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
startPolling();
|
||||
|
||||
if (window.whenWorkspacesReady) {
|
||||
window.whenWorkspacesReady().finally(refreshOperationalData);
|
||||
window.whenWorkspacesReady().finally(function() {
|
||||
if (deepLinkLogId) return Promise.all([loadExactLog(deepLinkLogId), loadApprovals()]);
|
||||
return refreshOperationalData();
|
||||
});
|
||||
} else {
|
||||
refreshOperationalData();
|
||||
if (deepLinkLogId) Promise.all([loadExactLog(deepLinkLogId), loadApprovals()]);
|
||||
else refreshOperationalData();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user