dev3000 0.0.45 → 0.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +13 -1
- package/mcp-server/app/logs/LogsClient.test.ts +1 -1
- package/mcp-server/app/logs/LogsClient.tsx +592 -379
- package/mcp-server/app/logs/page.tsx +2 -1
- package/mcp-server/app/logs/utils.ts +46 -0
- package/package.json +3 -1
|
@@ -1,125 +1,93 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useRef, useMemo } from
|
|
4
|
-
import { useRouter, useSearchParams } from
|
|
5
|
-
import {
|
|
3
|
+
import { useState, useEffect, useRef, useMemo } from "react";
|
|
4
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
5
|
+
import {
|
|
6
|
+
LogEntry,
|
|
7
|
+
LogsApiResponse,
|
|
8
|
+
ConfigApiResponse,
|
|
9
|
+
LogFile,
|
|
10
|
+
LogListResponse,
|
|
11
|
+
} from "@/types";
|
|
12
|
+
import { parseLogEntries } from "./utils";
|
|
6
13
|
|
|
7
14
|
// Hook for dark mode with system preference detection
|
|
8
15
|
function useDarkMode() {
|
|
9
16
|
const [darkMode, setDarkMode] = useState<boolean>(() => {
|
|
10
|
-
if (typeof window !==
|
|
17
|
+
if (typeof window !== "undefined") {
|
|
11
18
|
// Check localStorage first
|
|
12
|
-
const saved = localStorage.getItem(
|
|
19
|
+
const saved = localStorage.getItem("dev3000-dark-mode");
|
|
13
20
|
if (saved !== null) {
|
|
14
21
|
return JSON.parse(saved);
|
|
15
22
|
}
|
|
16
23
|
// Default to system preference
|
|
17
|
-
return window.matchMedia(
|
|
24
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
18
25
|
}
|
|
19
26
|
return false;
|
|
20
27
|
});
|
|
21
28
|
|
|
22
29
|
useEffect(() => {
|
|
23
30
|
// Save to localStorage
|
|
24
|
-
localStorage.setItem(
|
|
25
|
-
|
|
31
|
+
localStorage.setItem("dev3000-dark-mode", JSON.stringify(darkMode));
|
|
32
|
+
|
|
26
33
|
// Apply dark class to document
|
|
27
34
|
if (darkMode) {
|
|
28
|
-
document.documentElement.classList.add(
|
|
35
|
+
document.documentElement.classList.add("dark");
|
|
29
36
|
} else {
|
|
30
|
-
document.documentElement.classList.remove(
|
|
37
|
+
document.documentElement.classList.remove("dark");
|
|
31
38
|
}
|
|
32
39
|
}, [darkMode]);
|
|
33
40
|
|
|
34
41
|
// Listen for system theme changes
|
|
35
42
|
useEffect(() => {
|
|
36
|
-
const mediaQuery = window.matchMedia(
|
|
43
|
+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
37
44
|
const handler = (e: MediaQueryListEvent) => {
|
|
38
45
|
// Only update if no explicit choice has been made
|
|
39
|
-
const saved = localStorage.getItem(
|
|
46
|
+
const saved = localStorage.getItem("dev3000-dark-mode");
|
|
40
47
|
if (saved === null) {
|
|
41
48
|
setDarkMode(e.matches);
|
|
42
49
|
}
|
|
43
50
|
};
|
|
44
|
-
|
|
45
|
-
mediaQuery.addEventListener(
|
|
46
|
-
return () => mediaQuery.removeEventListener(
|
|
51
|
+
|
|
52
|
+
mediaQuery.addEventListener("change", handler);
|
|
53
|
+
return () => mediaQuery.removeEventListener("change", handler);
|
|
47
54
|
}, []);
|
|
48
55
|
|
|
49
56
|
return [darkMode, setDarkMode] as const;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
export function parseLogEntries(logContent: string): LogEntry[] {
|
|
53
|
-
// Split by timestamp pattern - each timestamp starts a new log entry
|
|
54
|
-
const timestampPattern = /\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\] \[([^\]]+)\] /;
|
|
55
|
-
|
|
56
|
-
const entries: LogEntry[] = [];
|
|
57
|
-
const lines = logContent.split('\n');
|
|
58
|
-
let currentEntry: LogEntry | null = null;
|
|
59
|
-
|
|
60
|
-
for (const line of lines) {
|
|
61
|
-
if (!line.trim()) continue;
|
|
62
|
-
|
|
63
|
-
const match = line.match(timestampPattern);
|
|
64
|
-
if (match) {
|
|
65
|
-
// Save previous entry if exists
|
|
66
|
-
if (currentEntry) {
|
|
67
|
-
entries.push(currentEntry);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Start new entry
|
|
71
|
-
const [fullMatch, timestamp, source] = match;
|
|
72
|
-
const message = line.substring(fullMatch.length);
|
|
73
|
-
const screenshot = message.match(/\[SCREENSHOT\] (.+)/)?.[1];
|
|
74
|
-
|
|
75
|
-
currentEntry = {
|
|
76
|
-
timestamp,
|
|
77
|
-
source,
|
|
78
|
-
message,
|
|
79
|
-
screenshot,
|
|
80
|
-
original: line
|
|
81
|
-
};
|
|
82
|
-
} else if (currentEntry) {
|
|
83
|
-
// Append to current entry's message
|
|
84
|
-
currentEntry.message += '\n' + line;
|
|
85
|
-
currentEntry.original += '\n' + line;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Don't forget the last entry
|
|
90
|
-
if (currentEntry) {
|
|
91
|
-
entries.push(currentEntry);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return entries;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
59
|
// Keep this for backwards compatibility, but it's not used anymore
|
|
98
60
|
function parseLogLine(line: string): LogEntry | null {
|
|
99
61
|
const match = line.match(/^\[([^\]]+)\] \[([^\]]+)\] (.*)$/s);
|
|
100
62
|
if (!match) return null;
|
|
101
|
-
|
|
63
|
+
|
|
102
64
|
const [, timestamp, source, message] = match;
|
|
103
65
|
const screenshot = message.match(/\[SCREENSHOT\] (.+)/)?.[1];
|
|
104
|
-
|
|
66
|
+
|
|
105
67
|
return {
|
|
106
68
|
timestamp,
|
|
107
69
|
source,
|
|
108
70
|
message,
|
|
109
71
|
screenshot,
|
|
110
|
-
original: line
|
|
72
|
+
original: line,
|
|
111
73
|
};
|
|
112
74
|
}
|
|
113
75
|
|
|
114
76
|
// Component to render truncated URLs with click-to-expand
|
|
115
|
-
function URLRenderer({
|
|
77
|
+
function URLRenderer({
|
|
78
|
+
url,
|
|
79
|
+
maxLength = 60,
|
|
80
|
+
}: {
|
|
81
|
+
url: string;
|
|
82
|
+
maxLength?: number;
|
|
83
|
+
}) {
|
|
116
84
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
117
|
-
|
|
85
|
+
|
|
118
86
|
if (url.length <= maxLength) {
|
|
119
87
|
return (
|
|
120
|
-
<a
|
|
121
|
-
href={url}
|
|
122
|
-
target="_blank"
|
|
88
|
+
<a
|
|
89
|
+
href={url}
|
|
90
|
+
target="_blank"
|
|
123
91
|
rel="noopener noreferrer"
|
|
124
92
|
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
|
|
125
93
|
>
|
|
@@ -127,16 +95,16 @@ function URLRenderer({ url, maxLength = 60 }: { url: string, maxLength?: number
|
|
|
127
95
|
</a>
|
|
128
96
|
);
|
|
129
97
|
}
|
|
130
|
-
|
|
131
|
-
const truncated = url.substring(0, maxLength) +
|
|
132
|
-
|
|
98
|
+
|
|
99
|
+
const truncated = url.substring(0, maxLength) + "...";
|
|
100
|
+
|
|
133
101
|
return (
|
|
134
102
|
<span className="inline-block">
|
|
135
103
|
{isExpanded ? (
|
|
136
104
|
<span>
|
|
137
|
-
<a
|
|
138
|
-
href={url}
|
|
139
|
-
target="_blank"
|
|
105
|
+
<a
|
|
106
|
+
href={url}
|
|
107
|
+
target="_blank"
|
|
140
108
|
rel="noopener noreferrer"
|
|
141
109
|
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
|
|
142
110
|
>
|
|
@@ -151,9 +119,9 @@ function URLRenderer({ url, maxLength = 60 }: { url: string, maxLength?: number
|
|
|
151
119
|
</span>
|
|
152
120
|
) : (
|
|
153
121
|
<span>
|
|
154
|
-
<a
|
|
155
|
-
href={url}
|
|
156
|
-
target="_blank"
|
|
122
|
+
<a
|
|
123
|
+
href={url}
|
|
124
|
+
target="_blank"
|
|
157
125
|
rel="noopener noreferrer"
|
|
158
126
|
className="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline"
|
|
159
127
|
>
|
|
@@ -174,79 +142,110 @@ function URLRenderer({ url, maxLength = 60 }: { url: string, maxLength?: number
|
|
|
174
142
|
// Component to render Chrome DevTools-style collapsible objects
|
|
175
143
|
function ObjectRenderer({ content }: { content: string }) {
|
|
176
144
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
177
|
-
|
|
145
|
+
|
|
178
146
|
try {
|
|
179
147
|
const obj = JSON.parse(content);
|
|
180
|
-
|
|
148
|
+
|
|
181
149
|
// Check if it's a Chrome DevTools object representation
|
|
182
|
-
if (
|
|
150
|
+
if (
|
|
151
|
+
obj &&
|
|
152
|
+
typeof obj === "object" &&
|
|
153
|
+
obj.type === "object" &&
|
|
154
|
+
obj.properties
|
|
155
|
+
) {
|
|
183
156
|
const properties = obj.properties;
|
|
184
|
-
const description = obj.description ||
|
|
157
|
+
const description = obj.description || "Object";
|
|
185
158
|
const overflow = obj.overflow;
|
|
186
|
-
|
|
159
|
+
|
|
187
160
|
return (
|
|
188
161
|
<div className="inline-block">
|
|
189
162
|
<button
|
|
190
163
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
191
164
|
className="inline-flex items-center gap-1 text-blue-600 hover:text-blue-800 font-mono text-sm"
|
|
192
165
|
>
|
|
193
|
-
<svg
|
|
194
|
-
className={`w-3 h-3 transition-transform ${
|
|
195
|
-
|
|
166
|
+
<svg
|
|
167
|
+
className={`w-3 h-3 transition-transform ${
|
|
168
|
+
isExpanded ? "rotate-90" : ""
|
|
169
|
+
}`}
|
|
170
|
+
fill="currentColor"
|
|
196
171
|
viewBox="0 0 20 20"
|
|
197
172
|
>
|
|
198
|
-
<path
|
|
173
|
+
<path
|
|
174
|
+
fillRule="evenodd"
|
|
175
|
+
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
176
|
+
clipRule="evenodd"
|
|
177
|
+
/>
|
|
199
178
|
</svg>
|
|
200
179
|
<span className="text-purple-600">{description}</span>
|
|
201
180
|
{!isExpanded && (
|
|
202
181
|
<span className="text-gray-500">
|
|
203
|
-
{overflow ?
|
|
182
|
+
{overflow ? "..." : ""} {"{"}
|
|
204
183
|
{properties.slice(0, 3).map((prop: any, idx: number) => (
|
|
205
184
|
<span key={idx}>
|
|
206
|
-
{idx > 0 &&
|
|
207
|
-
<span className="text-red-600">{prop.name}</span>:
|
|
185
|
+
{idx > 0 && ", "}
|
|
186
|
+
<span className="text-red-600">{prop.name}</span>:
|
|
208
187
|
<span className="text-blue-600">
|
|
209
|
-
{prop.type ===
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
188
|
+
{prop.type === "string"
|
|
189
|
+
? `"${prop.value}"`
|
|
190
|
+
: prop.type === "number"
|
|
191
|
+
? prop.value
|
|
192
|
+
: prop.type === "object"
|
|
193
|
+
? prop.subtype === "array"
|
|
194
|
+
? prop.value
|
|
195
|
+
: "{...}"
|
|
196
|
+
: prop.value}
|
|
213
197
|
</span>
|
|
214
198
|
</span>
|
|
215
199
|
))}
|
|
216
|
-
{properties.length > 3 &&
|
|
217
|
-
{
|
|
200
|
+
{properties.length > 3 && ", ..."}
|
|
201
|
+
{"}"}
|
|
218
202
|
</span>
|
|
219
203
|
)}
|
|
220
204
|
</button>
|
|
221
|
-
|
|
205
|
+
|
|
222
206
|
{isExpanded && (
|
|
223
207
|
<div className="mt-1 ml-4 border-l-2 border-gray-200 pl-3">
|
|
224
208
|
<div className="font-mono text-sm">
|
|
225
|
-
<div className="text-gray-600">
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
209
|
+
<div className="text-gray-600">
|
|
210
|
+
{description} {"{"}
|
|
211
|
+
<div className="ml-4">
|
|
212
|
+
{properties.map((prop: any, idx: number) => (
|
|
213
|
+
<div key={idx} className="py-0.5">
|
|
214
|
+
<span className="text-red-600">{prop.name}</span>
|
|
215
|
+
<span className="text-gray-500">: </span>
|
|
216
|
+
<span
|
|
217
|
+
className={
|
|
218
|
+
prop.type === "string"
|
|
219
|
+
? "text-green-600"
|
|
220
|
+
: prop.type === "number"
|
|
221
|
+
? "text-blue-600"
|
|
222
|
+
: prop.type === "object"
|
|
223
|
+
? "text-purple-600"
|
|
224
|
+
: "text-orange-600"
|
|
225
|
+
}
|
|
226
|
+
>
|
|
227
|
+
{prop.type === "string"
|
|
228
|
+
? `"${prop.value}"`
|
|
229
|
+
: prop.type === "number"
|
|
230
|
+
? prop.value
|
|
231
|
+
: prop.type === "object"
|
|
232
|
+
? prop.subtype === "array"
|
|
233
|
+
? prop.value
|
|
234
|
+
: "{...}"
|
|
235
|
+
: prop.value}
|
|
236
|
+
</span>
|
|
237
|
+
{idx < properties.length - 1 && (
|
|
238
|
+
<span className="text-gray-500">,</span>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
))}
|
|
242
|
+
{overflow && (
|
|
243
|
+
<div className="text-gray-500 italic">
|
|
244
|
+
... and more properties
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
<div className="text-gray-600">{"}"}</div>
|
|
250
249
|
</div>
|
|
251
250
|
</div>
|
|
252
251
|
</div>
|
|
@@ -254,7 +253,7 @@ function ObjectRenderer({ content }: { content: string }) {
|
|
|
254
253
|
</div>
|
|
255
254
|
);
|
|
256
255
|
}
|
|
257
|
-
|
|
256
|
+
|
|
258
257
|
// For regular JSON objects, render them nicely too
|
|
259
258
|
return (
|
|
260
259
|
<div className="inline-block">
|
|
@@ -262,21 +261,27 @@ function ObjectRenderer({ content }: { content: string }) {
|
|
|
262
261
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
263
262
|
className="inline-flex items-center gap-1 text-blue-600 hover:text-blue-800 font-mono text-sm"
|
|
264
263
|
>
|
|
265
|
-
<svg
|
|
266
|
-
className={`w-3 h-3 transition-transform ${
|
|
267
|
-
|
|
264
|
+
<svg
|
|
265
|
+
className={`w-3 h-3 transition-transform ${
|
|
266
|
+
isExpanded ? "rotate-90" : ""
|
|
267
|
+
}`}
|
|
268
|
+
fill="currentColor"
|
|
268
269
|
viewBox="0 0 20 20"
|
|
269
270
|
>
|
|
270
|
-
<path
|
|
271
|
+
<path
|
|
272
|
+
fillRule="evenodd"
|
|
273
|
+
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
274
|
+
clipRule="evenodd"
|
|
275
|
+
/>
|
|
271
276
|
</svg>
|
|
272
277
|
<span className="text-purple-600">Object</span>
|
|
273
278
|
{!isExpanded && (
|
|
274
279
|
<span className="text-gray-500">
|
|
275
|
-
{
|
|
280
|
+
{"{"}...{"}"}
|
|
276
281
|
</span>
|
|
277
282
|
)}
|
|
278
283
|
</button>
|
|
279
|
-
|
|
284
|
+
|
|
280
285
|
{isExpanded && (
|
|
281
286
|
<div className="mt-1 ml-4 border-l-2 border-gray-200 pl-3">
|
|
282
287
|
<pre className="font-mono text-sm text-gray-700 whitespace-pre-wrap">
|
|
@@ -295,99 +300,157 @@ function ObjectRenderer({ content }: { content: string }) {
|
|
|
295
300
|
function LogEntryComponent({ entry }: { entry: LogEntry }) {
|
|
296
301
|
// Parse log type from message patterns with dark mode support
|
|
297
302
|
const parseLogType = (message: string) => {
|
|
298
|
-
if (message.includes(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (message.includes(
|
|
306
|
-
|
|
303
|
+
if (message.includes("[INTERACTION]"))
|
|
304
|
+
return {
|
|
305
|
+
type: "INTERACTION",
|
|
306
|
+
color:
|
|
307
|
+
"bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800",
|
|
308
|
+
tag: "bg-purple-100 dark:bg-purple-800 text-purple-800 dark:text-purple-200",
|
|
309
|
+
};
|
|
310
|
+
if (message.includes("[CONSOLE ERROR]"))
|
|
311
|
+
return {
|
|
312
|
+
type: "ERROR",
|
|
313
|
+
color:
|
|
314
|
+
"bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800",
|
|
315
|
+
tag: "bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200",
|
|
316
|
+
};
|
|
317
|
+
if (message.includes("[CONSOLE WARN]"))
|
|
318
|
+
return {
|
|
319
|
+
type: "WARNING",
|
|
320
|
+
color:
|
|
321
|
+
"bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800",
|
|
322
|
+
tag: "bg-yellow-100 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200",
|
|
323
|
+
};
|
|
324
|
+
if (message.includes("[SCREENSHOT]"))
|
|
325
|
+
return {
|
|
326
|
+
type: "SCREENSHOT",
|
|
327
|
+
color:
|
|
328
|
+
"bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800",
|
|
329
|
+
tag: "bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200",
|
|
330
|
+
};
|
|
331
|
+
if (message.includes("[NAVIGATION]"))
|
|
332
|
+
return {
|
|
333
|
+
type: "NAVIGATION",
|
|
334
|
+
color:
|
|
335
|
+
"bg-indigo-50 dark:bg-indigo-900/20 border-indigo-200 dark:border-indigo-800",
|
|
336
|
+
tag: "bg-indigo-100 dark:bg-indigo-800 text-indigo-800 dark:text-indigo-200",
|
|
337
|
+
};
|
|
338
|
+
if (message.includes("[NETWORK ERROR]"))
|
|
339
|
+
return {
|
|
340
|
+
type: "NETWORK",
|
|
341
|
+
color:
|
|
342
|
+
"bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800",
|
|
343
|
+
tag: "bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200",
|
|
344
|
+
};
|
|
345
|
+
if (message.includes("[NETWORK REQUEST]"))
|
|
346
|
+
return {
|
|
347
|
+
type: "NETWORK",
|
|
348
|
+
color:
|
|
349
|
+
"bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700",
|
|
350
|
+
tag: "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300",
|
|
351
|
+
};
|
|
352
|
+
if (message.includes("[PAGE ERROR]"))
|
|
353
|
+
return {
|
|
354
|
+
type: "ERROR",
|
|
355
|
+
color:
|
|
356
|
+
"bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800",
|
|
357
|
+
tag: "bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200",
|
|
358
|
+
};
|
|
359
|
+
return {
|
|
360
|
+
type: "DEFAULT",
|
|
361
|
+
color: "border-gray-200 dark:border-gray-700",
|
|
362
|
+
tag: "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300",
|
|
363
|
+
};
|
|
307
364
|
};
|
|
308
365
|
|
|
309
366
|
const logTypeInfo = parseLogType(entry.message);
|
|
310
|
-
|
|
367
|
+
|
|
311
368
|
// Extract and highlight type tags, detect JSON objects and URLs
|
|
312
369
|
const renderMessage = (message: string) => {
|
|
313
370
|
const typeTagRegex = /\[([A-Z\s]+)\]/g;
|
|
314
371
|
const jsonRegex = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
|
|
315
372
|
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
316
|
-
|
|
373
|
+
|
|
317
374
|
const parts = [];
|
|
318
375
|
let lastIndex = 0;
|
|
319
376
|
let match;
|
|
320
|
-
|
|
377
|
+
|
|
321
378
|
// First, handle type tags
|
|
322
379
|
while ((match = typeTagRegex.exec(message)) !== null) {
|
|
323
380
|
// Add text before the tag
|
|
324
381
|
if (match.index > lastIndex) {
|
|
325
382
|
parts.push(message.slice(lastIndex, match.index));
|
|
326
383
|
}
|
|
327
|
-
|
|
384
|
+
|
|
328
385
|
// Add the tag with styling
|
|
329
386
|
parts.push(
|
|
330
|
-
<span
|
|
387
|
+
<span
|
|
331
388
|
key={match.index}
|
|
332
389
|
className={`inline-block px-1.5 py-0.5 rounded text-xs font-medium ${logTypeInfo.tag} mr-1`}
|
|
333
390
|
>
|
|
334
391
|
{match[1]}
|
|
335
392
|
</span>
|
|
336
393
|
);
|
|
337
|
-
|
|
394
|
+
|
|
338
395
|
lastIndex = match.index + match[0].length;
|
|
339
396
|
}
|
|
340
|
-
|
|
397
|
+
|
|
341
398
|
// Add remaining text
|
|
342
399
|
let remainingText = message.slice(lastIndex);
|
|
343
|
-
|
|
400
|
+
|
|
344
401
|
// Process remaining text for JSON objects and URLs
|
|
345
402
|
const processTextForObjects = (text: string, keyPrefix: string) => {
|
|
346
403
|
const jsonMatches = [...text.matchAll(jsonRegex)];
|
|
347
404
|
const urlMatches = [...text.matchAll(urlRegex)];
|
|
348
|
-
const allMatches = [
|
|
349
|
-
|
|
405
|
+
const allMatches = [
|
|
406
|
+
...jsonMatches.map((m) => ({ ...m, type: "json" })),
|
|
407
|
+
...urlMatches.map((m) => ({ ...m, type: "url" })),
|
|
408
|
+
];
|
|
409
|
+
|
|
350
410
|
// Sort matches by index
|
|
351
411
|
allMatches.sort((a, b) => a.index! - b.index!);
|
|
352
|
-
|
|
412
|
+
|
|
353
413
|
if (allMatches.length === 0) {
|
|
354
414
|
return [text];
|
|
355
415
|
}
|
|
356
|
-
|
|
416
|
+
|
|
357
417
|
const finalParts = [];
|
|
358
418
|
let textLastIndex = 0;
|
|
359
|
-
|
|
419
|
+
|
|
360
420
|
allMatches.forEach((objMatch, idx) => {
|
|
361
421
|
// Add text before match
|
|
362
422
|
if (objMatch.index! > textLastIndex) {
|
|
363
423
|
finalParts.push(text.slice(textLastIndex, objMatch.index));
|
|
364
424
|
}
|
|
365
|
-
|
|
425
|
+
|
|
366
426
|
// Add appropriate renderer
|
|
367
|
-
if (objMatch.type ===
|
|
427
|
+
if (objMatch.type === "json") {
|
|
368
428
|
finalParts.push(
|
|
369
|
-
<ObjectRenderer
|
|
429
|
+
<ObjectRenderer
|
|
430
|
+
key={`${keyPrefix}-json-${idx}`}
|
|
431
|
+
content={objMatch[0]}
|
|
432
|
+
/>
|
|
370
433
|
);
|
|
371
|
-
} else if (objMatch.type ===
|
|
434
|
+
} else if (objMatch.type === "url") {
|
|
372
435
|
finalParts.push(
|
|
373
436
|
<URLRenderer key={`${keyPrefix}-url-${idx}`} url={objMatch[0]} />
|
|
374
437
|
);
|
|
375
438
|
}
|
|
376
|
-
|
|
439
|
+
|
|
377
440
|
textLastIndex = objMatch.index! + objMatch[0].length;
|
|
378
441
|
});
|
|
379
|
-
|
|
442
|
+
|
|
380
443
|
// Add any text after the last match
|
|
381
444
|
if (textLastIndex < text.length) {
|
|
382
445
|
finalParts.push(text.slice(textLastIndex));
|
|
383
446
|
}
|
|
384
|
-
|
|
447
|
+
|
|
385
448
|
return finalParts;
|
|
386
449
|
};
|
|
387
|
-
|
|
388
|
-
const processedRemaining = processTextForObjects(remainingText,
|
|
450
|
+
|
|
451
|
+
const processedRemaining = processTextForObjects(remainingText, "main");
|
|
389
452
|
parts.push(...processedRemaining);
|
|
390
|
-
|
|
453
|
+
|
|
391
454
|
return parts.length > 0 ? parts : message;
|
|
392
455
|
};
|
|
393
456
|
|
|
@@ -399,27 +462,31 @@ function LogEntryComponent({ entry }: { entry: LogEntry }) {
|
|
|
399
462
|
<div className="text-xs text-gray-500 dark:text-gray-400 font-mono whitespace-nowrap pt-1">
|
|
400
463
|
{new Date(entry.timestamp).toLocaleTimeString()}
|
|
401
464
|
</div>
|
|
402
|
-
|
|
465
|
+
|
|
403
466
|
{/* Column 2: Source */}
|
|
404
|
-
<div
|
|
405
|
-
|
|
406
|
-
|
|
467
|
+
<div
|
|
468
|
+
className={`px-2 py-1 rounded text-xs font-medium whitespace-nowrap ${
|
|
469
|
+
entry.source === "SERVER"
|
|
470
|
+
? "bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200"
|
|
471
|
+
: "bg-green-100 dark:bg-green-800 text-green-800 dark:text-green-200"
|
|
472
|
+
}`}
|
|
473
|
+
>
|
|
407
474
|
{entry.source}
|
|
408
475
|
</div>
|
|
409
|
-
|
|
476
|
+
|
|
410
477
|
{/* Column 3: Message content */}
|
|
411
478
|
<div className="font-mono text-sm min-w-0 text-gray-900 dark:text-gray-100">
|
|
412
479
|
{renderMessage(entry.message)}
|
|
413
480
|
</div>
|
|
414
481
|
</div>
|
|
415
|
-
|
|
482
|
+
|
|
416
483
|
{entry.screenshot && (
|
|
417
484
|
<div className="mt-2">
|
|
418
|
-
<img
|
|
419
|
-
src={`/screenshots/${entry.screenshot}`}
|
|
420
|
-
alt="Screenshot"
|
|
485
|
+
<img
|
|
486
|
+
src={`/screenshots/${entry.screenshot}`}
|
|
487
|
+
alt="Screenshot"
|
|
421
488
|
className="max-w-full h-auto border rounded shadow-sm"
|
|
422
|
-
style={{ maxHeight:
|
|
489
|
+
style={{ maxHeight: "400px" }}
|
|
423
490
|
/>
|
|
424
491
|
</div>
|
|
425
492
|
)}
|
|
@@ -433,7 +500,7 @@ interface LogsClientProps {
|
|
|
433
500
|
logs: LogEntry[];
|
|
434
501
|
logFiles: any[];
|
|
435
502
|
currentLogFile: string;
|
|
436
|
-
mode:
|
|
503
|
+
mode: "head" | "tail";
|
|
437
504
|
};
|
|
438
505
|
}
|
|
439
506
|
|
|
@@ -442,15 +509,23 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
442
509
|
const searchParams = useSearchParams();
|
|
443
510
|
const [darkMode, setDarkMode] = useDarkMode();
|
|
444
511
|
const [logs, setLogs] = useState<LogEntry[]>(initialData?.logs || []);
|
|
445
|
-
const [mode, setMode] = useState<
|
|
512
|
+
const [mode, setMode] = useState<"head" | "tail">(
|
|
513
|
+
initialData?.mode || "tail"
|
|
514
|
+
);
|
|
446
515
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
447
516
|
const [isLoadingNew, setIsLoadingNew] = useState(false);
|
|
448
517
|
const [isInitialLoading, setIsInitialLoading] = useState(!initialData);
|
|
449
|
-
const [lastLogCount, setLastLogCount] = useState(
|
|
518
|
+
const [lastLogCount, setLastLogCount] = useState(
|
|
519
|
+
initialData?.logs.length || 0
|
|
520
|
+
);
|
|
450
521
|
const [lastFetched, setLastFetched] = useState<Date | null>(null);
|
|
451
|
-
const [availableLogs, setAvailableLogs] = useState<LogFile[]>(
|
|
452
|
-
|
|
453
|
-
|
|
522
|
+
const [availableLogs, setAvailableLogs] = useState<LogFile[]>(
|
|
523
|
+
initialData?.logFiles || []
|
|
524
|
+
);
|
|
525
|
+
const [currentLogFile, setCurrentLogFile] = useState<string>(
|
|
526
|
+
initialData?.currentLogFile || ""
|
|
527
|
+
);
|
|
528
|
+
const [projectName, setProjectName] = useState<string>("");
|
|
454
529
|
const [showLogSelector, setShowLogSelector] = useState(false);
|
|
455
530
|
const [isReplaying, setIsReplaying] = useState(false);
|
|
456
531
|
const [showFilters, setShowFilters] = useState(false);
|
|
@@ -461,7 +536,7 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
461
536
|
browser: true,
|
|
462
537
|
server: true,
|
|
463
538
|
interaction: true,
|
|
464
|
-
screenshot: true
|
|
539
|
+
screenshot: true,
|
|
465
540
|
});
|
|
466
541
|
const bottomRef = useRef<HTMLDivElement>(null);
|
|
467
542
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -471,7 +546,7 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
471
546
|
|
|
472
547
|
const loadAvailableLogs = async () => {
|
|
473
548
|
try {
|
|
474
|
-
const response = await fetch(
|
|
549
|
+
const response = await fetch("/api/logs/list");
|
|
475
550
|
if (response.ok) {
|
|
476
551
|
const data: LogListResponse = await response.json();
|
|
477
552
|
setAvailableLogs(data.files);
|
|
@@ -479,28 +554,28 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
479
554
|
setProjectName(data.projectName);
|
|
480
555
|
}
|
|
481
556
|
} catch (error) {
|
|
482
|
-
console.error(
|
|
557
|
+
console.error("Error loading available logs:", error);
|
|
483
558
|
}
|
|
484
559
|
};
|
|
485
560
|
|
|
486
561
|
const pollForNewLogs = async () => {
|
|
487
|
-
if (mode !==
|
|
488
|
-
|
|
562
|
+
if (mode !== "tail" || !isAtBottom) return;
|
|
563
|
+
|
|
489
564
|
try {
|
|
490
|
-
const response = await fetch(
|
|
565
|
+
const response = await fetch("/api/logs/tail?lines=1000");
|
|
491
566
|
if (!response.ok) {
|
|
492
567
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
493
568
|
}
|
|
494
|
-
|
|
569
|
+
|
|
495
570
|
const data: LogsApiResponse = await response.json();
|
|
496
|
-
|
|
571
|
+
|
|
497
572
|
if (!data.logs) {
|
|
498
|
-
console.warn(
|
|
573
|
+
console.warn("No logs data in response");
|
|
499
574
|
return;
|
|
500
575
|
}
|
|
501
|
-
|
|
576
|
+
|
|
502
577
|
const entries = parseLogEntries(data.logs);
|
|
503
|
-
|
|
578
|
+
|
|
504
579
|
if (entries.length > lastLogCount) {
|
|
505
580
|
setIsLoadingNew(true);
|
|
506
581
|
setLastFetched(new Date());
|
|
@@ -510,19 +585,19 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
510
585
|
setIsLoadingNew(false);
|
|
511
586
|
// Auto-scroll to bottom for new content
|
|
512
587
|
setTimeout(() => {
|
|
513
|
-
bottomRef.current?.scrollIntoView({ behavior:
|
|
588
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
514
589
|
}, 50);
|
|
515
590
|
}, 250);
|
|
516
591
|
}
|
|
517
592
|
} catch (error) {
|
|
518
|
-
console.error(
|
|
593
|
+
console.error("Error polling logs:", error);
|
|
519
594
|
// Don't spam console on network errors during polling
|
|
520
595
|
}
|
|
521
596
|
};
|
|
522
597
|
|
|
523
598
|
// Start/stop polling based on mode and scroll position
|
|
524
599
|
useEffect(() => {
|
|
525
|
-
if (mode ===
|
|
600
|
+
if (mode === "tail" && isAtBottom) {
|
|
526
601
|
pollIntervalRef.current = setInterval(pollForNewLogs, 2000); // Poll every 2 seconds
|
|
527
602
|
return () => {
|
|
528
603
|
if (pollIntervalRef.current) {
|
|
@@ -538,41 +613,41 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
538
613
|
|
|
539
614
|
const loadInitialLogs = async () => {
|
|
540
615
|
setIsInitialLoading(true);
|
|
541
|
-
|
|
616
|
+
|
|
542
617
|
// Load available logs list first
|
|
543
618
|
await loadAvailableLogs();
|
|
544
|
-
|
|
619
|
+
|
|
545
620
|
try {
|
|
546
621
|
const response = await fetch(`/api/logs/${mode}?lines=1000`);
|
|
547
622
|
if (!response.ok) {
|
|
548
623
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
549
624
|
}
|
|
550
|
-
|
|
625
|
+
|
|
551
626
|
const data: LogsApiResponse = await response.json();
|
|
552
|
-
|
|
627
|
+
|
|
553
628
|
if (!data.logs) {
|
|
554
|
-
console.warn(
|
|
629
|
+
console.warn("No logs data in response");
|
|
555
630
|
setLogs([]);
|
|
556
631
|
setIsInitialLoading(false);
|
|
557
632
|
return;
|
|
558
633
|
}
|
|
559
|
-
|
|
634
|
+
|
|
560
635
|
const entries = parseLogEntries(data.logs);
|
|
561
|
-
|
|
636
|
+
|
|
562
637
|
setLogs(entries);
|
|
563
638
|
setLastLogCount(entries.length);
|
|
564
639
|
setLastFetched(new Date());
|
|
565
640
|
setIsInitialLoading(false);
|
|
566
|
-
|
|
641
|
+
|
|
567
642
|
// Auto-scroll to bottom for tail mode
|
|
568
|
-
if (mode ===
|
|
643
|
+
if (mode === "tail") {
|
|
569
644
|
setTimeout(() => {
|
|
570
|
-
bottomRef.current?.scrollIntoView({ behavior:
|
|
645
|
+
bottomRef.current?.scrollIntoView({ behavior: "auto" });
|
|
571
646
|
setIsAtBottom(true);
|
|
572
647
|
}, 100);
|
|
573
648
|
}
|
|
574
649
|
} catch (error) {
|
|
575
|
-
console.error(
|
|
650
|
+
console.error("Error loading logs:", error);
|
|
576
651
|
setLogs([]);
|
|
577
652
|
}
|
|
578
653
|
};
|
|
@@ -584,7 +659,7 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
584
659
|
} else {
|
|
585
660
|
// We have initial data, just start polling for updates
|
|
586
661
|
setIsInitialLoading(false);
|
|
587
|
-
if (mode ===
|
|
662
|
+
if (mode === "tail" && isAtBottom) {
|
|
588
663
|
// Set up polling timer for new logs
|
|
589
664
|
pollIntervalRef.current = setInterval(() => {
|
|
590
665
|
pollForNewLogs();
|
|
@@ -604,17 +679,24 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
604
679
|
// Close dropdowns when clicking outside
|
|
605
680
|
useEffect(() => {
|
|
606
681
|
const handleClickOutside = (event: MouseEvent) => {
|
|
607
|
-
if (
|
|
682
|
+
if (
|
|
683
|
+
dropdownRef.current &&
|
|
684
|
+
!dropdownRef.current.contains(event.target as Node)
|
|
685
|
+
) {
|
|
608
686
|
setShowLogSelector(false);
|
|
609
687
|
}
|
|
610
|
-
if (
|
|
688
|
+
if (
|
|
689
|
+
filterDropdownRef.current &&
|
|
690
|
+
!filterDropdownRef.current.contains(event.target as Node)
|
|
691
|
+
) {
|
|
611
692
|
setShowFilters(false);
|
|
612
693
|
}
|
|
613
694
|
};
|
|
614
695
|
|
|
615
696
|
if (showLogSelector || showFilters) {
|
|
616
|
-
document.addEventListener(
|
|
617
|
-
return () =>
|
|
697
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
698
|
+
return () =>
|
|
699
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
618
700
|
}
|
|
619
701
|
}, [showLogSelector, showFilters]);
|
|
620
702
|
|
|
@@ -628,47 +710,53 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
628
710
|
|
|
629
711
|
const handleReplay = async () => {
|
|
630
712
|
if (isReplaying) return;
|
|
631
|
-
|
|
713
|
+
|
|
632
714
|
setIsReplaying(true);
|
|
633
|
-
|
|
715
|
+
|
|
634
716
|
try {
|
|
635
717
|
// Get replay data from logs
|
|
636
|
-
const response = await fetch(
|
|
718
|
+
const response = await fetch("/api/replay?action=parse");
|
|
637
719
|
if (!response.ok) {
|
|
638
|
-
throw new Error(
|
|
720
|
+
throw new Error("Failed to parse replay data");
|
|
639
721
|
}
|
|
640
|
-
|
|
722
|
+
|
|
641
723
|
const replayData = await response.json();
|
|
642
|
-
|
|
724
|
+
|
|
643
725
|
if (replayData.interactions.length === 0) {
|
|
644
|
-
alert(
|
|
726
|
+
alert("No user interactions found in logs to replay");
|
|
645
727
|
return;
|
|
646
728
|
}
|
|
647
|
-
|
|
729
|
+
|
|
648
730
|
// Generate CDP commands for replay
|
|
649
|
-
const response2 = await fetch(
|
|
650
|
-
method:
|
|
651
|
-
headers: {
|
|
731
|
+
const response2 = await fetch("/api/replay", {
|
|
732
|
+
method: "POST",
|
|
733
|
+
headers: { "Content-Type": "application/json" },
|
|
652
734
|
body: JSON.stringify({
|
|
653
|
-
action:
|
|
735
|
+
action: "execute",
|
|
654
736
|
replayData: replayData,
|
|
655
|
-
speed: 2
|
|
656
|
-
})
|
|
737
|
+
speed: 2,
|
|
738
|
+
}),
|
|
657
739
|
});
|
|
658
|
-
|
|
740
|
+
|
|
659
741
|
const result = await response2.json();
|
|
660
|
-
|
|
742
|
+
|
|
661
743
|
if (result.success) {
|
|
662
|
-
console.log(
|
|
744
|
+
console.log("Replay executed successfully:", result);
|
|
663
745
|
alert(`Replay completed! Executed ${result.totalCommands} commands.`);
|
|
664
746
|
} else {
|
|
665
|
-
console.log(
|
|
666
|
-
alert(
|
|
747
|
+
console.log("CDP execution failed, showing commands:", result);
|
|
748
|
+
alert(
|
|
749
|
+
`CDP execution not available. Generated ${
|
|
750
|
+
result.commands?.length || 0
|
|
751
|
+
} commands. Check console for details.`
|
|
752
|
+
);
|
|
667
753
|
}
|
|
668
|
-
|
|
669
754
|
} catch (error) {
|
|
670
|
-
console.error(
|
|
671
|
-
alert(
|
|
755
|
+
console.error("Replay error:", error);
|
|
756
|
+
alert(
|
|
757
|
+
"Failed to start replay: " +
|
|
758
|
+
(error instanceof Error ? error.message : "Unknown error")
|
|
759
|
+
);
|
|
672
760
|
} finally {
|
|
673
761
|
setIsReplaying(false);
|
|
674
762
|
}
|
|
@@ -677,8 +765,8 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
677
765
|
const loadReplayPreview = () => {
|
|
678
766
|
// Extract interactions from current logs instead of making API call
|
|
679
767
|
const interactions = logs
|
|
680
|
-
.filter(log => log.message.includes(
|
|
681
|
-
.map(log => {
|
|
768
|
+
.filter((log) => log.message.includes("[INTERACTION]"))
|
|
769
|
+
.map((log) => {
|
|
682
770
|
const match = log.message.match(/\[INTERACTION\] (.+)/);
|
|
683
771
|
if (match) {
|
|
684
772
|
try {
|
|
@@ -687,7 +775,7 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
687
775
|
return {
|
|
688
776
|
timestamp: log.timestamp,
|
|
689
777
|
type: data.type,
|
|
690
|
-
details: data
|
|
778
|
+
details: data,
|
|
691
779
|
};
|
|
692
780
|
} catch {
|
|
693
781
|
// Fallback to old format parsing
|
|
@@ -696,7 +784,7 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
696
784
|
return {
|
|
697
785
|
timestamp: log.timestamp,
|
|
698
786
|
type: oldMatch[1],
|
|
699
|
-
details: oldMatch[2]
|
|
787
|
+
details: oldMatch[2],
|
|
700
788
|
};
|
|
701
789
|
}
|
|
702
790
|
}
|
|
@@ -704,19 +792,19 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
704
792
|
return null;
|
|
705
793
|
})
|
|
706
794
|
.filter(Boolean);
|
|
707
|
-
|
|
795
|
+
|
|
708
796
|
setReplayEvents(interactions);
|
|
709
797
|
};
|
|
710
798
|
|
|
711
799
|
const handleRotateLog = async () => {
|
|
712
800
|
if (!currentLogFile || isRotatingLog) return;
|
|
713
|
-
|
|
801
|
+
|
|
714
802
|
setIsRotatingLog(true);
|
|
715
803
|
try {
|
|
716
|
-
const response = await fetch(
|
|
717
|
-
method:
|
|
804
|
+
const response = await fetch("/api/logs/rotate", {
|
|
805
|
+
method: "POST",
|
|
718
806
|
headers: {
|
|
719
|
-
|
|
807
|
+
"Content-Type": "application/json",
|
|
720
808
|
},
|
|
721
809
|
body: JSON.stringify({ currentLogPath: currentLogFile }),
|
|
722
810
|
});
|
|
@@ -726,38 +814,38 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
726
814
|
setLogs([]);
|
|
727
815
|
setLastLogCount(0);
|
|
728
816
|
setLastFetched(null);
|
|
729
|
-
|
|
817
|
+
|
|
730
818
|
// Reload available logs to show the new archived file
|
|
731
819
|
await loadAvailableLogs();
|
|
732
|
-
|
|
820
|
+
|
|
733
821
|
// Start fresh polling
|
|
734
822
|
await loadInitialLogs();
|
|
735
823
|
} else {
|
|
736
824
|
const error = await response.json();
|
|
737
|
-
console.error(
|
|
738
|
-
alert(
|
|
825
|
+
console.error("Failed to rotate log:", error);
|
|
826
|
+
alert("Failed to rotate log: " + error.error);
|
|
739
827
|
}
|
|
740
828
|
} catch (error) {
|
|
741
|
-
console.error(
|
|
742
|
-
alert(
|
|
829
|
+
console.error("Error rotating log:", error);
|
|
830
|
+
alert("Error rotating log");
|
|
743
831
|
} finally {
|
|
744
832
|
setIsRotatingLog(false);
|
|
745
833
|
}
|
|
746
834
|
};
|
|
747
835
|
|
|
748
836
|
const filteredLogs = useMemo(() => {
|
|
749
|
-
return logs.filter(entry => {
|
|
837
|
+
return logs.filter((entry) => {
|
|
750
838
|
// Check specific message types first (these override source filtering)
|
|
751
|
-
const isInteraction = entry.message.includes(
|
|
752
|
-
const isScreenshot = entry.message.includes(
|
|
753
|
-
|
|
839
|
+
const isInteraction = entry.message.includes("[INTERACTION]");
|
|
840
|
+
const isScreenshot = entry.message.includes("[SCREENSHOT]");
|
|
841
|
+
|
|
754
842
|
if (isInteraction) return filters.interaction;
|
|
755
843
|
if (isScreenshot) return filters.screenshot;
|
|
756
|
-
|
|
844
|
+
|
|
757
845
|
// For other logs, filter by source
|
|
758
|
-
if (entry.source ===
|
|
759
|
-
if (entry.source ===
|
|
760
|
-
|
|
846
|
+
if (entry.source === "SERVER") return filters.server;
|
|
847
|
+
if (entry.source === "BROWSER") return filters.browser;
|
|
848
|
+
|
|
761
849
|
return true;
|
|
762
850
|
});
|
|
763
851
|
}, [logs, filters]);
|
|
@@ -770,10 +858,13 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
770
858
|
<div className="flex items-center justify-between">
|
|
771
859
|
<div className="flex items-center gap-2 sm:gap-4">
|
|
772
860
|
<div className="flex items-center gap-1">
|
|
773
|
-
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white whitespace-nowrap">
|
|
774
|
-
|
|
861
|
+
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white whitespace-nowrap">
|
|
862
|
+
dev3000
|
|
863
|
+
</h1>
|
|
864
|
+
<span className="text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">
|
|
865
|
+
(v{version})
|
|
866
|
+
</span>
|
|
775
867
|
</div>
|
|
776
|
-
|
|
777
868
|
{/* Log File Selector */}
|
|
778
869
|
{availableLogs.length > 1 ? (
|
|
779
870
|
<div className="relative" ref={dropdownRef}>
|
|
@@ -783,63 +874,88 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
783
874
|
>
|
|
784
875
|
<span className="font-mono text-xs whitespace-nowrap">
|
|
785
876
|
{isInitialLoading && !currentLogFile ? (
|
|
786
|
-
<div
|
|
877
|
+
<div
|
|
878
|
+
className="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"
|
|
879
|
+
style={{ width: "220px" }}
|
|
880
|
+
/>
|
|
881
|
+
) : currentLogFile ? (
|
|
882
|
+
currentLogFile.split("/").pop()
|
|
787
883
|
) : (
|
|
788
|
-
|
|
884
|
+
"dev3000.log"
|
|
789
885
|
)}
|
|
790
886
|
</span>
|
|
791
|
-
<svg
|
|
792
|
-
className={`w-4 h-4 transition-transform ${
|
|
793
|
-
|
|
794
|
-
|
|
887
|
+
<svg
|
|
888
|
+
className={`w-4 h-4 transition-transform ${
|
|
889
|
+
showLogSelector ? "rotate-180" : ""
|
|
890
|
+
}`}
|
|
891
|
+
fill="none"
|
|
892
|
+
stroke="currentColor"
|
|
795
893
|
viewBox="0 0 24 24"
|
|
796
894
|
>
|
|
797
|
-
<path
|
|
895
|
+
<path
|
|
896
|
+
strokeLinecap="round"
|
|
897
|
+
strokeLinejoin="round"
|
|
898
|
+
strokeWidth={2}
|
|
899
|
+
d="M19 9l-7 7-7-7"
|
|
900
|
+
/>
|
|
798
901
|
</svg>
|
|
799
902
|
</button>
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
903
|
+
{/* Dropdown */}
|
|
904
|
+
{showLogSelector && availableLogs.length > 1 && (
|
|
905
|
+
<div className="absolute top-full left-0 mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-md shadow-lg z-20 min-w-80">
|
|
906
|
+
<div className="py-1 max-h-60 overflow-y-auto">
|
|
907
|
+
<div className="px-3 py-2 text-xs font-medium text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-600">
|
|
908
|
+
{projectName} logs ({availableLogs.length})
|
|
909
|
+
</div>
|
|
910
|
+
{availableLogs.map((logFile) => (
|
|
911
|
+
<button
|
|
912
|
+
key={logFile.path}
|
|
913
|
+
onClick={() => {
|
|
914
|
+
setShowLogSelector(false);
|
|
915
|
+
router.push(
|
|
916
|
+
`/logs?file=${encodeURIComponent(
|
|
917
|
+
logFile.name
|
|
918
|
+
)}&mode=${mode}`
|
|
919
|
+
);
|
|
920
|
+
}}
|
|
921
|
+
className={`w-full text-left px-3 py-2 text-sm hover:bg-gray-50 flex items-center justify-between ${
|
|
922
|
+
logFile.isCurrent
|
|
923
|
+
? "bg-blue-50 text-blue-900"
|
|
924
|
+
: "text-gray-700"
|
|
925
|
+
}`}
|
|
926
|
+
>
|
|
927
|
+
<div className="flex flex-col">
|
|
928
|
+
<span className="font-mono text-xs">
|
|
929
|
+
{logFile.name}
|
|
930
|
+
</span>
|
|
931
|
+
<span className="text-xs text-gray-500">
|
|
932
|
+
{new Date(logFile.mtime).toLocaleString()} •{" "}
|
|
933
|
+
{Math.round(logFile.size / 1024)}KB
|
|
934
|
+
</span>
|
|
935
|
+
</div>
|
|
936
|
+
{logFile.isCurrent && (
|
|
937
|
+
<span className="text-xs text-blue-600 font-medium">
|
|
938
|
+
current
|
|
939
|
+
</span>
|
|
940
|
+
)}
|
|
941
|
+
</button>
|
|
942
|
+
))}
|
|
807
943
|
</div>
|
|
808
|
-
{availableLogs.map((logFile) => (
|
|
809
|
-
<button
|
|
810
|
-
key={logFile.path}
|
|
811
|
-
onClick={() => {
|
|
812
|
-
setShowLogSelector(false);
|
|
813
|
-
router.push(`/logs?file=${encodeURIComponent(logFile.name)}&mode=${mode}`);
|
|
814
|
-
}}
|
|
815
|
-
className={`w-full text-left px-3 py-2 text-sm hover:bg-gray-50 flex items-center justify-between ${
|
|
816
|
-
logFile.isCurrent ? 'bg-blue-50 text-blue-900' : 'text-gray-700'
|
|
817
|
-
}`}
|
|
818
|
-
>
|
|
819
|
-
<div className="flex flex-col">
|
|
820
|
-
<span className="font-mono text-xs">
|
|
821
|
-
{logFile.name}
|
|
822
|
-
</span>
|
|
823
|
-
<span className="text-xs text-gray-500">
|
|
824
|
-
{new Date(logFile.mtime).toLocaleString()} • {Math.round(logFile.size / 1024)}KB
|
|
825
|
-
</span>
|
|
826
|
-
</div>
|
|
827
|
-
{logFile.isCurrent && (
|
|
828
|
-
<span className="text-xs text-blue-600 font-medium">current</span>
|
|
829
|
-
)}
|
|
830
|
-
</button>
|
|
831
|
-
))}
|
|
832
944
|
</div>
|
|
833
|
-
|
|
834
|
-
)}
|
|
945
|
+
)}
|
|
835
946
|
</div>
|
|
836
947
|
) : (
|
|
837
948
|
<div className="flex items-center gap-2">
|
|
838
949
|
<span className="font-mono text-xs text-gray-600 px-3 py-1 whitespace-nowrap">
|
|
839
950
|
{isInitialLoading && !currentLogFile ? (
|
|
840
|
-
<div
|
|
951
|
+
<div
|
|
952
|
+
className="h-4 bg-gray-200 rounded animate-pulse"
|
|
953
|
+
style={{ width: "220px" }}
|
|
954
|
+
/>
|
|
955
|
+
) : currentLogFile ? (
|
|
956
|
+
currentLogFile.split("/").pop()
|
|
841
957
|
) : (
|
|
842
|
-
|
|
958
|
+
"dev3000.log"
|
|
843
959
|
)}
|
|
844
960
|
</span>
|
|
845
961
|
{currentLogFile && !isInitialLoading && (
|
|
@@ -849,17 +965,18 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
849
965
|
className="px-2 py-1 text-xs bg-orange-100 text-orange-700 hover:bg-orange-200 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
850
966
|
title="Clear logs (rotate current log to archive and start fresh)"
|
|
851
967
|
>
|
|
852
|
-
{isRotatingLog ?
|
|
968
|
+
{isRotatingLog ? "..." : "Clear"}
|
|
853
969
|
</button>
|
|
854
970
|
)}
|
|
855
971
|
</div>
|
|
856
972
|
)}
|
|
857
|
-
|
|
858
973
|
{logs.length > 0 && (
|
|
859
|
-
<span className="text-sm text-gray-500 hidden sm:inline">
|
|
974
|
+
<span className="text-sm text-gray-500 hidden sm:inline">
|
|
975
|
+
{logs.length} entries
|
|
976
|
+
</span>
|
|
860
977
|
)}
|
|
861
978
|
</div>
|
|
862
|
-
|
|
979
|
+
|
|
863
980
|
{/* Mode Toggle with Replay Button */}
|
|
864
981
|
<div className="flex items-center gap-2">
|
|
865
982
|
{/* Replay Button with Hover Preview */}
|
|
@@ -876,8 +993,8 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
876
993
|
onMouseLeave={() => setShowReplayPreview(false)}
|
|
877
994
|
className={`flex items-center gap-1 px-3 py-1 rounded text-sm font-medium transition-colors whitespace-nowrap ${
|
|
878
995
|
isReplaying
|
|
879
|
-
?
|
|
880
|
-
:
|
|
996
|
+
? "bg-gray-100 text-gray-400 cursor-not-allowed"
|
|
997
|
+
: "bg-purple-100 text-purple-800 hover:bg-purple-200"
|
|
881
998
|
}`}
|
|
882
999
|
>
|
|
883
1000
|
{isReplaying ? (
|
|
@@ -887,14 +1004,23 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
887
1004
|
</>
|
|
888
1005
|
) : (
|
|
889
1006
|
<>
|
|
890
|
-
<svg
|
|
891
|
-
|
|
1007
|
+
<svg
|
|
1008
|
+
className="w-4 h-4"
|
|
1009
|
+
fill="none"
|
|
1010
|
+
stroke="currentColor"
|
|
1011
|
+
viewBox="0 0 24 24"
|
|
1012
|
+
>
|
|
1013
|
+
<path
|
|
1014
|
+
strokeLinecap="round"
|
|
1015
|
+
strokeLinejoin="round"
|
|
1016
|
+
strokeWidth={2}
|
|
1017
|
+
d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h1m4 0h1m6-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M7 7V4a1 1 0 011-1h4a1 1 0 011 1v3m-9 4h16m-5 4v1a1 1 0 01-1 1H8a1 1 0 01-1-1v-1m8 0V9a1 1 0 00-1-1H8a1 1 0 00-1 1v8.001"
|
|
1018
|
+
/>
|
|
892
1019
|
</svg>
|
|
893
1020
|
Replay
|
|
894
1021
|
</>
|
|
895
1022
|
)}
|
|
896
1023
|
</button>
|
|
897
|
-
|
|
898
1024
|
{/* Replay Preview Dropdown */}
|
|
899
1025
|
{showReplayPreview && !isReplaying && (
|
|
900
1026
|
<div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-30 w-80">
|
|
@@ -909,23 +1035,35 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
909
1035
|
</div>
|
|
910
1036
|
) : (
|
|
911
1037
|
replayEvents.map((event, index) => (
|
|
912
|
-
<div
|
|
1038
|
+
<div
|
|
1039
|
+
key={index}
|
|
1040
|
+
className="px-3 py-2 text-sm hover:bg-gray-50 border-b border-gray-100 last:border-b-0"
|
|
1041
|
+
>
|
|
913
1042
|
<div className="flex items-center gap-2">
|
|
914
|
-
<span
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1043
|
+
<span
|
|
1044
|
+
className={`px-1.5 py-0.5 rounded text-xs font-medium ${
|
|
1045
|
+
event.type === "CLICK"
|
|
1046
|
+
? "bg-blue-100 text-blue-800"
|
|
1047
|
+
: event.type === "SCROLL"
|
|
1048
|
+
? "bg-green-100 text-green-800"
|
|
1049
|
+
: "bg-gray-100 text-gray-700"
|
|
1050
|
+
}`}
|
|
1051
|
+
>
|
|
919
1052
|
{event.type}
|
|
920
1053
|
</span>
|
|
921
1054
|
<span className="text-xs text-gray-500 font-mono">
|
|
922
|
-
{new Date(
|
|
1055
|
+
{new Date(
|
|
1056
|
+
event.timestamp
|
|
1057
|
+
).toLocaleTimeString()}
|
|
923
1058
|
</span>
|
|
924
1059
|
</div>
|
|
925
1060
|
<div className="mt-1 text-xs text-gray-600 font-mono truncate">
|
|
926
|
-
{event.type ===
|
|
927
|
-
|
|
928
|
-
{event.type ===
|
|
1061
|
+
{event.type === "CLICK" &&
|
|
1062
|
+
`(${event.x}, ${event.y}) on ${event.target}`}
|
|
1063
|
+
{event.type === "SCROLL" &&
|
|
1064
|
+
`${event.direction} ${event.distance}px to (${event.x}, ${event.y})`}
|
|
1065
|
+
{event.type === "KEY" &&
|
|
1066
|
+
`${event.key} in ${event.target}`}
|
|
929
1067
|
</div>
|
|
930
1068
|
</div>
|
|
931
1069
|
))
|
|
@@ -935,19 +1073,27 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
935
1073
|
</div>
|
|
936
1074
|
)}
|
|
937
1075
|
</div>
|
|
938
|
-
|
|
939
1076
|
{/* Filter Button */}
|
|
940
1077
|
<div className="relative" ref={filterDropdownRef}>
|
|
941
1078
|
<button
|
|
942
1079
|
onClick={() => setShowFilters(!showFilters)}
|
|
943
1080
|
className="flex items-center gap-1 px-3 py-1 rounded text-sm font-medium transition-colors whitespace-nowrap bg-gray-100 text-gray-700 hover:bg-gray-200"
|
|
944
1081
|
>
|
|
945
|
-
<svg
|
|
946
|
-
|
|
1082
|
+
<svg
|
|
1083
|
+
className="w-4 h-4"
|
|
1084
|
+
fill="none"
|
|
1085
|
+
stroke="currentColor"
|
|
1086
|
+
viewBox="0 0 24 24"
|
|
1087
|
+
>
|
|
1088
|
+
<path
|
|
1089
|
+
strokeLinecap="round"
|
|
1090
|
+
strokeLinejoin="round"
|
|
1091
|
+
strokeWidth={2}
|
|
1092
|
+
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
|
|
1093
|
+
/>
|
|
947
1094
|
</svg>
|
|
948
1095
|
Filter
|
|
949
1096
|
</button>
|
|
950
|
-
|
|
951
1097
|
{/* Filter Dropdown */}
|
|
952
1098
|
{showFilters && (
|
|
953
1099
|
<div className="absolute top-full right-0 mt-1 bg-white border border-gray-200 rounded-md shadow-lg z-20 min-w-48">
|
|
@@ -956,10 +1102,32 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
956
1102
|
Log Types
|
|
957
1103
|
</div>
|
|
958
1104
|
{[
|
|
959
|
-
{
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1105
|
+
{
|
|
1106
|
+
key: "server",
|
|
1107
|
+
label: "Server",
|
|
1108
|
+
count: logs.filter((l) => l.source === "SERVER")
|
|
1109
|
+
.length,
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
key: "browser",
|
|
1113
|
+
label: "Browser",
|
|
1114
|
+
count: logs.filter((l) => l.source === "BROWSER")
|
|
1115
|
+
.length,
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
key: "interaction",
|
|
1119
|
+
label: "Interaction",
|
|
1120
|
+
count: logs.filter((l) =>
|
|
1121
|
+
l.message.includes("[INTERACTION]")
|
|
1122
|
+
).length,
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
key: "screenshot",
|
|
1126
|
+
label: "Screenshot",
|
|
1127
|
+
count: logs.filter((l) =>
|
|
1128
|
+
l.message.includes("[SCREENSHOT]")
|
|
1129
|
+
).length,
|
|
1130
|
+
},
|
|
963
1131
|
].map(({ key, label, count }) => (
|
|
964
1132
|
<label
|
|
965
1133
|
key={key}
|
|
@@ -969,7 +1137,12 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
969
1137
|
<input
|
|
970
1138
|
type="checkbox"
|
|
971
1139
|
checked={filters[key as keyof typeof filters]}
|
|
972
|
-
onChange={(e) =>
|
|
1140
|
+
onChange={(e) =>
|
|
1141
|
+
setFilters((prev) => ({
|
|
1142
|
+
...prev,
|
|
1143
|
+
[key]: e.target.checked,
|
|
1144
|
+
}))
|
|
1145
|
+
}
|
|
973
1146
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
974
1147
|
/>
|
|
975
1148
|
<span>{label}</span>
|
|
@@ -981,55 +1154,83 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
981
1154
|
</div>
|
|
982
1155
|
)}
|
|
983
1156
|
</div>
|
|
984
|
-
|
|
985
1157
|
<div className="flex items-center bg-gray-100 rounded-md p-1">
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1158
|
+
<button
|
|
1159
|
+
onClick={() => {
|
|
1160
|
+
const currentFile = searchParams.get("file");
|
|
1161
|
+
if (currentFile) {
|
|
1162
|
+
router.push(
|
|
1163
|
+
`/logs?file=${encodeURIComponent(
|
|
1164
|
+
currentFile
|
|
1165
|
+
)}&mode=head`
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
}}
|
|
1169
|
+
className={`px-2 sm:px-3 py-1 rounded text-xs sm:text-sm font-medium transition-colors whitespace-nowrap ${
|
|
1170
|
+
mode === "head"
|
|
1171
|
+
? "bg-white text-gray-900 shadow-sm"
|
|
1172
|
+
: "text-gray-600 hover:text-gray-900"
|
|
1173
|
+
}`}
|
|
1174
|
+
>
|
|
1175
|
+
Head
|
|
1176
|
+
</button>
|
|
1177
|
+
<button
|
|
1178
|
+
onClick={() => {
|
|
1179
|
+
const currentFile = searchParams.get("file");
|
|
1180
|
+
if (currentFile) {
|
|
1181
|
+
router.push(
|
|
1182
|
+
`/logs?file=${encodeURIComponent(
|
|
1183
|
+
currentFile
|
|
1184
|
+
)}&mode=tail`
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
}}
|
|
1188
|
+
className={`px-2 sm:px-3 py-1 rounded text-xs sm:text-sm font-medium transition-colors whitespace-nowrap ${
|
|
1189
|
+
mode === "tail"
|
|
1190
|
+
? "bg-white text-gray-900 shadow-sm"
|
|
1191
|
+
: "text-gray-600 hover:text-gray-900"
|
|
1192
|
+
}`}
|
|
1193
|
+
>
|
|
1194
|
+
Tail
|
|
1195
|
+
</button>
|
|
1016
1196
|
</div>
|
|
1017
|
-
|
|
1018
|
-
{/* Dark Mode Toggle */}
|
|
1197
|
+
{/* Dark Mode Toggle - moved to last item */}
|
|
1019
1198
|
<button
|
|
1020
1199
|
onClick={() => setDarkMode(!darkMode)}
|
|
1021
|
-
className="p-2 rounded-md text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
1022
|
-
title={
|
|
1200
|
+
className="p-2 rounded-md text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ml-2"
|
|
1201
|
+
title={
|
|
1202
|
+
darkMode ? "Switch to light mode" : "Switch to dark mode"
|
|
1203
|
+
}
|
|
1023
1204
|
>
|
|
1024
1205
|
{darkMode ? (
|
|
1025
1206
|
// Sun icon for light mode
|
|
1026
|
-
<svg
|
|
1027
|
-
|
|
1207
|
+
<svg
|
|
1208
|
+
className="w-4 h-4"
|
|
1209
|
+
fill="none"
|
|
1210
|
+
stroke="currentColor"
|
|
1211
|
+
viewBox="0 0 24 24"
|
|
1212
|
+
>
|
|
1213
|
+
<path
|
|
1214
|
+
strokeLinecap="round"
|
|
1215
|
+
strokeLinejoin="round"
|
|
1216
|
+
strokeWidth={2}
|
|
1217
|
+
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
|
1218
|
+
/>
|
|
1028
1219
|
</svg>
|
|
1029
1220
|
) : (
|
|
1030
1221
|
// Moon icon for dark mode
|
|
1031
|
-
<svg
|
|
1032
|
-
|
|
1222
|
+
<svg
|
|
1223
|
+
className="w-4 h-4"
|
|
1224
|
+
fill="none"
|
|
1225
|
+
stroke="currentColor"
|
|
1226
|
+
viewBox="0 0 24 24"
|
|
1227
|
+
>
|
|
1228
|
+
<path
|
|
1229
|
+
strokeLinecap="round"
|
|
1230
|
+
strokeLinejoin="round"
|
|
1231
|
+
strokeWidth={2}
|
|
1232
|
+
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
|
1233
|
+
/>
|
|
1033
1234
|
</svg>
|
|
1034
1235
|
)}
|
|
1035
1236
|
</button>
|
|
@@ -1040,7 +1241,7 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
1040
1241
|
|
|
1041
1242
|
{/* Content Area - Fills remaining space */}
|
|
1042
1243
|
<div className="flex-1 overflow-hidden">
|
|
1043
|
-
<div
|
|
1244
|
+
<div
|
|
1044
1245
|
ref={containerRef}
|
|
1045
1246
|
className="max-w-7xl mx-auto px-4 py-6 h-full overflow-y-auto"
|
|
1046
1247
|
onScroll={handleScroll}
|
|
@@ -1052,14 +1253,18 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
1052
1253
|
</div>
|
|
1053
1254
|
) : logs.length === 0 ? (
|
|
1054
1255
|
<div className="text-center py-12">
|
|
1055
|
-
<div className="text-gray-400 dark:text-gray-500 text-lg"
|
|
1256
|
+
<div className="text-gray-400 dark:text-gray-500 text-lg">
|
|
1257
|
+
📝 No logs yet
|
|
1258
|
+
</div>
|
|
1056
1259
|
<div className="text-gray-500 dark:text-gray-400 text-sm mt-2">
|
|
1057
1260
|
Logs will appear here as your development server runs
|
|
1058
1261
|
</div>
|
|
1059
1262
|
</div>
|
|
1060
1263
|
) : filteredLogs.length === 0 ? (
|
|
1061
1264
|
<div className="text-center py-12">
|
|
1062
|
-
<div className="text-gray-400 dark:text-gray-500 text-lg"
|
|
1265
|
+
<div className="text-gray-400 dark:text-gray-500 text-lg">
|
|
1266
|
+
🔍 No logs match current filters
|
|
1267
|
+
</div>
|
|
1063
1268
|
<div className="text-gray-500 dark:text-gray-400 text-sm mt-2">
|
|
1064
1269
|
Try adjusting your filter settings to see more logs
|
|
1065
1270
|
</div>
|
|
@@ -1074,7 +1279,7 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
1074
1279
|
)}
|
|
1075
1280
|
</div>
|
|
1076
1281
|
</div>
|
|
1077
|
-
|
|
1282
|
+
|
|
1078
1283
|
{/* Footer - Fixed */}
|
|
1079
1284
|
<div className="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 flex-none">
|
|
1080
1285
|
<div className="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
|
|
@@ -1082,7 +1287,9 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
1082
1287
|
{isLoadingNew && (
|
|
1083
1288
|
<div className="flex items-center gap-1">
|
|
1084
1289
|
<div className="w-3 h-3 border border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>
|
|
1085
|
-
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
1290
|
+
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
1291
|
+
Loading...
|
|
1292
|
+
</span>
|
|
1086
1293
|
</div>
|
|
1087
1294
|
)}
|
|
1088
1295
|
{!isLoadingNew && lastFetched && (
|
|
@@ -1091,9 +1298,9 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
1091
1298
|
</span>
|
|
1092
1299
|
)}
|
|
1093
1300
|
{currentLogFile && (
|
|
1094
|
-
<a
|
|
1095
|
-
href={`file://${currentLogFile}`}
|
|
1096
|
-
target="_blank"
|
|
1301
|
+
<a
|
|
1302
|
+
href={`file://${currentLogFile}`}
|
|
1303
|
+
target="_blank"
|
|
1097
1304
|
rel="noopener noreferrer"
|
|
1098
1305
|
className="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1"
|
|
1099
1306
|
>
|
|
@@ -1101,24 +1308,30 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
1101
1308
|
</a>
|
|
1102
1309
|
)}
|
|
1103
1310
|
</div>
|
|
1104
|
-
|
|
1311
|
+
|
|
1105
1312
|
{/* Live indicator or scroll to bottom button - positioned on the right */}
|
|
1106
1313
|
<div className="relative">
|
|
1107
1314
|
{/* Live indicator when at bottom */}
|
|
1108
|
-
<div
|
|
1315
|
+
<div
|
|
1109
1316
|
className={`flex items-center gap-1 text-green-600 ${
|
|
1110
|
-
mode ===
|
|
1317
|
+
mode === "tail" && isAtBottom && !isLoadingNew
|
|
1318
|
+
? "visible"
|
|
1319
|
+
: "invisible"
|
|
1111
1320
|
}`}
|
|
1112
1321
|
>
|
|
1113
1322
|
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
|
1114
1323
|
<span className="text-xs">Live</span>
|
|
1115
1324
|
</div>
|
|
1116
|
-
|
|
1325
|
+
|
|
1117
1326
|
{/* Scroll to bottom button when not at bottom */}
|
|
1118
1327
|
<button
|
|
1119
|
-
onClick={() =>
|
|
1328
|
+
onClick={() =>
|
|
1329
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth" })
|
|
1330
|
+
}
|
|
1120
1331
|
className={`absolute top-0 right-0 flex items-center gap-1 px-2 py-0.5 bg-blue-600 text-white rounded text-xs hover:bg-blue-700 whitespace-nowrap ${
|
|
1121
|
-
mode ===
|
|
1332
|
+
mode === "tail" && !isAtBottom && !isLoadingNew
|
|
1333
|
+
? "visible"
|
|
1334
|
+
: "invisible"
|
|
1122
1335
|
}`}
|
|
1123
1336
|
>
|
|
1124
1337
|
↓ Live
|
|
@@ -1128,4 +1341,4 @@ export default function LogsClient({ version, initialData }: LogsClientProps) {
|
|
|
1128
1341
|
</div>
|
|
1129
1342
|
</div>
|
|
1130
1343
|
);
|
|
1131
|
-
}
|
|
1344
|
+
}
|