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