diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 1293a29..54d3441 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -213,7 +213,6 @@ export function MainLayout() { const logout = useAuthStore((state) => state.logout); - const config = useConfigStore((state) => state.config); const fetchConfig = useConfigStore((state) => state.fetchConfig); const clearCache = useConfigStore((state) => state.clearCache); @@ -431,16 +430,12 @@ export function MainLayout() { metaKey: 'nav_meta.quota_management', icon: sidebarIcons.quota, }, - ...(config?.loggingToFile - ? [ - { - path: '/logs', - labelKey: 'nav.logs', - metaKey: 'nav_meta.logs', - icon: sidebarIcons.logs, - }, - ] - : []), + { + path: '/logs', + labelKey: 'nav.logs', + metaKey: 'nav_meta.logs', + icon: sidebarIcons.logs, + }, ], }, { diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index c64667c..dab3664 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -730,12 +730,16 @@ }, "logs": { "title": "Logs Viewer", + "runtime_unknown": "Current runtime: detecting", + "runtime_cpa": "Current runtime: CPA", + "runtime_home": "Current runtime: Home", "refresh_button": "Refresh Logs", "clear_button": "Clear Logs", "download_button": "Download Logs", "error_log_button": "Select Error Log", "error_logs_modal_title": "Error Request Logs", "error_logs_description": "Pick an error request log file to download (only generated when request logging is off).", + "error_logs_home_unavailable": "In Home mode, use Log Content for database logs. Error request log files only apply to CPA file logs.", "error_logs_request_log_enabled": "Request logging is enabled, so this list will always be empty. Disable request logging and refresh to view error logs.", "error_logs_empty": "No error request log files found", "error_logs_load_error": "Failed to load error log list", @@ -748,6 +752,13 @@ "request_log_download_success": "Request log downloaded successfully", "empty_title": "No Logs Available", "empty_desc": "When \"Enable logging to file\" is enabled, logs will be displayed here", + "cpa_file_logging_required": "The connected runtime is CPA. Log viewing requires logging to file to be enabled first.", + "cpa_file_logging_required_title": "File logging is required", + "cpa_file_logging_required_desc": "CPA reads this log view from local log files. Enable logging-to-file in the config, then refresh.", + "file_logging_required": "This logs endpoint requires logging to file before file logs can be read.", + "file_logging_required_title": "File logging is required", + "file_logging_required_desc": "This logs endpoint reads local log files. Enable logging-to-file in the config, then refresh.", + "home_clear_unavailable": "Home logs come from database queries and cannot be cleared here.", "log_content": "Log Content", "loading": "Loading logs...", "load_error": "Failed to load logs", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 668ba6e..911f717 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -727,12 +727,16 @@ }, "logs": { "title": "Просмотр журналов", + "runtime_unknown": "Текущий режим: определяется", + "runtime_cpa": "Текущий режим: CPA", + "runtime_home": "Текущий режим: Home", "refresh_button": "Обновить журналы", "clear_button": "Очистить журналы", "download_button": "Скачать журналы", "error_log_button": "Выбрать журнал ошибок", "error_logs_modal_title": "Журналы ошибок запросов", "error_logs_description": "Выберите файл журнала ошибок запроса для скачивания (создаётся только при отключённом журналировании запросов).", + "error_logs_home_unavailable": "В режиме Home смотрите журналы базы данных на вкладке содержимого. Файлы ошибок запросов относятся только к файловым журналам CPA.", "error_logs_request_log_enabled": "Журналирование запросов включено, поэтому этот список всегда будет пустым. Отключите журналирование запросов и обновите список, чтобы просмотреть журналы ошибок.", "error_logs_empty": "Файлы журнала ошибок запросов не найдены", "error_logs_load_error": "Не удалось загрузить список журналов ошибок", @@ -745,6 +749,13 @@ "request_log_download_success": "Журнал запросов успешно скачан", "empty_title": "Журналы недоступны", "empty_desc": "Когда включена опция \"Включить журналирование в файл\", журналы появятся здесь", + "cpa_file_logging_required": "Подключён режим CPA. Для просмотра журналов сначала включите запись журналов в файл.", + "cpa_file_logging_required_title": "Требуется файловое журналирование", + "cpa_file_logging_required_desc": "CPA читает этот журнал из локальных файлов. Включите logging-to-file в конфигурации и обновите страницу.", + "file_logging_required": "Этот endpoint журналов требует включить запись журналов в файл перед чтением файловых журналов.", + "file_logging_required_title": "Требуется файловое журналирование", + "file_logging_required_desc": "Этот endpoint журналов читает локальные файлы. Включите logging-to-file в конфигурации и обновите страницу.", + "home_clear_unavailable": "Журналы Home загружаются из базы данных и не могут быть очищены здесь.", "log_content": "Содержимое журнала", "loading": "Загрузка журналов...", "load_error": "Не удалось загрузить журналы", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 439d5c6..9f36295 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -730,12 +730,16 @@ }, "logs": { "title": "日志查看", + "runtime_unknown": "当前运行端:识别中", + "runtime_cpa": "当前运行端:CPA", + "runtime_home": "当前运行端:Home", "refresh_button": "刷新日志", "clear_button": "清空日志", "download_button": "下载日志", "error_log_button": "选择错误日志", "error_logs_modal_title": "错误请求日志", "error_logs_description": "请选择要下载的错误请求日志文件(仅在关闭请求日志时生成)。", + "error_logs_home_unavailable": "Home 模式下请在日志内容页查看数据库日志,错误请求日志列表仅适用于 CPA 文件日志。", "error_logs_request_log_enabled": "当前已开启请求日志,按接口约定错误请求日志列表会始终为空。关闭请求日志后再刷新即可查看。", "error_logs_empty": "暂无错误请求日志文件", "error_logs_load_error": "加载错误日志列表失败", @@ -748,6 +752,13 @@ "request_log_download_success": "报文下载成功", "empty_title": "暂无日志记录", "empty_desc": "当启用\"日志记录到文件\"功能后,日志将显示在这里", + "cpa_file_logging_required": "当前连接的是 CPA,日志查看需要先开启“日志记录到文件”。", + "cpa_file_logging_required_title": "需要开启文件日志", + "cpa_file_logging_required_desc": "CPA 的日志接口读取本地日志文件。请在配置中开启 logging-to-file 后再刷新查看。", + "file_logging_required": "当前日志接口需要先开启“日志记录到文件”后才能读取文件日志。", + "file_logging_required_title": "需要开启文件日志", + "file_logging_required_desc": "当前日志接口读取本地日志文件。请在配置中开启 logging-to-file 后再刷新查看。", + "home_clear_unavailable": "Home 日志来自数据库查询,当前不支持从这里清空。", "log_content": "日志内容", "loading": "正在加载日志...", "load_error": "加载日志失败", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index b67890d..d2e166b 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -756,12 +756,16 @@ }, "logs": { "title": "記錄檢視", + "runtime_unknown": "目前執行端:識別中", + "runtime_cpa": "目前執行端:CPA", + "runtime_home": "目前執行端:Home", "refresh_button": "重新整理記錄", "clear_button": "清空記錄", "download_button": "下載記錄", "error_log_button": "選擇錯誤記錄", "error_logs_modal_title": "錯誤請求記錄", "error_logs_description": "請選擇要下載的錯誤請求記錄檔案(僅在關閉請求記錄時產生)。", + "error_logs_home_unavailable": "Home 模式下請在記錄內容頁查看資料庫記錄,錯誤請求記錄清單僅適用於 CPA 檔案記錄。", "error_logs_request_log_enabled": "目前已開啟請求記錄,按介面約定錯誤請求記錄清單會始終為空。關閉請求記錄後再重新整理即可查看。", "error_logs_empty": "暫無錯誤請求記錄檔案", "error_logs_load_error": "載入錯誤記錄清單失敗", @@ -774,6 +778,13 @@ "request_log_download_success": "封包下載成功", "empty_title": "暫無記錄", "empty_desc": "當啟用「記錄到檔案」功能後,記錄將顯示在這裡", + "cpa_file_logging_required": "目前連線的是 CPA,記錄檢視需要先開啟「記錄到檔案」。", + "cpa_file_logging_required_title": "需要開啟檔案記錄", + "cpa_file_logging_required_desc": "CPA 的記錄介面會讀取本機記錄檔。請在設定中開啟 logging-to-file 後再重新整理查看。", + "file_logging_required": "目前記錄介面需要先開啟「記錄到檔案」後才能讀取檔案記錄。", + "file_logging_required_title": "需要開啟檔案記錄", + "file_logging_required_desc": "目前記錄介面會讀取本機記錄檔。請在設定中開啟 logging-to-file 後再重新整理查看。", + "home_clear_unavailable": "Home 記錄來自資料庫查詢,目前不支援從這裡清空。", "log_content": "記錄內容", "loading": "正在載入記錄...", "load_error": "載入記錄失敗", diff --git a/src/pages/LogsPage.module.scss b/src/pages/LogsPage.module.scss index 570c969..6454f90 100644 --- a/src/pages/LogsPage.module.scss +++ b/src/pages/LogsPage.module.scss @@ -13,11 +13,25 @@ } } +.pageHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: $spacing-md; + margin: 0 0 $spacing-lg 0; + + @include mobile { + align-items: flex-start; + flex-direction: column; + gap: $spacing-xs; + } +} + .pageTitle { font-size: 28px; font-weight: 700; color: var(--text-primary); - margin: 0 0 $spacing-lg 0; + margin: 0; } .tabBar { @@ -84,6 +98,17 @@ } } +.runtimeNotice { + flex: 0 0 auto; + padding: 4px 10px; + border: 1px solid var(--border-color); + border-radius: $radius-full; + color: var(--text-secondary); + background: var(--bg-secondary); + font-size: 12px; + line-height: 1.3; +} + .toolbar { display: flex; align-items: center; @@ -622,6 +647,9 @@ @media (max-height: 820px) { .pageTitle { font-size: 24px; + } + + .pageHeader { margin-bottom: $spacing-md; } @@ -671,6 +699,9 @@ @media (max-height: 600px) { .pageTitle { font-size: 20px; + } + + .pageHeader { margin-bottom: $spacing-sm; } diff --git a/src/pages/LogsPage.tsx b/src/pages/LogsPage.tsx index f6b738c..0890369 100644 --- a/src/pages/LogsPage.tsx +++ b/src/pages/LogsPage.tsx @@ -23,7 +23,8 @@ import { import { useHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores'; -import { logsApi } from '@/services/api/logs'; +import { logsApi, type LogsQuery } from '@/services/api/logs'; +import { versionApi } from '@/services/api/version'; import { copyToClipboard } from '@/utils/clipboard'; import { downloadBlob } from '@/utils/download'; import { MANAGEMENT_API_PREFIX } from '@/utils/constants'; @@ -61,14 +62,44 @@ const getErrorMessage = (err: unknown): string => { return typeof message === 'string' ? message : ''; }; +const getErrorPayloadText = (err: unknown): string => { + if (typeof err !== 'object' || err === null) return ''; + const payloads = [ + (err as { data?: unknown }).data, + (err as { details?: unknown }).details + ].filter((payload) => payload !== undefined); + return payloads + .map((payload) => { + if (typeof payload === 'string') return payload; + try { + return JSON.stringify(payload); + } catch { + return ''; + } + }) + .join(' '); +}; + +const isLoggingToFileDisabledError = (err: unknown): boolean => { + const text = `${getErrorMessage(err)} ${getErrorPayloadText(err)}`.toLowerCase(); + return text.includes('logging to file disabled'); +}; + type TabType = 'logs' | 'errors'; export function LogsPage() { const { t } = useTranslation(); const { showNotification, showConfirmation } = useNotificationStore(); const connectionStatus = useAuthStore((state) => state.connectionStatus); + const serverRuntimeKind = useAuthStore((state) => state.serverRuntimeKind); + const updateServerRuntimeKind = useAuthStore((state) => state.updateServerRuntimeKind); const config = useConfigStore((state) => state.config); const requestLogEnabled = config?.requestLog ?? false; + const loggingToFileEnabled = config?.loggingToFile ?? false; + const cpaNeedsFileLogging = serverRuntimeKind === 'cpa' && !loggingToFileEnabled; + const isHomeRuntime = serverRuntimeKind === 'home'; + const [fileLoggingRequired, setFileLoggingRequired] = useState(false); + const showFileLoggingRequired = cpaNeedsFileLogging || fileLoggingRequired; const [activeTab, setActiveTab] = useState('logs'); const [logState, setLogState] = useState({ buffer: [], visibleFrom: 0 }); @@ -93,6 +124,7 @@ export function LogsPage() { const [requestLogDownloading, setRequestLogDownloading] = useState(false); const logScrollerRef = useRef | null>(null); + const requestLogHomeIpByIdRef = useRef>({}); const longPressRef = useRef<{ timer: number | null; startX: number; @@ -102,10 +134,13 @@ export function LogsPage() { const logRequestInFlightRef = useRef(false); const pendingFullReloadRef = useRef(false); - // 保存最新时间戳用于增量获取 - const latestTimestampRef = useRef(0); + // 保存最新游标用于增量获取 + const latestCursorRef = useRef(undefined); const disableControls = connectionStatus !== 'connected'; + const refreshDisabled = disableControls || loading || cpaNeedsFileLogging; + const autoRefreshDisabled = disableControls || showFileLoggingRequired; + const clearDisabled = disableControls || showFileLoggingRequired || isHomeRuntime; const loadLogs = async (incremental = false) => { if (connectionStatus !== 'connected') { @@ -113,6 +148,18 @@ export function LogsPage() { return; } + if (cpaNeedsFileLogging) { + if (!incremental) { + latestCursorRef.current = undefined; + requestLogHomeIpByIdRef.current = {}; + setFileLoggingRequired(false); + setLogState({ buffer: [], visibleFrom: 0 }); + setError(''); + setLoading(false); + } + return; + } + if (logRequestInFlightRef.current) { if (!incremental) { pendingFullReloadRef.current = true; @@ -135,13 +182,25 @@ export function LogsPage() { scrollerInstance?.requestScrollToBottom(); } - const params = - incremental && latestTimestampRef.current > 0 ? { after: latestTimestampRef.current } : {}; + const params: LogsQuery = + incremental && latestCursorRef.current + ? { after: latestCursorRef.current, limit: MAX_BUFFER_LINES } + : { limit: MAX_BUFFER_LINES }; const data = await logsApi.fetchLogs(params); + setFileLoggingRequired(false); - // 更新时间戳 - if (data['latest-timestamp']) { - latestTimestampRef.current = data['latest-timestamp']; + // 更新游标 + if (data.latestCursor) { + latestCursorRef.current = data.latestCursor; + } else if (!incremental) { + latestCursorRef.current = undefined; + } + if (data.requestLogHomeIpById) { + requestLogHomeIpByIdRef.current = incremental + ? { ...requestLogHomeIpByIdRef.current, ...data.requestLogHomeIpById } + : data.requestLogHomeIpById; + } else if (!incremental) { + requestLogHomeIpByIdRef.current = {}; } const newLines = Array.isArray(data.lines) ? data.lines : []; @@ -170,6 +229,16 @@ export function LogsPage() { } } catch (err: unknown) { console.error('Failed to load logs:', err); + if (isLoggingToFileDisabledError(err)) { + if (!incremental) { + latestCursorRef.current = undefined; + requestLogHomeIpByIdRef.current = {}; + setFileLoggingRequired(true); + setLogState({ buffer: [], visibleFrom: 0 }); + setError(''); + } + return; + } if (!incremental) { setError(getErrorMessage(err) || t('logs.load_error')); } @@ -188,6 +257,18 @@ export function LogsPage() { useHeaderRefresh(() => loadLogs(false)); const clearLogs = async () => { + if (isHomeRuntime) { + showNotification(t('logs.home_clear_unavailable'), 'warning'); + return; + } + if (cpaNeedsFileLogging) { + showNotification(t('logs.cpa_file_logging_required'), 'warning'); + return; + } + if (fileLoggingRequired) { + showNotification(t('logs.file_logging_required'), 'warning'); + return; + } showConfirmation({ title: t('logs.clear_confirm_title', { defaultValue: 'Clear Logs' }), message: t('logs.clear_confirm'), @@ -197,7 +278,9 @@ export function LogsPage() { try { await logsApi.clearLogs(); setLogState({ buffer: [], visibleFrom: 0 }); - latestTimestampRef.current = 0; + latestCursorRef.current = undefined; + requestLogHomeIpByIdRef.current = {}; + setFileLoggingRequired(false); showNotification(t('logs.clear_success'), 'success'); } catch (err: unknown) { const message = getErrorMessage(err); @@ -221,6 +304,12 @@ export function LogsPage() { setLoadingErrors(false); return; } + if (isHomeRuntime) { + setLoadingErrors(false); + setErrorLogs([]); + setErrorLogsError(''); + return; + } setLoadingErrors(true); setErrorLogsError(''); @@ -256,11 +345,28 @@ export function LogsPage() { useEffect(() => { if (connectionStatus === 'connected') { - latestTimestampRef.current = 0; + latestCursorRef.current = undefined; + requestLogHomeIpByIdRef.current = {}; + setFileLoggingRequired(false); loadLogs(false); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [connectionStatus]); + }, [connectionStatus, loggingToFileEnabled]); + + useEffect(() => { + if (connectionStatus !== 'connected' || serverRuntimeKind !== 'unknown') return; + let cancelled = false; + const detectRuntime = async () => { + const runtimeKind = await versionApi.detectRuntimeKind(); + if (!cancelled && (runtimeKind === 'cpa' || runtimeKind === 'home')) { + updateServerRuntimeKind(runtimeKind); + } + }; + void detectRuntime(); + return () => { + cancelled = true; + }; + }, [connectionStatus, serverRuntimeKind, updateServerRuntimeKind]); useEffect(() => { if (activeTab !== 'errors') return; @@ -270,7 +376,7 @@ export function LogsPage() { }, [activeTab, connectionStatus, requestLogEnabled]); useEffect(() => { - if (!autoRefresh || connectionStatus !== 'connected') { + if (!autoRefresh || connectionStatus !== 'connected' || showFileLoggingRequired) { return; } const id = window.setInterval(() => { @@ -278,7 +384,7 @@ export function LogsPage() { }, 8000); return () => window.clearInterval(id); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [autoRefresh, connectionStatus]); + }, [autoRefresh, connectionStatus, showFileLoggingRequired]); const visibleLines = useMemo( () => logState.buffer.slice(logState.visibleFrom), @@ -423,7 +529,10 @@ export function LogsPage() { const downloadRequestLog = async (id: string) => { setRequestLogDownloading(true); try { - const response = await logsApi.downloadRequestLogById(id); + const response = await logsApi.downloadRequestLogById( + id, + requestLogHomeIpByIdRef.current[id] + ); downloadBlob({ filename: `request-${id}.log`, blob: new Blob([response.data], { type: 'text/plain' }) @@ -452,7 +561,12 @@ export function LogsPage() { return (
-

{t('logs.title')}

+
+

{t('logs.title')}

+
+ {t(`logs.runtime_${serverRuntimeKind}`)} +
+