@appkit/llamacpp-cli 1.12.0 → 1.13.0
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 +294 -168
- package/dist/cli.js +35 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/launch/claude.d.ts +6 -0
- package/dist/commands/launch/claude.d.ts.map +1 -0
- package/dist/commands/launch/claude.js +277 -0
- package/dist/commands/launch/claude.js.map +1 -0
- package/dist/lib/integration-checker.d.ts +26 -0
- package/dist/lib/integration-checker.d.ts.map +1 -0
- package/dist/lib/integration-checker.js +77 -0
- package/dist/lib/integration-checker.js.map +1 -0
- package/dist/lib/router-manager.d.ts +4 -0
- package/dist/lib/router-manager.d.ts.map +1 -1
- package/dist/lib/router-manager.js +10 -0
- package/dist/lib/router-manager.js.map +1 -1
- package/dist/lib/router-server.d.ts +13 -0
- package/dist/lib/router-server.d.ts.map +1 -1
- package/dist/lib/router-server.js +267 -7
- package/dist/lib/router-server.js.map +1 -1
- package/dist/types/integration-config.d.ts +28 -0
- package/dist/types/integration-config.d.ts.map +1 -0
- package/dist/types/integration-config.js +3 -0
- package/dist/types/integration-config.js.map +1 -0
- package/package.json +10 -2
- package/web/dist/assets/index-Bin89Lwr.css +1 -0
- package/web/dist/assets/index-CVmonw3T.js +17 -0
- package/web/{index.html → dist/index.html} +2 -1
- package/.versionrc.json +0 -16
- package/CHANGELOG.md +0 -213
- package/docs/images/.gitkeep +0 -1
- package/docs/images/web-ui-servers.png +0 -0
- package/src/cli.ts +0 -523
- package/src/commands/admin/config.ts +0 -121
- package/src/commands/admin/logs.ts +0 -91
- package/src/commands/admin/restart.ts +0 -26
- package/src/commands/admin/start.ts +0 -27
- package/src/commands/admin/status.ts +0 -84
- package/src/commands/admin/stop.ts +0 -16
- package/src/commands/config-global.ts +0 -38
- package/src/commands/config.ts +0 -323
- package/src/commands/create.ts +0 -183
- package/src/commands/delete.ts +0 -74
- package/src/commands/list.ts +0 -37
- package/src/commands/logs-all.ts +0 -251
- package/src/commands/logs.ts +0 -345
- package/src/commands/monitor.ts +0 -110
- package/src/commands/ps.ts +0 -84
- package/src/commands/pull.ts +0 -44
- package/src/commands/rm.ts +0 -107
- package/src/commands/router/config.ts +0 -116
- package/src/commands/router/logs.ts +0 -256
- package/src/commands/router/restart.ts +0 -36
- package/src/commands/router/start.ts +0 -60
- package/src/commands/router/status.ts +0 -119
- package/src/commands/router/stop.ts +0 -33
- package/src/commands/run.ts +0 -233
- package/src/commands/search.ts +0 -107
- package/src/commands/server-show.ts +0 -161
- package/src/commands/show.ts +0 -207
- package/src/commands/start.ts +0 -101
- package/src/commands/stop.ts +0 -39
- package/src/commands/tui.ts +0 -25
- package/src/lib/admin-manager.ts +0 -435
- package/src/lib/admin-server.ts +0 -1243
- package/src/lib/config-generator.ts +0 -130
- package/src/lib/download-job-manager.ts +0 -213
- package/src/lib/history-manager.ts +0 -172
- package/src/lib/launchctl-manager.ts +0 -225
- package/src/lib/metrics-aggregator.ts +0 -257
- package/src/lib/model-downloader.ts +0 -328
- package/src/lib/model-scanner.ts +0 -157
- package/src/lib/model-search.ts +0 -114
- package/src/lib/models-dir-setup.ts +0 -46
- package/src/lib/port-manager.ts +0 -80
- package/src/lib/router-logger.ts +0 -201
- package/src/lib/router-manager.ts +0 -414
- package/src/lib/router-server.ts +0 -538
- package/src/lib/state-manager.ts +0 -206
- package/src/lib/status-checker.ts +0 -113
- package/src/lib/system-collector.ts +0 -315
- package/src/tui/ConfigApp.ts +0 -1085
- package/src/tui/HistoricalMonitorApp.ts +0 -587
- package/src/tui/ModelsApp.ts +0 -368
- package/src/tui/MonitorApp.ts +0 -386
- package/src/tui/MultiServerMonitorApp.ts +0 -1833
- package/src/tui/RootNavigator.ts +0 -74
- package/src/tui/SearchApp.ts +0 -511
- package/src/tui/SplashScreen.ts +0 -149
- package/src/types/admin-config.ts +0 -25
- package/src/types/global-config.ts +0 -26
- package/src/types/history-types.ts +0 -39
- package/src/types/model-info.ts +0 -8
- package/src/types/monitor-types.ts +0 -162
- package/src/types/router-config.ts +0 -25
- package/src/types/server-config.ts +0 -46
- package/src/utils/downsample-utils.ts +0 -128
- package/src/utils/file-utils.ts +0 -146
- package/src/utils/format-utils.ts +0 -98
- package/src/utils/log-parser.ts +0 -284
- package/src/utils/log-utils.ts +0 -178
- package/src/utils/process-utils.ts +0 -316
- package/src/utils/prompt-utils.ts +0 -47
- package/test-load.sh +0 -100
- package/tsconfig.json +0 -20
- package/web/eslint.config.js +0 -23
- package/web/llamacpp-web-dist.tar.gz +0 -0
- package/web/package-lock.json +0 -4017
- package/web/package.json +0 -38
- package/web/postcss.config.js +0 -6
- package/web/src/App.css +0 -42
- package/web/src/App.tsx +0 -86
- package/web/src/assets/react.svg +0 -1
- package/web/src/components/ApiKeyPrompt.tsx +0 -71
- package/web/src/components/CreateServerModal.tsx +0 -372
- package/web/src/components/DownloadProgress.tsx +0 -123
- package/web/src/components/Nav.tsx +0 -89
- package/web/src/components/RouterConfigModal.tsx +0 -240
- package/web/src/components/SearchModal.tsx +0 -306
- package/web/src/components/ServerConfigModal.tsx +0 -291
- package/web/src/hooks/useApi.ts +0 -259
- package/web/src/index.css +0 -42
- package/web/src/lib/api.ts +0 -226
- package/web/src/main.tsx +0 -10
- package/web/src/pages/Dashboard.tsx +0 -103
- package/web/src/pages/Models.tsx +0 -258
- package/web/src/pages/Router.tsx +0 -270
- package/web/src/pages/RouterLogs.tsx +0 -201
- package/web/src/pages/ServerLogs.tsx +0 -553
- package/web/src/pages/Servers.tsx +0 -358
- package/web/src/types/api.ts +0 -140
- package/web/tailwind.config.js +0 -31
- package/web/tsconfig.app.json +0 -28
- package/web/tsconfig.json +0 -7
- package/web/tsconfig.node.json +0 -26
- package/web/vite.config.ts +0 -25
- /package/web/{public → dist}/vite.svg +0 -0
|
@@ -1,553 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef } from 'react';
|
|
2
|
-
import { useParams, useNavigate, Link } from 'react-router-dom';
|
|
3
|
-
import { ArrowLeft, Loader2, ChevronDown, Trash2 } from 'lucide-react';
|
|
4
|
-
import { useServerLogs, useServer } from '../hooks/useApi';
|
|
5
|
-
|
|
6
|
-
type LogFilter = 'all' | 'requests' | 'errors' | 'warnings' | 'system';
|
|
7
|
-
type LogSort = 'newest' | 'oldest';
|
|
8
|
-
type ViewMode = 'formatted' | 'raw';
|
|
9
|
-
|
|
10
|
-
interface ParsedLogLine {
|
|
11
|
-
raw: string;
|
|
12
|
-
formatted?: string;
|
|
13
|
-
timestamp?: string;
|
|
14
|
-
type: 'request' | 'error' | 'warning' | 'system';
|
|
15
|
-
isHealthCheck?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Health check endpoints to filter by default
|
|
19
|
-
const HEALTH_CHECK_ENDPOINTS = ['/health', '/slots', '/props'];
|
|
20
|
-
|
|
21
|
-
// Cache for timestamps - maps raw log content to its assigned timestamp
|
|
22
|
-
const timestampCache = new Map<string, string>();
|
|
23
|
-
|
|
24
|
-
// Generate a current timestamp
|
|
25
|
-
function getCurrentTimestamp(): string {
|
|
26
|
-
const now = new Date();
|
|
27
|
-
return now.toISOString().substring(0, 19).replace('T', ' ');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Log parser utilities - returns cached timestamp or generates new one
|
|
31
|
-
function extractTimestamp(line: string, rawKey: string): string {
|
|
32
|
-
// Try bracket format: [2025-12-09 10:13:45]
|
|
33
|
-
const bracketMatch = line.match(/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
|
|
34
|
-
if (bracketMatch) return bracketMatch[1];
|
|
35
|
-
|
|
36
|
-
// Try plain format at start of line: 2025-12-09 10:13:45
|
|
37
|
-
const plainMatch = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
|
|
38
|
-
if (plainMatch) return plainMatch[1];
|
|
39
|
-
|
|
40
|
-
// Check cache for previously generated timestamp
|
|
41
|
-
const cached = timestampCache.get(rawKey);
|
|
42
|
-
if (cached) return cached;
|
|
43
|
-
|
|
44
|
-
// Generate and cache new timestamp
|
|
45
|
-
const newTimestamp = getCurrentTimestamp();
|
|
46
|
-
timestampCache.set(rawKey, newTimestamp);
|
|
47
|
-
return newTimestamp;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function extractJson(line: string): any {
|
|
51
|
-
const jsonStart = line.indexOf('{');
|
|
52
|
-
if (jsonStart === -1) return null;
|
|
53
|
-
try {
|
|
54
|
-
return JSON.parse(line.substring(jsonStart));
|
|
55
|
-
} catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function extractUserMessage(requestJson: any): string {
|
|
61
|
-
const messages = requestJson.messages || [];
|
|
62
|
-
const userMsg = messages.find((m: any) => m.role === 'user');
|
|
63
|
-
if (!userMsg || !userMsg.content) return '';
|
|
64
|
-
|
|
65
|
-
let content: string;
|
|
66
|
-
if (Array.isArray(userMsg.content)) {
|
|
67
|
-
const textPart = userMsg.content.find((p: any) => p.type === 'text');
|
|
68
|
-
content = textPart?.text || '';
|
|
69
|
-
} else {
|
|
70
|
-
content = userMsg.content;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
content = content.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
74
|
-
return content.length > 50 ? content.substring(0, 47) + '...' : content;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function extractResponseTime(responseJson: any): number {
|
|
78
|
-
const verboseTimings = responseJson.__verbose?.timings;
|
|
79
|
-
if (verboseTimings) {
|
|
80
|
-
return Math.round((verboseTimings.prompt_ms || 0) + (verboseTimings.predicted_ms || 0));
|
|
81
|
-
}
|
|
82
|
-
const timings = responseJson.timings;
|
|
83
|
-
if (timings) {
|
|
84
|
-
return Math.round((timings.prompt_ms || 0) + (timings.predicted_ms || 0));
|
|
85
|
-
}
|
|
86
|
-
return 0;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function parseLogsToFormatted(lines: string[]): ParsedLogLine[] {
|
|
90
|
-
const results: ParsedLogLine[] = [];
|
|
91
|
-
let buffer: string[] = [];
|
|
92
|
-
let isBuffering = false;
|
|
93
|
-
|
|
94
|
-
const isRequestStatusLine = (line: string): boolean => {
|
|
95
|
-
return (
|
|
96
|
-
(line.includes('log_server_r: request:') || line.includes('log_server_r: done request:')) &&
|
|
97
|
-
!line.includes('{') &&
|
|
98
|
-
/(?:done )?request: (POST|GET|PUT|DELETE)/.test(line)
|
|
99
|
-
);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const consolidateRequest = (bufferedLines: string[]): ParsedLogLine | null => {
|
|
103
|
-
try {
|
|
104
|
-
const firstLine = bufferedLines[0];
|
|
105
|
-
const rawKey = bufferedLines.join('\n'); // Use full request as cache key
|
|
106
|
-
const timestamp = extractTimestamp(firstLine, rawKey);
|
|
107
|
-
const requestMatch = firstLine.match(/(?:done )?request: (POST|GET|PUT|DELETE) (\/[^\s]+) ([^\s]+) (\d+)/);
|
|
108
|
-
if (!requestMatch) return null;
|
|
109
|
-
|
|
110
|
-
const [, method, endpoint, ip, status] = requestMatch;
|
|
111
|
-
|
|
112
|
-
// Parse request JSON
|
|
113
|
-
const requestLine = bufferedLines.find(l => l.includes('log_server_r: request:') && l.includes('{'));
|
|
114
|
-
let userMessage = '';
|
|
115
|
-
if (requestLine) {
|
|
116
|
-
const requestJson = extractJson(requestLine);
|
|
117
|
-
if (requestJson) {
|
|
118
|
-
userMessage = extractUserMessage(requestJson);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Parse response JSON
|
|
123
|
-
const responseLine = bufferedLines.find(l => l.includes('log_server_r: response:'));
|
|
124
|
-
let tokensIn = 0;
|
|
125
|
-
let tokensOut = 0;
|
|
126
|
-
let responseTimeMs = 0;
|
|
127
|
-
|
|
128
|
-
if (responseLine) {
|
|
129
|
-
const responseJson = extractJson(responseLine);
|
|
130
|
-
if (responseJson) {
|
|
131
|
-
tokensIn = responseJson.usage?.prompt_tokens || 0;
|
|
132
|
-
tokensOut = responseJson.usage?.completion_tokens || 0;
|
|
133
|
-
responseTimeMs = extractResponseTime(responseJson);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const formatted = `${timestamp} ${method} ${endpoint} ${ip} ${status} "${userMessage}" ${tokensIn} ${tokensOut} ${responseTimeMs}`;
|
|
138
|
-
|
|
139
|
-
// Check if this is a health check request
|
|
140
|
-
const isHealthCheck = HEALTH_CHECK_ENDPOINTS.some(ep => endpoint === ep);
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
raw: bufferedLines.join('\n'),
|
|
144
|
-
formatted,
|
|
145
|
-
timestamp,
|
|
146
|
-
type: 'request',
|
|
147
|
-
isHealthCheck,
|
|
148
|
-
};
|
|
149
|
-
} catch {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const parseSimpleFormat = (line: string): ParsedLogLine | null => {
|
|
155
|
-
try {
|
|
156
|
-
const timestamp = extractTimestamp(line, line); // Use line as cache key
|
|
157
|
-
const requestMatch = line.match(/(?:done )?request: (POST|GET|PUT|DELETE) ([^\s]+) ([^\s]+) (\d+)/);
|
|
158
|
-
if (!requestMatch) return null;
|
|
159
|
-
|
|
160
|
-
const [, method, endpoint, ip, status] = requestMatch;
|
|
161
|
-
const formatted = `${timestamp} ${method} ${endpoint} ${ip} ${status} "" 0 0 0`;
|
|
162
|
-
|
|
163
|
-
// Check if this is a health check request
|
|
164
|
-
const isHealthCheck = HEALTH_CHECK_ENDPOINTS.some(ep => endpoint === ep);
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
raw: line,
|
|
168
|
-
formatted,
|
|
169
|
-
timestamp,
|
|
170
|
-
type: 'request',
|
|
171
|
-
isHealthCheck,
|
|
172
|
-
};
|
|
173
|
-
} catch {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const parseNonRequestLine = (line: string): ParsedLogLine => {
|
|
179
|
-
const trimmed = line.trim();
|
|
180
|
-
|
|
181
|
-
// Error patterns
|
|
182
|
-
if (trimmed.toLowerCase().includes('error') ||
|
|
183
|
-
trimmed.toLowerCase().includes('failed') ||
|
|
184
|
-
trimmed.toLowerCase().includes('exception') ||
|
|
185
|
-
trimmed.includes('ERR') ||
|
|
186
|
-
trimmed.includes('FATAL')) {
|
|
187
|
-
return { raw: line, type: 'error' };
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Warning patterns
|
|
191
|
-
if (trimmed.toLowerCase().includes('warn') ||
|
|
192
|
-
trimmed.toLowerCase().includes('warning') ||
|
|
193
|
-
trimmed.includes('WARN')) {
|
|
194
|
-
return { raw: line, type: 'warning' };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return { raw: line, type: 'system' };
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
for (const line of lines) {
|
|
201
|
-
if (isRequestStatusLine(line)) {
|
|
202
|
-
if (isBuffering) {
|
|
203
|
-
// Process previous buffer
|
|
204
|
-
const result = consolidateRequest(buffer);
|
|
205
|
-
if (result) results.push(result);
|
|
206
|
-
buffer = [];
|
|
207
|
-
isBuffering = false;
|
|
208
|
-
}
|
|
209
|
-
isBuffering = true;
|
|
210
|
-
buffer = [line];
|
|
211
|
-
} else if (isBuffering) {
|
|
212
|
-
buffer.push(line);
|
|
213
|
-
if (line.includes('log_server_r: response:')) {
|
|
214
|
-
const result = consolidateRequest(buffer);
|
|
215
|
-
if (result) results.push(result);
|
|
216
|
-
buffer = [];
|
|
217
|
-
isBuffering = false;
|
|
218
|
-
}
|
|
219
|
-
} else {
|
|
220
|
-
// Non-buffered line (not part of a request)
|
|
221
|
-
const trimmed = line.trim();
|
|
222
|
-
if (trimmed && !trimmed.includes('log_server_r')) {
|
|
223
|
-
results.push(parseNonRequestLine(line));
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Flush remaining buffer (simple format without response)
|
|
229
|
-
if (isBuffering && buffer.length === 1) {
|
|
230
|
-
const result = parseSimpleFormat(buffer[0]);
|
|
231
|
-
if (result) results.push(result);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return results;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function ServerLogs() {
|
|
238
|
-
const { id } = useParams<{ id: string }>();
|
|
239
|
-
const navigate = useNavigate();
|
|
240
|
-
|
|
241
|
-
const [activeFilter, setActiveFilter] = useState<LogFilter>('all');
|
|
242
|
-
const [sortOrder, setSortOrder] = useState<LogSort>('newest');
|
|
243
|
-
const [viewMode, setViewMode] = useState<ViewMode>(() => {
|
|
244
|
-
const saved = localStorage.getItem('serverLogs.viewMode');
|
|
245
|
-
return (saved === 'raw' || saved === 'formatted') ? saved : 'formatted';
|
|
246
|
-
});
|
|
247
|
-
const [showSortDropdown, setShowSortDropdown] = useState(false);
|
|
248
|
-
const [autoScroll, setAutoScroll] = useState(true);
|
|
249
|
-
const [showHealthChecks, setShowHealthChecks] = useState(false);
|
|
250
|
-
|
|
251
|
-
// Persist viewMode to localStorage
|
|
252
|
-
useEffect(() => {
|
|
253
|
-
localStorage.setItem('serverLogs.viewMode', viewMode);
|
|
254
|
-
}, [viewMode]);
|
|
255
|
-
|
|
256
|
-
const logContainerRef = useRef<HTMLDivElement>(null);
|
|
257
|
-
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
258
|
-
|
|
259
|
-
const { data: serverData, isLoading: serverLoading } = useServer(id || '');
|
|
260
|
-
// Fetch many lines since verbose logs have lots of non-request output
|
|
261
|
-
// CLI uses lines * 100 multiplier, we use 50000 to get more parsed request lines
|
|
262
|
-
const { data: logsData, isLoading: logsLoading } = useServerLogs(id || null, 50000);
|
|
263
|
-
|
|
264
|
-
const server = serverData?.server;
|
|
265
|
-
|
|
266
|
-
// Auto-scroll to bottom when new logs arrive
|
|
267
|
-
useEffect(() => {
|
|
268
|
-
if (autoScroll && logContainerRef.current) {
|
|
269
|
-
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
|
|
270
|
-
}
|
|
271
|
-
}, [logsData, autoScroll, sortOrder]);
|
|
272
|
-
|
|
273
|
-
// Close dropdown on outside click
|
|
274
|
-
useEffect(() => {
|
|
275
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
276
|
-
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
|
277
|
-
setShowSortDropdown(false);
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
281
|
-
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
282
|
-
}, []);
|
|
283
|
-
|
|
284
|
-
const getFilteredLogs = (): { logs: ParsedLogLine[]; hasRawLogs: boolean; formattedCount: number } => {
|
|
285
|
-
if (!logsData) return { logs: [], hasRawLogs: false, formattedCount: 0 };
|
|
286
|
-
|
|
287
|
-
// Combine stdout and stderr
|
|
288
|
-
const allLines = [
|
|
289
|
-
...(logsData.stderr || '').split('\n'),
|
|
290
|
-
...(logsData.stdout || '').split('\n'),
|
|
291
|
-
].filter(line => line.trim());
|
|
292
|
-
|
|
293
|
-
// Parse logs
|
|
294
|
-
const parsed = parseLogsToFormatted(allLines);
|
|
295
|
-
|
|
296
|
-
// Track counts for empty state messaging
|
|
297
|
-
const hasRawLogs = allLines.length > 0;
|
|
298
|
-
const formattedCount = parsed.filter(log => log.formatted && !log.isHealthCheck).length;
|
|
299
|
-
|
|
300
|
-
// In formatted view, only show request logs (lines with formatted output)
|
|
301
|
-
let filtered = parsed;
|
|
302
|
-
if (viewMode === 'formatted') {
|
|
303
|
-
filtered = parsed.filter(log => log.formatted);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Filter out health check requests unless toggle is enabled
|
|
307
|
-
if (!showHealthChecks) {
|
|
308
|
-
filtered = filtered.filter(log => !log.isHealthCheck);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Apply filter
|
|
312
|
-
if (activeFilter !== 'all') {
|
|
313
|
-
filtered = filtered.filter(log => {
|
|
314
|
-
switch (activeFilter) {
|
|
315
|
-
case 'requests':
|
|
316
|
-
return log.type === 'request';
|
|
317
|
-
case 'errors':
|
|
318
|
-
return log.type === 'error';
|
|
319
|
-
case 'warnings':
|
|
320
|
-
return log.type === 'warning';
|
|
321
|
-
case 'system':
|
|
322
|
-
return log.type === 'system';
|
|
323
|
-
default:
|
|
324
|
-
return true;
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Apply sort
|
|
330
|
-
if (sortOrder === 'oldest') {
|
|
331
|
-
return { logs: filtered, hasRawLogs, formattedCount };
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return { logs: [...filtered].reverse(), hasRawLogs, formattedCount };
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
const { logs: filteredLogs, hasRawLogs, formattedCount } = getFilteredLogs();
|
|
338
|
-
|
|
339
|
-
const filters: { id: LogFilter; label: string }[] = [
|
|
340
|
-
{ id: 'all', label: 'All' },
|
|
341
|
-
{ id: 'requests', label: 'Requests' },
|
|
342
|
-
{ id: 'errors', label: 'Errors' },
|
|
343
|
-
{ id: 'warnings', label: 'Warnings' },
|
|
344
|
-
{ id: 'system', label: 'System' },
|
|
345
|
-
];
|
|
346
|
-
|
|
347
|
-
const getLogTypeColor = (type: string): string => {
|
|
348
|
-
switch (type) {
|
|
349
|
-
case 'request':
|
|
350
|
-
return 'text-blue-400';
|
|
351
|
-
case 'error':
|
|
352
|
-
return 'text-red-400';
|
|
353
|
-
case 'warning':
|
|
354
|
-
return 'text-yellow-400';
|
|
355
|
-
default:
|
|
356
|
-
return 'text-gray-400';
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
if (serverLoading) {
|
|
361
|
-
return (
|
|
362
|
-
<div className="max-w-6xl mx-auto px-4 py-12">
|
|
363
|
-
<p className="text-gray-500 text-center">Loading...</p>
|
|
364
|
-
</div>
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (!server) {
|
|
369
|
-
return (
|
|
370
|
-
<div className="max-w-6xl mx-auto px-4 py-12">
|
|
371
|
-
<p className="text-gray-500 text-center">Server not found</p>
|
|
372
|
-
<div className="text-center mt-4">
|
|
373
|
-
<Link
|
|
374
|
-
to="/servers"
|
|
375
|
-
className="text-sm text-blue-600 hover:text-blue-800"
|
|
376
|
-
>
|
|
377
|
-
Back to Servers
|
|
378
|
-
</Link>
|
|
379
|
-
</div>
|
|
380
|
-
</div>
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return (
|
|
385
|
-
<div className="h-[calc(100vh-56px)] flex flex-col">
|
|
386
|
-
{/* Header */}
|
|
387
|
-
<div className="flex items-center px-4 py-3 border-b border-gray-200 bg-white">
|
|
388
|
-
<div className="flex items-center gap-3">
|
|
389
|
-
<button
|
|
390
|
-
onClick={() => navigate('/servers')}
|
|
391
|
-
className="p-1.5 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
|
392
|
-
>
|
|
393
|
-
<ArrowLeft className="w-5 h-5" />
|
|
394
|
-
</button>
|
|
395
|
-
<div>
|
|
396
|
-
<h1 className="text-lg font-semibold text-gray-900">Server Logs</h1>
|
|
397
|
-
<p className="text-sm text-gray-500">{server.modelName.replace('.gguf', '')} · Port {server.port}</p>
|
|
398
|
-
</div>
|
|
399
|
-
</div>
|
|
400
|
-
</div>
|
|
401
|
-
|
|
402
|
-
{/* Filter Bar */}
|
|
403
|
-
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-100 bg-gray-50">
|
|
404
|
-
{/* Filter Pills */}
|
|
405
|
-
<div className="flex items-center gap-2">
|
|
406
|
-
{filters.map((filter) => (
|
|
407
|
-
<button
|
|
408
|
-
key={filter.id}
|
|
409
|
-
onClick={() => setActiveFilter(filter.id)}
|
|
410
|
-
className={`px-3 py-1.5 text-sm font-medium rounded-full border transition-colors cursor-pointer ${
|
|
411
|
-
activeFilter === filter.id
|
|
412
|
-
? 'bg-gray-900 text-white border-gray-900'
|
|
413
|
-
: 'bg-white text-gray-600 border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
|
414
|
-
}`}
|
|
415
|
-
>
|
|
416
|
-
{filter.label}
|
|
417
|
-
</button>
|
|
418
|
-
))}
|
|
419
|
-
</div>
|
|
420
|
-
|
|
421
|
-
{/* Health Checks Toggle + View Mode + Sort */}
|
|
422
|
-
<div className="flex items-center gap-2">
|
|
423
|
-
{/* Health Checks Toggle */}
|
|
424
|
-
<button
|
|
425
|
-
onClick={() => setShowHealthChecks(!showHealthChecks)}
|
|
426
|
-
className={`px-3 py-1.5 text-sm font-medium rounded-lg border transition-colors cursor-pointer ${
|
|
427
|
-
showHealthChecks
|
|
428
|
-
? 'bg-blue-50 text-blue-700 border-blue-200'
|
|
429
|
-
: 'bg-white text-gray-500 border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
|
430
|
-
}`}
|
|
431
|
-
title="Show /health, /slots, /props requests (filtered by default)"
|
|
432
|
-
>
|
|
433
|
-
Health Checks
|
|
434
|
-
</button>
|
|
435
|
-
|
|
436
|
-
{/* View Mode Toggle */}
|
|
437
|
-
<div className="flex items-center bg-white border border-gray-200 rounded-lg overflow-hidden">
|
|
438
|
-
<button
|
|
439
|
-
onClick={() => setViewMode('formatted')}
|
|
440
|
-
className={`px-3 py-1.5 text-sm font-medium transition-colors cursor-pointer ${
|
|
441
|
-
viewMode === 'formatted'
|
|
442
|
-
? 'bg-gray-100 text-gray-900'
|
|
443
|
-
: 'text-gray-600 hover:bg-gray-50'
|
|
444
|
-
}`}
|
|
445
|
-
>
|
|
446
|
-
Formatted
|
|
447
|
-
</button>
|
|
448
|
-
<button
|
|
449
|
-
onClick={() => setViewMode('raw')}
|
|
450
|
-
className={`px-3 py-1.5 text-sm font-medium transition-colors cursor-pointer ${
|
|
451
|
-
viewMode === 'raw'
|
|
452
|
-
? 'bg-gray-100 text-gray-900'
|
|
453
|
-
: 'text-gray-600 hover:bg-gray-50'
|
|
454
|
-
}`}
|
|
455
|
-
>
|
|
456
|
-
Raw
|
|
457
|
-
</button>
|
|
458
|
-
</div>
|
|
459
|
-
|
|
460
|
-
{/* Sort Dropdown */}
|
|
461
|
-
<div className="relative" ref={dropdownRef}>
|
|
462
|
-
<button
|
|
463
|
-
onClick={() => setShowSortDropdown(!showSortDropdown)}
|
|
464
|
-
className="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-lg hover:border-gray-300 transition-colors cursor-pointer"
|
|
465
|
-
>
|
|
466
|
-
{sortOrder === 'newest' ? 'Newest' : 'Oldest'}
|
|
467
|
-
<ChevronDown className="w-4 h-4" />
|
|
468
|
-
</button>
|
|
469
|
-
|
|
470
|
-
{showSortDropdown && (
|
|
471
|
-
<div className="absolute right-0 top-full mt-1 w-32 bg-white border border-gray-200 rounded-lg shadow-lg z-10">
|
|
472
|
-
<button
|
|
473
|
-
onClick={() => { setSortOrder('newest'); setShowSortDropdown(false); }}
|
|
474
|
-
className={`w-full text-left px-3 py-2 text-sm transition-colors cursor-pointer ${
|
|
475
|
-
sortOrder === 'newest' ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:bg-gray-50'
|
|
476
|
-
}`}
|
|
477
|
-
>
|
|
478
|
-
Newest
|
|
479
|
-
</button>
|
|
480
|
-
<button
|
|
481
|
-
onClick={() => { setSortOrder('oldest'); setShowSortDropdown(false); }}
|
|
482
|
-
className={`w-full text-left px-3 py-2 text-sm transition-colors cursor-pointer ${
|
|
483
|
-
sortOrder === 'oldest' ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:bg-gray-50'
|
|
484
|
-
}`}
|
|
485
|
-
>
|
|
486
|
-
Oldest
|
|
487
|
-
</button>
|
|
488
|
-
</div>
|
|
489
|
-
)}
|
|
490
|
-
</div>
|
|
491
|
-
</div>
|
|
492
|
-
</div>
|
|
493
|
-
|
|
494
|
-
{/* Log Content */}
|
|
495
|
-
<div
|
|
496
|
-
ref={logContainerRef}
|
|
497
|
-
className="flex-1 overflow-y-auto bg-gray-900 p-4 font-mono text-sm"
|
|
498
|
-
onScroll={(e) => {
|
|
499
|
-
const target = e.target as HTMLDivElement;
|
|
500
|
-
const isAtBottom = target.scrollHeight - target.scrollTop <= target.clientHeight + 50;
|
|
501
|
-
setAutoScroll(isAtBottom);
|
|
502
|
-
}}
|
|
503
|
-
>
|
|
504
|
-
{logsLoading ? (
|
|
505
|
-
<div className="flex items-center justify-center h-full">
|
|
506
|
-
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
|
|
507
|
-
</div>
|
|
508
|
-
) : filteredLogs.length === 0 ? (
|
|
509
|
-
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
|
510
|
-
<Trash2 className="w-8 h-8 mb-2 opacity-50" />
|
|
511
|
-
<p>No {viewMode === 'formatted' ? 'formatted ' : ''}logs found</p>
|
|
512
|
-
{viewMode === 'formatted' && hasRawLogs && formattedCount === 0 ? (
|
|
513
|
-
<p className="text-sm mt-1">
|
|
514
|
-
No HTTP requests yet.{' '}
|
|
515
|
-
<button
|
|
516
|
-
onClick={() => setViewMode('raw')}
|
|
517
|
-
className="text-blue-400 hover:text-blue-300 underline cursor-pointer"
|
|
518
|
-
>
|
|
519
|
-
View raw logs
|
|
520
|
-
</button>
|
|
521
|
-
</p>
|
|
522
|
-
) : activeFilter !== 'all' ? (
|
|
523
|
-
<p className="text-sm mt-1">Try selecting a different filter</p>
|
|
524
|
-
) : null}
|
|
525
|
-
</div>
|
|
526
|
-
) : (
|
|
527
|
-
<div className="space-y-0.5">
|
|
528
|
-
{filteredLogs.map((log, index) => (
|
|
529
|
-
<div
|
|
530
|
-
key={index}
|
|
531
|
-
className={`${getLogTypeColor(log.type)} break-all whitespace-pre-wrap leading-relaxed`}
|
|
532
|
-
>
|
|
533
|
-
{viewMode === 'formatted' && log.formatted ? log.formatted : log.raw}
|
|
534
|
-
</div>
|
|
535
|
-
))}
|
|
536
|
-
</div>
|
|
537
|
-
)}
|
|
538
|
-
</div>
|
|
539
|
-
|
|
540
|
-
{/* Footer with stats */}
|
|
541
|
-
<div className="flex items-center justify-between px-4 py-2 border-t border-gray-200 bg-gray-50 text-sm text-gray-500">
|
|
542
|
-
<span>
|
|
543
|
-
{filteredLogs.length} {filteredLogs.length === 1 ? 'line' : 'lines'}
|
|
544
|
-
{activeFilter !== 'all' && ` (filtered)`}
|
|
545
|
-
</span>
|
|
546
|
-
<span className="flex items-center gap-2">
|
|
547
|
-
<span className={`w-2 h-2 rounded-full ${autoScroll ? 'bg-green-500' : 'bg-gray-300'}`} />
|
|
548
|
-
{autoScroll ? 'Auto-scroll on' : 'Auto-scroll off'}
|
|
549
|
-
</span>
|
|
550
|
-
</div>
|
|
551
|
-
</div>
|
|
552
|
-
);
|
|
553
|
-
}
|