From d80651e4d81f88a26153f81559a1e0a3baef053a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:16:43 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 6 ++ web/static/i18n/en-US.json | 14 +++ web/static/i18n/zh-CN.json | 14 +++ web/static/js/hitl.js | 207 +++++++++++++++++++++++++++++++++++-- web/templates/index.html | 12 +++ 5 files changed, 245 insertions(+), 8 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index fc4320e1..850960b0 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -2042,6 +2042,12 @@ header { margin-top: 14px; } +.hitl-logs-retention-hint { + margin: 0 0 12px; + font-size: 13px; + color: #64748b; +} + .hitl-logs-empty-hint { margin: 8px 0 0; font-size: 13px; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index af2fca55..a02c2bb4 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -736,6 +736,20 @@ "saveFailed": "Save failed", "deleteConfirm": "Delete this audit log?", "deleteFailed": "Delete failed", + "retentionHint": "Audit logs are kept for {{days}} days, then purged automatically.", + "selectedCount": "{{count}} selected", + "selectAll": "Select all", + "deselectAll": "Deselect all", + "batchDelete": "Batch delete", + "batchDeleteConfirm": "Delete the selected {{count}} audit log(s)? This cannot be undone.", + "batchDeleteSuccess": "Successfully deleted {{count}} audit log(s)", + "batchDeleteFailed": "Batch delete failed", + "clearAll": "Clear all", + "clearAllConfirm": "Clear all {{count}} audit log(s) matching the current filters? This cannot be undone.", + "clearAllConfirmNoFilter": "No filters are set. This will clear all {{count}} audit log(s). This cannot be undone. Continue?", + "clearAllSuccess": "Cleared {{count}} audit log(s)", + "clearAllFailed": "Clear failed", + "selectLogsFirst": "Select audit logs to delete first", "loading": "Loading...", "emptyState": "No pending approvals", "dismiss": "Dismiss", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index be583e13..3b6e5b7a 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -724,6 +724,20 @@ "saveFailed": "保存失败", "deleteConfirm": "确定删除这条审计日志?", "deleteFailed": "删除失败", + "retentionHint": "审计日志保留 {{days}} 天,超期自动清理", + "selectedCount": "已选择 {{count}} 项", + "selectAll": "全选", + "deselectAll": "取消全选", + "batchDelete": "批量删除", + "batchDeleteConfirm": "确定删除选中的 {{count}} 条审计日志?此操作不可恢复。", + "batchDeleteSuccess": "成功删除 {{count}} 条审计日志", + "batchDeleteFailed": "批量删除失败", + "clearAll": "清空", + "clearAllConfirm": "确定清空当前筛选条件下的全部 {{count}} 条审计日志?此操作不可恢复。", + "clearAllConfirmNoFilter": "未设置筛选条件,将清空全部 {{count}} 条审计日志。此操作不可恢复,是否继续?", + "clearAllSuccess": "已清空 {{count}} 条审计日志", + "clearAllFailed": "清空失败", + "selectLogsFirst": "请先选择要删除的审计日志", "loading": "加载中...", "emptyState": "暂无待审批项", "dismiss": "忽略", diff --git a/web/static/js/hitl.js b/web/static/js/hitl.js index b6907939..d35d0b10 100644 --- a/web/static/js/hitl.js +++ b/web/static/js/hitl.js @@ -786,6 +786,8 @@ let hitlLogsPageSize = 20; let hitlLogsTotal = 0; let hitlLogsCache = []; let hitlLogsLoaded = false; +let hitlLogsRetentionDays = 0; +const hitlSelectedLogs = new Set(); let hitlPendingPage = 1; let hitlPendingPageSize = 20; let hitlPendingTotal = 0; @@ -983,6 +985,182 @@ function hitlFormatTime(v) { } } +function hitlLogsHasActiveFilters() { + const qEl = document.getElementById('hitl-logs-search'); + const decEl = document.getElementById('hitl-logs-decision-filter'); + const byEl = document.getElementById('hitl-logs-decidedby-filter'); + return Boolean( + (qEl && qEl.value.trim()) || + (decEl && decEl.value && decEl.value !== 'all') || + (byEl && byEl.value && byEl.value !== 'all') + ); +} + +function hitlLogsFilterParams() { + const params = new URLSearchParams(); + const qEl = document.getElementById('hitl-logs-search'); + const decEl = document.getElementById('hitl-logs-decision-filter'); + const byEl = document.getElementById('hitl-logs-decidedby-filter'); + if (qEl && qEl.value.trim()) params.set('q', qEl.value.trim()); + if (decEl && decEl.value && decEl.value !== 'all') params.set('decision', decEl.value); + if (byEl && byEl.value && byEl.value !== 'all') params.set('decidedBy', byEl.value); + return params; +} + +function updateHitlLogsRetentionHint() { + const el = document.getElementById('hitl-logs-retention-hint'); + if (!el) return; + if (typeof hitlLogsRetentionDays === 'number' && hitlLogsRetentionDays > 0) { + el.textContent = hitlT('retentionHint', 'Audit logs are kept for {{days}} days, then purged automatically.', { days: hitlLogsRetentionDays }); + el.hidden = false; + } else { + el.textContent = ''; + el.hidden = true; + } +} + +function updateHitlLogsBatchActionsState() { + const selectedCount = hitlSelectedLogs.size; + const batchActions = document.getElementById('hitl-logs-batch-actions'); + const selectedCountSpan = document.getElementById('hitl-logs-selected-count'); + if (batchActions) { + batchActions.style.display = selectedCount > 0 ? 'flex' : 'none'; + } + if (selectedCountSpan) { + selectedCountSpan.textContent = hitlT('selectedCount', '{{count}} selected', { count: selectedCount }); + } + const selectAllCheckbox = document.getElementById('hitl-logs-select-all'); + if (selectAllCheckbox) { + const allCheckboxes = document.querySelectorAll('.hitl-log-checkbox'); + if (allCheckboxes.length === 0) { + selectAllCheckbox.checked = false; + selectAllCheckbox.indeterminate = false; + } else { + const checkedOnPage = Array.from(allCheckboxes).filter(function (cb) { + return hitlSelectedLogs.has(cb.value); + }).length; + selectAllCheckbox.checked = checkedOnPage === allCheckboxes.length; + selectAllCheckbox.indeterminate = checkedOnPage > 0 && checkedOnPage < allCheckboxes.length; + } + } +} + +function toggleHitlLogSelection(id, checked) { + if (!id) return; + if (checked) { + hitlSelectedLogs.add(id); + } else { + hitlSelectedLogs.delete(id); + } + updateHitlLogsBatchActionsState(); +} + +function toggleHitlLogsSelectAll(checkbox) { + const checkboxes = document.querySelectorAll('.hitl-log-checkbox'); + checkboxes.forEach(function (cb) { + cb.checked = checkbox.checked; + if (checkbox.checked) { + hitlSelectedLogs.add(cb.value); + } else { + hitlSelectedLogs.delete(cb.value); + } + }); + updateHitlLogsBatchActionsState(); +} + +function selectAllHitlLogs() { + const checkboxes = document.querySelectorAll('.hitl-log-checkbox'); + checkboxes.forEach(function (cb) { + cb.checked = true; + hitlSelectedLogs.add(cb.value); + }); + const selectAllCheckbox = document.getElementById('hitl-logs-select-all'); + if (selectAllCheckbox) { + selectAllCheckbox.checked = true; + selectAllCheckbox.indeterminate = false; + } + updateHitlLogsBatchActionsState(); +} + +function deselectAllHitlLogs() { + const checkboxes = document.querySelectorAll('.hitl-log-checkbox'); + checkboxes.forEach(function (cb) { + cb.checked = false; + }); + hitlSelectedLogs.clear(); + const selectAllCheckbox = document.getElementById('hitl-logs-select-all'); + if (selectAllCheckbox) { + selectAllCheckbox.checked = false; + selectAllCheckbox.indeterminate = false; + } + updateHitlLogsBatchActionsState(); +} + +async function batchDeleteHitlLogs() { + const ids = Array.from(hitlSelectedLogs); + if (!ids.length) { + alert(hitlT('selectLogsFirst', 'Select audit logs to delete first')); + return; + } + const count = ids.length; + if (!confirm(hitlT('batchDeleteConfirm', 'Delete the selected {{count}} audit log(s)? This cannot be undone.', { count: count }))) { + return; + } + try { + const resp = await hitlApiFetch('/api/hitl/logs', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ ids: ids }) + }); + if (!resp.ok) { + const err = await resp.json().catch(function () { return {}; }); + throw new Error(err.error || hitlT('batchDeleteFailed', 'Batch delete failed')); + } + const result = await resp.json().catch(function () { return {}; }); + const deletedCount = typeof result.deleted === 'number' ? result.deleted : count; + ids.forEach(function (id) { hitlSelectedLogs.delete(id); }); + await refreshHitlLogs(); + alert(hitlT('batchDeleteSuccess', 'Successfully deleted {{count}} audit log(s)', { count: deletedCount })); + } catch (e) { + console.error('batchDeleteHitlLogs', e); + alert(hitlT('batchDeleteFailed', 'Batch delete failed') + ': ' + (e && e.message ? e.message : String(e))); + } +} + +async function clearHitlLogs() { + const count = hitlLogsTotal || 0; + if (count <= 0) { + return; + } + const confirmKey = hitlLogsHasActiveFilters() ? 'clearAllConfirm' : 'clearAllConfirmNoFilter'; + if (!confirm(hitlT(confirmKey, 'Clear all {{count}} audit log(s)? This cannot be undone.', { count: count }))) { + return; + } + try { + const params = hitlLogsFilterParams(); + const resp = await hitlApiFetch('/api/hitl/logs' + (params.toString() ? '?' + params.toString() : ''), { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ all: true }) + }); + if (!resp.ok) { + const err = await resp.json().catch(function () { return {}; }); + throw new Error(err.error || hitlT('clearAllFailed', 'Clear failed')); + } + const result = await resp.json().catch(function () { return {}; }); + const deletedCount = typeof result.deleted === 'number' ? result.deleted : count; + hitlSelectedLogs.clear(); + hitlLogsPage = 1; + await refreshHitlLogs(); + alert(hitlT('clearAllSuccess', 'Cleared {{count}} audit log(s)', { count: deletedCount })); + } catch (e) { + console.error('clearHitlLogs', e); + alert(hitlT('clearAllFailed', 'Clear failed') + ': ' + (e && e.message ? e.message : String(e))); + } +} + function renderHitlLogsTable(items) { const wrap = document.getElementById('hitl-logs-table-wrap'); if (!wrap) return; @@ -993,18 +1171,24 @@ function renderHitlLogsTable(items) { '
' + escapeHtml(hitlT('logsEmpty', 'No audit logs')) + '
' + '' + escapeHtml(hitlT('logsEmptyHint', 'Records appear here after HITL decisions.')) + '
' + ''; + const batchActions = document.getElementById('hitl-logs-batch-actions'); + if (batchActions) batchActions.style.display = 'none'; renderHitlLogsPagination(); return; } const rows = list.map(function (item) { - const id = escapeHtml(String(item.id || '')); - const qId = JSON.stringify(String(item.id || '')).replace(/"/g, '"'); + const rawId = String(item.id || ''); + const id = escapeHtml(rawId); + const jsId = rawId.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + const qId = JSON.stringify(rawId).replace(/"/g, '"'); + const isSelected = hitlSelectedLogs.has(rawId); const payloadObj = hitlParsePayloadObject(item.payload || ''); const decision = String(item.decision || '-'); const decisionCls = decision === 'approve' ? 'hitl-decision--approve' : (decision === 'reject' ? 'hitl-decision--reject' : ''); const summary = hitlPayloadSummary(payloadObj); return ( '| ' + ' | ' + escapeHtml(hitlT('colId', 'ID')) + ' | ' + '' + escapeHtml(hitlT('colTool', 'Tool')) + ' | ' + '' + escapeHtml(hitlT('colConversation', 'Conversation')) + ' | ' + @@ -1030,6 +1215,7 @@ function renderHitlLogsTable(items) { '' + escapeHtml(hitlT('colTime', 'Time')) + ' | ' + '' + escapeHtml(hitlT('colActions', 'Actions')) + ' | ' + '
|---|