nitrostack 1.0.22 → 1.0.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitrostack",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "description": "NitroStack - Build powerful MCP servers with TypeScript",
5
5
  "type": "module",
6
6
  "main": "dist/core/index.js",
@@ -0,0 +1,279 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useRef } from 'react';
4
+ import { Terminal, Download, Copy, Trash2, Play, Pause, Filter } from 'lucide-react';
5
+
6
+ interface LogEntry {
7
+ timestamp: string;
8
+ level: 'info' | 'error' | 'warn' | 'debug';
9
+ message: string;
10
+ data?: any;
11
+ }
12
+
13
+ export default function LogsPage() {
14
+ const [logs, setLogs] = useState<LogEntry[]>([]);
15
+ const [isStreaming, setIsStreaming] = useState(true);
16
+ const [autoScroll, setAutoScroll] = useState(true);
17
+ const [filter, setFilter] = useState<string>('all');
18
+ const [error, setError] = useState<string | null>(null);
19
+ const logsEndRef = useRef<HTMLDivElement>(null);
20
+ const eventSourceRef = useRef<EventSource | null>(null);
21
+
22
+ // Auto-scroll to bottom
23
+ useEffect(() => {
24
+ if (autoScroll && logsEndRef.current) {
25
+ logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
26
+ }
27
+ }, [logs, autoScroll]);
28
+
29
+ // Connect to SSE stream
30
+ useEffect(() => {
31
+ if (!isStreaming) return;
32
+
33
+ const eventSource = new EventSource('http://localhost:3004/mcp-logs');
34
+ eventSourceRef.current = eventSource;
35
+
36
+ eventSource.onopen = () => {
37
+ setError(null);
38
+ };
39
+
40
+ eventSource.onmessage = (event) => {
41
+ try {
42
+ const { level, message, timestamp, ...data } = JSON.parse(event.data);
43
+ const log: LogEntry = { level, message, timestamp, data };
44
+ setLogs((prev) => [...prev, log]);
45
+ } catch (error) {
46
+ console.error('Failed to parse log:', error);
47
+ }
48
+ };
49
+
50
+ eventSource.onerror = (error) => {
51
+ console.error('SSE error:', error);
52
+ setError('Failed to connect to log stream. Is the MCP server running?');
53
+ // Don't close the event source here, it will try to reconnect automatically
54
+ };
55
+
56
+ return () => {
57
+ eventSource.close();
58
+ };
59
+ }, [isStreaming]);
60
+
61
+ const clearLogs = () => {
62
+ setLogs([]);
63
+ };
64
+
65
+ const downloadLogs = () => {
66
+ const logsText = logs.map(log =>
67
+ `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}${log.data ? '\n' + JSON.stringify(log.data, null, 2) : ''}`
68
+ ).join('\n\n');
69
+
70
+ const blob = new Blob([logsText], { type: 'text/plain' });
71
+ const url = URL.createObjectURL(blob);
72
+ const a = document.createElement('a');
73
+ a.href = url;
74
+ a.download = `nitrostack-logs-${new Date().toISOString()}.txt`;
75
+ a.click();
76
+ URL.revokeObjectURL(url);
77
+ };
78
+
79
+ const copyLogs = async () => {
80
+ const logsText = logs.map(log =>
81
+ `[${log.timestamp}] [${log.level.toUpperCase()}] ${log.message}${log.data ? '\n' + JSON.stringify(log.data, null, 2) : ''}`
82
+ ).join('\n\n');
83
+
84
+ await navigator.clipboard.writeText(logsText);
85
+ // Could add a toast notification here
86
+ };
87
+
88
+ const toggleStreaming = () => {
89
+ setIsStreaming(!isStreaming);
90
+ };
91
+
92
+ const filteredLogs = filter === 'all'
93
+ ? logs
94
+ : logs.filter(log => log.level === filter);
95
+
96
+ const getLevelColor = (level: string) => {
97
+ switch (level) {
98
+ case 'error': return 'text-red-400';
99
+ case 'warn': return 'text-yellow-400';
100
+ case 'debug': return 'text-blue-400';
101
+ default: return 'text-emerald-400';
102
+ }
103
+ };
104
+
105
+ const getLevelBg = (level: string) => {
106
+ switch (level) {
107
+ case 'error': return 'bg-red-500/10 border-red-500/20';
108
+ case 'warn': return 'bg-yellow-500/10 border-yellow-500/20';
109
+ case 'debug': return 'bg-blue-500/10 border-blue-500/20';
110
+ default: return 'bg-emerald-500/10 border-emerald-500/20';
111
+ }
112
+ };
113
+
114
+ const formatData = (data: any) => {
115
+ try {
116
+ return JSON.stringify(data, null, 2);
117
+ } catch {
118
+ return String(data);
119
+ }
120
+ };
121
+
122
+ return (
123
+ <div className="flex flex-col h-screen bg-black">
124
+ {/* Header */}
125
+ <div className="flex items-center justify-between px-6 py-4 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 border-b border-slate-700">
126
+ <div className="flex items-center gap-3">
127
+ <div className="p-2 rounded-lg bg-emerald-500/10 border border-emerald-500/20">
128
+ <Terminal className="w-5 h-5 text-emerald-400" />
129
+ </div>
130
+ <div>
131
+ <h1 className="text-xl font-bold text-white">Server Logs</h1>
132
+ <p className="text-sm text-slate-400">Real-time MCP server logging</p>
133
+ </div>
134
+ </div>
135
+
136
+ <div className="flex items-center gap-2">
137
+ {/* Filter */}
138
+ <select
139
+ value={filter}
140
+ onChange={(e) => setFilter(e.target.value)}
141
+ className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white focus:outline-none focus:ring-2 focus:ring-emerald-500"
142
+ >
143
+ <option value="all">All Levels</option>
144
+ <option value="info">Info</option>
145
+ <option value="warn">Warnings</option>
146
+ <option value="error">Errors</option>
147
+ <option value="debug">Debug</option>
148
+ </select>
149
+
150
+ {/* Auto-scroll toggle */}
151
+ <button
152
+ onClick={() => setAutoScroll(!autoScroll)}
153
+ className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
154
+ autoScroll
155
+ ? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
156
+ : 'bg-slate-800 text-slate-400 border border-slate-700 hover:bg-slate-700'
157
+ }`}
158
+ >
159
+ Auto-scroll
160
+ </button>
161
+
162
+ {/* Streaming toggle */}
163
+ <button
164
+ onClick={toggleStreaming}
165
+ className={`p-2 rounded-lg transition-colors ${
166
+ isStreaming
167
+ ? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
168
+ : 'bg-slate-800 text-slate-400 border border-slate-700 hover:bg-slate-700'
169
+ }`}
170
+ title={isStreaming ? 'Pause streaming' : 'Resume streaming'}
171
+ >
172
+ {isStreaming ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
173
+ </button>
174
+
175
+ {/* Copy */}
176
+ <button
177
+ onClick={copyLogs}
178
+ className="p-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-400 hover:bg-slate-700 hover:text-white transition-colors"
179
+ title="Copy logs"
180
+ >
181
+ <Copy className="w-4 h-4" />
182
+ </button>
183
+
184
+ {/* Download */}
185
+ <button
186
+ onClick={downloadLogs}
187
+ className="p-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-400 hover:bg-slate-700 hover:text-white transition-colors"
188
+ title="Download logs"
189
+ >
190
+ <Download className="w-4 h-4" />
191
+ </button>
192
+
193
+ {/* Clear */}
194
+ <button
195
+ onClick={clearLogs}
196
+ className="p-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-400 hover:bg-red-900/50 hover:text-red-400 hover:border-red-500/30 transition-colors"
197
+ title="Clear logs"
198
+ >
199
+ <Trash2 className="w-4 h-4" />
200
+ </button>
201
+ </div>
202
+ </div>
203
+
204
+ {/* Error Display */}
205
+ {error && (
206
+ <div className="bg-red-500/10 text-red-400 px-6 py-3 border-b border-red-500/20 text-sm">
207
+ <strong>Connection Error:</strong> {error}
208
+ </div>
209
+ )}
210
+
211
+ {/* Stats Bar */}
212
+ <div className="flex items-center gap-4 px-6 py-3 bg-slate-900/50 border-b border-slate-800">
213
+ <div className="flex items-center gap-2 text-sm">
214
+ <span className="text-slate-400">Total:</span>
215
+ <span className="font-mono font-bold text-white">{logs.length}</span>
216
+ </div>
217
+ <div className="flex items-center gap-2 text-sm">
218
+ <span className="text-slate-400">Filtered:</span>
219
+ <span className="font-mono font-bold text-white">{filteredLogs.length}</span>
220
+ </div>
221
+ <div className="flex items-center gap-2 text-sm">
222
+ <div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
223
+ <span className="text-slate-400">
224
+ {isStreaming ? 'Streaming' : 'Paused'}
225
+ </span>
226
+ </div>
227
+ </div>
228
+
229
+ {/* Logs Container */}
230
+ <div className="flex-1 overflow-y-auto bg-black font-mono text-sm">
231
+ {filteredLogs.length === 0 ? (
232
+ <div className="flex flex-col items-center justify-center h-full text-slate-600">
233
+ <Terminal className="w-16 h-16 mb-4 opacity-20" />
234
+ <p className="text-lg font-medium">No logs yet</p>
235
+ <p className="text-sm">Logs will appear here as they are generated</p>
236
+ </div>
237
+ ) : (
238
+ <div className="p-4 space-y-2">
239
+ {filteredLogs.map((log, index) => (
240
+ <div
241
+ key={index}
242
+ className={`p-3 rounded-lg border ${getLevelBg(log.level)} hover:border-opacity-50 transition-all`}
243
+ >
244
+ <div className="flex items-start gap-3">
245
+ {/* Timestamp */}
246
+ <span className="text-slate-500 text-xs whitespace-nowrap">
247
+ {log.timestamp}
248
+ </span>
249
+
250
+ {/* Level Badge */}
251
+ <span
252
+ className={`px-2 py-0.5 rounded text-xs font-bold uppercase ${getLevelColor(log.level)}`}
253
+ >
254
+ {log.level}
255
+ </span>
256
+
257
+ {/* Message */}
258
+ <div className="flex-1">
259
+ <p className="text-white break-words">{log.message}</p>
260
+
261
+ {/* Data (if present) */}
262
+ {log.data && (
263
+ <pre className="mt-2 p-3 bg-black/50 rounded border border-slate-800 overflow-x-auto">
264
+ <code className="text-xs text-slate-300">
265
+ {formatData(log.data)}
266
+ </code>
267
+ </pre>
268
+ )}
269
+ </div>
270
+ </div>
271
+ </div>
272
+ ))}
273
+ <div ref={logsEndRef} />
274
+ </div>
275
+ )}
276
+ </div>
277
+ </div>
278
+ );
279
+ }