forked from lightningpixel/modly
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLogsSection.tsx
More file actions
140 lines (125 loc) · 4.94 KB
/
Copy pathLogsSection.tsx
File metadata and controls
140 lines (125 loc) · 4.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import { useEffect, useState, useCallback } from 'react'
const LOG_FILES = [
{ id: 'errors.log', label: 'Errors', description: 'All errors from Electron and Python' },
{ id: 'runtime.log', label: 'Runtime', description: 'FastAPI / Python output' },
{ id: 'modly.log', label: 'App', description: 'General Electron logs' },
]
function formatSession(id: string): string {
// id format: 2026-03-20T10-23-45
try {
const iso = id.replace(/T(\d{2})-(\d{2})-(\d{2})$/, 'T$1:$2:$3')
const d = new Date(iso)
return d.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })
} catch {
return id
}
}
export function LogsSection(): JSX.Element {
const [sessions, setSessions] = useState<string[]>([])
const [activeSession, setActiveSession] = useState<string | null>(null) // null = current
const [activeFile, setActiveFile] = useState('errors.log')
const [logs, setLogs] = useState<Record<string, string>>({})
const [loading, setLoading] = useState(true)
const [copied, setCopied] = useState(false)
const loadSessions = useCallback(async () => {
const list = await window.electron.log.listSessions()
setSessions(list)
}, [])
const loadLogs = useCallback(async (session: string | null) => {
setLoading(true)
const result = await window.electron.log.readAll(session ?? undefined)
setLogs(result)
setLoading(false)
}, [])
useEffect(() => {
loadSessions()
loadLogs(null)
}, [loadSessions, loadLogs])
const handleSessionChange = (value: string) => {
const session = value === 'current' ? null : value
setActiveSession(session)
loadLogs(session)
}
const handleRefresh = () => {
loadSessions()
loadLogs(activeSession)
}
const content = logs[activeFile] ?? ''
const handleCopy = () => {
navigator.clipboard.writeText(content).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 2000)
})
}
return (
<div className="flex flex-col gap-6">
<div>
<h2 className="text-sm font-semibold text-zinc-100">Logs</h2>
<p className="text-xs text-zinc-500 mt-0.5">Application log files — share these when reporting issues.</p>
</div>
{/* Session selector */}
<div className="flex items-center gap-3">
<label className="text-xs text-zinc-500 shrink-0">Session</label>
<select
value={activeSession ?? 'current'}
onChange={(e) => handleSessionChange(e.target.value)}
className="flex-1 bg-zinc-800/80 border border-zinc-700/60 text-zinc-200 text-xs rounded-lg px-3 py-2 outline-none focus:border-zinc-600"
>
<option value="current">Current session</option>
{sessions.map((s) => (
<option key={s} value={s}>{formatSession(s)}</option>
))}
</select>
</div>
{/* File tabs */}
<div className="flex items-center gap-1 border-b border-zinc-800">
{LOG_FILES.map((f) => (
<button
key={f.id}
onClick={() => setActiveFile(f.id)}
title={f.description}
className={`px-4 py-2 text-xs font-medium transition-colors border-b-2 -mb-px ${
activeFile === f.id
? 'border-accent text-accent-light'
: 'border-transparent text-zinc-500 hover:text-zinc-300'
}`}
>
{f.label}
</button>
))}
<div className="ml-auto flex items-center gap-2 pb-1">
<button
onClick={handleRefresh}
disabled={loading}
className="flex items-center gap-1.5 px-3 py-1.5 text-[11px] text-zinc-400 hover:text-zinc-200 disabled:opacity-40 transition-colors"
>
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className={loading ? 'animate-spin' : ''}>
<polyline points="23 4 23 10 17 10" />
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
</svg>
Refresh
</button>
<button
onClick={handleCopy}
disabled={!content}
className="px-3 py-1.5 text-[11px] font-semibold rounded-lg bg-zinc-700/60 hover:bg-zinc-700 text-zinc-200 disabled:opacity-40 transition-colors"
>
{copied ? 'Copied!' : 'Copy all'}
</button>
</div>
</div>
{/* Log content */}
{loading ? (
<div className="flex items-center justify-center h-48 text-zinc-600 text-xs">Loading…</div>
) : content ? (
<pre className="text-[11px] font-mono text-zinc-400 bg-zinc-900/60 border border-zinc-800 rounded-xl px-4 py-3 max-h-[480px] overflow-y-auto whitespace-pre-wrap break-words select-text leading-relaxed">
{content}
</pre>
) : (
<div className="flex items-center justify-center h-48 text-zinc-600 text-xs border border-zinc-800 rounded-xl">
No entries in {activeFile}
</div>
)}
</div>
)
}