feat: add service log display in log page

This commit is contained in:
Kuingsmile
2025-07-22 17:07:44 +08:00
parent 386147d5ff
commit 06e54d1b01
8 changed files with 151 additions and 74 deletions

View File

@@ -5,7 +5,7 @@ use std::process::Command;
use tauri::State;
use crate::object::structs::AppState;
use crate::utils::path::{get_app_logs_dir, get_default_openlist_data_dir};
use crate::utils::path::{get_app_logs_dir, get_default_openlist_data_dir, get_service_log_path};
fn generate_random_password() -> String {
use std::collections::hash_map::DefaultHasher;
@@ -110,6 +110,7 @@ async fn execute_openlist_admin_set(
fn resolve_log_paths(source: Option<&str>, data_dir: Option<&str>) -> Result<Vec<PathBuf>, String> {
let logs_dir = get_app_logs_dir()?;
let service_path = get_service_log_path()?;
let openlist_log_base = if let Some(dir) = data_dir.filter(|d| !d.is_empty()) {
PathBuf::from(dir)
@@ -124,11 +125,13 @@ fn resolve_log_paths(source: Option<&str>, data_dir: Option<&str>) -> Result<Vec
Some("app") => paths.push(logs_dir.join("app.log")),
Some("rclone") => paths.push(logs_dir.join("process_rclone.log")),
Some("openlist_core") => paths.push(logs_dir.join("process_openlist_core.log")),
None => {
Some("service") => paths.push(service_path),
Some("all") => {
paths.push(openlist_log_base.join("log/log.log"));
paths.push(logs_dir.join("app.log"));
paths.push(logs_dir.join("process_rclone.log"));
paths.push(logs_dir.join("process_openlist_core.log"));
paths.push(service_path);
}
_ => return Err("Invalid log source".into()),
}
@@ -225,9 +228,13 @@ pub async fn get_logs(
let mut logs = Vec::new();
for path in paths {
let content =
std::fs::read_to_string(&path).map_err(|e| format!("Failed to read {path:?}: {e}"))?;
logs.extend(content.lines().map(str::to_string));
if path.exists() {
let content = std::fs::read_to_string(&path)
.map_err(|e| format!("Failed to read {path:?}: {e}"))?;
logs.extend(content.lines().map(str::to_string));
} else {
log::info!("Log file does not exist: {path:?}");
}
}
Ok(logs)
}

View File

@@ -106,3 +106,21 @@ pub fn get_default_openlist_data_dir() -> Result<PathBuf, String> {
Ok(get_app_dir()?.join("data"))
}
}
pub fn get_service_log_path() -> Result<PathBuf, String> {
#[cfg(target_os = "macos")]
{
let home = env::var("HOME").map_err(|_| "Failed to get HOME environment variable")?;
let logs = PathBuf::from(home)
.join("Library")
.join("Application Support")
.join("io.github.openlistteam.openlist.service.bundle")
.join("openlist-desktop-service.log");
Ok(logs)
}
#[cfg(not(target_os = "macos"))]
{
Ok(get_app_dir()?.join("openlist-desktop-service.log"))
}
}

View File

@@ -79,9 +79,9 @@ export class TauriAPI {
// --- Logs management ---
static logs = {
get: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core'): Promise<string[]> =>
get: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core' | 'service' | 'all'): Promise<string[]> =>
call('get_logs', { source: src }),
clear: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core'): Promise<boolean> =>
clear: (src?: 'openlist' | 'rclone' | 'app' | 'openlist_core' | 'service' | 'all'): Promise<boolean> =>
call('clear_logs', { source: src }),
adminPassword: (): Promise<string> => call('get_admin_password'),
resetAdminPassword: (): Promise<string> => call('reset_admin_password'),

View File

@@ -2,6 +2,7 @@
"common": {
"save": "Save",
"cancel": "Cancel",
"confirm": "Confirm",
"reset": "Reset",
"close": "Close",
"minimize": "Minimize",
@@ -102,7 +103,10 @@
"subtitle": "Configure your OpenList Desktop application",
"saveChanges": "Save Changes",
"resetToDefaults": "Reset to defaults",
"confirmReset": "Are you sure you want to reset all settings to defaults? This action cannot be undone.",
"confirmReset": {
"title": "Reset Settings",
"message": "Are you sure you want to reset all settings to defaults? This action cannot be undone."
},
"saved": "Settings saved successfully!",
"saveFailed": "Failed to save settings. Please try again.",
"resetSuccess": "Settings reset to defaults successfully!",
@@ -283,7 +287,8 @@
"sources": {
"all": "All Sources",
"rclone": "Rclone",
"openlist": "OpenList"
"openlist": "OpenList",
"service": "Service"
},
"actions": {
"selectAll": "Select All (Ctrl+A)",
@@ -303,7 +308,8 @@
"stripAnsiColors": "Strip ANSI Colors"
},
"messages": {
"confirmClear": "Are you sure you want to clear all logs?"
"confirmClear": "Are you sure you want to clear all logs?",
"confirmTitle": "Clear Logs"
},
"headers": {
"timestamp": "Timestamp",

View File

@@ -2,6 +2,7 @@
"common": {
"save": "保存",
"cancel": "取消",
"confirm": "确认",
"reset": "重置",
"close": "关闭",
"minimize": "最小化",
@@ -102,7 +103,10 @@
"subtitle": "配置您的 OpenList 桌面应用程序",
"saveChanges": "保存更改",
"resetToDefaults": "重置为默认值",
"confirmReset": "您确定要将所有设置重置为默认值吗?此操作无法撤消。",
"confirmReset": {
"title": "重置设置",
"message": "您确定要将所有设置重置为默认值吗?此操作无法撤消。"
},
"saved": "设置保存成功!",
"saveFailed": "保存设置失败,请重试。",
"resetSuccess": "设置已重置为默认值!",
@@ -283,7 +287,8 @@
"sources": {
"all": "所有来源",
"rclone": "Rclone",
"openlist": "OpenList"
"openlist": "OpenList",
"service": "服务"
},
"actions": {
"selectAll": "全选 (Ctrl+A)",
@@ -303,7 +308,8 @@
"stripAnsiColors": "去除 ANSI 颜色"
},
"messages": {
"confirmClear": "您确定要清除所有日志吗?"
"confirmClear": "您确定要清除所有日志吗?",
"confirmTitle": "清除日志"
},
"headers": {
"timestamp": "时间",

View File

@@ -553,7 +553,7 @@ export const useAppStore = defineStore('app', () => {
}
}
async function loadLogs(source?: 'openlist' | 'rclone' | 'app') {
async function loadLogs(source?: 'openlist' | 'rclone' | 'app' | 'service' | 'all') {
try {
source = source || 'openlist'
const logEntries = await TauriAPI.logs.get(source)
@@ -563,9 +563,10 @@ export const useAppStore = defineStore('app', () => {
}
}
async function clearLogs(source?: 'openlist' | 'rclone' | 'app') {
async function clearLogs(source?: 'openlist' | 'rclone' | 'app' | 'service' | 'all') {
try {
loading.value = true
source = source || 'openlist'
const result = await TauriAPI.logs.clear(source)
if (result) {
logs.value = []

View File

@@ -22,6 +22,9 @@ import {
FolderOpen
} from 'lucide-vue-next'
import * as chrono from 'chrono-node'
import ConfirmDialog from '../components/ui/ConfirmDialog.vue'
type filterSourceType = 'openlist' | 'rclone' | 'app' | 'service' | 'all'
const appStore = useAppStore()
const { t } = useTranslation()
@@ -30,10 +33,10 @@ const searchInputRef = ref<HTMLInputElement>()
const autoScroll = ref(true)
const isPaused = ref(false)
const filterLevel = ref<string>('all')
const filterSource = ref<string>(localStorage.getItem('logFilterSource') || 'all')
const filterSource = ref<string>(localStorage.getItem('logFilterSource') || 'openlist')
const searchQuery = ref('')
const selectedEntries = ref<Set<number>>(new Set())
const showFilters = ref(false)
const showFilters = ref(true)
const showSettings = ref(false)
const fontSize = ref(13)
const maxLines = ref(1000)
@@ -43,12 +46,17 @@ const stripAnsiColors = ref(true)
const showNotification = ref(false)
const notificationMessage = ref('')
const notificationType = ref<'success' | 'info' | 'warning' | 'error'>('success')
const showConfirmDialog = ref(false)
const confirmDialogConfig = ref({
title: '',
message: '',
onConfirm: () => {},
onCancel: () => {}
})
watch(filterSource, async newValue => {
localStorage.setItem('logFilterSource', newValue)
await appStore.loadLogs(
(newValue !== 'all' && newValue !== 'gin' ? newValue : 'openlist') as 'openlist' | 'rclone' | 'app'
)
await appStore.loadLogs((newValue !== 'gin' ? newValue : 'openlist') as filterSourceType)
await scrollToBottom()
})
@@ -115,11 +123,8 @@ const parseLogEntry = (logText: string) => {
}
}
}
} else if (cleanText.includes('openlist_desktop') || cleanText.includes('tao::')) {
source = 'app'
level = 'info'
} else if (cleanText.toLowerCase().includes('rclone')) {
source = 'rclone'
} else {
source = filterSource.value
}
message = message
@@ -198,21 +203,30 @@ const scrollToTop = () => {
}
const clearLogs = async () => {
if (confirm(t('logs.messages.confirmClear'))) {
try {
await appStore.clearLogs(
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
| 'openlist'
| 'rclone'
| 'app'
)
selectedEntries.value.clear()
showNotificationMessage(t('logs.notifications.clearSuccess'), 'success')
} catch (error) {
console.error('Failed to clear logs:', error)
showNotificationMessage(t('logs.notifications.clearFailed'), 'error')
confirmDialogConfig.value = {
title: t('logs.messages.confirmTitle') || t('common.confirm'),
message: t('logs.messages.confirmClear'),
onConfirm: async () => {
showConfirmDialog.value = false
try {
await appStore.clearLogs(
(filterSource.value !== 'all' && filterSource.value !== 'gin'
? filterSource.value
: 'openlist') as filterSourceType
)
selectedEntries.value.clear()
showNotificationMessage(t('logs.notifications.clearSuccess'), 'success')
} catch (error) {
console.error('Failed to clear logs:', error)
showNotificationMessage(t('logs.notifications.clearFailed'), 'error')
}
},
onCancel: () => {
showConfirmDialog.value = false
}
}
showConfirmDialog.value = true
}
const copyLogsToClipboard = async () => {
@@ -285,10 +299,7 @@ const togglePause = () => {
const refreshLogs = async () => {
await appStore.loadLogs(
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
| 'openlist'
| 'rclone'
| 'app'
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType
)
await scrollToBottom()
if (isPaused.value) {
@@ -363,28 +374,16 @@ const handleKeydown = (event: KeyboardEvent) => {
}
onMounted(async () => {
appStore
.loadLogs(
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
| 'openlist'
| 'rclone'
| 'app'
)
.then(() => {
scrollToBottom()
})
appStore.loadLogs((filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType).then(() => {
scrollToBottom()
})
document.addEventListener('keydown', handleKeydown)
logRefreshInterval = setInterval(async () => {
if (!isPaused.value) {
const oldLength = appStore.logs.length
await appStore.loadLogs(
(filterSource.value !== 'all' && filterSource.value !== 'gin' ? filterSource.value : 'openlist') as
| 'openlist'
| 'rclone'
| 'app'
)
await appStore.loadLogs((filterSource.value !== 'gin' ? filterSource.value : 'openlist') as filterSourceType)
if (appStore.logs.length > oldLength) {
await scrollToBottom()
@@ -532,6 +531,7 @@ onUnmounted(() => {
<option value="openlist">{{ t('logs.filters.sources.openlist') }}</option>
<option value="gin">GIN Server</option>
<option value="rclone">{{ t('logs.filters.sources.rclone') }}</option>
<option value="service">{{ t('logs.filters.sources.service') }}</option>
<option value="app">{{ t('logs.filters.app') }}</option>
</select>
</div>
@@ -659,6 +659,17 @@ onUnmounted(() => {
</div>
</div>
</Transition>
<ConfirmDialog
:is-open="showConfirmDialog"
:title="confirmDialogConfig.title"
:message="confirmDialogConfig.message"
:confirm-text="t('common.confirm')"
:cancel-text="t('common.cancel')"
variant="danger"
@confirm="confirmDialogConfig.onConfirm"
@cancel="confirmDialogConfig.onCancel"
/>
</div>
</template>

View File

@@ -16,6 +16,7 @@ import {
} from 'lucide-vue-next'
import { enable, isEnabled, disable } from '@tauri-apps/plugin-autostart'
import { open } from '@tauri-apps/plugin-dialog'
import ConfirmDialog from '../components/ui/ConfirmDialog.vue'
const appStore = useAppStore()
const route = useRoute()
@@ -27,6 +28,13 @@ const activeTab = ref('openlist')
const rcloneConfigJson = ref('')
const autoStartApp = ref(false)
const isResettingPassword = ref(false)
const showConfirmDialog = ref(false)
const confirmDialogConfig = ref({
title: '',
message: '',
onConfirm: () => {},
onCancel: () => {}
})
const openlistCoreSettings = reactive({ ...appStore.settings.openlist })
const rcloneSettings = reactive({ ...appStore.settings.rclone })
@@ -167,24 +175,33 @@ const handleSave = async () => {
}
const handleReset = async () => {
if (!confirm(t('settings.confirmReset'))) {
return
confirmDialogConfig.value = {
title: t('settings.confirmReset.title'),
message: t('settings.confirmReset.message'),
onConfirm: async () => {
showConfirmDialog.value = false
try {
await appStore.resetSettings()
Object.assign(openlistCoreSettings, appStore.settings.openlist)
Object.assign(rcloneSettings, appStore.settings.rclone)
Object.assign(appSettings, appStore.settings.app)
rcloneConfigJson.value = JSON.stringify(rcloneSettings.config, null, 2)
message.value = t('settings.resetSuccess')
messageType.value = 'info'
} catch (error) {
message.value = t('settings.resetFailed')
messageType.value = 'error'
}
},
onCancel: () => {
showConfirmDialog.value = false
}
}
try {
await appStore.resetSettings()
Object.assign(openlistCoreSettings, appStore.settings.openlist)
Object.assign(rcloneSettings, appStore.settings.rclone)
Object.assign(appSettings, appStore.settings.app)
rcloneConfigJson.value = JSON.stringify(rcloneSettings.config, null, 2)
message.value = t('settings.resetSuccess')
messageType.value = 'info'
} catch (error) {
message.value = t('settings.resetFailed')
messageType.value = 'error'
}
showConfirmDialog.value = true
}
const handleSelectDataDir = async () => {
@@ -594,6 +611,17 @@ const loadCurrentAdminPassword = async () => {
</div>
</div>
</div>
<ConfirmDialog
:is-open="showConfirmDialog"
:title="confirmDialogConfig.title"
:message="confirmDialogConfig.message"
:confirm-text="t('common.confirm')"
:cancel-text="t('common.cancel')"
variant="danger"
@confirm="confirmDialogConfig.onConfirm"
@cancel="confirmDialogConfig.onCancel"
/>
</div>
</template>