feat: initial app logs
This commit is contained in:
parent
ad962c2cf6
commit
0c73035b3c
14
src-tauri/capabilities/log-app-window.json
Normal file
14
src-tauri/capabilities/log-app-window.json
Normal 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"
|
||||
]
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
export const route = {
|
||||
// home as new chat or thread
|
||||
home: '/',
|
||||
appLogs: '/logs',
|
||||
assistant: '/assistant',
|
||||
settings: {
|
||||
index: '/settings',
|
||||
|
||||
5
web-app/src/constants/windows.ts
Normal file
5
web-app/src/constants/windows.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const windowKey = {
|
||||
logsAppWindow: 'logs-app-window',
|
||||
logsWindowLocalApiServer: 'logs-window-local-api-server',
|
||||
systemMonitorWindow: 'system-monitor-window',
|
||||
}
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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
116
web-app/src/routes/logs.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user