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 = ''; if (viewName === 'error') { this.elements.errorFilters.classList.remove('hidden'); this.elements.systemControls.classList.add('hidden'); const template = this.elements.errorTemplate.content.cloneNode(true); this.elements.contentContainer.appendChild(template); requestAnimationFrame(() => { this._initErrorLogView(); }); } else if (viewName === 'system') { this.elements.errorFilters.classList.add('hidden'); this.elements.systemControls.classList.remove('hidden'); 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 = ` `; const template = document.createElement('template'); template.className = 'custom-select-panel-template'; template.innerHTML = `