Files
gemini-banlancer/frontend/js/pages/logs/index.js

723 lines
33 KiB
JavaScript

// Filename: frontend/js/pages/logs/index.js
import { apiFetchJson } from '../../services/api.js';
import LogList from './logList.js';
import CustomSelectV2 from '../../components/customSelectV2.js';
import { debounce } from '../../utils/utils.js';
import FilterPopover from '../../components/filterPopover.js';
import { STATIC_ERROR_MAP, STATUS_CODE_MAP } from './logList.js';
import SystemLogTerminal from './systemLog.js';
import { initBatchActions } from './batchActions.js';
import flatpickr from '../../vendor/flatpickr.js';
import LogSettingsModal from './logSettingsModal.js';
const dataStore = {
groups: new Map(),
keys: new Map(),
};
class LogsPage {
constructor() {
this.state = {
logs: [],
pagination: { page: 1, pages: 1, total: 0, page_size: 20 },
isLoading: true,
filters: {
page: 1,
page_size: 20,
q: '',
key_ids: new Set(),
group_ids: new Set(),
error_types: new Set(),
status_codes: new Set(),
start_date: null,
end_date: null,
},
selectedLogIds: new Set(),
currentView: 'error',
};
this.elements = {
tabsContainer: document.querySelector('[data-sliding-tabs-container]'),
contentContainer: document.getElementById('log-content-container'),
errorFilters: document.getElementById('error-logs-filters'),
systemControls: document.getElementById('system-logs-controls'),
errorTemplate: document.getElementById('error-logs-template'),
systemTemplate: document.getElementById('system-logs-template'),
settingsBtn: document.querySelector('button[aria-label="日志设置"]'),
};
this.initialized = !!this.elements.contentContainer;
if (this.initialized) {
this.logList = null;
this.systemLogTerminal = null;
this.debouncedLoadAndRender = debounce(() => this.loadAndRenderLogs(), 300);
this.fp = null;
this.themeObserver = null;
this.settingsModal = null;
this.currentSettings = {};
}
}
async init() {
if (!this.initialized) return;
this._initPermanentEventListeners();
await this.loadCurrentSettings();
this._initSettingsModal();
await this.loadGroupsOnce();
this.state.currentView = null;
this.switchToView('error');
}
_initSettingsModal() {
if (!this.elements.settingsBtn) return;
this.settingsModal = new LogSettingsModal({
onSave: this.handleSaveSettings.bind(this)
});
this.elements.settingsBtn.addEventListener('click', () => {
const settingsForModal = {
log_level: this.currentSettings.log_level,
auto_cleanup: {
enabled: this.currentSettings.log_auto_cleanup_enabled,
retention_days: this.currentSettings.log_auto_cleanup_retention_days,
exec_time: this.currentSettings.log_auto_cleanup_time,
interval: 'daily',
}
};
this.settingsModal.open(settingsForModal);
});
}
async loadCurrentSettings() {
try {
const { success, data } = await apiFetchJson('/admin/settings');
if (success) {
this.currentSettings = data;
} else {
console.error('Failed to load settings from server.');
this.currentSettings = { log_auto_cleanup_time: '04:05' };
}
} catch (error) {
console.error('Failed to load log settings:', error);
this.currentSettings = { log_auto_cleanup_time: '04:05' };
}
}
async handleSaveSettings(settingsData) {
const partialPayload = {
"log_level": settingsData.log_level,
"log_auto_cleanup_enabled": settingsData.auto_cleanup.enabled,
"log_auto_cleanup_time": settingsData.auto_cleanup.exec_time,
};
if (settingsData.auto_cleanup.enabled) {
let retentionDays = settingsData.auto_cleanup.retention_days;
if (retentionDays === null || retentionDays <= 0) {
retentionDays = 30;
}
partialPayload.log_auto_cleanup_retention_days = retentionDays;
}
console.log('Sending PARTIAL settings update to /admin/settings:', partialPayload);
try {
const { success, message } = await apiFetchJson('/admin/settings', {
method: 'PUT',
body: JSON.stringify(partialPayload)
});
if (!success) {
throw new Error(message || 'Failed to save settings');
}
Object.assign(this.currentSettings, partialPayload);
} catch (error) {
console.error('Error saving log settings:', error);
throw error;
}
}
_initPermanentEventListeners() {
this.elements.tabsContainer.addEventListener('click', (event) => {
const tabItem = event.target.closest('[data-tab-target]');
if (!tabItem) return;
event.preventDefault();
const viewName = tabItem.dataset.tabTarget;
if (viewName) {
this.switchToView(viewName);
}
});
}
switchToView(viewName) {
if (this.state.currentView === viewName && this.elements.contentContainer.innerHTML !== '') return;
if (this.systemLogTerminal) {
this.systemLogTerminal.disconnect();
this.systemLogTerminal = null;
}
if (this.fp) {
this.fp.destroy();
this.fp = null;
}
if (this.themeObserver) {
this.themeObserver.disconnect();
this.themeObserver = null;
}
this.state.currentView = viewName;
this.elements.contentContainer.innerHTML = '';
const isErrorView = viewName === 'error';
this.elements.errorFilters.style.display = isErrorView ? 'flex' : 'none';
this.elements.systemControls.style.display = isErrorView ? 'none' : 'flex';
if (isErrorView) {
const template = this.elements.errorTemplate.content.cloneNode(true);
this.elements.contentContainer.appendChild(template);
requestAnimationFrame(() => {
this._initErrorLogView();
});
} else if (viewName === 'system') {
const template = this.elements.systemTemplate.content.cloneNode(true);
this.elements.contentContainer.appendChild(template);
requestAnimationFrame(() => {
this._initSystemLogView();
});
}
}
_initErrorLogView() {
this.elements.tableBody = document.getElementById('logs-table-body');
this.elements.selectedCount = document.querySelector('.flex-1.text-sm span.font-semibold:nth-child(1)');
this.elements.totalCount = document.querySelector('.flex-1.text-sm span:last-child');
this.elements.pageSizeSelect = document.querySelector('[data-component="custom-select-v2"] select');
this.elements.pageInfo = document.querySelector('.flex.w-\\[100px\\]');
this.elements.paginationBtns = document.querySelectorAll('[data-pagination-controls] button');
this.elements.selectAllCheckbox = document.querySelector('thead .table-head-cell input[type="checkbox"]');
this.elements.searchInput = document.getElementById('log-search-input');
this.elements.errorTypeFilterBtn = document.getElementById('filter-error-type-btn');
this.elements.errorCodeFilterBtn = document.getElementById('filter-error-code-btn');
this.elements.dateRangeFilterBtn = document.getElementById('filter-date-range-btn');
this.logList = new LogList(this.elements.tableBody, dataStore);
const selectContainer = document.querySelector('[data-component="custom-select-v2"]');
if (selectContainer) { new CustomSelectV2(selectContainer); }
this.initFilterPopovers();
this.initDateRangePicker();
this.initEventListeners();
this._observeThemeChanges();
initBatchActions(this);
this.loadAndRenderLogs();
}
_observeThemeChanges() {
const applyTheme = () => {
if (!this.fp || !this.fp.calendarContainer) return;
if (document.documentElement.classList.contains('dark')) {
this.fp.calendarContainer.classList.add('dark');
} else {
this.fp.calendarContainer.classList.remove('dark');
}
};
this.themeObserver = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
applyTheme();
}
}
});
this.themeObserver.observe(document.documentElement, { attributes: true });
applyTheme();
}
_initSystemLogView() {
this.systemLogTerminal = new SystemLogTerminal(
this.elements.contentContainer,
this.elements.systemControls
);
Swal.fire({
width: '20rem',
backdrop: `rgba(0,0,0,0.5)`,
heightAuto: false,
customClass: { popup: `swal2-custom-style ${document.documentElement.classList.contains('dark') ? 'swal2-dark' : ''}` },
title: '系统终端日志',
text: '您即将连接到实时系统日志流窗口。',
showCancelButton: true,
confirmButtonText: '确认',
cancelButtonText: '取消',
reverseButtons: false,
confirmButtonColor: 'rgba(31, 102, 255, 0.8)',
cancelButtonColor: '#6b7280',
focusConfirm: false,
focusCancel: false,
target: '#main-content-wrapper',
}).then((result) => {
if (result.isConfirmed) {
this.systemLogTerminal.connect();
} else {
const errorLogTab = Array.from(this.elements.tabsContainer.querySelectorAll('[data-tab-target="error"]'))[0];
if (errorLogTab) errorLogTab.click();
}
});
}
initFilterPopovers() {
const errorTypeOptions = [
...Object.values(STATUS_CODE_MAP).map(v => ({ value: v.type, label: v.type })),
...Object.values(STATIC_ERROR_MAP).map(v => ({ value: v.type, label: v.type }))
];
const uniqueErrorTypeOptions = Array.from(new Map(errorTypeOptions.map(item => [item.value, item])).values());
if (this.elements.errorTypeFilterBtn) {
new FilterPopover(this.elements.errorTypeFilterBtn, uniqueErrorTypeOptions, '筛选错误类型');
}
const statusCodeOptions = Object.keys(STATUS_CODE_MAP).map(code => ({ value: code, label: code }));
if (this.elements.errorCodeFilterBtn) {
new FilterPopover(this.elements.errorCodeFilterBtn, statusCodeOptions, '筛选状态码');
}
}
initDateRangePicker() {
if (!this.elements.dateRangeFilterBtn) return;
const buttonTextSpan = this.elements.dateRangeFilterBtn.querySelector('span');
const originalText = buttonTextSpan.textContent;
this.fp = flatpickr(this.elements.dateRangeFilterBtn, {
mode: 'range',
dateFormat: 'Y-m-d',
onClose: (selectedDates) => {
if (selectedDates.length === 2) {
const [start, end] = selectedDates;
end.setHours(23, 59, 59, 999);
this.state.filters.start_date = start.toISOString();
this.state.filters.end_date = end.toISOString();
const startDateStr = start.toISOString().split('T')[0];
const endDateStr = end.toISOString().split('T')[0];
buttonTextSpan.textContent = `${startDateStr} ~ ${endDateStr}`;
this.elements.dateRangeFilterBtn.classList.add('!border-primary', '!text-primary');
this.state.filters.page = 1;
this.loadAndRenderLogs();
}
},
onReady: (selectedDates, dateStr, instance) => {
if (document.documentElement.classList.contains('dark')) {
instance.calendarContainer.classList.add('dark');
}
const clearButton = document.createElement('button');
clearButton.textContent = '清除';
clearButton.className = 'button flatpickr-button flatpickr-clear-button';
clearButton.addEventListener('click', (e) => {
e.preventDefault();
instance.clear();
this.state.filters.start_date = null;
this.state.filters.end_date = null;
buttonTextSpan.textContent = originalText;
this.elements.dateRangeFilterBtn.classList.remove('!border-primary', '!text-primary');
this.state.filters.page = 1;
this.loadAndRenderLogs();
instance.close();
});
instance.calendarContainer.appendChild(clearButton);
const nativeMonthSelect = instance.monthsDropdownContainer;
if (!nativeMonthSelect) return;
const monthYearContainer = nativeMonthSelect.parentElement;
const wrapper = document.createElement('div');
wrapper.className = 'custom-select-v2-container relative inline-block text-left';
wrapper.innerHTML = `
<button type="button" class="custom-select-trigger inline-flex justify-center items-center w-22 gap-x-1.5 rounded-md bg-transparent px-1 py-1 text-xs font-semibold text-foreground shadow-sm ring-0 ring-inset ring-input hover:bg-accent focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-offset-background focus:ring-primary" aria-haspopup="true">
<span class="truncate"></span>
</button>
`;
const template = document.createElement('template');
template.className = 'custom-select-panel-template';
template.innerHTML = `
<div class="custom-select-panel absolute z-1000 my-2 w-24 origin-top-right rounded-md bg-popover dark:bg-zinc-900 shadow-lg ring-1 ring-zinc-500/30 ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" tabindex="-1">
</div>
`;
nativeMonthSelect.classList.add('hidden');
wrapper.appendChild(nativeMonthSelect);
wrapper.appendChild(template);
monthYearContainer.prepend(wrapper);
const customSelect = new CustomSelectV2(wrapper);
instance.customMonthSelect = customSelect;
},
onMonthChange: (selectedDates, dateStr, instance) => {
if (instance.customMonthSelect) {
instance.customMonthSelect.updateTriggerText();
}
},
});
}
initEventListeners() {
if (this.elements.pageSizeSelect) {
this.elements.pageSizeSelect.addEventListener('change', (e) => this.changePageSize(parseInt(e.target.value, 10)));
}
if (this.elements.paginationBtns.length >= 4) {
this.elements.paginationBtns[0].addEventListener('click', () => this.goToPage(1));
this.elements.paginationBtns[1].addEventListener('click', () => this.goToPage(this.state.pagination.page - 1));
this.elements.paginationBtns[2].addEventListener('click', () => this.goToPage(this.state.pagination.page + 1));
this.elements.paginationBtns[3].addEventListener('click', () => this.goToPage(this.state.pagination.pages));
}
if (this.elements.selectAllCheckbox) {
this.elements.selectAllCheckbox.addEventListener('change', (event) => this.handleSelectAllChange(event));
}
if (this.elements.tableBody) {
this.elements.tableBody.addEventListener('click', (event) => {
const checkbox = event.target.closest('input[type="checkbox"]');
const actionButton = event.target.closest('button[data-action]');
if (checkbox) {
this.handleSelectionChange(checkbox);
} else if (actionButton) {
this._handleLogRowAction(actionButton);
}
});
}
if (this.elements.searchInput) {
this.elements.searchInput.addEventListener('input', (event) => this.handleSearchInput(event));
}
if (this.elements.errorTypeFilterBtn) {
this.elements.errorTypeFilterBtn.addEventListener('filter-change', (e) => this.handleFilterChange(e));
}
if (this.elements.errorCodeFilterBtn) {
this.elements.errorCodeFilterBtn.addEventListener('filter-change', (e) => this.handleFilterChange(e));
}
}
handleFilterChange(event) {
const { filterKey, selected } = event.detail;
if (filterKey === 'filter-error-type-btn') {
this.state.filters.error_types = selected;
} else if (filterKey === 'filter-error-code-btn') {
this.state.filters.status_codes = selected;
}
this.state.filters.page = 1;
this.loadAndRenderLogs();
}
handleSearchInput(event) {
const searchTerm = event.target.value.trim().toLowerCase();
this.state.filters.page = 1;
this.state.filters.q = '';
this.state.filters.key_ids = new Set();
this.state.filters.group_ids = new Set();
if (searchTerm === '') {
this.debouncedLoadAndRender();
return;
}
const matchedGroupIds = new Set();
dataStore.groups.forEach(group => {
if (group.display_name.toLowerCase().includes(searchTerm)) {
matchedGroupIds.add(group.id);
}
});
const matchedKeyIds = new Set();
dataStore.keys.forEach(key => {
if (key.APIKey && key.APIKey.toLowerCase().includes(searchTerm)) {
matchedKeyIds.add(key.ID);
}
});
if (matchedGroupIds.size > 0) this.state.filters.group_ids = matchedGroupIds;
if (matchedKeyIds.size > 0) this.state.filters.key_ids = matchedKeyIds;
if (matchedGroupIds.size === 0 && matchedKeyIds.size === 0) {
this.state.filters.q = searchTerm;
}
this.debouncedLoadAndRender();
}
handleSelectionChange(checkbox) {
const row = checkbox.closest('.table-row');
if (!row) return;
const logId = parseInt(row.dataset.logId, 10);
if (isNaN(logId)) return;
if (checkbox.checked) {
this.state.selectedLogIds.add(logId);
} else {
this.state.selectedLogIds.delete(logId);
}
this.syncSelectionUI();
}
handleSelectAllChange(event) {
const isChecked = event.target.checked;
this.state.logs.forEach(log => {
if (isChecked) {
this.state.selectedLogIds.add(log.ID);
} else {
this.state.selectedLogIds.delete(log.ID);
}
});
this.syncRowCheckboxes();
this.syncSelectionUI();
}
syncRowCheckboxes() {
const isAllChecked = this.elements.selectAllCheckbox.checked;
this.elements.tableBody.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.checked = isAllChecked;
});
}
syncSelectionUI() {
if (!this.elements.selectAllCheckbox || !this.elements.selectedCount) return;
const selectedCount = this.state.selectedLogIds.size;
const visibleLogsCount = this.state.logs.length;
if (selectedCount === 0) {
this.elements.selectAllCheckbox.checked = false;
this.elements.selectAllCheckbox.indeterminate = false;
} else if (selectedCount < visibleLogsCount) {
this.elements.selectAllCheckbox.checked = false;
this.elements.selectAllCheckbox.indeterminate = true;
} else if (selectedCount === visibleLogsCount && visibleLogsCount > 0) {
this.elements.selectAllCheckbox.checked = true;
this.elements.selectAllCheckbox.indeterminate = false;
}
this.elements.selectedCount.textContent = selectedCount;
const hasSelection = selectedCount > 0;
const deleteSelectedBtn = document.getElementById('delete-selected-logs-btn');
if (deleteSelectedBtn) {
deleteSelectedBtn.disabled = !hasSelection;
}
}
async _handleLogRowAction(button) {
const action = button.dataset.action;
const row = button.closest('.table-row');
const isDarkMode = document.documentElement.classList.contains('dark');
if (!row) return;
const logId = parseInt(row.dataset.logId, 10);
const log = this.state.logs.find(l => l.ID === logId);
if (!log) {
Swal.fire({ toast: true, position: 'top-end', icon: 'error', title: '找不到日志数据', showConfirmButton: false, timer: 2000 });
return;
}
switch (action) {
case 'view-log-details': {
const detailsHtml = `
<div class="space-y-3 text-left text-sm p-2">
<div class="flex"><p class="w-24 font-semibold text-zinc-500 shrink-0">状态码</p><p class="font-mono text-zinc-800 dark:text-zinc-200">${log.StatusCode || 'N/A'}</p></div>
<div class="flex"><p class="w-24 font-semibold text-zinc-500 shrink-0">状态</p><p class="font-mono text-zinc-800 dark:text-zinc-200">${log.Status || 'N/A'}</p></div>
<div class="flex"><p class="w-24 font-semibold text-zinc-500 shrink-0">模型</p><p class="font-mono text-zinc-800 dark:text-zinc-200">${log.ModelName || 'N/A'}</p></div>
<div class="border-t border-zinc-200 dark:border-zinc-700 my-2"></div>
<div>
<p class="font-semibold text-zinc-500 mb-1">错误消息</p>
<div class="max-h-40 overflow-y-auto bg-zinc-100 dark:bg-zinc-800 p-2 rounded-md text-zinc-700 dark:text-zinc-300 wrap-break-word text-xs">
${log.ErrorMessage ? log.ErrorMessage.replace(/\n/g, '<br>') : '无错误消息。'}
</div>
</div>
</div>
`;
Swal.fire({
target: '#main-content-wrapper',
width: '32rem',
backdrop: `rgba(0,0,0,0.5)`,
heightAuto: false,
customClass: {
popup: `swal2-custom-style rounded-xl ${document.documentElement.classList.contains('dark') ? 'swal2-dark' : ''}`,
title: 'text-lg font-bold',
htmlContainer: 'm-0 text-left',
},
title: '日志详情',
html: detailsHtml,
showCloseButton: false,
showConfirmButton: false,
});
break;
}
case 'copy-api-key': {
const key = dataStore.keys.get(log.KeyID);
if (key && key.APIKey) {
navigator.clipboard.writeText(key.APIKey).then(() => {
Swal.fire({ toast: true, position: 'top-end', customClass: { popup: `swal2-custom-style ${document.documentElement.classList.contains('dark') ? 'swal2-dark' : ''}` }, icon: 'success', title: 'API Key 已复制', showConfirmButton: false, timer: 1500 });
}).catch(err => {
Swal.fire({ toast: true, position: 'top-end', icon: 'error', title: '复制失败', text: err.message, showConfirmButton: false, timer: 2000 });
});
} else {
Swal.fire({ toast: true, position: 'top-end', icon: 'warning', title: '未找到完整的API Key', showConfirmButton: false, timer: 2000 });
return;
}
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(key.APIKey).then(() => {
Swal.fire({ toast: true, position: 'top-end', icon: 'success', title: 'API Key 已复制', showConfirmButton: false, timer: 1500 });
}).catch(err => {
Swal.fire({ toast: true, position: 'top-end', icon: 'error', title: '复制失败', text: err.message, showConfirmButton: false, timer: 2000 });
});
} else {
// 如果不可用,则提供明确的错误提示
Swal.fire({
icon: 'error',
title: '复制失败',
text: '此功能需要安全连接 (HTTPS) 或在 localhost 环境下使用。',
target: '#main-content-wrapper',
customClass: { popup: `swal2-custom-style ${document.documentElement.classList.contains('dark') ? 'swal2-dark' : ''}` },
});
}
break;
}
case 'delete-log': {
Swal.fire({
width: '20rem',
backdrop: `rgba(0,0,0,0.5)`,
heightAuto: false,
customClass: { popup: `swal2-custom-style ${document.documentElement.classList.contains('dark') ? 'swal2-dark' : ''}` },
title: '确认删除',
text: `您确定要删除这条日志吗?此操作不可撤销。`,
showCancelButton: true,
confirmButtonText: '确认删除',
cancelButtonText: '取消',
reverseButtons: false,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#6b7280',
focusCancel: true,
target: '#main-content-wrapper',
}).then(async (result) => {
if (result.isConfirmed) {
try {
const url = `/admin/logs?ids=${logId}`;
const { success, message } = await apiFetchJson(url, { method: 'DELETE' });
if (success) {
Swal.fire({ toast: true, position: 'top-end', icon: 'success', title: '删除成功', showConfirmButton: false, timer: 2000, timerProgressBar: true });
this.loadAndRenderLogs();
} else {
throw new Error(message || '删除失败,请稍后重试。');
}
} catch (error) {
Swal.fire({ icon: 'error', title: '操作失败', text: error.message, target: '#main-content-wrapper' });
}
}
});
break;
}
}
}
changePageSize(newSize) {
this.state.filters.page_size = newSize;
this.state.filters.page = 1;
this.loadAndRenderLogs();
}
goToPage(page) {
if (page < 1 || page > this.state.pagination.pages || this.state.isLoading) return;
this.state.filters.page = page;
this.loadAndRenderLogs();
}
updatePaginationUI() {
const { page, pages, total } = this.state.pagination;
if (this.elements.pageInfo) {
this.elements.pageInfo.textContent = `${page} / ${pages}`;
}
if (this.elements.totalCount) {
this.elements.totalCount.textContent = total;
}
if (this.elements.paginationBtns.length >= 4) {
const isFirstPage = page === 1;
const isLastPage = page === pages || pages === 0;
this.elements.paginationBtns[0].disabled = isFirstPage;
this.elements.paginationBtns[1].disabled = isFirstPage;
this.elements.paginationBtns[2].disabled = isLastPage;
this.elements.paginationBtns[3].disabled = isLastPage;
}
}
async loadGroupsOnce() {
if (dataStore.groups.size > 0) return;
try {
const { success, data } = await apiFetchJson("/admin/keygroups");
if (success && Array.isArray(data)) {
data.forEach(group => dataStore.groups.set(group.id, group));
}
} catch (error) {
console.error("Failed to load key groups:", error);
}
}
async loadAndRenderLogs() {
this.state.isLoading = true;
this.state.selectedLogIds.clear();
this.logList.renderLoading();
this.updatePaginationUI();
this.syncSelectionUI();
try {
const finalParams = {};
const { filters } = this.state;
Object.keys(filters).forEach(key => {
if (!(filters[key] instanceof Set)) {
finalParams[key] = filters[key];
}
});
// --- [MODIFIED] START: Combine all error-related filters into a single parameter for OR logic ---
const allErrorCodes = new Set();
const allStatusCodes = new Set(filters.status_codes);
if (filters.error_types.size > 0) {
filters.error_types.forEach(type => {
// Find matching static error codes (e.g., 'API_KEY_INVALID')
for (const [code, obj] of Object.entries(STATIC_ERROR_MAP)) {
if (obj.type === type) {
allErrorCodes.add(code);
}
}
// Find matching status codes (e.g., 400, 401)
for (const [code, obj] of Object.entries(STATUS_CODE_MAP)) {
if (obj.type === type) {
allStatusCodes.add(code);
}
}
});
}
// Pass the combined codes to the backend. The backend will handle the OR logic.
if (allErrorCodes.size > 0) finalParams.error_codes = [...allErrorCodes].join(',');
if (allStatusCodes.size > 0) finalParams.status_codes = [...allStatusCodes].join(',');
// --- [MODIFIED] END ---
if (filters.key_ids.size > 0) finalParams.key_ids = [...filters.key_ids].join(',');
if (filters.group_ids.size > 0) finalParams.group_ids = [...filters.group_ids].join(',');
Object.keys(finalParams).forEach(key => {
if (finalParams[key] === '' || finalParams[key] === null || finalParams[key] === undefined) {
delete finalParams[key];
}
});
const query = new URLSearchParams(finalParams);
const { success, data } = await apiFetchJson(
`/admin/logs?${query.toString()}`,
{ cache: 'no-cache', noCache: true }
);
if (success && typeof data === 'object' && data.items) {
const { items, total, page, page_size } = data;
this.state.logs = items;
const totalPages = Math.ceil(total / page_size);
this.state.pagination = { page, page_size, total, pages: totalPages > 0 ? totalPages : 1 };
await this.enrichLogsWithKeyNames(items);
this.logList.render(this.state.logs, this.state.pagination, this.state.selectedLogIds);
} else {
this.state.logs = [];
this.state.pagination = { ...this.state.pagination, total: 0, pages: 1, page: 1 };
this.logList.render([], this.state.pagination);
}
} catch (error) {
console.error("Failed to load logs:", error);
this.state.logs = [];
this.state.pagination = { ...this.state.pagination, total: 0, pages: 1, page: 1 };
this.logList.render([], this.state.pagination);
} finally {
this.state.isLoading = false;
this.updatePaginationUI();
this.syncSelectionUI();
}
}
async enrichLogsWithKeyNames(logs) {
const missingKeyIds = [...new Set(
logs.filter(log => log.KeyID && !dataStore.keys.has(log.KeyID)).map(log => log.KeyID)
)];
if (missingKeyIds.length === 0) return;
try {
const idsQuery = missingKeyIds.join(',');
const { success, data } = await apiFetchJson(`/admin/apikeys?ids=${idsQuery}`);
if (success && Array.isArray(data)) {
data.forEach(key => dataStore.keys.set(key.ID, key));
}
} catch (error) {
console.error(`Failed to fetch key details:`, error);
}
}
}
export default function() {
const page = new LogsPage();
page.init();
}