feat: initial app logs

This commit is contained in:
Faisal Amir 2025-05-22 00:46:28 +07:00
parent ad962c2cf6
commit 0c73035b3c
9 changed files with 246 additions and 22 deletions

View File

@ -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"
]
}

View File

@ -1,6 +1,7 @@
export const route = {
// home as new chat or thread
home: '/',
appLogs: '/logs',
assistant: '/assistant',
settings: {
index: '/settings',

View File

@ -0,0 +1,5 @@
export const windowKey = {
logsAppWindow: 'logs-app-window',
logsWindowLocalApiServer: 'logs-window-local-api-server',
systemMonitorWindow: 'system-monitor-window',
}

View File

@ -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"
},

View File

@ -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 (
<Fragment>

116
web-app/src/routes/logs.tsx Normal file
View File

@ -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.localApiServerlogs 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<LogEntry[]>([])
const logsContainerRef = useRef<HTMLDivElement>(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 (
<div className="flex flex-col h-full bg-main-view">
<div className="flex-1 overflow-auto" ref={logsContainerRef}>
<div className="font-mono p-2">
{logs.length === 0 ? (
<div className="text-center text-main-view-fg/50 py-8">
No logs available
</div>
) : (
logs.map((log, index) => (
<div key={index} className="mb-1 flex">
<span className="text-muted-foreground mr-2">
[{formatTimestamp(log.timestamp)}]
</span>
<span
className={`mr-2 font-semibold ${getLogLevelColor(log.level)}`}
>
{log.level.toUpperCase()}
</span>
<span>{log.message}</span>
</div>
))
)}
</div>
</div>
</div>
)
}

View File

@ -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 (
<div className="flex flex-col h-full">
<HeaderPage>
@ -66,9 +105,7 @@ function General() {
<CardItem
title="App Version"
actions={
<>
<span className="text-main-view-fg/80">v{VERSION}</span>
</>
<span className="text-main-view-fg/80">v{VERSION}</span>
}
/>
<CardItem
@ -110,6 +147,7 @@ function General() {
variant="link"
size="sm"
className="hover:no-underline"
title="App Data Folder"
onClick={async () => {
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={
<Button
variant="link"
size="sm"
onClick={handleOpenLogs}
title="App Logs"
>
{/* Open Logs */}
<div className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out">
<IconLogs size={18} className="text-main-view-fg/50" />
</div>
</Button>
}
/>
</Card>

View File

@ -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,

View File

@ -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={
<Button variant="link" size="sm" onClick={handleOpenLogs}>
Open Logs
<Button
variant="link"
size="sm"
onClick={handleOpenLogs}
title="Server Logs"
>
<div className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out">
<IconLogs size={18} className="text-main-view-fg/50" />
</div>
</Button>
}
/>