dev3000 0.0.0 → 0.0.3
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/README.md +59 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/dev-environment.d.ts +36 -0
- package/dist/dev-environment.d.ts.map +1 -0
- package/dist/dev-environment.js +602 -0
- package/dist/dev-environment.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/mcp-server/app/api/config/route.ts +10 -0
- package/mcp-server/app/api/logs/head/route.ts +32 -0
- package/mcp-server/app/api/logs/stream/route.ts +61 -0
- package/mcp-server/app/api/logs/tail/route.ts +32 -0
- package/mcp-server/app/api/mcp/[transport]/route.ts +188 -0
- package/mcp-server/app/layout.tsx +15 -0
- package/mcp-server/app/logs/LogsClient.tsx +292 -0
- package/mcp-server/app/logs/page.tsx +7 -0
- package/mcp-server/app/page.tsx +45 -0
- package/mcp-server/next-env.d.ts +6 -0
- package/mcp-server/next.config.ts +14 -0
- package/mcp-server/package.json +23 -0
- package/mcp-server/tsconfig.json +40 -0
- package/package.json +53 -7
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { ConfigApiResponse } from '../../types';
|
|
3
|
+
|
|
4
|
+
export async function GET(): Promise<NextResponse> {
|
|
5
|
+
const response: ConfigApiResponse = {
|
|
6
|
+
version: process.env.DEV_PLAYWRIGHT_VERSION || '0.0.0'
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
return NextResponse.json(response);
|
|
10
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { LogsApiResponse, LogsApiError } from '../../../types';
|
|
4
|
+
|
|
5
|
+
export async function GET(request: NextRequest): Promise<Response> {
|
|
6
|
+
try {
|
|
7
|
+
const { searchParams } = new URL(request.url);
|
|
8
|
+
const lines = parseInt(searchParams.get('lines') || '50');
|
|
9
|
+
const logPath = searchParams.get('logPath') || process.env.LOG_FILE_PATH || './ai-dev-tools/consolidated.log';
|
|
10
|
+
|
|
11
|
+
if (!existsSync(logPath)) {
|
|
12
|
+
const errorResponse: LogsApiError = { error: 'Log file not found' };
|
|
13
|
+
return Response.json(errorResponse, { status: 404 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const logContent = readFileSync(logPath, 'utf-8');
|
|
17
|
+
const allLines = logContent.split('\n').filter(line => line.trim());
|
|
18
|
+
const headLines = allLines.slice(0, lines);
|
|
19
|
+
|
|
20
|
+
const response: LogsApiResponse = {
|
|
21
|
+
logs: headLines.join('\n'),
|
|
22
|
+
total: allLines.length
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return Response.json(response);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const errorResponse: LogsApiError = {
|
|
28
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
29
|
+
};
|
|
30
|
+
return Response.json(errorResponse, { status: 500 });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { existsSync, watchFile, readFileSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
export async function GET(request: NextRequest) {
|
|
5
|
+
const { searchParams } = new URL(request.url);
|
|
6
|
+
const logPath = searchParams.get('logPath') || process.env.LOG_FILE_PATH || './ai-dev-tools/consolidated.log';
|
|
7
|
+
|
|
8
|
+
if (!existsSync(logPath)) {
|
|
9
|
+
return new Response('Log file not found', { status: 404 });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
let lastSize = 0;
|
|
14
|
+
|
|
15
|
+
const stream = new ReadableStream({
|
|
16
|
+
start(controller) {
|
|
17
|
+
// Send initial content
|
|
18
|
+
try {
|
|
19
|
+
const content = readFileSync(logPath, 'utf-8');
|
|
20
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
21
|
+
lastSize = content.length;
|
|
22
|
+
|
|
23
|
+
// Send initial lines
|
|
24
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ lines })}\n\n`));
|
|
25
|
+
} catch (error) {
|
|
26
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: 'Failed to read log' })}\n\n`));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Watch for file changes
|
|
30
|
+
const watcher = watchFile(logPath, { interval: 1000 }, () => {
|
|
31
|
+
try {
|
|
32
|
+
const content = readFileSync(logPath, 'utf-8');
|
|
33
|
+
if (content.length > lastSize) {
|
|
34
|
+
const newContent = content.slice(lastSize);
|
|
35
|
+
const newLines = newContent.split('\n').filter(line => line.trim());
|
|
36
|
+
if (newLines.length > 0) {
|
|
37
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ newLines })}\n\n`));
|
|
38
|
+
}
|
|
39
|
+
lastSize = content.length;
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: 'Failed to read log updates' })}\n\n`));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Cleanup on close
|
|
47
|
+
request.signal.addEventListener('abort', () => {
|
|
48
|
+
watcher.unref();
|
|
49
|
+
controller.close();
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return new Response(stream, {
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'text/event-stream',
|
|
57
|
+
'Cache-Control': 'no-cache',
|
|
58
|
+
'Connection': 'keep-alive',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { LogsApiResponse, LogsApiError } from '../../../types';
|
|
4
|
+
|
|
5
|
+
export async function GET(request: NextRequest): Promise<Response> {
|
|
6
|
+
try {
|
|
7
|
+
const { searchParams } = new URL(request.url);
|
|
8
|
+
const lines = parseInt(searchParams.get('lines') || '50');
|
|
9
|
+
const logPath = searchParams.get('logPath') || process.env.LOG_FILE_PATH || './ai-dev-tools/consolidated.log';
|
|
10
|
+
|
|
11
|
+
if (!existsSync(logPath)) {
|
|
12
|
+
const errorResponse: LogsApiError = { error: 'Log file not found' };
|
|
13
|
+
return Response.json(errorResponse, { status: 404 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const logContent = readFileSync(logPath, 'utf-8');
|
|
17
|
+
const allLines = logContent.split('\n').filter(line => line.trim());
|
|
18
|
+
const tailLines = allLines.slice(-lines);
|
|
19
|
+
|
|
20
|
+
const response: LogsApiResponse = {
|
|
21
|
+
logs: tailLines.join('\n'),
|
|
22
|
+
total: allLines.length
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return Response.json(response);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const errorResponse: LogsApiError = {
|
|
28
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
29
|
+
};
|
|
30
|
+
return Response.json(errorResponse, { status: 500 });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { createMcpHandler } from "mcp-handler";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { readFileSync, existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
const handler = createMcpHandler((server) => {
|
|
7
|
+
// Tool to read consolidated logs
|
|
8
|
+
server.tool(
|
|
9
|
+
"read_consolidated_logs",
|
|
10
|
+
"Read the consolidated development logs (server + browser)",
|
|
11
|
+
{
|
|
12
|
+
lines: z.number().optional().describe("Number of recent lines to read (default: 50)"),
|
|
13
|
+
filter: z.string().optional().describe("Filter logs by text content"),
|
|
14
|
+
logPath: z.string().optional().describe("Path to log file (default: ./ai-dev-tools/consolidated.log)"),
|
|
15
|
+
},
|
|
16
|
+
async ({ lines = 50, filter, logPath = "./ai-dev-tools/consolidated.log" }) => {
|
|
17
|
+
try {
|
|
18
|
+
if (!existsSync(logPath)) {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: "text",
|
|
23
|
+
text: `No log file found at ${logPath}. Make sure the dev environment is running.`
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const logContent = readFileSync(logPath, "utf-8");
|
|
30
|
+
let logLines = logContent.split("\n").filter(line => line.trim());
|
|
31
|
+
|
|
32
|
+
// Apply filter if provided
|
|
33
|
+
if (filter) {
|
|
34
|
+
logLines = logLines.filter(line =>
|
|
35
|
+
line.toLowerCase().includes(filter.toLowerCase())
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get recent lines
|
|
40
|
+
const recentLines = logLines.slice(-lines);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "text",
|
|
46
|
+
text: recentLines.length > 0
|
|
47
|
+
? recentLines.join("\n")
|
|
48
|
+
: "No matching log entries found."
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return {
|
|
54
|
+
content: [
|
|
55
|
+
{
|
|
56
|
+
type: "text",
|
|
57
|
+
text: `Error reading logs: ${error instanceof Error ? error.message : String(error)}`
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Tool to search logs
|
|
66
|
+
server.tool(
|
|
67
|
+
"search_logs",
|
|
68
|
+
"Search through consolidated logs with regex patterns",
|
|
69
|
+
{
|
|
70
|
+
pattern: z.string().describe("Regex pattern to search for"),
|
|
71
|
+
context: z.number().optional().describe("Number of lines of context around matches (default: 2)"),
|
|
72
|
+
logPath: z.string().optional().describe("Path to log file (default: ./ai-dev-tools/consolidated.log)"),
|
|
73
|
+
},
|
|
74
|
+
async ({ pattern, context = 2, logPath = "./ai-dev-tools/consolidated.log" }) => {
|
|
75
|
+
try {
|
|
76
|
+
if (!existsSync(logPath)) {
|
|
77
|
+
return {
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: `No log file found at ${logPath}.`
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const logContent = readFileSync(logPath, "utf-8");
|
|
88
|
+
const logLines = logContent.split("\n");
|
|
89
|
+
|
|
90
|
+
const regex = new RegExp(pattern, "gi");
|
|
91
|
+
const matches: string[] = [];
|
|
92
|
+
|
|
93
|
+
logLines.forEach((line, index) => {
|
|
94
|
+
if (regex.test(line)) {
|
|
95
|
+
const start = Math.max(0, index - context);
|
|
96
|
+
const end = Math.min(logLines.length, index + context + 1);
|
|
97
|
+
const contextLines = logLines.slice(start, end);
|
|
98
|
+
|
|
99
|
+
matches.push(`Match at line ${index + 1}:\n${contextLines.join("\n")}\n---`);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: matches.length > 0
|
|
108
|
+
? matches.join("\n\n")
|
|
109
|
+
: "No matches found for the given pattern."
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
};
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return {
|
|
115
|
+
content: [
|
|
116
|
+
{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: `Error searching logs: ${error instanceof Error ? error.message : String(error)}`
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Tool to get browser errors
|
|
127
|
+
server.tool(
|
|
128
|
+
"get_browser_errors",
|
|
129
|
+
"Get recent browser errors and page errors from logs",
|
|
130
|
+
{
|
|
131
|
+
hours: z.number().optional().describe("Hours to look back (default: 1)"),
|
|
132
|
+
logPath: z.string().optional().describe("Path to log file (default: ./ai-dev-tools/consolidated.log)"),
|
|
133
|
+
},
|
|
134
|
+
async ({ hours = 1, logPath = "./ai-dev-tools/consolidated.log" }) => {
|
|
135
|
+
try {
|
|
136
|
+
if (!existsSync(logPath)) {
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: `No log file found at ${logPath}.`
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const logContent = readFileSync(logPath, "utf-8");
|
|
148
|
+
const logLines = logContent.split("\n");
|
|
149
|
+
|
|
150
|
+
const cutoffTime = new Date(Date.now() - hours * 60 * 60 * 1000);
|
|
151
|
+
const errorLines = logLines.filter(line => {
|
|
152
|
+
if (!line.includes("[BROWSER]")) return false;
|
|
153
|
+
if (!(line.includes("ERROR") || line.includes("CONSOLE ERROR") || line.includes("PAGE ERROR"))) return false;
|
|
154
|
+
|
|
155
|
+
// Extract timestamp
|
|
156
|
+
const timestampMatch = line.match(/\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\]/);
|
|
157
|
+
if (timestampMatch) {
|
|
158
|
+
const logTime = new Date(timestampMatch[1]);
|
|
159
|
+
return logTime > cutoffTime;
|
|
160
|
+
}
|
|
161
|
+
return true; // Include if we can't parse timestamp
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
content: [
|
|
166
|
+
{
|
|
167
|
+
type: "text",
|
|
168
|
+
text: errorLines.length > 0
|
|
169
|
+
? errorLines.join("\n")
|
|
170
|
+
: "No browser errors found in the specified time period."
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
};
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
content: [
|
|
177
|
+
{
|
|
178
|
+
type: "text",
|
|
179
|
+
text: `Error getting browser errors: ${error instanceof Error ? error.message : String(error)}`
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
export { handler as GET, handler as POST };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export default function RootLayout({
|
|
4
|
+
children,
|
|
5
|
+
}: any) {
|
|
6
|
+
return (
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<title>🎭 Dev Playwright</title>
|
|
10
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
11
|
+
</head>
|
|
12
|
+
<body>{children}</body>
|
|
13
|
+
</html>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
4
|
+
import { LogEntry, LogsApiResponse, ConfigApiResponse } from '../../types';
|
|
5
|
+
|
|
6
|
+
function parseLogLine(line: string): LogEntry | null {
|
|
7
|
+
const match = line.match(/\[([^\]]+)\] \[([^\]]+)\] (.+)/);
|
|
8
|
+
if (!match) return null;
|
|
9
|
+
|
|
10
|
+
const [, timestamp, source, message] = match;
|
|
11
|
+
const screenshot = message.match(/\[SCREENSHOT\] (.+)/)?.[1];
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
timestamp,
|
|
15
|
+
source,
|
|
16
|
+
message,
|
|
17
|
+
screenshot,
|
|
18
|
+
original: line
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function LogEntryComponent({ entry }: { entry: LogEntry }) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="border-l-4 border-gray-200 pl-4 py-2">
|
|
25
|
+
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
26
|
+
<span className="font-mono">
|
|
27
|
+
{new Date(entry.timestamp).toLocaleTimeString()}
|
|
28
|
+
</span>
|
|
29
|
+
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
|
30
|
+
entry.source === 'SERVER' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'
|
|
31
|
+
}`}>
|
|
32
|
+
{entry.source}
|
|
33
|
+
</span>
|
|
34
|
+
</div>
|
|
35
|
+
<div className="mt-1 font-mono text-sm whitespace-pre-wrap">
|
|
36
|
+
{entry.message}
|
|
37
|
+
</div>
|
|
38
|
+
{entry.screenshot && (
|
|
39
|
+
<div className="mt-2">
|
|
40
|
+
<img
|
|
41
|
+
src={entry.screenshot}
|
|
42
|
+
alt="Screenshot"
|
|
43
|
+
className="max-w-full h-auto border rounded shadow-sm"
|
|
44
|
+
style={{ maxHeight: '400px' }}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface LogsClientProps {
|
|
53
|
+
version: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default function LogsClient({ version }: LogsClientProps) {
|
|
57
|
+
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
58
|
+
const [mode, setMode] = useState<'head' | 'tail'>('tail');
|
|
59
|
+
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
60
|
+
const [isLoadingNew, setIsLoadingNew] = useState(false);
|
|
61
|
+
const [lastLogCount, setLastLogCount] = useState(0);
|
|
62
|
+
const [lastFetched, setLastFetched] = useState<Date | null>(null);
|
|
63
|
+
const bottomRef = useRef<HTMLDivElement>(null);
|
|
64
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
65
|
+
const pollIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
66
|
+
|
|
67
|
+
const pollForNewLogs = async () => {
|
|
68
|
+
if (mode !== 'tail' || !isAtBottom) return;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch('/api/logs/tail?lines=1000');
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const data: LogsApiResponse = await response.json();
|
|
77
|
+
|
|
78
|
+
if (!data.logs) {
|
|
79
|
+
console.warn('No logs data in response');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const entries = data.logs
|
|
84
|
+
.split('\n')
|
|
85
|
+
.filter((line: string) => line.trim())
|
|
86
|
+
.map(parseLogLine)
|
|
87
|
+
.filter((entry: LogEntry | null): entry is LogEntry => entry !== null);
|
|
88
|
+
|
|
89
|
+
if (entries.length > lastLogCount) {
|
|
90
|
+
setIsLoadingNew(true);
|
|
91
|
+
setLastFetched(new Date());
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
setLogs(entries);
|
|
94
|
+
setLastLogCount(entries.length);
|
|
95
|
+
setIsLoadingNew(false);
|
|
96
|
+
// Auto-scroll to bottom for new content
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
99
|
+
}, 50);
|
|
100
|
+
}, 250);
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Error polling logs:', error);
|
|
104
|
+
// Don't spam console on network errors during polling
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Start/stop polling based on mode and scroll position
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (mode === 'tail' && isAtBottom) {
|
|
111
|
+
pollIntervalRef.current = setInterval(pollForNewLogs, 2000); // Poll every 2 seconds
|
|
112
|
+
return () => {
|
|
113
|
+
if (pollIntervalRef.current) {
|
|
114
|
+
clearInterval(pollIntervalRef.current);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
} else {
|
|
118
|
+
if (pollIntervalRef.current) {
|
|
119
|
+
clearInterval(pollIntervalRef.current);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}, [mode, isAtBottom, lastLogCount]);
|
|
123
|
+
|
|
124
|
+
const loadInitialLogs = async () => {
|
|
125
|
+
try {
|
|
126
|
+
const response = await fetch(`/api/logs/${mode}?lines=1000`);
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const data: LogsApiResponse = await response.json();
|
|
132
|
+
|
|
133
|
+
if (!data.logs) {
|
|
134
|
+
console.warn('No logs data in response');
|
|
135
|
+
setLogs([]);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const entries = data.logs
|
|
140
|
+
.split('\n')
|
|
141
|
+
.filter((line: string) => line.trim())
|
|
142
|
+
.map(parseLogLine)
|
|
143
|
+
.filter((entry: LogEntry | null): entry is LogEntry => entry !== null);
|
|
144
|
+
|
|
145
|
+
setLogs(entries);
|
|
146
|
+
setLastLogCount(entries.length);
|
|
147
|
+
setLastFetched(new Date());
|
|
148
|
+
|
|
149
|
+
// Auto-scroll to bottom for tail mode
|
|
150
|
+
if (mode === 'tail') {
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
bottomRef.current?.scrollIntoView({ behavior: 'auto' });
|
|
153
|
+
setIsAtBottom(true);
|
|
154
|
+
}, 100);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error('Error loading logs:', error);
|
|
158
|
+
setLogs([]);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
loadInitialLogs();
|
|
164
|
+
}, [mode]);
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
return () => {
|
|
168
|
+
if (pollIntervalRef.current) {
|
|
169
|
+
clearInterval(pollIntervalRef.current);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
const handleScroll = () => {
|
|
175
|
+
if (containerRef.current) {
|
|
176
|
+
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
|
177
|
+
const atBottom = scrollTop + clientHeight >= scrollHeight - 10;
|
|
178
|
+
setIsAtBottom(atBottom);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const filteredLogs = useMemo(() => {
|
|
183
|
+
return logs;
|
|
184
|
+
}, [logs]);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className="min-h-screen bg-gray-50">
|
|
188
|
+
<div className="bg-white shadow-sm border-b sticky top-0 z-10">
|
|
189
|
+
<div className="max-w-7xl mx-auto px-4 py-3">
|
|
190
|
+
<div className="flex items-center justify-between">
|
|
191
|
+
<div className="flex items-center gap-4">
|
|
192
|
+
<h1 className="text-2xl font-bold text-gray-900">🎯 dev3000</h1>
|
|
193
|
+
<span className="text-xs text-gray-400 ml-2">(v{version})</span>
|
|
194
|
+
<span className="text-sm text-gray-500">{logs.length} entries</span>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div className="flex items-center gap-4 text-sm">
|
|
198
|
+
{/* Mode Toggle */}
|
|
199
|
+
<div className="flex items-center bg-gray-100 rounded-md p-1">
|
|
200
|
+
<button
|
|
201
|
+
onClick={() => {
|
|
202
|
+
setMode('head');
|
|
203
|
+
// Scroll to top when switching to head mode
|
|
204
|
+
containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
205
|
+
}}
|
|
206
|
+
className={`px-3 py-1 rounded text-sm font-medium transition-colors ${
|
|
207
|
+
mode === 'head'
|
|
208
|
+
? 'bg-white text-gray-900 shadow-sm'
|
|
209
|
+
: 'text-gray-600 hover:text-gray-900'
|
|
210
|
+
}`}
|
|
211
|
+
>
|
|
212
|
+
📄 Head
|
|
213
|
+
</button>
|
|
214
|
+
<button
|
|
215
|
+
onClick={() => setMode('tail')}
|
|
216
|
+
className={`px-3 py-1 rounded text-sm font-medium transition-colors ${
|
|
217
|
+
mode === 'tail'
|
|
218
|
+
? 'bg-white text-gray-900 shadow-sm'
|
|
219
|
+
: 'text-gray-600 hover:text-gray-900'
|
|
220
|
+
}`}
|
|
221
|
+
>
|
|
222
|
+
📺 Tail
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
{/* Live indicator */}
|
|
227
|
+
<div
|
|
228
|
+
className={`flex items-center gap-1 text-green-600 ${
|
|
229
|
+
mode === 'tail' && isAtBottom ? 'visible' : 'invisible'
|
|
230
|
+
}`}
|
|
231
|
+
>
|
|
232
|
+
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
|
233
|
+
<span className="text-xs">Live</span>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<div
|
|
241
|
+
ref={containerRef}
|
|
242
|
+
className="max-w-7xl mx-auto px-4 py-6 pb-14 max-h-screen overflow-y-auto"
|
|
243
|
+
onScroll={handleScroll}
|
|
244
|
+
>
|
|
245
|
+
{filteredLogs.length === 0 ? (
|
|
246
|
+
<div className="text-center py-12">
|
|
247
|
+
<div className="text-gray-400 text-lg">📝 No logs yet</div>
|
|
248
|
+
<div className="text-gray-500 text-sm mt-2">
|
|
249
|
+
Logs will appear here as your development server runs
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
) : (
|
|
253
|
+
<div className="space-y-1">
|
|
254
|
+
{filteredLogs.map((entry, index) => (
|
|
255
|
+
<LogEntryComponent key={index} entry={entry} />
|
|
256
|
+
))}
|
|
257
|
+
<div ref={bottomRef} />
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
{/* Footer with status and scroll indicator - full width like header */}
|
|
263
|
+
<div className="py-2 border-t border-gray-200 bg-gray-50 fixed bottom-0 left-0 right-0">
|
|
264
|
+
<div className="max-w-7xl mx-auto px-4 flex items-center justify-between">
|
|
265
|
+
<div className="flex items-center">
|
|
266
|
+
{isLoadingNew && (
|
|
267
|
+
<div className="flex items-center gap-1">
|
|
268
|
+
<div className="w-3 h-3 border border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>
|
|
269
|
+
<span className="text-xs text-gray-500">Loading...</span>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
{!isLoadingNew && isAtBottom && lastFetched && (
|
|
273
|
+
<span className="text-xs text-gray-400 font-mono">
|
|
274
|
+
Last updated {lastFetched.toLocaleTimeString()}
|
|
275
|
+
</span>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
{/* Scroll to bottom button - positioned on the right */}
|
|
280
|
+
{mode === 'tail' && !isAtBottom && !isLoadingNew && (
|
|
281
|
+
<button
|
|
282
|
+
onClick={() => bottomRef.current?.scrollIntoView({ behavior: 'smooth' })}
|
|
283
|
+
className="flex items-center gap-1 px-2 py-0.5 bg-blue-600 text-white rounded text-xs hover:bg-blue-700"
|
|
284
|
+
>
|
|
285
|
+
↓ Live updates
|
|
286
|
+
</button>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export default function HomePage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
6
|
+
<a
|
|
7
|
+
href="https://github.com/vercel-labs/dev3000"
|
|
8
|
+
target="_blank"
|
|
9
|
+
rel="noopener noreferrer"
|
|
10
|
+
className="fixed top-4 right-4 text-gray-600 hover:text-gray-900 transition-colors"
|
|
11
|
+
title="View on GitHub"
|
|
12
|
+
>
|
|
13
|
+
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
|
14
|
+
<path d="M12 0C5.374 0 0 5.373 0 12 0 17.302 3.438 21.8 8.207 23.387c.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/>
|
|
15
|
+
</svg>
|
|
16
|
+
</a>
|
|
17
|
+
<div className="bg-white rounded-lg shadow-lg p-8 max-w-md w-full">
|
|
18
|
+
<h1 className="text-3xl font-bold text-gray-900 mb-6 text-center">
|
|
19
|
+
🎯 dev3000
|
|
20
|
+
</h1>
|
|
21
|
+
|
|
22
|
+
<div className="space-y-4">
|
|
23
|
+
<a
|
|
24
|
+
href="/logs"
|
|
25
|
+
className="block w-full bg-blue-500 text-white text-center py-3 px-4 rounded hover:bg-blue-600 transition-colors"
|
|
26
|
+
>
|
|
27
|
+
📊 View Development Logs
|
|
28
|
+
</a>
|
|
29
|
+
|
|
30
|
+
<a
|
|
31
|
+
href="/api/mcp/http"
|
|
32
|
+
className="block w-full bg-green-500 text-white text-center py-3 px-4 rounded hover:bg-green-600 transition-colors"
|
|
33
|
+
>
|
|
34
|
+
🤖 MCP Endpoint
|
|
35
|
+
</a>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div className="mt-6 text-sm text-gray-600 text-center">
|
|
39
|
+
<p>Real-time development monitoring with visual context</p>
|
|
40
|
+
<p className="mt-2">Server logs + Browser events + Screenshots</p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|