diff --git a/src-tauri/capabilities/log-app-window.json b/src-tauri/capabilities/log-app-window.json new file mode 100644 index 000000000..9f95d1bb9 --- /dev/null +++ b/src-tauri/capabilities/log-app-window.json @@ -0,0 +1,14 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "logs-app-window", + "description": "enables permissions for the logs app window", + "windows": ["logs-app-window"], + "permissions": [ + "core:default", + "core:window:allow-start-dragging", + "core:window:allow-set-theme", + "log:default", + "core:webview:allow-create-webview-window", + "core:window:allow-set-focus" + ] +} diff --git a/web-app/src/constants/routes.ts b/web-app/src/constants/routes.ts index a0ffa6581..db741e9e6 100644 --- a/web-app/src/constants/routes.ts +++ b/web-app/src/constants/routes.ts @@ -1,6 +1,7 @@ export const route = { // home as new chat or thread home: '/', + appLogs: '/logs', assistant: '/assistant', settings: { index: '/settings', diff --git a/web-app/src/constants/windows.ts b/web-app/src/constants/windows.ts new file mode 100644 index 000000000..11822964a --- /dev/null +++ b/web-app/src/constants/windows.ts @@ -0,0 +1,5 @@ +export const windowKey = { + logsAppWindow: 'logs-app-window', + logsWindowLocalApiServer: 'logs-window-local-api-server', + systemMonitorWindow: 'system-monitor-window', +} diff --git a/web-app/src/routeTree.gen.ts b/web-app/src/routeTree.gen.ts index 61e30f007..52782cb2e 100644 --- a/web-app/src/routeTree.gen.ts +++ b/web-app/src/routeTree.gen.ts @@ -12,6 +12,7 @@ import { Route as rootRoute } from './routes/__root' import { Route as SystemMonitorImport } from './routes/system-monitor' +import { Route as LogsImport } from './routes/logs' import { Route as HubImport } from './routes/hub' import { Route as AssistantImport } from './routes/assistant' import { Route as IndexImport } from './routes/index' @@ -36,6 +37,12 @@ const SystemMonitorRoute = SystemMonitorImport.update({ getParentRoute: () => rootRoute, } as any) +const LogsRoute = LogsImport.update({ + id: '/logs', + path: '/logs', + getParentRoute: () => rootRoute, +} as any) + const HubRoute = HubImport.update({ id: '/hub', path: '/hub', @@ -152,6 +159,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof HubImport parentRoute: typeof rootRoute } + '/logs': { + id: '/logs' + path: '/logs' + fullPath: '/logs' + preLoaderRoute: typeof LogsImport + parentRoute: typeof rootRoute + } '/system-monitor': { id: '/system-monitor' path: '/system-monitor' @@ -252,6 +266,7 @@ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/assistant': typeof AssistantRoute '/hub': typeof HubRoute + '/logs': typeof LogsRoute '/system-monitor': typeof SystemMonitorRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute '/settings/appearance': typeof SettingsAppearanceRoute @@ -271,6 +286,7 @@ export interface FileRoutesByTo { '/': typeof IndexRoute '/assistant': typeof AssistantRoute '/hub': typeof HubRoute + '/logs': typeof LogsRoute '/system-monitor': typeof SystemMonitorRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute '/settings/appearance': typeof SettingsAppearanceRoute @@ -291,6 +307,7 @@ export interface FileRoutesById { '/': typeof IndexRoute '/assistant': typeof AssistantRoute '/hub': typeof HubRoute + '/logs': typeof LogsRoute '/system-monitor': typeof SystemMonitorRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute '/settings/appearance': typeof SettingsAppearanceRoute @@ -312,6 +329,7 @@ export interface FileRouteTypes { | '/' | '/assistant' | '/hub' + | '/logs' | '/system-monitor' | '/local-api-server/logs' | '/settings/appearance' @@ -330,6 +348,7 @@ export interface FileRouteTypes { | '/' | '/assistant' | '/hub' + | '/logs' | '/system-monitor' | '/local-api-server/logs' | '/settings/appearance' @@ -348,6 +367,7 @@ export interface FileRouteTypes { | '/' | '/assistant' | '/hub' + | '/logs' | '/system-monitor' | '/local-api-server/logs' | '/settings/appearance' @@ -368,6 +388,7 @@ export interface RootRouteChildren { IndexRoute: typeof IndexRoute AssistantRoute: typeof AssistantRoute HubRoute: typeof HubRoute + LogsRoute: typeof LogsRoute SystemMonitorRoute: typeof SystemMonitorRoute LocalApiServerLogsRoute: typeof LocalApiServerLogsRoute SettingsAppearanceRoute: typeof SettingsAppearanceRoute @@ -387,6 +408,7 @@ const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, AssistantRoute: AssistantRoute, HubRoute: HubRoute, + LogsRoute: LogsRoute, SystemMonitorRoute: SystemMonitorRoute, LocalApiServerLogsRoute: LocalApiServerLogsRoute, SettingsAppearanceRoute: SettingsAppearanceRoute, @@ -415,6 +437,7 @@ export const routeTree = rootRoute "/", "/assistant", "/hub", + "/logs", "/system-monitor", "/local-api-server/logs", "/settings/appearance", @@ -439,6 +462,9 @@ export const routeTree = rootRoute "/hub": { "filePath": "hub.tsx" }, + "/logs": { + "filePath": "logs.tsx" + }, "/system-monitor": { "filePath": "system-monitor.tsx" }, diff --git a/web-app/src/routes/__root.tsx b/web-app/src/routes/__root.tsx index 413a4c577..8c7282bbc 100644 --- a/web-app/src/routes/__root.tsx +++ b/web-app/src/routes/__root.tsx @@ -60,7 +60,8 @@ function RootLayout() { const router = useRouterState() const isLocalAPIServerLogsRoute = router.location.pathname === route.localApiServerlogs || - router.location.pathname === route.systemMonitor + router.location.pathname === route.systemMonitor || + router.location.pathname === route.appLogs return ( diff --git a/web-app/src/routes/logs.tsx b/web-app/src/routes/logs.tsx new file mode 100644 index 000000000..ad92baf4f --- /dev/null +++ b/web-app/src/routes/logs.tsx @@ -0,0 +1,116 @@ +import { createFileRoute } from '@tanstack/react-router' +import { route } from '@/constants/routes' + +import { useEffect, useState, useRef } from 'react' +import { parseLogLine, readLogs } from '@/services/app' +import { listen } from '@tauri-apps/api/event' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const Route = createFileRoute(route.appLogs as any)({ + component: LogsViewer, +}) + +// Define log entry type +interface LogEntry { + timestamp: string + level: 'info' | 'warn' | 'error' | 'debug' + target: string + message: string +} + +const LOG_EVENT_NAME = 'log://log' + +function LogsViewer() { + const [logs, setLogs] = useState([]) + const logsContainerRef = useRef(null) + + useEffect(() => { + readLogs().then((logData) => { + const logs = logData.filter(Boolean) as LogEntry[] + setLogs(logs) + + // Scroll to bottom after initial logs are loaded + setTimeout(() => { + scrollToBottom() + }, 100) + }) + let unsubscribe = () => {} + listen(LOG_EVENT_NAME, (event) => { + const { message } = event.payload as { message: string } + const log: LogEntry | undefined = parseLogLine(message) + if (log) { + setLogs((prevLogs) => { + const newLogs = [...prevLogs, log] + // Schedule scroll to bottom after state update + setTimeout(() => { + scrollToBottom() + }, 0) + return newLogs + }) + } + }).then((unsub) => { + unsubscribe = unsub + }) + return () => { + unsubscribe() + } + }, []) + + // Function to scroll to the bottom of the logs container + const scrollToBottom = () => { + if (logsContainerRef.current) { + const { scrollHeight, clientHeight } = logsContainerRef.current + logsContainerRef.current.scrollTop = scrollHeight - clientHeight + } + } + + // Function to get appropriate color for log level + const getLogLevelColor = (level: string) => { + switch (level) { + case 'error': + return 'text-red-500' + case 'warn': + return 'text-yellow-500' + case 'info': + return 'text-blue-500' + case 'debug': + return 'text-gray-500' + default: + return 'text-gray-500' + } + } + + // Format timestamp to be more readable + const formatTimestamp = (timestamp: string) => { + const date = new Date(timestamp) + return date.toLocaleTimeString() + } + + return ( +
+
+
+ {logs.length === 0 ? ( +
+ No logs available +
+ ) : ( + logs.map((log, index) => ( +
+ + [{formatTimestamp(log.timestamp)}] + + + {log.level.toUpperCase()} + + {log.message} +
+ )) + )} +
+
+
+ ) +} diff --git a/web-app/src/routes/settings/general.tsx b/web-app/src/routes/settings/general.tsx index 12a99a27e..f79bdce73 100644 --- a/web-app/src/routes/settings/general.tsx +++ b/web-app/src/routes/settings/general.tsx @@ -26,7 +26,9 @@ import { getJanDataFolder, relocateJanDataFolder, } from '@/services/app' -import { IconFolder } from '@tabler/icons-react' +import { IconFolder, IconLogs } from '@tabler/icons-react' +import { WebviewWindow } from '@tauri-apps/api/webviewWindow' +import { windowKey } from '@/constants/windows' // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute(route.settings.general as any)({ @@ -52,6 +54,43 @@ function General() { await factoryReset() } + const handleOpenLogs = async () => { + try { + // Check if logs window already exists + const existingWindow = await WebviewWindow.getByLabel( + windowKey.logsAppWindow + ) + + if (existingWindow) { + // If window exists, focus it + await existingWindow.setFocus() + console.log('Focused existing logs window') + } else { + // Create a new logs window using Tauri v2 WebviewWindow API + const logsWindow = new WebviewWindow(windowKey.logsAppWindow, { + url: route.appLogs, + title: 'App Logs - Jan', + width: 800, + height: 600, + resizable: true, + center: true, + }) + + // Listen for window creation + logsWindow.once('tauri://created', () => { + console.log('Logs window created') + }) + + // Listen for window errors + logsWindow.once('tauri://error', (e) => { + console.error('Error creating logs window:', e) + }) + } + } catch (error) { + console.error('Failed to open logs window:', error) + } + } + return (
@@ -66,9 +105,7 @@ function General() { - v{VERSION} - + v{VERSION} } /> { const selectedPath = await open({ multiple: false, @@ -137,10 +175,20 @@ function General() { title={t('settings.dataFolder.appLogs', { ns: 'settings', })} - description={t('settings.dataFolder.appLogsDesc', { - ns: 'settings', - })} - actions={<>} + description="View detailed logs of the App" + actions={ + + } /> diff --git a/web-app/src/routes/settings/hardware.tsx b/web-app/src/routes/settings/hardware.tsx index 9f7a446ef..bd52e8f52 100644 --- a/web-app/src/routes/settings/hardware.tsx +++ b/web-app/src/routes/settings/hardware.tsx @@ -31,6 +31,7 @@ import { import { getHardwareInfo } from '@/services/hardware' import { WebviewWindow } from '@tauri-apps/api/webviewWindow' import { formatMegaBytes } from '@/lib/utils' +import { windowKey } from '@/constants/windows' // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute(route.settings.hardware as any)({ @@ -167,7 +168,7 @@ function Hardware() { try { // Check if system monitor window already exists const existingWindow = await WebviewWindow.getByLabel( - 'system-monitor-window' + windowKey.systemMonitorWindow ) if (existingWindow) { @@ -176,7 +177,7 @@ function Hardware() { console.log('Focused existing system monitor window') } else { // Create a new system monitor window - const monitorWindow = new WebviewWindow('system-monitor-window', { + const monitorWindow = new WebviewWindow(windowKey.systemMonitorWindow, { url: route.systemMonitor, title: 'System Monitor - Jan', width: 900, diff --git a/web-app/src/routes/settings/local-api-server.tsx b/web-app/src/routes/settings/local-api-server.tsx index 28a6cd082..0501a917c 100644 --- a/web-app/src/routes/settings/local-api-server.tsx +++ b/web-app/src/routes/settings/local-api-server.tsx @@ -12,6 +12,8 @@ import { ApiPrefixInput } from '@/containers/ApiPrefixInput' import { useLocalApiServer } from '@/hooks/useLocalApiServer' import { WebviewWindow } from '@tauri-apps/api/webviewWindow' import { useAppState } from '@/hooks/useAppState' +import { windowKey } from '@/constants/windows' +import { IconLogs } from '@tabler/icons-react' // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute(route.settings.local_api_server as any)({ @@ -63,7 +65,7 @@ function LocalAPIServer() { try { // Check if logs window already exists const existingWindow = await WebviewWindow.getByLabel( - 'logs-window-local-api-server' + windowKey.logsWindowLocalApiServer ) if (existingWindow) { @@ -72,14 +74,17 @@ function LocalAPIServer() { console.log('Focused existing logs window') } else { // Create a new logs window using Tauri v2 WebviewWindow API - const logsWindow = new WebviewWindow('logs-window-local-api-server', { - url: '/local-api-server/logs', - title: 'Local API server Logs - Jan', - width: 800, - height: 600, - resizable: true, - center: true, - }) + const logsWindow = new WebviewWindow( + windowKey.logsWindowLocalApiServer, + { + url: route.localApiServerlogs, + title: 'Local API server Logs - Jan', + width: 800, + height: 600, + resizable: true, + center: true, + } + ) // Listen for window creation logsWindow.once('tauri://created', () => { @@ -131,8 +136,15 @@ function LocalAPIServer() { title="Server Logs" description="View detailed logs of the local API server" actions={ - } />