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.
@@ -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,7 @@
1
+ import LogsClient from './LogsClient';
2
+
3
+ export default function LogsPage() {
4
+ const version = process.env.DEV_PLAYWRIGHT_VERSION || '0.0.0';
5
+
6
+ return <LogsClient version={version} />;
7
+ }
@@ -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
+ }